PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta1
Elementor Website Builder – more than just a page builder v4.3.0-beta1
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / modules / agents / module.php

module.php in Elementor Website Builder – more than just a page builder 4.3.0-beta1, at modules/agents/module.php

223 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Modules\Agents;
4
5 use Elementor\Core\Base\Module as BaseModule;
6 use Elementor\Core\Experiments\Manager as Experiments_Manager;
7 use Elementor\Core\Kits\Documents\Kit;
8 use Elementor\Core\Kits\Documents\Tabs\Settings_Agents;
9 use Elementor\Plugin;
10 use Elementor\Utils;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 class Module extends BaseModule {
17
18 const EXPERIMENT_NAME = 'agents_llms_txt';
19
20 const PACKAGES = [
21 'editor-agents',
22 ];
23
24 const DEFAULT_CACHE_MAX_AGE = 300;
25
26 public function get_name() {
27 return 'agents';
28 }
29
30 public static function get_experimental_data() {
31 return [
32 'name' => self::EXPERIMENT_NAME,
33 'title' => esc_html__( 'Agents llms.txt', 'elementor' ),
34 'description' => esc_html__( 'Expose llms.txt from site settings at the site root for AI agents.', 'elementor' ),
35 'hidden' => true,
36 'default' => Experiments_Manager::STATE_INACTIVE,
37 'release_status' => Experiments_Manager::RELEASE_STATUS_DEV,
38 ];
39 }
40
41 public function __construct() {
42 parent::__construct();
43
44 add_action( 'template_redirect', [ $this, 'maybe_serve_llms_txt' ], 1 );
45 add_action( 'elementor/kit/register_tabs', [ $this, 'register_kit_tabs' ] );
46 add_action( 'elementor/document/after_save', [ $this, 'maybe_invalidate_llms_txt_cache' ] );
47
48 if ( Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME ) ) {
49 add_filter( 'elementor/editor/v2/packages', fn ( $packages ) => $this->add_packages( $packages ) );
50 }
51 }
52
53 /**
54 * @param \Elementor\Core\Kits\Documents\Kit $kit
55 */
56 public function register_kit_tabs( $kit ) {
57 if ( ! Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME ) ) {
58 return;
59 }
60
61 $kit->register_tab( 'settings-agents', Settings_Agents::class );
62 }
63
64 public function maybe_serve_llms_txt() {
65 if ( ! $this->is_llms_txt_request() ) {
66 return;
67 }
68
69 $llms = $this->get_llms_txt_content();
70
71 if ( '' === $llms ) {
72 return;
73 }
74
75 $etag = $this->get_etag( $llms );
76 $last_modified = $this->get_last_modified();
77
78 header( 'X-Content-Type-Options: nosniff' );
79 header( 'Cache-Control: public, max-age=' . $this->get_cache_max_age() );
80 header( 'ETag: ' . $etag );
81
82 if ( $last_modified ) {
83 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified ) . ' GMT' );
84 }
85
86 if ( $this->is_client_cache_fresh( $etag, $last_modified ) ) {
87 status_header( 304 );
88 exit;
89 }
90
91 status_header( 200 );
92 header( 'Content-Type: text/plain; charset=utf-8' );
93 Utils::print_unescaped_internal_string( $llms );
94 exit;
95 }
96
97 /**
98 * The served content is derived from kit settings, so a kit save is the only way it can change.
99 *
100 * @param mixed $document
101 */
102 public function maybe_invalidate_llms_txt_cache( $document ) {
103 if ( ! $document instanceof Kit ) {
104 return;
105 }
106
107 /**
108 * Fires when the /llms.txt response may have changed, so external page caches and CDNs
109 * can purge their copy instead of serving it until `max-age` expires.
110 *
111 * @param int $kit_id The saved kit ID.
112 */
113 do_action( 'elementor/agents/llms_txt/cache_invalidated', $document->get_main_id() );
114 }
115
116 public function get_llms_txt_content(): string {
117 $kit = Plugin::$instance->kits_manager->get_active_kit();
118 $agents = $kit->get_settings( 'agents' );
119
120 if ( ! is_array( $agents ) || ! isset( $agents['llms'] ) ) {
121 return '';
122 }
123
124 $llms = $agents['llms'];
125
126 return is_string( $llms ) ? $llms : '';
127 }
128
129 private function add_packages( $packages ) {
130 return array_merge( $packages, self::PACKAGES );
131 }
132
133 private function get_cache_max_age(): int {
134 /**
135 * Filters how long (in seconds) clients and shared caches may reuse /llms.txt without revalidating.
136 *
137 * @param int $max_age Cache lifetime in seconds.
138 */
139 $max_age = (int) apply_filters( 'elementor/agents/llms_txt/cache_max_age', self::DEFAULT_CACHE_MAX_AGE );
140
141 return max( 0, $max_age );
142 }
143
144 private function get_etag( string $content ): string {
145 return '"' . md5( $content ) . '"';
146 }
147
148 private function get_last_modified(): int {
149 $modified = get_post_modified_time( 'U', true, Plugin::$instance->kits_manager->get_active_id() );
150
151 return is_numeric( $modified ) ? (int) $modified : 0;
152 }
153
154 private function is_client_cache_fresh( string $etag, int $last_modified ): bool {
155 $if_none_match = isset( $_SERVER['HTTP_IF_NONE_MATCH'] )
156 ? trim( sanitize_text_field( wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) ) )
157 : '';
158
159 // An ETag mismatch must win over If-Modified-Since, which only has one-second resolution.
160 if ( '' !== $if_none_match ) {
161 return $this->etag_matches( $if_none_match, $etag );
162 }
163
164 $if_modified_since = isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] )
165 ? trim( sanitize_text_field( wp_unslash( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) )
166 : '';
167
168 if ( ! $last_modified || '' === $if_modified_since ) {
169 return false;
170 }
171
172 $since = strtotime( $if_modified_since );
173
174 return false !== $since && $since >= $last_modified;
175 }
176
177 private function etag_matches( string $header, string $etag ): bool {
178 foreach ( explode( ',', $header ) as $candidate ) {
179 $candidate = trim( $candidate );
180
181 if ( '*' === $candidate ) {
182 return true;
183 }
184
185 if ( 0 === strpos( $candidate, 'W/' ) ) {
186 $candidate = substr( $candidate, 2 );
187 }
188
189 if ( $candidate === $etag ) {
190 return true;
191 }
192 }
193
194 return false;
195 }
196
197 private function is_llms_txt_request(): bool {
198 $request_uri = isset( $_SERVER['REQUEST_URI'] )
199 ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
200 : '';
201
202 $path = wp_parse_url( $request_uri, PHP_URL_PATH );
203
204 if ( ! is_string( $path ) ) {
205 return false;
206 }
207
208 $home_path = wp_parse_url( home_url(), PHP_URL_PATH );
209
210 if ( is_string( $home_path ) && '' !== $home_path && '/' !== $home_path ) {
211 $home_path = untrailingslashit( $home_path );
212
213 if ( 0 === strpos( $path, $home_path ) ) {
214 $path = substr( $path, strlen( $home_path ) );
215 }
216 }
217
218 $path = untrailingslashit( $path );
219
220 return '/llms.txt' === $path || 'llms.txt' === ltrim( $path, '/' );
221 }
222 }
223