15 * MINUTE_IN_SECONDS, 'display' => 'Every 15 minutes (Citami)', ); } return $schedules; }); /** Schedules the pull lazily so installs updated in place get it too. */ function citami_sync_ensure_cron() { if (!wp_next_scheduled(CITAMI_SYNC_HOOK)) { wp_schedule_event(time() + 5 * MINUTE_IN_SECONDS, 'citami_15min', CITAMI_SYNC_HOOK); } } add_action('init', 'citami_sync_ensure_cron'); add_action(CITAMI_SYNC_HOOK, 'citami_sync_pull_handler'); // 2.66 — the excluded pages travel with every pull. Absent key = the // platform could not read the settings: keep the last list. add_action('citami_sync_response', function ($data) { if (!is_array($data) || !array_key_exists('exclude_pages', $data) || !is_array($data['exclude_pages'])) { return; } $clean = array(); foreach (array_slice($data['exclude_pages'], 0, 50) as $p) { $p = trim(sanitize_text_field((string) $p)); if ($p !== '' && strlen($p) <= 200) $clean[] = $p; } update_option(CITAMI_EXCLUDE_PATHS_OPT, array_values(array_unique($clean)), false); }); /** Is this platform fix already in the local queue? (dedupe across runs) */ function citami_sync_seen_fix($fix_id) { $queue = get_option(CITAMI_FIX_QUEUE_OPT, array()); if (!is_array($queue)) { return false; } foreach ($queue as $entry) { if (is_array($entry) && isset($entry['fix_id']) && $entry['fix_id'] === $fix_id) { return true; } } return false; } /** Tell the platform how the apply went (best-effort, never throws). */ /** The sheet edited on the site (wizard or Brand tab), or null when the platform manages it or it is empty. */ function citami_sync_local_sheet() { if (!function_exists('citami_truth_sheet')) { return null; } $s = citami_truth_sheet(); if (!is_array($s) || $s['source'] !== 'local' || $s['name'] === '') { return null; } unset($s['source'], $s['received_at']); return $s; } function citami_sync_report($fix_id, $success, $message = '', $outcome = '') { $key = citami_get_api_key(); if (empty($key) || $fix_id === '') { return; } $payload = array('fix_id' => $fix_id, 'success' => (bool) $success); // 2.65 — what happened, so the platform sets the right status (and // stops auto-pushing a fix rejected or revoked here). if (in_array($outcome, array('applied', 'failed', 'rejected', 'revoked'), true)) { $payload['outcome'] = $outcome; } if (!$success) { $payload['error_message'] = mb_substr((string) $message, 0, 500); } else { $payload['applied_at'] = gmdate('c'); } wp_remote_post(citami_get_endpoint() . '/api/plugin/fix-applied', array( 'timeout' => 10, 'headers' => array( 'Authorization' => 'Bearer ' . $key, 'Content-Type' => 'application/json', 'User-Agent' => 'CitamiConnector/' . CITAMI_PLUGIN_VERSION . '; ' . home_url('/'), ), 'body' => wp_json_encode($payload), )); } /** * One pull: fetch pending fixes, apply what we can, report. Returns a * summary array (also stored in CITAMI_SYNC_LAST_OPT for the admin page). */ function citami_sync_pull_handler() { if (function_exists('citami_cron_heartbeat')) citami_cron_heartbeat(); // 2.46 $summary = array('ts' => time(), 'ok' => false, 'fetched' => 0, 'applied' => 0, 'queued' => 0, 'failed' => 0, 'message' => ''); $key = citami_get_api_key(); if (empty($key)) { $summary['message'] = citami_t('API key mancante', 'API key missing'); update_option(CITAMI_SYNC_LAST_OPT, $summary, false); return $summary; } $response = wp_remote_post(citami_get_endpoint() . '/api/plugin/sync', array( 'timeout' => 15, 'headers' => array( 'Authorization' => 'Bearer ' . $key, 'Content-Type' => 'application/json', 'User-Agent' => 'CitamiConnector/' . CITAMI_PLUGIN_VERSION . '; ' . home_url('/'), ), 'body' => wp_json_encode(array( 'plugin_version' => CITAMI_PLUGIN_VERSION, 'wp_version' => get_bloginfo('version'), 'seo_plugins_detected' => array_values(array_filter(array(citami_active_seo_plugin()))), // WP15 — modules add what the dashboard cannot see (content health). 'site_stats' => apply_filters('citami_sync_site_stats', array()), // WP25 — a sheet filled in WordPress seeds the platform's when it has none. 'truth_sheet' => citami_sync_local_sheet(), )), )); if (is_wp_error($response)) { $summary['message'] = $response->get_error_message(); update_option(CITAMI_SYNC_LAST_OPT, $summary, false); return $summary; } $code = (int) wp_remote_retrieve_response_code($response); if ($code < 200 || $code >= 300) { $summary['message'] = 'HTTP ' . $code; update_option(CITAMI_SYNC_LAST_OPT, $summary, false); return $summary; } $data = json_decode(wp_remote_retrieve_body($response), true); $pending = (is_array($data) && isset($data['pending_fixes']) && is_array($data['pending_fixes'])) ? $data['pending_fixes'] : array(); // WP17 — modules read the rest of the response (truth sheet from the dashboard). do_action('citami_sync_response', is_array($data) ? $data : array()); $summary['ok'] = true; $summary['fetched'] = count($pending); // 2.65 — the platform decides. A pulled fix goes live only when the // platform released it for auto-apply (auto_apply: true: customer // opted in, type allowed, review window over, schema complete); // everything else is queued here for approval, whatever the local // review-only setting. Before, the install-time auto-apply constant // or review-only off applied every pulled fix, skipping the // platform's checks (and on PHP 8 the old call crashed the pull). $applicable = citami_sync_applicable_types(); foreach ($pending as $fix) { if (!is_array($fix) || empty($fix['id']) || empty($fix['fix_type'])) { continue; } $fix_id = (string) $fix['id']; if (!in_array((string) $fix['fix_type'], $applicable, true)) { continue; } if (citami_sync_seen_fix($fix_id)) { continue; } // 2.65 — not cleared by the platform (incomplete schema…): never here. if (array_key_exists('ready_to_apply', $fix) && empty($fix['ready_to_apply'])) { continue; } $force = !empty($fix['auto_apply']); $body = array( 'fix_id' => $fix_id, 'fix_type' => (string) $fix['fix_type'], 'title' => isset($fix['title']) ? (string) $fix['title'] : ucfirst(str_replace('_', ' ', (string) $fix['fix_type'])), 'description' => isset($fix['description']) ? (string) $fix['description'] : '', 'code_snippet' => isset($fix['code_snippet']) ? (string) $fix['code_snippet'] : '', ); $result = citami_apply_fix_payload($body, $force, !$force); if (is_wp_error($result)) { $summary['failed']++; citami_sync_report($fix_id, false, $result->get_error_message(), 'failed'); continue; } $out = $result->get_data(); if (!empty($out['review_only'])) { $summary['queued']++; continue; // the platform keeps it pending; the admin decides } if (!empty($out['success'])) { $summary['applied']++; citami_sync_report($fix_id, true, '', 'applied'); } else { $summary['failed']++; citami_sync_report($fix_id, false, isset($out['message']) ? (string) $out['message'] : 'apply failed', 'failed'); } } update_option(CITAMI_SYNC_LAST_OPT, $summary, false); return $summary; }