@relvehq/agent-ready 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -25
- package/SETUP.md +354 -337
- package/dist/bin/agent-ready.js +105 -11
- package/dist/refine/check.d.ts +3 -1
- package/dist/refine/check.js +43 -0
- package/dist/refine/remote.d.ts +57 -0
- package/dist/refine/remote.js +238 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +550 -363
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-converter.php +762 -762
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-llms.php +192 -192
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-markdown-builder.php +455 -447
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-negotiation.php +334 -334
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-refiner.php +894 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-response.php +226 -226
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-router.php +421 -421
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +204 -115
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +583 -543
- package/dist/wp/relve-agent-ready/includes/index.php +2 -2
- package/dist/wp/relve-agent-ready/index.php +2 -2
- package/dist/wp/relve-agent-ready/readme.txt +112 -75
- package/dist/wp/relve-agent-ready/relve-agent-ready.php +81 -70
- package/dist/wp/relve-agent-ready/uninstall.php +35 -24
- package/package.json +1 -1
|
@@ -1,421 +1,421 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
/**
|
|
3
|
-
* Router: when to answer with Markdown, and with what.
|
|
4
|
-
*
|
|
5
|
-
* do_parse_request (filter, priority 1), before WordPress parses the URL:
|
|
6
|
-
* - detect the request (Relve_AR_Negotiation::detect): `.md` suffix,
|
|
7
|
-
* `?format=md`, Accept, or llms.txt; GET/HEAD only
|
|
8
|
-
* - llms.txt: answered right here
|
|
9
|
-
* - excluded path: `.md` / `?format=md` get a plain 404, Accept goes on
|
|
10
|
-
* - `.md`: REQUEST_URI is rewritten without the suffix, so WordPress
|
|
11
|
-
* resolves the post as usual (no rewrite rules involved, works with any
|
|
12
|
-
* permalink structure and never reaches WordPress's 404 for ".md")
|
|
13
|
-
* template_redirect (priority 1), after the main query, before
|
|
14
|
-
* redirect_canonical:
|
|
15
|
-
* - a published, public, non-password post/page of an enabled type, the
|
|
16
|
-
* posts front page or a category / tag / author archive -> Markdown
|
|
17
|
-
* (a stored refined file first, else built from WordPress data)
|
|
18
|
-
* - anything else: Accept requests carry on untouched; `.md` and
|
|
19
|
-
* `?format=md` get a plain-text 404
|
|
20
|
-
*
|
|
21
|
-
* Nothing at all happens while the "Enabled" setting is off.
|
|
22
|
-
*
|
|
23
|
-
* @package Relve_Agent_Ready
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
if ( ! defined( 'ABSPATH' ) ) {
|
|
27
|
-
exit;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Front-end request handling.
|
|
32
|
-
*/
|
|
33
|
-
class Relve_AR_Router {
|
|
34
|
-
|
|
35
|
-
/** Detection for the current request, set in do_parse_request. @var array|null */
|
|
36
|
-
private static $detection = null;
|
|
37
|
-
|
|
38
|
-
/** Settings for the current request. @var array|null */
|
|
39
|
-
private static $settings = null;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Register hooks.
|
|
43
|
-
*
|
|
44
|
-
* @return void
|
|
45
|
-
*/
|
|
46
|
-
public static function init() {
|
|
47
|
-
add_filter( 'do_parse_request', array( __CLASS__, 'on_parse_request' ), 1, 1 );
|
|
48
|
-
add_action( 'template_redirect', array( __CLASS__, 'on_template_redirect' ), 1 );
|
|
49
|
-
add_filter( 'rocket_cache_reject_uri', array( __CLASS__, 'rocket_reject_uri' ) );
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── paths ─────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Path of home_url() without trailing slash: "" or e.g. "/blog".
|
|
56
|
-
*
|
|
57
|
-
* @return string
|
|
58
|
-
*/
|
|
59
|
-
public static function home_path() {
|
|
60
|
-
$path = (string) wp_parse_url( home_url(), PHP_URL_PATH );
|
|
61
|
-
return rtrim( $path, '/' );
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* scheme://host[:port] of home_url().
|
|
66
|
-
*
|
|
67
|
-
* @return string
|
|
68
|
-
*/
|
|
69
|
-
public static function home_origin() {
|
|
70
|
-
$p = wp_parse_url( home_url() );
|
|
71
|
-
if ( ! is_array( $p ) || empty( $p['host'] ) ) {
|
|
72
|
-
return '';
|
|
73
|
-
}
|
|
74
|
-
return ( isset( $p['scheme'] ) ? $p['scheme'] : 'https' ) . '://' . $p['host'] . ( isset( $p['port'] ) ? ':' . $p['port'] : '' );
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Static prefix of the permalink structure ("/blog" for
|
|
79
|
-
* "/blog/%postname%/"), "" when there is none.
|
|
80
|
-
*
|
|
81
|
-
* @return string
|
|
82
|
-
*/
|
|
83
|
-
public static function permalink_prefix() {
|
|
84
|
-
global $wp_rewrite;
|
|
85
|
-
$front = ( $wp_rewrite && isset( $wp_rewrite->front ) ) ? (string) $wp_rewrite->front : '/';
|
|
86
|
-
if ( false !== strpos( $front, '%' ) ) {
|
|
87
|
-
return '';
|
|
88
|
-
}
|
|
89
|
-
$front = rtrim( $front, '/' );
|
|
90
|
-
return null === Relve_AR_Negotiation::normalize_path( '' === $front ? '/' : $front ) ? '' : $front;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Paths llms.txt is answered at: "<home path>/llms.txt", plus
|
|
95
|
-
* "<home path><permalink prefix>/llms.txt" when posts live under a static
|
|
96
|
-
* prefix such as "/blog" (a site whose home is the root but whose blog is
|
|
97
|
-
* routed to WordPress under /blog). Empty when the plugin should not
|
|
98
|
-
* answer llms.txt.
|
|
99
|
-
*
|
|
100
|
-
* @param array $settings Settings.
|
|
101
|
-
* @return string[]
|
|
102
|
-
*/
|
|
103
|
-
public static function llms_paths( $settings ) {
|
|
104
|
-
if ( ! Relve_AR_Llms::should_serve( $settings['llms_mode'], Relve_AR_Llms::yoast_serves(), Relve_AR_Llms::physical_file_exists() ) ) {
|
|
105
|
-
return array();
|
|
106
|
-
}
|
|
107
|
-
$home = self::home_path();
|
|
108
|
-
$paths = array( $home . '/llms.txt' );
|
|
109
|
-
$pre = self::permalink_prefix();
|
|
110
|
-
if ( '' !== $pre ) {
|
|
111
|
-
$paths[] = $home . $pre . '/llms.txt';
|
|
112
|
-
}
|
|
113
|
-
return array_values( array_unique( $paths ) );
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── hooks ─────────────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* do_parse_request: detect, answer llms.txt, strip ".md" for WordPress.
|
|
120
|
-
*
|
|
121
|
-
* @param bool $do Whether WordPress should parse the request.
|
|
122
|
-
* @return bool
|
|
123
|
-
*/
|
|
124
|
-
public static function on_parse_request( $do ) {
|
|
125
|
-
self::$detection = null;
|
|
126
|
-
if ( ! $do || is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
|
|
127
|
-
return $do;
|
|
128
|
-
}
|
|
129
|
-
$settings = Relve_AR_Settings::get();
|
|
130
|
-
if ( ! $settings['enabled'] ) {
|
|
131
|
-
return $do;
|
|
132
|
-
}
|
|
133
|
-
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
|
|
134
|
-
if ( 'GET' !== $method && 'HEAD' !== $method ) {
|
|
135
|
-
return $do;
|
|
136
|
-
}
|
|
137
|
-
$uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only matched, never output.
|
|
138
|
-
$accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? wp_unslash( $_SERVER['HTTP_ACCEPT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only parsed.
|
|
139
|
-
$home = self::home_path();
|
|
140
|
-
|
|
141
|
-
$d = Relve_AR_Negotiation::detect( $method, (string) $uri, (string) $accept, $home, self::llms_paths( $settings ) );
|
|
142
|
-
if ( ! $d['markdown'] ) {
|
|
143
|
-
return $do;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if ( $d['llms'] ) {
|
|
147
|
-
self::serve_llms( $settings, $method );
|
|
148
|
-
exit;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if ( Relve_AR_Negotiation::is_excluded( $d['path'], $settings['excluded'], $home ) ) {
|
|
152
|
-
if ( 'accept' === $d['via'] ) {
|
|
153
|
-
return $do;
|
|
154
|
-
}
|
|
155
|
-
self::no_page_cache();
|
|
156
|
-
Relve_AR_Response::send( Relve_AR_Response::plain( 404, $method ) );
|
|
157
|
-
exit;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if ( 'suffix' === $d['via'] && null !== $d['uri'] ) {
|
|
161
|
-
$_SERVER['REQUEST_URI'] = wp_slash( $d['uri'] );
|
|
162
|
-
if ( ! empty( $_SERVER['PATH_INFO'] ) ) {
|
|
163
|
-
$_SERVER['PATH_INFO'] = self::strip_md_from_path_info( wp_unslash( $_SERVER['PATH_INFO'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
self::$detection = $d;
|
|
168
|
-
self::$settings = $settings;
|
|
169
|
-
return $do;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* PATH_INFO without a trailing ".md" (some servers set it).
|
|
174
|
-
*
|
|
175
|
-
* @param string $path_info PATH_INFO.
|
|
176
|
-
* @return string
|
|
177
|
-
*/
|
|
178
|
-
private static function strip_md_from_path_info( $path_info ) {
|
|
179
|
-
$lower = strtolower( $path_info );
|
|
180
|
-
if ( '/index.md' === substr( $lower, -9 ) ) {
|
|
181
|
-
return substr( $path_info, 0, -8 );
|
|
182
|
-
}
|
|
183
|
-
if ( '.md' === substr( $lower, -3 ) ) {
|
|
184
|
-
return substr( $path_info, 0, -3 );
|
|
185
|
-
}
|
|
186
|
-
return $path_info;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* template_redirect: serve the Markdown, or step aside.
|
|
191
|
-
*
|
|
192
|
-
* @return void
|
|
193
|
-
*/
|
|
194
|
-
public static function on_template_redirect() {
|
|
195
|
-
if ( null === self::$detection ) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
$d = self::$detection;
|
|
199
|
-
$settings = self::$settings;
|
|
200
|
-
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
|
|
201
|
-
$inm = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- compared only.
|
|
202
|
-
$negotiated = ( 'accept' === $d['via'] );
|
|
203
|
-
|
|
204
|
-
$target = self::resolve_target( $settings );
|
|
205
|
-
if ( null === $target ) {
|
|
206
|
-
if ( $negotiated ) {
|
|
207
|
-
return; // WordPress serves its normal response.
|
|
208
|
-
}
|
|
209
|
-
self::no_page_cache();
|
|
210
|
-
Relve_AR_Response::send( Relve_AR_Response::plain( 404, $method ) );
|
|
211
|
-
exit;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
$source = 'native';
|
|
216
|
-
$store = new Relve_AR_Store();
|
|
217
|
-
$stored = $store->get( $d['path'], self::home_path() );
|
|
218
|
-
if ( null !== $stored ) {
|
|
219
|
-
$source = 'stored';
|
|
220
|
-
$body = Relve_AR_Markdown_Builder::for_stored( $stored, $target['canonical'], isset( $target['post'] ) ? $target['post'] : null, $target['title'] );
|
|
221
|
-
} elseif ( 'post' === $target['kind'] ) {
|
|
222
|
-
$body = Relve_AR_Markdown_Builder::for_post( $target['post'], $target['canonical'] );
|
|
223
|
-
} else {
|
|
224
|
-
$body = Relve_AR_Markdown_Builder::for_archive( $target['kind'], $target['id'], $target['canonical'] );
|
|
225
|
-
}
|
|
226
|
-
} catch ( Throwable $e ) {
|
|
227
|
-
$body = null;
|
|
228
|
-
}
|
|
229
|
-
if ( null === $body ) {
|
|
230
|
-
if ( $negotiated ) {
|
|
231
|
-
return; // An agent asking by Accept is never worse off than a browser.
|
|
232
|
-
}
|
|
233
|
-
self::no_page_cache();
|
|
234
|
-
Relve_AR_Response::send( Relve_AR_Response::plain( 500, $method ) );
|
|
235
|
-
exit;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
self::no_page_cache();
|
|
239
|
-
Relve_AR_Response::send(
|
|
240
|
-
Relve_AR_Response::build(
|
|
241
|
-
$body,
|
|
242
|
-
array(
|
|
243
|
-
'method' => $method,
|
|
244
|
-
'if_none_match' => $inm,
|
|
245
|
-
'canonical' => $target['canonical'],
|
|
246
|
-
'cache_control' => Relve_AR_Response::cache_control_for( $d['via'] ),
|
|
247
|
-
'source' => $source,
|
|
248
|
-
)
|
|
249
|
-
)
|
|
250
|
-
);
|
|
251
|
-
exit;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* What the main query resolved to, if it is something we serve.
|
|
256
|
-
*
|
|
257
|
-
* @param array $settings Settings.
|
|
258
|
-
* @return array|null { kind: post|home|category|post_tag|author, post?, id, canonical, title }
|
|
259
|
-
*/
|
|
260
|
-
public static function resolve_target( $settings ) {
|
|
261
|
-
if ( is_404() || is_feed() || is_search() || is_preview() || is_embed() || is_robots() || is_trackback() || is_attachment() ) {
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
$types = (array) $settings['post_types'];
|
|
265
|
-
if ( is_singular() ) {
|
|
266
|
-
$post = get_queried_object();
|
|
267
|
-
if ( ! self::servable_post( $post, $types ) ) {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
$canonical = get_permalink( $post );
|
|
271
|
-
if ( ! $canonical ) {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
return array(
|
|
275
|
-
'kind' => 'post',
|
|
276
|
-
'post' => $post,
|
|
277
|
-
'id' => $post->ID,
|
|
278
|
-
'canonical' => $canonical,
|
|
279
|
-
'title' => '',
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
if ( ! in_array( 'post', $types, true ) ) {
|
|
283
|
-
return null; // Lists of posts need posts enabled.
|
|
284
|
-
}
|
|
285
|
-
if ( is_home() ) {
|
|
286
|
-
$page = (int) get_option( 'page_for_posts' );
|
|
287
|
-
$canonical = ( 'page' === get_option( 'show_on_front' ) && $page ) ? get_permalink( $page ) : home_url( '/' );
|
|
288
|
-
return array(
|
|
289
|
-
'kind' => 'home',
|
|
290
|
-
'id' => 0,
|
|
291
|
-
'canonical' => $canonical,
|
|
292
|
-
'title' => html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' ),
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
if ( is_category() || is_tag() ) {
|
|
296
|
-
$term = get_queried_object();
|
|
297
|
-
if ( ! $term || ! isset( $term->term_id ) ) {
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
$link = get_term_link( $term );
|
|
301
|
-
if ( is_wp_error( $link ) ) {
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
return array(
|
|
305
|
-
'kind' => is_category() ? 'category' : 'post_tag',
|
|
306
|
-
'id' => (int) $term->term_id,
|
|
307
|
-
'canonical' => $link,
|
|
308
|
-
'title' => html_entity_decode( $term->name, ENT_QUOTES, 'UTF-8' ),
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
if ( is_author() ) {
|
|
312
|
-
$user = get_queried_object();
|
|
313
|
-
if ( ! $user || ! isset( $user->ID ) ) {
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
return array(
|
|
317
|
-
'kind' => 'author',
|
|
318
|
-
'id' => (int) $user->ID,
|
|
319
|
-
'canonical' => get_author_posts_url( $user->ID ),
|
|
320
|
-
'title' => html_entity_decode( (string) $user->display_name, ENT_QUOTES, 'UTF-8' ),
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Published, public, not password-protected, of an enabled type.
|
|
328
|
-
*
|
|
329
|
-
* @param mixed $post Queried object.
|
|
330
|
-
* @param string[] $types Enabled post types.
|
|
331
|
-
* @return bool
|
|
332
|
-
*/
|
|
333
|
-
public static function servable_post( $post, $types ) {
|
|
334
|
-
if ( ! ( $post instanceof WP_Post ) ) {
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
337
|
-
if ( 'publish' !== $post->post_status || '' !== (string) $post->post_password ) {
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
if ( ! in_array( $post->post_type, (array) $types, true ) || ! is_post_type_viewable( $post->post_type ) ) {
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Answer llms.txt.
|
|
348
|
-
*
|
|
349
|
-
* @param array $settings Settings.
|
|
350
|
-
* @param string $method GET or HEAD.
|
|
351
|
-
* @return void
|
|
352
|
-
*/
|
|
353
|
-
private static function serve_llms( $settings, $method ) {
|
|
354
|
-
$inm = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- compared only.
|
|
355
|
-
$body = Relve_AR_Llms::generate( $settings );
|
|
356
|
-
self::no_page_cache();
|
|
357
|
-
Relve_AR_Response::send(
|
|
358
|
-
Relve_AR_Response::build(
|
|
359
|
-
$body,
|
|
360
|
-
array(
|
|
361
|
-
'method' => $method,
|
|
362
|
-
'if_none_match' => $inm,
|
|
363
|
-
'canonical' => null, // llms.txt is meant to be found and read.
|
|
364
|
-
'noindex' => false,
|
|
365
|
-
'content_type' => Relve_AR_Response::TEXT_CONTENT_TYPE,
|
|
366
|
-
'cache_control' => Relve_AR_Response::cache_control_for( 'llms' ),
|
|
367
|
-
'source' => 'llms',
|
|
368
|
-
)
|
|
369
|
-
)
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ── caches ────────────────────────────────────────────────────────────
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Tell page caches not to store this response as the page.
|
|
377
|
-
*
|
|
378
|
-
* @return void
|
|
379
|
-
*/
|
|
380
|
-
public static function no_page_cache() {
|
|
381
|
-
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
|
|
382
|
-
define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- shared page-cache convention.
|
|
383
|
-
}
|
|
384
|
-
add_filter( 'do_rocket_generate_caching_files', '__return_false', 999 );
|
|
385
|
-
do_action( 'litespeed_control_set_nocache', 'relve-agent-ready: markdown response' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed Cache's hook.
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* WP Rocket: never cache ".md" URLs or llms.txt.
|
|
390
|
-
*
|
|
391
|
-
* @param mixed $uris Reject patterns.
|
|
392
|
-
* @return array
|
|
393
|
-
*/
|
|
394
|
-
public static function rocket_reject_uri( $uris ) {
|
|
395
|
-
$uris = is_array( $uris ) ? $uris : array();
|
|
396
|
-
$uris[] = '/(.+)\.md';
|
|
397
|
-
$uris[] = '/(.*)llms\.txt';
|
|
398
|
-
return array_values( array_unique( $uris ) );
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Regenerate WP Rocket's config so the reject list is current.
|
|
403
|
-
*
|
|
404
|
-
* @return void
|
|
405
|
-
*/
|
|
406
|
-
public static function refresh_rocket() {
|
|
407
|
-
if ( function_exists( 'rocket_generate_config_file' ) ) {
|
|
408
|
-
rocket_generate_config_file();
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Reset per-request state (tests).
|
|
414
|
-
*
|
|
415
|
-
* @return void
|
|
416
|
-
*/
|
|
417
|
-
public static function reset() {
|
|
418
|
-
self::$detection = null;
|
|
419
|
-
self::$settings = null;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Router: when to answer with Markdown, and with what.
|
|
4
|
+
*
|
|
5
|
+
* do_parse_request (filter, priority 1), before WordPress parses the URL:
|
|
6
|
+
* - detect the request (Relve_AR_Negotiation::detect): `.md` suffix,
|
|
7
|
+
* `?format=md`, Accept, or llms.txt; GET/HEAD only
|
|
8
|
+
* - llms.txt: answered right here
|
|
9
|
+
* - excluded path: `.md` / `?format=md` get a plain 404, Accept goes on
|
|
10
|
+
* - `.md`: REQUEST_URI is rewritten without the suffix, so WordPress
|
|
11
|
+
* resolves the post as usual (no rewrite rules involved, works with any
|
|
12
|
+
* permalink structure and never reaches WordPress's 404 for ".md")
|
|
13
|
+
* template_redirect (priority 1), after the main query, before
|
|
14
|
+
* redirect_canonical:
|
|
15
|
+
* - a published, public, non-password post/page of an enabled type, the
|
|
16
|
+
* posts front page or a category / tag / author archive -> Markdown
|
|
17
|
+
* (a stored refined file first, else built from WordPress data)
|
|
18
|
+
* - anything else: Accept requests carry on untouched; `.md` and
|
|
19
|
+
* `?format=md` get a plain-text 404
|
|
20
|
+
*
|
|
21
|
+
* Nothing at all happens while the "Enabled" setting is off.
|
|
22
|
+
*
|
|
23
|
+
* @package Relve_Agent_Ready
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
if ( ! defined( 'ABSPATH' ) ) {
|
|
27
|
+
exit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Front-end request handling.
|
|
32
|
+
*/
|
|
33
|
+
class Relve_AR_Router {
|
|
34
|
+
|
|
35
|
+
/** Detection for the current request, set in do_parse_request. @var array|null */
|
|
36
|
+
private static $detection = null;
|
|
37
|
+
|
|
38
|
+
/** Settings for the current request. @var array|null */
|
|
39
|
+
private static $settings = null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register hooks.
|
|
43
|
+
*
|
|
44
|
+
* @return void
|
|
45
|
+
*/
|
|
46
|
+
public static function init() {
|
|
47
|
+
add_filter( 'do_parse_request', array( __CLASS__, 'on_parse_request' ), 1, 1 );
|
|
48
|
+
add_action( 'template_redirect', array( __CLASS__, 'on_template_redirect' ), 1 );
|
|
49
|
+
add_filter( 'rocket_cache_reject_uri', array( __CLASS__, 'rocket_reject_uri' ) );
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── paths ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Path of home_url() without trailing slash: "" or e.g. "/blog".
|
|
56
|
+
*
|
|
57
|
+
* @return string
|
|
58
|
+
*/
|
|
59
|
+
public static function home_path() {
|
|
60
|
+
$path = (string) wp_parse_url( home_url(), PHP_URL_PATH );
|
|
61
|
+
return rtrim( $path, '/' );
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* scheme://host[:port] of home_url().
|
|
66
|
+
*
|
|
67
|
+
* @return string
|
|
68
|
+
*/
|
|
69
|
+
public static function home_origin() {
|
|
70
|
+
$p = wp_parse_url( home_url() );
|
|
71
|
+
if ( ! is_array( $p ) || empty( $p['host'] ) ) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
return ( isset( $p['scheme'] ) ? $p['scheme'] : 'https' ) . '://' . $p['host'] . ( isset( $p['port'] ) ? ':' . $p['port'] : '' );
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Static prefix of the permalink structure ("/blog" for
|
|
79
|
+
* "/blog/%postname%/"), "" when there is none.
|
|
80
|
+
*
|
|
81
|
+
* @return string
|
|
82
|
+
*/
|
|
83
|
+
public static function permalink_prefix() {
|
|
84
|
+
global $wp_rewrite;
|
|
85
|
+
$front = ( $wp_rewrite && isset( $wp_rewrite->front ) ) ? (string) $wp_rewrite->front : '/';
|
|
86
|
+
if ( false !== strpos( $front, '%' ) ) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
$front = rtrim( $front, '/' );
|
|
90
|
+
return null === Relve_AR_Negotiation::normalize_path( '' === $front ? '/' : $front ) ? '' : $front;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Paths llms.txt is answered at: "<home path>/llms.txt", plus
|
|
95
|
+
* "<home path><permalink prefix>/llms.txt" when posts live under a static
|
|
96
|
+
* prefix such as "/blog" (a site whose home is the root but whose blog is
|
|
97
|
+
* routed to WordPress under /blog). Empty when the plugin should not
|
|
98
|
+
* answer llms.txt.
|
|
99
|
+
*
|
|
100
|
+
* @param array $settings Settings.
|
|
101
|
+
* @return string[]
|
|
102
|
+
*/
|
|
103
|
+
public static function llms_paths( $settings ) {
|
|
104
|
+
if ( ! Relve_AR_Llms::should_serve( $settings['llms_mode'], Relve_AR_Llms::yoast_serves(), Relve_AR_Llms::physical_file_exists() ) ) {
|
|
105
|
+
return array();
|
|
106
|
+
}
|
|
107
|
+
$home = self::home_path();
|
|
108
|
+
$paths = array( $home . '/llms.txt' );
|
|
109
|
+
$pre = self::permalink_prefix();
|
|
110
|
+
if ( '' !== $pre ) {
|
|
111
|
+
$paths[] = $home . $pre . '/llms.txt';
|
|
112
|
+
}
|
|
113
|
+
return array_values( array_unique( $paths ) );
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── hooks ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* do_parse_request: detect, answer llms.txt, strip ".md" for WordPress.
|
|
120
|
+
*
|
|
121
|
+
* @param bool $do Whether WordPress should parse the request.
|
|
122
|
+
* @return bool
|
|
123
|
+
*/
|
|
124
|
+
public static function on_parse_request( $do ) {
|
|
125
|
+
self::$detection = null;
|
|
126
|
+
if ( ! $do || is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
|
|
127
|
+
return $do;
|
|
128
|
+
}
|
|
129
|
+
$settings = Relve_AR_Settings::get();
|
|
130
|
+
if ( ! $settings['enabled'] ) {
|
|
131
|
+
return $do;
|
|
132
|
+
}
|
|
133
|
+
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
|
|
134
|
+
if ( 'GET' !== $method && 'HEAD' !== $method ) {
|
|
135
|
+
return $do;
|
|
136
|
+
}
|
|
137
|
+
$uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only matched, never output.
|
|
138
|
+
$accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? wp_unslash( $_SERVER['HTTP_ACCEPT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only parsed.
|
|
139
|
+
$home = self::home_path();
|
|
140
|
+
|
|
141
|
+
$d = Relve_AR_Negotiation::detect( $method, (string) $uri, (string) $accept, $home, self::llms_paths( $settings ) );
|
|
142
|
+
if ( ! $d['markdown'] ) {
|
|
143
|
+
return $do;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if ( $d['llms'] ) {
|
|
147
|
+
self::serve_llms( $settings, $method );
|
|
148
|
+
exit;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if ( Relve_AR_Negotiation::is_excluded( $d['path'], $settings['excluded'], $home ) ) {
|
|
152
|
+
if ( 'accept' === $d['via'] ) {
|
|
153
|
+
return $do;
|
|
154
|
+
}
|
|
155
|
+
self::no_page_cache();
|
|
156
|
+
Relve_AR_Response::send( Relve_AR_Response::plain( 404, $method ) );
|
|
157
|
+
exit;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if ( 'suffix' === $d['via'] && null !== $d['uri'] ) {
|
|
161
|
+
$_SERVER['REQUEST_URI'] = wp_slash( $d['uri'] );
|
|
162
|
+
if ( ! empty( $_SERVER['PATH_INFO'] ) ) {
|
|
163
|
+
$_SERVER['PATH_INFO'] = self::strip_md_from_path_info( wp_unslash( $_SERVER['PATH_INFO'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
self::$detection = $d;
|
|
168
|
+
self::$settings = $settings;
|
|
169
|
+
return $do;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* PATH_INFO without a trailing ".md" (some servers set it).
|
|
174
|
+
*
|
|
175
|
+
* @param string $path_info PATH_INFO.
|
|
176
|
+
* @return string
|
|
177
|
+
*/
|
|
178
|
+
private static function strip_md_from_path_info( $path_info ) {
|
|
179
|
+
$lower = strtolower( $path_info );
|
|
180
|
+
if ( '/index.md' === substr( $lower, -9 ) ) {
|
|
181
|
+
return substr( $path_info, 0, -8 );
|
|
182
|
+
}
|
|
183
|
+
if ( '.md' === substr( $lower, -3 ) ) {
|
|
184
|
+
return substr( $path_info, 0, -3 );
|
|
185
|
+
}
|
|
186
|
+
return $path_info;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* template_redirect: serve the Markdown, or step aside.
|
|
191
|
+
*
|
|
192
|
+
* @return void
|
|
193
|
+
*/
|
|
194
|
+
public static function on_template_redirect() {
|
|
195
|
+
if ( null === self::$detection ) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
$d = self::$detection;
|
|
199
|
+
$settings = self::$settings;
|
|
200
|
+
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
|
|
201
|
+
$inm = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- compared only.
|
|
202
|
+
$negotiated = ( 'accept' === $d['via'] );
|
|
203
|
+
|
|
204
|
+
$target = self::resolve_target( $settings );
|
|
205
|
+
if ( null === $target ) {
|
|
206
|
+
if ( $negotiated ) {
|
|
207
|
+
return; // WordPress serves its normal response.
|
|
208
|
+
}
|
|
209
|
+
self::no_page_cache();
|
|
210
|
+
Relve_AR_Response::send( Relve_AR_Response::plain( 404, $method ) );
|
|
211
|
+
exit;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
$source = 'native';
|
|
216
|
+
$store = new Relve_AR_Store();
|
|
217
|
+
$stored = $store->get( $d['path'], self::home_path() );
|
|
218
|
+
if ( null !== $stored ) {
|
|
219
|
+
$source = 'stored';
|
|
220
|
+
$body = Relve_AR_Markdown_Builder::for_stored( $stored, $target['canonical'], isset( $target['post'] ) ? $target['post'] : null, $target['title'] );
|
|
221
|
+
} elseif ( 'post' === $target['kind'] ) {
|
|
222
|
+
$body = Relve_AR_Markdown_Builder::for_post( $target['post'], $target['canonical'] );
|
|
223
|
+
} else {
|
|
224
|
+
$body = Relve_AR_Markdown_Builder::for_archive( $target['kind'], $target['id'], $target['canonical'] );
|
|
225
|
+
}
|
|
226
|
+
} catch ( Throwable $e ) {
|
|
227
|
+
$body = null;
|
|
228
|
+
}
|
|
229
|
+
if ( null === $body ) {
|
|
230
|
+
if ( $negotiated ) {
|
|
231
|
+
return; // An agent asking by Accept is never worse off than a browser.
|
|
232
|
+
}
|
|
233
|
+
self::no_page_cache();
|
|
234
|
+
Relve_AR_Response::send( Relve_AR_Response::plain( 500, $method ) );
|
|
235
|
+
exit;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
self::no_page_cache();
|
|
239
|
+
Relve_AR_Response::send(
|
|
240
|
+
Relve_AR_Response::build(
|
|
241
|
+
$body,
|
|
242
|
+
array(
|
|
243
|
+
'method' => $method,
|
|
244
|
+
'if_none_match' => $inm,
|
|
245
|
+
'canonical' => $target['canonical'],
|
|
246
|
+
'cache_control' => Relve_AR_Response::cache_control_for( $d['via'] ),
|
|
247
|
+
'source' => $source,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
exit;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* What the main query resolved to, if it is something we serve.
|
|
256
|
+
*
|
|
257
|
+
* @param array $settings Settings.
|
|
258
|
+
* @return array|null { kind: post|home|category|post_tag|author, post?, id, canonical, title }
|
|
259
|
+
*/
|
|
260
|
+
public static function resolve_target( $settings ) {
|
|
261
|
+
if ( is_404() || is_feed() || is_search() || is_preview() || is_embed() || is_robots() || is_trackback() || is_attachment() ) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
$types = (array) $settings['post_types'];
|
|
265
|
+
if ( is_singular() ) {
|
|
266
|
+
$post = get_queried_object();
|
|
267
|
+
if ( ! self::servable_post( $post, $types ) ) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
$canonical = get_permalink( $post );
|
|
271
|
+
if ( ! $canonical ) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return array(
|
|
275
|
+
'kind' => 'post',
|
|
276
|
+
'post' => $post,
|
|
277
|
+
'id' => $post->ID,
|
|
278
|
+
'canonical' => $canonical,
|
|
279
|
+
'title' => '',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if ( ! in_array( 'post', $types, true ) ) {
|
|
283
|
+
return null; // Lists of posts need posts enabled.
|
|
284
|
+
}
|
|
285
|
+
if ( is_home() ) {
|
|
286
|
+
$page = (int) get_option( 'page_for_posts' );
|
|
287
|
+
$canonical = ( 'page' === get_option( 'show_on_front' ) && $page ) ? get_permalink( $page ) : home_url( '/' );
|
|
288
|
+
return array(
|
|
289
|
+
'kind' => 'home',
|
|
290
|
+
'id' => 0,
|
|
291
|
+
'canonical' => $canonical,
|
|
292
|
+
'title' => html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' ),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if ( is_category() || is_tag() ) {
|
|
296
|
+
$term = get_queried_object();
|
|
297
|
+
if ( ! $term || ! isset( $term->term_id ) ) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
$link = get_term_link( $term );
|
|
301
|
+
if ( is_wp_error( $link ) ) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
return array(
|
|
305
|
+
'kind' => is_category() ? 'category' : 'post_tag',
|
|
306
|
+
'id' => (int) $term->term_id,
|
|
307
|
+
'canonical' => $link,
|
|
308
|
+
'title' => html_entity_decode( $term->name, ENT_QUOTES, 'UTF-8' ),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if ( is_author() ) {
|
|
312
|
+
$user = get_queried_object();
|
|
313
|
+
if ( ! $user || ! isset( $user->ID ) ) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return array(
|
|
317
|
+
'kind' => 'author',
|
|
318
|
+
'id' => (int) $user->ID,
|
|
319
|
+
'canonical' => get_author_posts_url( $user->ID ),
|
|
320
|
+
'title' => html_entity_decode( (string) $user->display_name, ENT_QUOTES, 'UTF-8' ),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Published, public, not password-protected, of an enabled type.
|
|
328
|
+
*
|
|
329
|
+
* @param mixed $post Queried object.
|
|
330
|
+
* @param string[] $types Enabled post types.
|
|
331
|
+
* @return bool
|
|
332
|
+
*/
|
|
333
|
+
public static function servable_post( $post, $types ) {
|
|
334
|
+
if ( ! ( $post instanceof WP_Post ) ) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
if ( 'publish' !== $post->post_status || '' !== (string) $post->post_password ) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if ( ! in_array( $post->post_type, (array) $types, true ) || ! is_post_type_viewable( $post->post_type ) ) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Answer llms.txt.
|
|
348
|
+
*
|
|
349
|
+
* @param array $settings Settings.
|
|
350
|
+
* @param string $method GET or HEAD.
|
|
351
|
+
* @return void
|
|
352
|
+
*/
|
|
353
|
+
private static function serve_llms( $settings, $method ) {
|
|
354
|
+
$inm = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- compared only.
|
|
355
|
+
$body = Relve_AR_Llms::generate( $settings );
|
|
356
|
+
self::no_page_cache();
|
|
357
|
+
Relve_AR_Response::send(
|
|
358
|
+
Relve_AR_Response::build(
|
|
359
|
+
$body,
|
|
360
|
+
array(
|
|
361
|
+
'method' => $method,
|
|
362
|
+
'if_none_match' => $inm,
|
|
363
|
+
'canonical' => null, // llms.txt is meant to be found and read.
|
|
364
|
+
'noindex' => false,
|
|
365
|
+
'content_type' => Relve_AR_Response::TEXT_CONTENT_TYPE,
|
|
366
|
+
'cache_control' => Relve_AR_Response::cache_control_for( 'llms' ),
|
|
367
|
+
'source' => 'llms',
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── caches ────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Tell page caches not to store this response as the page.
|
|
377
|
+
*
|
|
378
|
+
* @return void
|
|
379
|
+
*/
|
|
380
|
+
public static function no_page_cache() {
|
|
381
|
+
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
|
|
382
|
+
define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- shared page-cache convention.
|
|
383
|
+
}
|
|
384
|
+
add_filter( 'do_rocket_generate_caching_files', '__return_false', 999 );
|
|
385
|
+
do_action( 'litespeed_control_set_nocache', 'relve-agent-ready: markdown response' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed Cache's hook.
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* WP Rocket: never cache ".md" URLs or llms.txt.
|
|
390
|
+
*
|
|
391
|
+
* @param mixed $uris Reject patterns.
|
|
392
|
+
* @return array
|
|
393
|
+
*/
|
|
394
|
+
public static function rocket_reject_uri( $uris ) {
|
|
395
|
+
$uris = is_array( $uris ) ? $uris : array();
|
|
396
|
+
$uris[] = '/(.+)\.md';
|
|
397
|
+
$uris[] = '/(.*)llms\.txt';
|
|
398
|
+
return array_values( array_unique( $uris ) );
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Regenerate WP Rocket's config so the reject list is current.
|
|
403
|
+
*
|
|
404
|
+
* @return void
|
|
405
|
+
*/
|
|
406
|
+
public static function refresh_rocket() {
|
|
407
|
+
if ( function_exists( 'rocket_generate_config_file' ) ) {
|
|
408
|
+
rocket_generate_config_file();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Reset per-request state (tests).
|
|
414
|
+
*
|
|
415
|
+
* @return void
|
|
416
|
+
*/
|
|
417
|
+
public static function reset() {
|
|
418
|
+
self::$detection = null;
|
|
419
|
+
self::$settings = null;
|
|
420
|
+
}
|
|
421
|
+
}
|