PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / public / class-metasync-public.php

class-metasync-public.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at public/class-metasync-public.php

432 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7
8 /**
9 * The public-facing functionality of the plugin.
10 *
11 * Thin coordinator that instantiates Metasync_Rest_Api and Metasync_Seo_Output
12 * and retains only asset enqueue methods, plugin links, and shortcode init.
13 *
14 * @link https://searchatlas.com
15 * @since 1.0.0
16 *
17 * @package Metasync
18 * @subpackage Metasync/public
19 */
20
21 /**
22 * The public-facing functionality of the plugin.
23 *
24 * Defines the plugin name, version, and two examples hooks for how to
25 * enqueue the public-facing stylesheet and JavaScript.
26 *
27 * @package Metasync
28 * @subpackage Metasync/public
29 * @author Engineering Team <[email protected]>
30 */
31
32 class Metasync_Public
33 {
34
35 /**
36 * The ID of this plugin.
37 *
38 * @since 1.0.0
39 * @access private
40 * @var string $plugin_name The ID of this plugin.
41 */
42 private $plugin_name;
43
44 /**
45 * The version of this plugin.
46 *
47 * @since 1.0.0
48 * @access private
49 * @var string $version The current version of this plugin.
50 */
51 private $version;
52
53 /**
54 * REST API handler instance.
55 *
56 * @since 1.0.0
57 * @access private
58 * @var Metasync_Rest_Api $rest_api
59 */
60 private $rest_api;
61
62 /**
63 * SEO Output handler instance.
64 *
65 * @since 1.0.0
66 * @access private
67 * @var Metasync_Seo_Output $seo_output
68 */
69 private $seo_output;
70
71 /**
72 * Initialize the class and set its properties.
73 *
74 * @since 1.0.0
75 * @param string $plugin_name The name of the plugin.
76 * @param string $version The version of this plugin.
77 */
78 public function __construct($plugin_name, $version)
79 {
80 $this->plugin_name = $plugin_name;
81 $this->version = $version;
82
83 $this->rest_api = new Metasync_Rest_Api($plugin_name, $version);
84 $this->seo_output = new Metasync_Seo_Output($plugin_name, $version);
85 }
86
87 /**
88 * Get the REST API handler instance.
89 *
90 * @return Metasync_Rest_Api
91 */
92 public function get_rest_api()
93 {
94 return $this->rest_api;
95 }
96
97 /**
98 * Get the SEO Output handler instance.
99 *
100 * @return Metasync_Seo_Output
101 */
102 public function get_seo_output()
103 {
104 return $this->seo_output;
105 }
106
107 /**
108 * Register the stylesheets for the public-facing side of the site.
109 *
110 * @since 1.0.0
111 */
112 public function enqueue_styles()
113 {
114
115 /**
116 * This function is provided for demonstration purposes only.
117 *
118 * An instance of this class should be passed to the run() function
119 * defined in Metasync_Loader as all of the hooks are defined
120 * in that particular class.
121 *
122 * The Metasync_Loader will then create the relationship
123 * between the defined hooks and the functions defined in this
124 * class.
125 */
126 $enabled_plugin_css = Metasync::get_option('general')['enabled_plugin_css'] ?? '';
127 if($enabled_plugin_css!=="default" && $enabled_plugin_css!=='' ){
128 wp_enqueue_style($this->plugin_name, plugin_dir_url(__FILE__) . 'css/metasync-public.css', array(), $this->version, 'all');
129 }
130 }
131
132 /**
133 * Register the JavaScript for the public-facing side of the site.
134 *
135 * @since 1.0.0
136 */
137 public function enqueue_scripts()
138 {
139
140 /**
141 * This function is provided for demonstration purposes only.
142 *
143 * An instance of this class should be passed to the run() function
144 * defined in Metasync_Loader as all of the hooks are defined
145 * in that particular class.
146 *
147 * The Metasync_Loader will then create the relationship
148 * between the defined hooks and the functions defined in this
149 * class.
150 */
151
152 # Get the Otto Pixel UUID from plugin settings.
153 $otto_uuid = Metasync::get_option('general')['otto_pixel_uuid'] ?? '';
154
155 # The tracker self-exits when window.saOttoData is absent, and saOttoData is
156 # only localized when a UUID is configured — so without a UUID the request
157 # ships a script that can never do anything. Skip it entirely instead.
158 # OTTO SSR is always enabled by default when UUID is set.
159 if (!empty($otto_uuid)) {
160
161 # Enqueue the JavaScript file 'otto-tracker.js' with defer for better performance
162 # Load in footer (true) and add defer attribute to prevent render blocking
163 # No jQuery dependency: the tracker is plain ES5/ES6 and uses no $.
164 wp_enqueue_script($this->plugin_name . '-tracker', plugin_dir_url(__FILE__) . 'js/otto-tracker.min.js', array(), $this->version, true);
165
166 # Get the current full page URL safely.
167 $page_url = esc_url(home_url(add_query_arg([], $_SERVER['REQUEST_URI'])));
168
169 # Pass PHP data to the JavaScript file using wp_localize_script.
170 wp_localize_script($this->plugin_name . '-tracker', 'saOttoData', [
171 'otto_uuid' => $otto_uuid,
172 'page_url' => $page_url,
173 'context' => null,
174 'enable_metadesc' => true, // Always enabled by default
175 ]);
176 }
177
178 # metasync-public.js drives nothing but the [accordion_metasync] shortcode,
179 # so it only needs to load on a page that actually renders that shortcode.
180 if ($this->has_accordion_shortcode()) {
181 wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/metasync-public.js', array(), $this->version, true);
182
183 # CSS and JS gate on the same predicate so a placement the scan misses degrades to visible-but-unstyled content, never hidden content.
184 wp_enqueue_style($this->plugin_name . '-accordion', plugin_dir_url(__FILE__) . 'css/metasync-accordion.css', array(), $this->version, 'all');
185 }
186 }
187
188 /**
189 * Whether the current request is likely to render the [accordion_metasync] shortcode.
190 *
191 * The shortcode is registered globally, so it can render from more than the
192 * singular post_content: the excerpt, Elementor's `_elementor_data` post meta
193 * (JSON, but has_shortcode() still matches the shortcode text inside it), and
194 * classic Text/Block widgets placed in a sidebar shown on any template.
195 *
196 * This is a static scan, not a guarantee — it can't see every builder/theme
197 * storage format. The `metasync_load_accordion_assets` filter is the escape
198 * hatch: `add_filter('metasync_load_accordion_assets', '__return_true')` to
199 * force-load the accordion assets when a placement here is missed.
200 *
201 * @since 1.0.0
202 * @return bool
203 */
204 private function has_accordion_shortcode()
205 {
206 $load = false;
207
208 if (is_singular()) {
209 $post = get_post();
210 if ($post) {
211 if (!empty($post->post_content) && has_shortcode($post->post_content, 'accordion_metasync')) {
212 $load = true;
213 } elseif (!empty($post->post_excerpt) && has_shortcode($post->post_excerpt, 'accordion_metasync')) {
214 $load = true;
215 } else {
216 $elementor_data = get_post_meta($post->ID, '_elementor_data', true);
217 if (is_string($elementor_data) && $elementor_data !== '' && has_shortcode($elementor_data, 'accordion_metasync')) {
218 $load = true;
219 }
220 }
221 }
222 }
223
224 if (!$load) {
225 foreach (array('widget_text', 'widget_block') as $widget_option) {
226 $instances = get_option($widget_option);
227 if (empty($instances) || !is_array($instances)) {
228 continue;
229 }
230
231 foreach ($instances as $instance) {
232 if (!is_array($instance)) {
233 continue;
234 }
235 foreach ($instance as $field_value) {
236 if (is_string($field_value) && has_shortcode($field_value, 'accordion_metasync')) {
237 $load = true;
238 break 3;
239 }
240 }
241 }
242 }
243 }
244
245 return apply_filters('metasync_load_accordion_assets', $load);
246 }
247
248 /**
249 * Add defer attribute to OTTO scripts to prevent render blocking
250 * This improves PageSpeed scores by allowing scripts to load asynchronously
251 *
252 * @since 1.0.0
253 * @param string $tag The script tag
254 * @param string $handle The script handle
255 * @return string Modified script tag with defer attribute
256 */
257 public function add_defer_attribute($tag, $handle) {
258 # This filter is registered globally and also runs on admin screens, where
259 # the admin bundle handle ('metasync', js/metasync-admin.js) collides with
260 # our own plugin_name handle. Deferring the admin head script would run it
261 # after metasync-dashboard.js (its footer-loaded dependent), inverting the
262 # dependency order. Bail out on admin requests before the handle check.
263 if (is_admin()) {
264 return $tag;
265 }
266
267 # Only add defer to our OTTO scripts
268 if ($handle === $this->plugin_name . '-tracker' || $handle === $this->plugin_name) {
269 # Check if a defer attribute is not already present. A plain strpos()
270 # for 'defer' false-positives on any script URL that merely contains
271 # the substring (e.g. "...?deferred=1"), so match the attribute itself.
272 if (!preg_match('/\sdefer(\s|=|>)/i', $tag)) {
273 # Add defer attribute before the closing >
274 $tag = str_replace(' src', ' defer src', $tag);
275 }
276 }
277 return $tag;
278 }
279
280 public function metasync_plugin_init()
281 {
282 $this->rest_api->rest_authorization_middleware();
283 $this->shortcodes_init();
284 }
285
286 public function shortcodes_init()
287 {
288 # add_shortcode('accordion', 'metasync_accordion');
289
290 # name changed from accordion to accordion_metasync to avoid conflict with other plugins
291 add_shortcode('accordion_metasync', 'metasync_accordion');
292 // add_shortcode('markdown', 'metasync_markdown');
293
294
295 function metasync_accordion($atts, $content = "")
296 {
297 // SECURITY FIX: Properly escape shortcode attributes
298 $title = isset($atts['title']) ? esc_html($atts['title']) : '';
299 $safe_content = wp_kses_post($content);
300
301 $block = "<div class=\"metasync-accordion-block\">
302 <button class=\"metasync-accordion\">{$title}</button>
303 <div class=\"metasync-panel\">{$safe_content}</div>
304 </div>";
305 return $block;
306 }
307 }
308
309 public function metasync_plugin_links($links)
310 {
311 # Removed Sync link as requested
312
313 # Solving Issue 103
314 # get the neneral options into a var
315 $general_options = Metasync::get_option('general');
316
317 # set the menu slug to the default
318 $menu_slug = 'searchatlas';
319
320 #now check if the menu slug is in the general opts
321 if (is_array($general_options) && !empty($general_options['white_label_plugin_menu_slug'])) {
322
323 #set the slug to the modified
324 $menu_slug = $general_options['white_label_plugin_menu_slug'];
325 }
326
327 $links[] = '<a href="' . get_admin_url(null, 'admin.php?page=' .$menu_slug) . '">' . esc_html__('Settings', 'metasync') . '</a>';
328 return $links;
329 }
330
331 /**
332 * Enqueue custom CSS stored in post meta for converted pages.
333 * Runs at priority 999 (after theme CSS) so custom styles win.
334 */
335 public function enqueue_page_custom_css() {
336 if ( ! is_singular() ) {
337 return;
338 }
339
340 $post_id = get_the_ID();
341 if ( ! $post_id ) {
342 return;
343 }
344
345 $custom_css = get_post_meta( $post_id, '_metasync_custom_css', true );
346 if ( empty( $custom_css ) ) {
347 return;
348 }
349
350 $scoped_css = $this->scope_css_to_body_class( $custom_css, $post_id );
351
352 wp_register_style( 'metasync-page-css-' . $post_id, false );
353 wp_enqueue_style( 'metasync-page-css-' . $post_id );
354 wp_add_inline_style( 'metasync-page-css-' . $post_id, $scoped_css );
355 }
356
357 /**
358 * Enqueue custom CSS into Elementor editor preview iframe.
359 */
360 public function enqueue_elementor_editor_css() {
361 $post_id = get_the_ID();
362 if ( ! $post_id ) {
363 return;
364 }
365
366 $custom_css = get_post_meta( $post_id, '_metasync_custom_css', true );
367 if ( empty( $custom_css ) ) {
368 return;
369 }
370
371 $scoped_css = $this->scope_css_to_body_class( $custom_css, $post_id );
372
373 wp_register_style( 'metasync-elementor-css-' . $post_id, false );
374 wp_enqueue_style( 'metasync-elementor-css-' . $post_id );
375 wp_add_inline_style( 'metasync-elementor-css-' . $post_id, $scoped_css );
376 }
377
378 /**
379 * Enqueue custom CSS when Divi Visual Builder is active.
380 */
381 public function enqueue_divi_builder_css() {
382 if ( empty( $_GET['et_fb'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only builder flag for CSS enqueue
383 return;
384 }
385
386 $post_id = get_the_ID();
387 if ( ! $post_id ) {
388 return;
389 }
390
391 $custom_css = get_post_meta( $post_id, '_metasync_custom_css', true );
392 if ( empty( $custom_css ) ) {
393 return;
394 }
395
396 $scoped_css = $this->scope_css_to_body_class( $custom_css, $post_id );
397
398 wp_register_style( 'metasync-divi-css-' . $post_id, false );
399 wp_enqueue_style( 'metasync-divi-css-' . $post_id );
400 wp_add_inline_style( 'metasync-divi-css-' . $post_id, $scoped_css );
401 }
402
403 /**
404 * Scope all CSS selectors to body.postid-{ID} for higher specificity.
405 *
406 * @param string $css Raw CSS.
407 * @param int $post_id Post ID.
408 * @return string Scoped CSS.
409 */
410 private function scope_css_to_body_class( $css, $post_id ) {
411 $prefix = 'body.postid-' . intval( $post_id );
412 $scoped = '';
413 $pattern = '/([^{]+)\{([^}]*)\}/s';
414
415 preg_match_all( $pattern, $css, $matches, PREG_SET_ORDER );
416
417 foreach ( $matches as $match ) {
418 $selectors = array_map( 'trim', explode( ',', $match[1] ) );
419 $scoped_selectors = array_map( function( $sel ) use ( $prefix ) {
420 if ( empty( $sel ) || strpos( $sel, '@' ) === 0 ) {
421 return $sel;
422 }
423 return $prefix . ' ' . $sel;
424 }, $selectors );
425 $scoped .= implode( ', ', $scoped_selectors ) . ' {' . $match[2] . "}\n";
426 }
427
428 return $scoped ?: $css;
429 }
430
431 }
432