PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
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
← All changes | public/class-metasync-rest-api.php +74 -120 2.7.0 → 2.7.1 View file →
@@ -75,16 +75,10 @@
75 75 );
76 76 $this->common = new Metasync_Common();
77 77 // get all options
78 78 $this->metasync_option_data = Metasync::get_option('general');
79 - $this->init_ajax_hooks();
80 79 }
81 80
82 - public function init_ajax_hooks()
83 - {
84 - add_action('wp_ajax_metasync_otto_ajax_action', array($this,'metasyn_otto_ajax'));
85 - }
86 -
87 81 /**
88 82 * Encode non-ASCII characters in a UTF-8 string as numeric HTML entities.
89 83 *
90 84 * Uses mb_encode_numericentity when the mbstring extension is loaded;
@@ -170,31 +164,9 @@
170 164 }
171 165 return $posts;
172 166 }
173 167
174 - public function metasyn_otto_ajax() {
175 - // Check nonce for security
176 - if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'otto_nonce')) {
177 - wp_send_json_error('Invalid nonce');
178 - wp_die();
179 - }
180 - $post_id = sanitize_text_field($_POST['post_id']);
181 - $current_url = get_permalink($post_id);
182 -
183 - $header_html = get_post_meta($post_id, '_otto_header_html_json', true);
184 168
185 - // Example API call using wp_remote_get()
186 -
187 -
188 - if (is_wp_error($header_html)) {
189 - wp_send_json_error('API call failed');
190 - } else {
191 - wp_send_json_success(json_decode($header_html, true));
192 - }
193 -
194 - wp_die(); // Always terminate after an AJAX call
195 - }
196 -
197 169 public function rest_authorization_middleware($request = null)
198 170 {
199 171 $api_key = '';
200 172
@@ -211,10 +183,10 @@
211 183 $api_key = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_API_KEY']));
212 184 }
213 185
214 186 // Fallback: ?apikey= query param (deprecated)
215 - if (empty($api_key) && isset($_GET['apikey'])) {
216 - $api_key = sanitize_text_field($_GET['apikey']);
187 + if (empty($api_key) && isset($_GET['apikey'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- API key credential in REST auth, not cookie auth
188 + $api_key = sanitize_text_field(wp_unslash($_GET['apikey'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- API key credential in REST auth, not cookie auth
217 189 }
218 190
219 191 if (empty($api_key)) {
220 192 return false;
@@ -368,10 +340,10 @@
368 340 return '/' . ltrim((string) $GLOBALS['wp']->query_vars['rest_route'], '/');
369 341 }
370 342
371 343 // ?rest_route= form, used when pretty permalinks are disabled.
372 - if (!empty($_GET['rest_route'])) {
373 - return '/' . ltrim(sanitize_text_field(wp_unslash($_GET['rest_route'])), '/');
344 + if (!empty($_GET['rest_route'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only route check for REST auth
345 + return '/' . ltrim(sanitize_text_field(wp_unslash($_GET['rest_route'])), '/'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only route check for REST auth
374 346 }
375 347
376 348 // Last resort: the request path with the /wp-json/ prefix stripped.
377 349 $uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
@@ -378,9 +350,9 @@
378 350 if ($uri === '') {
379 351 return '';
380 352 }
381 353
382 - $path = (string) parse_url($uri, PHP_URL_PATH);
354 + $path = (string) wp_parse_url($uri, PHP_URL_PATH);
383 355 $url_prefix = function_exists('rest_get_url_prefix') ? rest_get_url_prefix() : 'wp-json';
384 356 $needle = '/' . trim((string) $url_prefix, '/') . '/';
385 357
386 358 $pos = strpos($path, $needle);
@@ -421,10 +393,10 @@
421 393 $api_key = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_API_KEY']));
422 394 }
423 395
424 396 // Fallback: ?apikey= query param (deprecated).
425 - if ($api_key === '' && !empty($_GET['apikey'])) {
426 - $api_key = sanitize_text_field(wp_unslash($_GET['apikey']));
397 + if ($api_key === '' && !empty($_GET['apikey'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- API key credential in REST auth, not cookie auth
398 + $api_key = sanitize_text_field(wp_unslash($_GET['apikey'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- API key credential in REST auth, not cookie auth
427 399 }
428 400
429 401 if ($api_key === '') {
430 402 return false;
@@ -578,9 +550,9 @@
578 550 array(
579 551 array(
580 552 'methods' => 'GET',
581 553 'callback' => function () {
582 - $getPostID = url_to_postid(sanitize_url($_GET['url']));
554 + $getPostID = url_to_postid(sanitize_url(wp_unslash($_GET['url']))); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST route authenticated by API key, not cookies
583 555
584 556 if ($getPostID==0) {
585 557 $response = false;
586 558 }else{
@@ -1109,9 +1081,9 @@
1109 1081 }
1110 1082
1111 1083 public function get_errorlogs()
1112 1084 {
1113 - $get_data = metasync_sanitize_input_array($_GET);
1085 + $get_data = metasync_sanitize_input_array($_GET); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST route authenticated by API key, not cookies
1114 1086 if (!isset($get_data['limit']))
1115 1087 return false;
1116 1088 $limit = sanitize_text_field($get_data['limit']) ?? null;
1117 1089
@@ -1299,9 +1271,9 @@
1299 1271 }
1300 1272 } catch (Throwable $e) {
1301 1273 // Fallback to DOMDocument if Dom\HTMLDocument fails
1302 1274 if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
1303 - error_log('MetaSync: Dom\HTMLDocument error, falling back to DOMDocument: ' . $e->getMessage());
1275 + error_log('MetaSync: Dom\HTMLDocument error, falling back to DOMDocument: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
1304 1276 }
1305 1277 $dom = new DOMDocument();
1306 1278 libxml_use_internal_errors(true);
1307 1279 $encoded_content = $this->encode_numeric_entities($content);
@@ -1501,9 +1473,9 @@
1501 1473 $image_id = attachment_url_to_postid($node->getAttribute('src') );
1502 1474 $image_alt = get_post_meta($image_id, '_wp_attachment_image_alt', TRUE);
1503 1475 $result ='[et_pb_image src="'.$node->getAttribute('src') .'" url="'.$node->getAttribute('src'). '" _builder_version="'.ET_BUILDER_VERSION.'" _module_preset="default" hover_enabled="0" global_colors_info="{}" sticky_enabled="0"][/et_pb_image]';
1504 1476
1505 - error_log(json_encode($e));
1477 + error_log(json_encode($e)); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
1506 1478
1507 1479 }
1508 1480 }elseif($node->nodeName === 'iframe'){
1509 1481 $result= '[et_pb_code _builder_version="'.ET_BUILDER_VERSION.'" _module_preset="default" global_colors_info="{}"]'.$node->ownerDocument->saveHTML($node).'[/et_pb_code]' ;
@@ -1653,9 +1625,9 @@
1653 1625 return $content;
1654 1626 }
1655 1627
1656 1628 # Entity-decode both sides so "Mobility & Recovery" matches "Mobility & Recovery"
1657 - $h1_text = trim(html_entity_decode(strip_tags($h1_match[1]), ENT_QUOTES | ENT_HTML5));
1629 + $h1_text = trim(html_entity_decode(wp_strip_all_tags($h1_match[1]), ENT_QUOTES | ENT_HTML5));
1658 1630 $title_text = trim(html_entity_decode((string) $title, ENT_QUOTES | ENT_HTML5));
1659 1631 if ($h1_text === '' || strcasecmp($h1_text, $title_text) !== 0) {
1660 1632 return $content;
1661 1633 }
@@ -1924,13 +1896,13 @@
1924 1896 $new_post['post_name'] = sanitize_text_field($item['permalink']);
1925 1897 }
1926 1898
1927 1899 if (isset($item['post_date']) && !empty($item['post_date'])) {
1928 - $is_valid_date = date('Y-m-d', strtotime($item['post_date'])) === $item['post_date'];
1900 + $is_valid_date = gmdate('Y-m-d', strtotime($item['post_date'])) === $item['post_date'];
1929 1901 if (!$is_valid_date) {
1930 1902 return new WP_Error(
1931 1903 'rest_post_invalid_date',
1932 - esc_html__('Post date is not valid'),
1904 + esc_html__('Post date is not valid', 'metasync'),
1933 1905 array('status' => 400)
1934 1906 );
1935 1907 }
1936 1908
@@ -2142,9 +2114,9 @@
2142 2114 # Track Content Genius event in GA4
2143 2115 try {
2144 2116 Metasync_GA4::get_instance()->track_content_genius_event($post_id, strtolower($action));
2145 2117 } catch (Exception $e) {
2146 - error_log('MetaSync: Analytics tracking failed for Content Genius - ' . $e->getMessage());
2118 + error_log('MetaSync: Analytics tracking failed for Content Genius - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
2147 2119 }
2148 2120 }
2149 2121
2150 2122 # For standard synced posts, learn (once per post-type) whether the
@@ -2154,9 +2126,9 @@
2154 2126 && $new_post['post_status'] === 'publish') {
2155 2127 try {
2156 2128 $this->record_theme_title_verdict($post_id, $permalink, $new_post['post_title'], $new_post['post_type']);
2157 2129 } catch (Exception $e) {
2158 - error_log('MetaSync title verdict failed: ' . $e->getMessage());
2130 + error_log('MetaSync title verdict failed: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
2159 2131 }
2160 2132 }
2161 2133
2162 2134 $respCreatePosts[$index] = array_merge($new_post, $post_meta);
@@ -2201,9 +2173,9 @@
2201 2173 }
2202 2174
2203 2175 public function delete_item()
2204 2176 {
2205 - $get_data = metasync_sanitize_input_array($_GET);
2177 + $get_data = metasync_sanitize_input_array($_GET); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST route authenticated by API key, not cookies
2206 2178 if (!isset($get_data['ID']))
2207 2179 return false;
2208 2180
2209 2181 $post_id = sanitize_text_field($get_data['ID']) ?? null;
@@ -2209,18 +2181,18 @@
2209 2181 $post_id = sanitize_text_field($get_data['ID']) ?? null;
2210 2182 $post = get_post($post_id);
2211 2183 if ($post) {
2212 2184 wp_delete_post($post_id);
2185 + // HTTP 204 requires no body for response, so no message is set.
2213 2186 return new WP_Error(
2214 2187 'rest_post_delete_success',
2215 - esc_html__(''),
2216 - // HTTP 204 requires no body for response
2188 + '',
2217 2189 array('status' => 204)
2218 2190 );
2219 2191 }
2220 2192 return new WP_Error(
2221 2193 'rest_post_delete_fail',
2222 - esc_html__('No post found in the database with requested ID.'),
2194 + esc_html__('No post found in the database with requested ID.', 'metasync'),
2223 2195 array('status' => 400)
2224 2196 );
2225 2197 }
2226 2198
@@ -2225,9 +2197,9 @@
2225 2197 }
2226 2198
2227 2199 public function get_items($request)
2228 2200 {
2229 - $get_data = metasync_sanitize_input_array($_GET);
2201 + $get_data = metasync_sanitize_input_array($_GET); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST route authenticated by API key, not cookies
2230 2202
2231 2203 # let us check if request send post id and is valid int
2232 2204 if(isset($get_data['post_id']) AND intval($get_data['post_id']) > 0){
2233 2205
@@ -2567,9 +2539,9 @@
2567 2539 $post_data = get_post($post_id);
2568 2540 if (!$post_data) {
2569 2541 return new WP_Error(
2570 2542 'rest_post_update_fail',
2571 - esc_html__('No post found in the database with requested ID.'),
2543 + esc_html__('No post found in the database with requested ID.', 'metasync'),
2572 2544 array('status' => 400)
2573 2545 );
2574 2546 }
2575 2547
@@ -2707,40 +2679,8 @@
2707 2679 );
2708 2680 }
2709 2681
2710 2682
2711 - if (isset($post['post_date']) && !empty($post['post_date']) && false) {
2712 - $is_valid_date = date('Y-m-d', strtotime($post['post_date'])) == $post['post_date'];
2713 - if (!$is_valid_date) {
2714 - return new WP_Error(
2715 - 'rest_post_invalid_date',
2716 - esc_html__('Post date is not valid'),
2717 - array('status' => 400)
2718 - );
2719 - }
2720 -
2721 - $date_limit_str = strtotime(date('Y-m-d') . '-2 month');
2722 - $post_date_str = strtotime($post['post_date']);
2723 -
2724 - if ($date_limit_str >= $post_date_str) {
2725 - $newDate = date('Y-m-d', strtotime('-2 month'));
2726 - return new WP_Error(
2727 - 'rest_post_greater_date',
2728 - esc_html__("Post date should be greater then " . $newDate),
2729 - array('status' => 400)
2730 - );
2731 - }
2732 -
2733 - if ($post_date_str > strtotime(date('Y-m-d'))) {
2734 - return new WP_Error(
2735 - 'rest_post_greater_date',
2736 - esc_html__('Post date should be less then Today'),
2737 - array('status' => 400)
2738 - );
2739 - }
2740 - $update_params['post_date'] = sanitize_text_field($post['post_date'] . date(' h:i:s'));
2741 - }
2742 -
2743 2683 $post_cattegories = [];
2744 2684 # if ($post_data && $post_data->post_type === 'post' && is_array(@$post['post_categories'])) {
2745 2685
2746 2686 # fixed Undefined array key issue
@@ -2884,9 +2824,9 @@
2884 2824 # Track Content Genius event in GA4
2885 2825 try {
2886 2826 Metasync_GA4::get_instance()->track_content_genius_event($post_id, 'updated');
2887 2827 } catch (Exception $e) {
2888 - error_log('MetaSync: Analytics tracking failed for Content Genius - ' . $e->getMessage());
2828 + error_log('MetaSync: Analytics tracking failed for Content Genius - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
2889 2829 }
2890 2830
2891 2831 # Learn (once per post-type) whether the theme renders the title,
2892 2832 # so future syncs prepend only when the theme genuinely omits it.
@@ -2894,9 +2834,9 @@
2894 2834 && $sync_status === 'published') {
2895 2835 try {
2896 2836 $this->record_theme_title_verdict($post_id, ($permalink ?? get_permalink($post_id)), $post_title, $post_type);
2897 2837 } catch (Exception $e) {
2898 - error_log('MetaSync title verdict failed: ' . $e->getMessage());
2838 + error_log('MetaSync title verdict failed: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
2899 2839 }
2900 2840 }
2901 2841 }
2902 2842
@@ -3018,9 +2958,9 @@
3018 2958 $post_data = get_post($payload['post_id']);
3019 2959 if(!isset($post_data->post_type)){
3020 2960 return new WP_Error(
3021 2961 'rest_page_type_fail',
3022 - esc_html__('No page found in the database with requested ID.'),
2962 + esc_html__('No page found in the database with requested ID.', 'metasync'),
3023 2963 array('status' => 400)
3024 2964 );
3025 2965 }
3026 2966 if ($post_data->post_type !== 'page') {
@@ -3113,20 +3053,19 @@
3113 3053
3114 3054 global $wpdb;
3115 3055
3116 3056 // Never propagate a template this plugin wrote itself.
3117 - $owned = $this->metasync_plugin_owned_templates();
3118 - $placeholders = implode(',', array_fill(0, count($owned), '%s'));
3057 + $owned = $this->metasync_plugin_owned_templates();
3119 3058
3120 - $rows = $wpdb->get_results(
3059 + $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint over the plugin custom table — no WordPress API exists
3121 3060 $wpdb->prepare(
3122 - "SELECT pm.meta_value AS tpl, COUNT(*) AS hits
3123 - FROM {$wpdb->postmeta} pm
3124 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
3061 + 'SELECT pm.meta_value AS tpl, COUNT(*) AS hits
3062 + FROM ' . $wpdb->postmeta . ' pm
3063 + INNER JOIN ' . $wpdb->posts . " p ON p.ID = pm.post_id
3125 3064 WHERE pm.meta_key = '_wp_page_template'
3126 3065 AND pm.meta_value <> ''
3127 3066 AND pm.meta_value <> 'default'
3128 - AND pm.meta_value NOT IN ($placeholders)
3067 + AND pm.meta_value NOT IN (" . implode(',', array_fill(0, count($owned), '%s')) . ")
3129 3068 AND p.post_type = %s
3130 3069 AND p.post_status = 'publish'
3131 3070 GROUP BY pm.meta_value
3132 3071 ORDER BY hits DESC",
@@ -3141,9 +3080,9 @@
3141 3080 // not just the ones that set a template. Counting only the latter
3142 3081 // would let a handful of outliers on a special template read as a
3143 3082 // site-wide convention on a site whose posts otherwise use the
3144 3083 // theme default.
3145 - $total = (int) $wpdb->get_var(
3084 + $total = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint over the plugin custom table — no WordPress API exists
3146 3085 $wpdb->prepare(
3147 3086 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'",
3148 3087 $post_type
3149 3088 )
@@ -3320,8 +3259,17 @@
3320 3259 }
3321 3260
3322 3261 public function linkgraph_login()
3323 3262 {
3263 + # Relays caller-supplied credentials to the token API, so verify the
3264 + # nonce sent by admin/js (metasyncLGLogin) and confirm plugin access
3265 + # first; otherwise any logged-in user could use the site as a proxy.
3266 + check_ajax_referer('metasync_lglogin', 'nonce');
3267 +
3268 + if (!Metasync::current_user_has_plugin_access()) {
3269 + wp_send_json_error(['message' => 'Not authorized']);
3270 + }
3271 +
3324 3272 $post_data = metasync_sanitize_input_array($_POST);
3325 3273 $payload = array(
3326 3274 'username' => wp_unslash(sanitize_email($post_data['username'])),
3327 3275 'password' => wp_unslash(sanitize_text_field($post_data['password']))
@@ -3338,9 +3286,9 @@
3338 3286 ));
3339 3287
3340 3288 # Error handling for timeout or connection failures
3341 3289 if (is_wp_error($response)) {
3342 - error_log('MetaSync: Token API failed: ' . $response->get_error_message());
3290 + error_log('MetaSync: Token API failed: ' . $response->get_error_message()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
3343 3291 wp_send_json_error(array('message' => 'Token API request failed'));
3344 3292 wp_die();
3345 3293 }
3346 3294
@@ -3474,9 +3422,9 @@
3474 3422 $option_name = '_transient_metasync_sa_connect_active_' . $nonce_token;
3475 3423 $timeout_name = '_transient_timeout_metasync_sa_connect_active_' . $nonce_token;
3476 3424
3477 3425 // Check expiry first
3478 - $timeout = $wpdb->get_var(
3426 + $timeout = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3479 3427 $wpdb->prepare(
3480 3428 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
3481 3429 $timeout_name
3482 3430 )
@@ -3483,14 +3431,14 @@
3483 3431 );
3484 3432
3485 3433 if ($timeout && (int) $timeout < time()) {
3486 3434 // Expired — clean up orphan rows
3487 - $wpdb->delete($wpdb->options, array('option_name' => $option_name));
3488 - $wpdb->delete($wpdb->options, array('option_name' => $timeout_name));
3435 + $wpdb->delete($wpdb->options, array('option_name' => $option_name)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3436 + $wpdb->delete($wpdb->options, array('option_name' => $timeout_name)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3489 3437 return false;
3490 3438 }
3491 3439
3492 - $value = $wpdb->get_var(
3440 + $value = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3493 3441 $wpdb->prepare(
3494 3442 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
3495 3443 $option_name
3496 3444 )
@@ -3514,9 +3462,9 @@
3514 3462 $option_name = '_transient_' . $transient_key;
3515 3463 $timeout_name = '_transient_timeout_' . $transient_key;
3516 3464
3517 3465 // Check expiry first
3518 - $timeout = $wpdb->get_var(
3466 + $timeout = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3519 3467 $wpdb->prepare(
3520 3468 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
3521 3469 $timeout_name
3522 3470 )
@@ -3522,14 +3470,14 @@
3522 3470 )
3523 3471 );
3524 3472
3525 3473 if ($timeout && (int) $timeout < time()) {
3526 - $wpdb->delete($wpdb->options, array('option_name' => $option_name));
3527 - $wpdb->delete($wpdb->options, array('option_name' => $timeout_name));
3474 + $wpdb->delete($wpdb->options, array('option_name' => $option_name)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3475 + $wpdb->delete($wpdb->options, array('option_name' => $timeout_name)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3528 3476 return false;
3529 3477 }
3530 3478
3531 - $value = $wpdb->get_var(
3479 + $value = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3532 3480 $wpdb->prepare(
3533 3481 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
3534 3482 $option_name
3535 3483 )
@@ -3556,10 +3504,10 @@
3556 3504 {
3557 3505 delete_transient($transient_name);
3558 3506 if (wp_using_ext_object_cache()) {
3559 3507 global $wpdb;
3560 - $wpdb->delete($wpdb->options, array('option_name' => '_transient_' . $transient_name));
3561 - $wpdb->delete($wpdb->options, array('option_name' => '_transient_timeout_' . $transient_name));
3508 + $wpdb->delete($wpdb->options, array('option_name' => '_transient_' . $transient_name)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3509 + $wpdb->delete($wpdb->options, array('option_name' => '_transient_timeout_' . $transient_name)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3562 3510 }
3563 3511 }
3564 3512
3565 3513 /**
@@ -3576,14 +3524,14 @@
3576 3524 {
3577 3525 set_transient($transient_name, $value, $expiration);
3578 3526 if (wp_using_ext_object_cache()) {
3579 3527 global $wpdb;
3580 - $wpdb->replace($wpdb->options, array(
3528 + $wpdb->replace($wpdb->options, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3581 3529 'option_name' => '_transient_' . $transient_name,
3582 3530 'option_value' => maybe_serialize($value),
3583 3531 'autoload' => 'no',
3584 3532 ));
3585 - $wpdb->replace($wpdb->options, array(
3533 + $wpdb->replace($wpdb->options, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
3586 3534 'option_name' => '_transient_timeout_' . $transient_name,
3587 3535 'option_value' => time() + $expiration,
3588 3536 'autoload' => 'no',
3589 3537 ));
@@ -4020,9 +3968,9 @@
4020 3968 } elseif (strlen($whitelabel_domain) > 255) {
4021 3969 $validation_errors['whitelabel_domain'] = 'Whitelabel domain must not exceed 255 characters';
4022 3970 } elseif (!filter_var($whitelabel_domain, FILTER_VALIDATE_URL)) {
4023 3971 $validation_errors['whitelabel_domain'] = 'Whitelabel domain must be a valid URL';
4024 - } elseif (!in_array(parse_url($whitelabel_domain, PHP_URL_SCHEME), ['http', 'https'])) {
3972 + } elseif (!in_array(wp_parse_url($whitelabel_domain, PHP_URL_SCHEME), ['http', 'https'])) {
4025 3973 $validation_errors['whitelabel_domain'] = 'Whitelabel domain must use http or https protocol';
4026 3974 }
4027 3975 }
4028 3976
@@ -4035,9 +3983,9 @@
4035 3983 $validation_errors['whitelabel_logo'] = 'Whitelabel logo URL must not exceed 500 characters';
4036 3984 } else {
4037 3985 // If URL is invalid, we'll clear it later but not fail the POST
4038 3986 if (!filter_var($whitelabel_logo, FILTER_VALIDATE_URL) ||
4039 - !in_array(parse_url($whitelabel_logo, PHP_URL_SCHEME), ['http', 'https'])) {
3987 + !in_array(wp_parse_url($whitelabel_logo, PHP_URL_SCHEME), ['http', 'https'])) {
4040 3988 // Don't add to validation_errors - let POST succeed but clear the field
4041 3989 }
4042 3990 }
4043 3991 }
@@ -4209,9 +4157,9 @@
4209 4157 $save_result = Metasync::set_option($options);
4210 4158 Metasync::invalidate_api_key_cache();
4211 4159
4212 4160 if (!$save_result) {
4213 - error_log('MetaSync SA Connect: mark_searchatlas_nonce_used - Failed to save plugin options');
4161 + error_log('MetaSync SA Connect: mark_searchatlas_nonce_used - Failed to save plugin options'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
4214 4162 } else {
4215 4163 if ($status_code === 200) {
4216 4164 // Option 1: Set heartbeat cache and last-known to CONNECTED so dashboard shows iframe immediately.
4217 4165 // If the immediate heartbeat then fails (e.g. backoff), Option 2 prevents overwriting this to DISCONNECTED.
@@ -4230,9 +4178,9 @@
4230 4178
4231 4179 return true;
4232 4180
4233 4181 } catch (Exception $e) {
4234 - error_log('MetaSync SA Connect: mark_searchatlas_nonce_used Error - ' . $e->getMessage());
4182 + error_log('MetaSync SA Connect: mark_searchatlas_nonce_used Error - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
4235 4183 return false;
4236 4184 }
4237 4185 }
4238 4186
@@ -4244,9 +4192,9 @@
4244 4192 {
4245 4193 global $wpdb;
4246 4194
4247 4195 // Clear all JWT token transients
4248 - $deleted = $wpdb->query(
4196 + $deleted = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
4249 4197 $wpdb->prepare(
4250 4198 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
4251 4199 '_transient_metasync_jwt_token_%'
4252 4200 )
@@ -4252,9 +4200,9 @@
4252 4200 )
4253 4201 );
4254 4202
4255 4203 // Also clear timeout transients
4256 - $wpdb->query(
4204 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- REST admin endpoint — manual transient/option surgery with no API equivalent
4257 4205 $wpdb->prepare(
4258 4206 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
4259 4207 '_transient_timeout_metasync_jwt_token_%'
4260 4208 )
@@ -4277,9 +4225,9 @@
4277 4225 // Also ensure heartbeat cron is scheduled now that we have an API key
4278 4226 do_action('metasync_ensure_heartbeat_cron_scheduled');
4279 4227
4280 4228 } catch (Exception $e) {
4281 - error_log('MetaSync SA Connect: Error triggering immediate heartbeat check - ' . $e->getMessage());
4229 + error_log('MetaSync SA Connect: Error triggering immediate heartbeat check - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
4282 4230 }
4283 4231 }
4284 4232
4285 4233 public function get_item_schema()
@@ -4297,15 +4245,15 @@
4297 4245 'type' => 'object',
4298 4246 // In JSON Schema you can specify object properties in the properties attribute.
4299 4247 'properties' => array(
4300 4248 'id' => array(
4301 - 'description' => esc_html__('Unique identifier for the object.', 'my-textdomain'),
4249 + 'description' => esc_html__('Unique identifier for the object.', 'metasync'),
4302 4250 'type' => 'integer',
4303 4251 'context' => array('view', 'edit', 'embed'),
4304 4252 'readonly' => true,
4305 4253 ),
4306 4254 'content' => array(
4307 - 'description' => esc_html__('The content for the object.', 'my-textdomain'),
4255 + 'description' => esc_html__('The content for the object.', 'metasync'),
4308 4256 'type' => 'string',
4309 4257 ),
4310 4258 ),
4311 4259 );
@@ -4396,11 +4344,11 @@
4396 4344
4397 4345 # Try alternative parameter methods
4398 4346 $body_params = $request->get_body_params();
4399 4347 $key_param = $request->get_param('key');
4400 - $post_key = $_POST['key'] ?? null;
4401 - $request_key = $_REQUEST['key'] ?? null;
4402 - $get_key = $_GET['key'] ?? null;
4348 + $post_key = $_POST['key'] ?? null; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST route authenticated by API key, not cookies
4349 + $request_key = $_REQUEST['key'] ?? null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST route authenticated by API key, not cookies
4350 + $get_key = $_GET['key'] ?? null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST route authenticated by API key, not cookies
4403 4351
4404 4352 # Try to get key from multiple sources
4405 4353 $key_value = null;
4406 4354
@@ -4492,9 +4440,15 @@
4492 4440 # delete it so it stops shadowing the virtual route and let WordPress serve
4493 4441 # the key from PHP instead.
4494 4442 $serving = 'virtual';
4495 4443
4496 - if (is_writable($wp_root) && @file_put_contents($file_path, $safe_key) !== false) {
4444 + # ABSPATH is intentional and verified: the Bing site-verification key
4445 + # must be served from the site's web root exactly as Bing fetches it
4446 + # (/{key}.txt). $safe_key is basename()+sanitize_file_name()'d and the
4447 + # realpath check above confines $file_path to the root, so this cannot
4448 + # escape it. Plugin Check's ABSPATHDetected warning is a false
4449 + # positive — a verification file cannot live in the uploads directory.
4450 + if (wp_is_writable($wp_root) && @file_put_contents($file_path, $safe_key) !== false) {
4497 4451 # Cache-buster avoids a stale 404 cached by a CDN/nginx for this path.
4498 4452 $probe = wp_remote_get(
4499 4453 home_url('/' . $safe_key . '.txt?_msverify=' . time()),
4500 4454 array('timeout' => 10, 'sslverify' => false)
@@ -4506,9 +4460,9 @@
4506 4460 $serving = 'physical';
4507 4461 } else {
4508 4462 # 403 / 404 / wrong body — a present file shadows the virtual route,
4509 4463 # so remove it and let WordPress serve the key instead.
4510 - @unlink($file_path);
4464 + @wp_delete_file($file_path);
4511 4465 $serving = 'virtual';
4512 4466 }
4513 4467 }
4514 4468