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
← All changes | custom-pages/class-metasync-custom-pages-api.php +68 -20 2.7.0 → 2.7.2 View file →
@@ -129,9 +129,9 @@
129 129 * Uses the same authentication as otto_crawl_notify endpoint
130 130 */
131 131 public function validate_api_key($request)
132 132 {
133 - // Get API key from Authorization: Bearer header (preferred), x-api-key header, or apikey query parameter
133 + // Get API key from Authorization: Bearer header, x-api-key header, or apikey query parameter
134 134 $api_key = '';
135 135
136 136 $auth_header = $request->get_header('authorization');
137 137 if (!empty($auth_header) && preg_match('/^Bearer\s+(.+)$/i', $auth_header, $matches)) {
@@ -616,9 +616,9 @@
616 616 'post_status' => array('any', 'trash'),
617 617 'numberposts' => 1,
618 618 'fields' => 'ids',
619 619 'no_found_rows' => true,
620 - 'suppress_filters' => true,
620 + 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- identity lookup for the duplicate-assets guard; query-filter plugins (WPML/Polylang) must not narrow it
621 621 'exclude' => array($page_id),
622 622 'meta_key' => Metasync_Custom_Pages::META_ASSETS_FOLDER,
623 623 'meta_value' => $assets_folder,
624 624 ));
@@ -851,10 +851,10 @@
851 851 }
852 852
853 853 $zip_path = $tmp_file;
854 854
855 - } elseif (!empty($_FILES['zip_file']) && is_array($_FILES['zip_file'])) {
856 - $file = $_FILES['zip_file'];
855 + } elseif (!empty($_FILES['zip_file']) && is_array($_FILES['zip_file'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST route authenticated by API key, not cookies
856 + $file = $_FILES['zip_file']; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST route authenticated by API key, not cookies
857 857 $_lps_input['source_type'] = 'upload';
858 858
859 859 if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
860 860 $_lps_http_st = 400;
@@ -929,9 +929,9 @@
929 929 return $response;
930 930
931 931 } finally {
932 932 if (!empty($tmp_file) && file_exists($tmp_file)) {
933 - @unlink($tmp_file);
933 + @wp_delete_file($tmp_file);
934 934 }
935 935 }
936 936 } finally {
937 937 self::write_lps_import_audit(array(
@@ -941,12 +941,60 @@
941 941 'start_ms' => $_lps_start_ms,
942 942 'auth_method' => $_lps_auth_method,
943 943 'api_key_prefix' => $_lps_api_key_prefix,
944 944 ));
945 + self::track_lps_import($_lps_result, $_lps_http_st, $_lps_input);
945 946 }
946 947 }
947 948
948 949 /**
950 + * Consent-gated analytics for Website Studio imports.
951 + *
952 + * Successes are rolled up hourly (a multi-page publish is one burst of
953 + * work, not N events). Failures and partial imports send one event per
954 + * error code per hour so a retry loop on the platform cannot flood it.
955 + * Hooked here, not in extract_and_create_lps_page(), because the MCP tool
956 + * also calls that and is already counted by the MCP rollup.
957 + *
958 + * @param WP_Error|array|null $result Final import result.
959 + * @param int $http_status HTTP status returned.
960 + * @param array $input Audit input (pages_in_manifest).
961 + */
962 + private static function track_lps_import($result, $http_status, array $input) {
963 + $data = (is_array($result) && isset($result['data']) && is_array($result['data'])) ? $result['data'] : array();
964 + $mode = (isset($data['mode']) && in_array($data['mode'], array('single', 'multi'), true)) ? $data['mode'] : 'unknown';
965 + $pages = isset($input['pages_in_manifest']) ? (int) $input['pages_in_manifest'] : 0;
966 +
967 + if ((int) $http_status === 200) {
968 + Metasync_PostHog::rollup('lps_page_imported', array(
969 + 'imports_succeeded' => 1,
970 + 'imports_' . $mode => 1,
971 + 'pages_total' => $pages,
972 + ), 'hour');
973 + return;
974 + }
975 +
976 + if (is_wp_error($result)) {
977 + $error_code = (string) $result->get_error_code();
978 + } elseif ((int) $http_status === 207) {
979 + $error_code = 'partial_failure';
980 + } elseif (is_array($result)) {
981 + $error_code = 'all_pages_failed';
982 + } else {
983 + // The import threw before producing a result.
984 + $error_code = 'unexpected_error';
985 + }
986 +
987 + Metasync_PostHog::feature_once('lps_page_imported', array(
988 + 'outcome' => (int) $http_status === 207 ? 'partial' : 'failed',
989 + 'mode' => $mode,
990 + 'pages_total' => $pages,
991 + 'error_code' => substr(sanitize_key($error_code), 0, 60),
992 + 'http_status' => (int) $http_status,
993 + ), $error_code, HOUR_IN_SECONDS);
994 + }
995 +
996 + /**
949 997 * Shared helper: extract a ZIP archive and create or update a custom HTML page.
950 998 *
951 999 * Made public so the MCP tool (in a different class) can invoke it directly.
952 1000 *
@@ -1290,9 +1338,9 @@
1290 1338
1291 1339 } finally {
1292 1340 if (is_resource($lps_import_lock)) {
1293 1341 @flock($lps_import_lock, LOCK_UN);
1294 - @fclose($lps_import_lock);
1342 + @fclose($lps_import_lock); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- operates on a native stream handle
1295 1343 }
1296 1344 }
1297 1345 }
1298 1346
@@ -1316,14 +1364,14 @@
1316 1364 wp_mkdir_p($lock_dir);
1317 1365 }
1318 1366 $lock_path = trailingslashit($lock_dir) . md5($assets_folder) . '.lock';
1319 1367
1320 - $handle = @fopen($lock_path, 'c');
1368 + $handle = @fopen($lock_path, 'c'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming file I/O; WP_Filesystem cannot return a raw stream handle
1321 1369 if ($handle === false) {
1322 1370 return new WP_Error('lock_unavailable', 'Could not open the import lock file.', array('status' => 500));
1323 1371 }
1324 1372 if (!flock($handle, LOCK_EX | LOCK_NB)) {
1325 - fclose($handle);
1373 + fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- operates on a native stream handle
1326 1374 return new WP_Error(
1327 1375 'import_in_progress',
1328 1376 'Another import for this assets_folder is already in progress. Retry shortly.',
1329 1377 array('status' => 423)
@@ -1344,9 +1392,9 @@
1344 1392 {
1345 1393 if (file_exists($target_dir)) {
1346 1394 $this->recursive_rmdir($target_dir);
1347 1395 }
1348 - if (!@rename($staging_dir, $target_dir)) {
1396 + if (!@rename($staging_dir, $target_dir)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- atomic same-filesystem replace; WP_Filesystem::move() needs credential bootstrapping on non-direct hosts
1349 1397 $this->recursive_rmdir($staging_dir);
1350 1398 return new WP_Error(
1351 1399 'assets_swap_failed',
1352 1400 'Failed to move the extracted assets into place.',
@@ -1425,9 +1473,9 @@
1425 1473 if (class_exists('Metasync_Edge_Cache_Purge')) {
1426 1474 Metasync_Edge_Cache_Purge::purge($urls);
1427 1475 }
1428 1476 } catch (\Throwable $e) {
1429 - error_log('MetaSync: LPS import cache purge failed - ' . $e->getMessage());
1477 + error_log('MetaSync: LPS import cache purge failed - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
1430 1478 }
1431 1479 }
1432 1480
1433 1481 /**
@@ -1455,9 +1503,9 @@
1455 1503 'post_status' => array('publish', 'future', 'draft', 'pending', 'private'),
1456 1504 'numberposts' => 1,
1457 1505 'fields' => 'ids',
1458 1506 'no_found_rows' => true,
1459 - 'suppress_filters' => true,
1507 + 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- LPS home-page identity lookup; language/query filters must not hide the tagged page
1460 1508 'meta_query' => array(
1461 1509 'relation' => 'AND',
1462 1510 array('key' => Metasync_Custom_Pages::META_LPS_PROJECT_REF, 'value' => $external_ref),
1463 1511 array('key' => Metasync_Custom_Pages::META_LPS_HOME, 'compare' => 'EXISTS'),
@@ -1473,9 +1521,9 @@
1473 1521 'post_status' => array('publish', 'future', 'draft', 'pending', 'private'),
1474 1522 'numberposts' => 1,
1475 1523 'fields' => 'ids',
1476 1524 'no_found_rows' => true,
1477 - 'suppress_filters' => true,
1525 + 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- LPS home-page identity lookup; language/query filters must not hide the tagged page
1478 1526 'meta_key' => Metasync_Custom_Pages::META_LPS_HOME,
1479 1527 'meta_value' => $assets_folder,
1480 1528 ));
1481 1529 return !empty($ids) ? (int) $ids[0] : 0;
@@ -1908,9 +1956,9 @@
1908 1956 'post_type' => 'page',
1909 1957 'post_status' => array('publish', 'future', 'draft', 'pending', 'private'),
1910 1958 'posts_per_page' => 1,
1911 1959 'fields' => 'ids',
1912 - 'suppress_filters' => true,
1960 + 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- identity lookup mirroring find_lps_home_page(); query hooks must not alter the match
1913 1961 'no_found_rows' => true,
1914 1962 'meta_query' => array(
1915 1963 'relation' => 'AND',
1916 1964 $project_clause,
@@ -1964,9 +2012,9 @@
1964 2012 return true;
1965 2013 }
1966 2014
1967 2015 if (!is_dir($dir)) {
1968 - return @unlink($dir);
2016 + return @unlink($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- boolean return value is consumed
1969 2017 }
1970 2018
1971 2019 $iterator = new RecursiveIteratorIterator(
1972 2020 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
@@ -1974,15 +2022,15 @@
1974 2022 );
1975 2023
1976 2024 foreach ($iterator as $item) {
1977 2025 if ($item->isDir()) {
1978 - @rmdir($item->getPathname());
2026 + @rmdir($item->getPathname()); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- WP_Filesystem would prompt for FTP credentials on non-direct hosts
1979 2027 } else {
1980 - @unlink($item->getPathname());
2028 + @wp_delete_file($item->getPathname());
1981 2029 }
1982 2030 }
1983 2031
1984 - return @rmdir($dir);
2032 + return @rmdir($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- WP_Filesystem would prompt for FTP credentials on non-direct hosts
1985 2033 }
1986 2034
1987 2035 /**
1988 2036 * Write exactly one persistent audit record for an LPS ZIP import.
@@ -2100,9 +2148,9 @@
2100 2148
2101 2149 $site_url = function_exists('get_site_url') ? get_site_url() : '';
2102 2150
2103 2151 $meta_payload = array(
2104 - 'timestamp' => function_exists('current_time') ? current_time('mysql') : date('Y-m-d H:i:s'),
2152 + 'timestamp' => function_exists('current_time') ? current_time('mysql') : gmdate('Y-m-d H:i:s'),
2105 2153 'site_url' => $site_url,
2106 2154 'api_key_prefix' => $api_key_prefix,
2107 2155 'plugin_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '',
2108 2156 'auth_method' => $auth_method,
@@ -2142,9 +2190,9 @@
2142 2190
2143 2191 // Optional deep-debug breadcrumb — the DB write above always happens
2144 2192 // regardless of WP_DEBUG; this line is purely supplementary.
2145 2193 if (defined('WP_DEBUG') && WP_DEBUG) {
2146 - error_log(sprintf(
2194 + error_log(sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
2147 2195 'Metasync LPS import audit: status=%s http=%d created=%d updated=%d failed=%d',
2148 2196 $overall_status,
2149 2197 $http_status,
2150 2198 $counts['created'],
@@ -2153,9 +2201,9 @@
2153 2201 ));
2154 2202 }
2155 2203 } catch (\Throwable $e) {
2156 2204 if (defined('WP_DEBUG') && WP_DEBUG) {
2157 - error_log('Metasync LPS audit log failed: ' . $e->getMessage());
2205 + error_log('Metasync LPS audit log failed: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
2158 2206 }
2159 2207 }
2160 2208 }
2161 2209 }