/** * Plugin Name: SV Bridge BE Sender Optimized * Description: Invia dal backend al frontend post, meta, tassonomie e featured image con coda, hash payload, badge admin e mapping attachment backend/frontend, verifica reale hash contenuto FE, badge modifica confermata e ricezione modifiche FE -> BE solo se il post FE è publish. * Version: 5.7.0-state-truth-log24h */ if (!defined('ABSPATH')) exit; class SV_Bridge_BE_Sender_Optimized { protected $is_normalizing_backend_content = false; protected $is_receiving_frontend_sync = false; const QUEUE_OPTION = 'sv_bridge_be_queue'; const CRON_HOOK = 'sv_bridge_be_process_queue'; const META_LAST_HASH = '_sv_bridge_last_payload_hash'; const META_LAST_SENT_HASH = '_sv_bridge_last_sent_payload_hash'; const META_LAST_TRY = '_sv_bridge_last_try'; const META_LAST_ERROR = '_sv_bridge_last_error'; const META_LAST_RESPONSE_CODE = '_sv_bridge_last_response_code'; const META_LAST_RESPONSE_BODY = '_sv_bridge_last_response_body'; const META_LAST_DIAG = '_sv_bridge_last_diag'; const META_QUEUE_STATUS = '_sv_bridge_queue_status'; const META_QUEUE_AT = '_sv_bridge_queue_at'; const META_DELIVERED = '_sv_bridge_delivered_to_frontend'; const META_DELIVERED_AT = '_sv_bridge_delivered_to_frontend_at'; const META_FEATURED_FAIL = '_sv_bridge_featured_sync_failed'; const META_FEATURED_FAIL_MSG = '_sv_bridge_featured_sync_failed_msg'; const META_WAITING_FEATURED_SINCE = '_sv_bridge_waiting_featured_since'; const META_REMOTE_POST_ID = '_sv_bridge_remote_post_id'; const META_REMOTE_STATUS = '_sv_bridge_remote_status'; const META_REMOTE_TITLE = '_sv_bridge_remote_title'; const META_REMOTE_LAST_SYNC = '_sv_bridge_remote_last_sync'; const META_REMOTE_FEATURED_OK = '_sv_bridge_remote_featured_ok'; const META_REMOTE_LEGACY_LINKED_AT = '_sv_bridge_remote_legacy_linked_at'; const META_REMOTE_LAST_VERIFIED_AT = '_sv_bridge_remote_last_verified_at'; const META_REMOTE_CONTENT_VERIFIED = '_sv_bridge_remote_content_verified'; const META_REMOTE_CONTENT_HASH = '_sv_bridge_remote_content_hash'; const META_REMOTE_EXPECTED_CONTENT_HASH = '_sv_bridge_remote_expected_content_hash'; const META_REMOTE_TITLE_HASH = '_sv_bridge_remote_title_hash'; const META_REMOTE_EXCERPT_HASH = '_sv_bridge_remote_excerpt_hash'; const META_REMOTE_VERIFICATION_MSG = '_sv_bridge_remote_verification_msg'; const META_REMOTE_POST_TYPE = '_sv_bridge_remote_post_type'; const META_REMOTE_IS_ATTACHMENT = '_sv_bridge_remote_is_attachment'; const META_LAST_UPDATE_FROM_FE_AT = '_sv_bridge_last_update_from_fe_at'; const META_LAST_FE_SYNC_STATUS = '_sv_bridge_last_fe_sync_status'; const META_LAST_FE_SYNC_MSG = '_sv_bridge_last_fe_sync_msg'; const META_LAST_FE_POST_ID = '_sv_bridge_last_fe_post_id'; const META_LAST_FE_CHANGED_FIELDS = '_sv_bridge_last_fe_changed_fields'; const META_LAST_FE_SYNCED_META_KEYS = '_sv_bridge_last_fe_synced_meta_keys'; const META_LAST_FE_SYNCED_TAXONOMIES = '_sv_bridge_last_fe_synced_taxonomies'; const LOCK_PREFIX = 'sv_bridge_be_lock_'; const META_ARTICLE_LOG = '_sv_bridge_article_state_log'; const GLOBAL_LOG_OPTION = 'sv_bridge_be_global_log_24h'; const LOG_CLEANUP_TRANSIENT = 'sv_bridge_be_log_cleanup_lock'; const SCHEDULER_VERSION = '5.6.0'; const SCHEDULER_OPTION = 'sv_bridge_be_scheduler_version'; protected $frontend_uploads_base = 'https://www.cronachedellacampania.it/wp-content/uploads'; public function __construct() { add_filter('upload_dir', [$this, 'force_frontend_upload_urls'], 20); add_filter('wp_get_attachment_url', [$this, 'force_attachment_url_to_frontend'], 20, 2); add_filter('wp_calculate_image_srcset', [$this, 'force_srcset_to_frontend'], 20); add_action('rest_api_init', [$this, 'routes']); add_action('save_post', [$this, 'on_save_post'], 20, 3); add_action('added_post_meta', [$this, 'on_thumbnail_meta_changed'], 20, 4); add_action('updated_post_meta', [$this, 'on_thumbnail_meta_changed'], 20, 4); /* * Sync cancellazioni: * - wp_trash_post intercetta lo spostamento nel cestino dal backend. * - transition_post_status copre plugin/codici che impostano direttamente post_status=trash. * - before_delete_post resta come fallback per eliminazione definitiva. */ add_action('wp_trash_post', [$this, 'on_trash_post'], 20); add_action('transition_post_status', [$this, 'on_transition_to_trash'], 20, 3); add_action('transition_post_status', [$this, 'on_transition_from_auto_draft_to_real_status'], 25, 3); add_action('before_delete_post', [$this, 'on_delete_post'], 20); add_action(self::CRON_HOOK, [$this, 'process_queue']); add_action('init', [$this, 'ensure_cron']); add_action('init', [$this, 'cleanup_stale_delivered_queue_items'], 30); add_filter('cron_schedules', [$this, 'add_cron_interval']); add_action('admin_menu', [$this, 'add_settings_page']); add_action('admin_menu', [$this, 'add_observability_page'], 30); add_action('admin_post_sv_bridge_clear_be_log', [$this, 'handle_clear_global_log']); add_action('admin_post_sv_bridge_check_fe_post', [$this, 'handle_check_fe_post']); add_action('init', [$this, 'cleanup_global_log_24h'], 45); add_action('admin_init', [$this, 'maybe_kick_queue_on_admin'], 20); add_action('add_meta_boxes', [$this, 'add_metabox']); add_action('admin_post_sv_bridge_resend_post', [$this, 'handle_manual_resend']); // CSS compatto per la colonna Frontend in edit.php, così il badge non invade celle vicine. add_action('admin_head-edit.php', [$this, 'output_admin_list_css']); add_action('admin_head-post.php', [$this, 'output_admin_list_css']); add_action('admin_head-post-new.php', [$this, 'output_admin_list_css']); add_filter('manage_post_posts_columns', [$this, 'add_admin_column']); add_action('manage_post_posts_custom_column', [$this, 'render_admin_column'], 10, 2); add_filter('manage_page_posts_columns', [$this, 'add_admin_column']); add_action('manage_page_posts_custom_column', [$this, 'render_admin_column'], 10, 2); } protected function endpoint() { return defined('SV_BRIDGE_FE_ENDPOINT') ? SV_BRIDGE_FE_ENDPOINT : ''; } protected function status_endpoint() { $endpoint = $this->endpoint(); if (!$endpoint) return ''; return preg_replace('#/sync/?$#', '/status', $endpoint); } protected function secret() { return defined('SV_BRIDGE_SECRET') ? SV_BRIDGE_SECRET : ''; } public function routes() { register_rest_route('sv-bridge/v1', '/fe-sync', [ 'methods' => 'POST', 'callback' => [$this, 'handle_frontend_sync'], 'permission_callback' => '__return_true', ]); } protected function get_incoming_bridge_key(WP_REST_Request $request) { $headers = $request->get_headers(); $candidates = [ $headers['x-bridge-key'][0] ?? '', $headers['X-Bridge-Key'][0] ?? '', $_SERVER['HTTP_X_BRIDGE_KEY'] ?? '', $_SERVER['REDIRECT_HTTP_X_BRIDGE_KEY'] ?? '', ]; foreach ($candidates as $candidate) { $candidate = trim((string)$candidate); if ($candidate !== '') return $candidate; } return ''; } protected function auth_ok(WP_REST_Request $request) { $secret = $this->secret(); $incoming = $this->get_incoming_bridge_key($request); return ($secret !== '' && $incoming !== '' && hash_equals($secret, $incoming)); } protected function skip_frontend_to_backend_meta_key($key) { if ($this->skip_meta_key($key)) return true; /* * Non sincronizziamo ID media del frontend verso il backend: * i due siti possono avere ID attachment diversi anche se condividono uploads. */ $blocked = ['_thumbnail_id']; return in_array((string)$key, $blocked, true); } protected function sync_frontend_terms_to_backend($post_id, $terms_payload) { $post_id = (int)$post_id; $changed_taxonomies = []; if (!$post_id || !is_array($terms_payload)) return $changed_taxonomies; foreach ($terms_payload as $taxonomy => $terms) { $taxonomy = sanitize_key((string)$taxonomy); if (!$taxonomy || !taxonomy_exists($taxonomy) || !is_array($terms)) continue; $before = wp_get_object_terms($post_id, $taxonomy, ['fields' => 'ids']); if (is_wp_error($before)) $before = []; $before = array_values(array_unique(array_map('intval', (array)$before))); sort($before); $term_ids = []; foreach ($terms as $term_payload) { if (!is_array($term_payload)) continue; $slug = !empty($term_payload['slug']) ? sanitize_title((string)$term_payload['slug']) : ''; $name = !empty($term_payload['name']) ? wp_strip_all_tags((string)$term_payload['name']) : $slug; if ($name === '' && $slug === '') continue; $term = null; if ($slug) $term = get_term_by('slug', $slug, $taxonomy); if (!$term && $name) $term = get_term_by('name', $name, $taxonomy); if (!$term) { $args = []; if ($slug) $args['slug'] = $slug; if (!empty($term_payload['description'])) $args['description'] = wp_kses_post((string)$term_payload['description']); $created = wp_insert_term($name ?: $slug, $taxonomy, $args); if (is_wp_error($created)) continue; $term_ids[] = (int)$created['term_id']; } else { $term_ids[] = (int)$term->term_id; } } $term_ids = array_values(array_unique($term_ids)); $after = $term_ids; sort($after); if (maybe_serialize($before) !== maybe_serialize($after)) { wp_set_object_terms($post_id, $term_ids, $taxonomy, false); $changed_taxonomies[] = $taxonomy; } } return array_values(array_unique($changed_taxonomies)); } protected function sync_frontend_meta_to_backend($post_id, $incoming_meta) { $post_id = (int)$post_id; if (!$post_id || !is_array($incoming_meta)) return []; $synced = []; foreach ($incoming_meta as $key => $vals) { $key = (string)$key; if ($this->skip_frontend_to_backend_meta_key($key)) continue; $values = []; foreach ((array)$vals as $v) { $values[] = maybe_unserialize($v); } $current = get_post_meta($post_id, $key, false); if (maybe_serialize($current) === maybe_serialize($values)) { continue; } delete_post_meta($post_id, $key); foreach ($values as $v) { add_post_meta($post_id, $key, $v); } $synced[] = $key; } return $synced; } public function handle_frontend_sync(WP_REST_Request $request) { if (!$this->auth_ok($request)) { return new WP_REST_Response([ 'success' => false, 'message' => 'Unauthorized', ], 403); } $payload = $request->get_json_params(); if (!is_array($payload)) $payload = []; $entity = !empty($payload['entity']) ? sanitize_key((string)$payload['entity']) : ''; $action = !empty($payload['action']) ? sanitize_key((string)$payload['action']) : ''; $data = !empty($payload['data']) && is_array($payload['data']) ? $payload['data'] : []; if ($entity !== 'post' || $action !== 'frontend_upsert') { return new WP_REST_Response([ 'success' => false, 'message' => 'Azione FE -> BE non supportata', ], 400); } $incoming_post = !empty($data['post']) && is_array($data['post']) ? $data['post'] : []; $frontend_status = !empty($incoming_post['post_status']) ? sanitize_key((string)$incoming_post['post_status']) : ''; if ($frontend_status !== 'publish') { $blocked_backend_id = !empty($data['backend_post_id']) ? (int)$data['backend_post_id'] : 0; if ($blocked_backend_id) $this->add_article_log($blocked_backend_id, 'FE→BE', 'Sync bloccata: FE non publish', ['stato_fe' => $frontend_status ?: 'missing']); return new WP_REST_Response([ 'success' => false, 'message' => 'Sync FE -> BE bloccata: il post sul frontend non risulta publish.', 'frontend_post_id' => !empty($data['frontend_post_id']) ? (int)$data['frontend_post_id'] : 0, 'frontend_status' => $frontend_status ?: 'missing', ], 409); } $backend_post_id = !empty($data['backend_post_id']) ? (int)$data['backend_post_id'] : 0; if (!$backend_post_id && !empty($data['post']['ID'])) { $backend_post_id = (int)$data['post']['ID']; } $post = $backend_post_id ? get_post($backend_post_id) : null; if (!$post || !in_array($post->post_type, ['post', 'page', 'product'], true)) { return new WP_REST_Response([ 'success' => false, 'message' => 'Articolo backend non trovato o post type non valido', 'backend_post_id' => $backend_post_id, ], 404); } $changed_fields = []; $field_labels = [ 'post_title' => 'titolo', 'post_content' => 'contenuto', 'post_excerpt' => 'excerpt', 'post_name' => 'permalink/slug', 'comment_status' => 'stato commenti', 'ping_status' => 'stato ping', 'post_password' => 'password articolo', ]; foreach ($field_labels as $field => $label) { if (!array_key_exists($field, $incoming_post)) continue; $current_value = (string)get_post_field($field, $backend_post_id, 'raw'); if ((string)$incoming_post[$field] !== $current_value) { $changed_fields[] = $label; } } if (isset($incoming_post['menu_order']) && (int)$incoming_post['menu_order'] !== (int)get_post_field('menu_order', $backend_post_id, 'raw')) { $changed_fields[] = 'ordine menu'; } if (!empty($incoming_post['post_status']) && $incoming_post['post_status'] === 'publish' && get_post_status($backend_post_id) !== 'publish') { $changed_fields[] = 'stato articolo → publish'; } $update = ['ID' => $backend_post_id]; foreach (['post_title', 'post_content', 'post_excerpt', 'post_name', 'comment_status', 'ping_status', 'post_password'] as $field) { if (array_key_exists($field, $incoming_post)) { $update[$field] = wp_slash((string)$incoming_post[$field]); } } if (isset($incoming_post['menu_order'])) { $update['menu_order'] = (int)$incoming_post['menu_order']; } // FE -> BE può portare lo stato solo a publish. Stati FE diversi da publish sono bloccati prima. if (!empty($incoming_post['post_status']) && $incoming_post['post_status'] === 'publish') { $update['post_status'] = 'publish'; } $this->is_receiving_frontend_sync = true; $updated = wp_update_post($update, true); if (is_wp_error($updated)) { $this->is_receiving_frontend_sync = false; $this->add_article_log($backend_post_id, 'FE→BE', 'Errore aggiornamento BE da FE', ['errore' => $updated->get_error_message()]); return new WP_REST_Response([ 'success' => false, 'message' => $updated->get_error_message(), ], 500); } $changed_taxonomies = []; if (!empty($data['terms']) && is_array($data['terms'])) { $changed_taxonomies = $this->sync_frontend_terms_to_backend($backend_post_id, $data['terms']); } $synced_meta = []; if (!empty($data['meta']) && is_array($data['meta'])) { $synced_meta = $this->sync_frontend_meta_to_backend($backend_post_id, $data['meta']); } $this->is_receiving_frontend_sync = false; $payload_hash = !empty($data['frontend_hash']) ? sanitize_text_field((string)$data['frontend_hash']) : md5(wp_json_encode($data)); update_post_meta($backend_post_id, self::META_LAST_UPDATE_FROM_FE_AT, current_time('mysql')); update_post_meta($backend_post_id, self::META_LAST_FE_SYNC_STATUS, 'received'); update_post_meta($backend_post_id, self::META_LAST_FE_SYNC_MSG, 'Modifica ricevuta dal frontend e applicata al backend.'); if (!empty($data['frontend_post_id'])) { update_post_meta($backend_post_id, self::META_LAST_FE_POST_ID, (int)$data['frontend_post_id']); } update_post_meta($backend_post_id, self::META_LAST_FE_CHANGED_FIELDS, array_values(array_unique($changed_fields))); update_post_meta($backend_post_id, self::META_LAST_FE_SYNCED_META_KEYS, array_values(array_unique($synced_meta))); update_post_meta($backend_post_id, self::META_LAST_FE_SYNCED_TAXONOMIES, array_values(array_unique($changed_taxonomies))); $this->add_article_log($backend_post_id, 'FE→BE', 'Modifica FE applicata al BE', [ 'frontend_post_id' => !empty($data['frontend_post_id']) ? (int)$data['frontend_post_id'] : 0, 'campi' => implode(',', array_slice(array_values(array_unique($changed_fields)), 0, 8)), 'meta' => implode(',', array_slice(array_values(array_unique($synced_meta)), 0, 8)), 'tax' => implode(',', array_slice(array_values(array_unique($changed_taxonomies)), 0, 8)), ]); update_post_meta($backend_post_id, self::META_LAST_HASH, $payload_hash); update_post_meta($backend_post_id, self::META_LAST_SENT_HASH, $payload_hash); update_post_meta($backend_post_id, self::META_QUEUE_STATUS, 'synced_from_frontend'); update_post_meta($backend_post_id, self::META_LAST_DIAG, 'Modificato nel frontend e sincronizzato al backend. FE post ID: ' . (!empty($data['frontend_post_id']) ? (int)$data['frontend_post_id'] : 0) . '.'); clean_post_cache($backend_post_id); return new WP_REST_Response([ 'success' => true, 'message' => 'Modifica FE applicata al BE', 'backend_post_id' => $backend_post_id, 'frontend_post_id' => !empty($data['frontend_post_id']) ? (int)$data['frontend_post_id'] : 0, 'post_status' => get_post_status($backend_post_id), 'post_title_md5' => md5((string)get_post_field('post_title', $backend_post_id, 'raw')), 'post_content_md5' => md5((string)get_post_field('post_content', $backend_post_id, 'raw')), 'post_excerpt_md5' => md5((string)get_post_field('post_excerpt', $backend_post_id, 'raw')), 'changed_fields' => array_values(array_unique($changed_fields)), 'synced_meta_keys' => $synced_meta, 'synced_taxonomies' => $changed_taxonomies, 'synced_at' => current_time('mysql'), ], 200); } protected function be_to_fe_meta_blacklist_option_name() { return 'sv_bridge_be_to_fe_meta_blacklist'; } protected function normalize_meta_blacklist_input($raw) { if (is_array($raw)) { $parts = $raw; } else { $raw = (string)$raw; $parts = preg_split('/[\r\n,;]+/', $raw); } $out = []; foreach ((array)$parts as $key) { $key = trim((string)$key); if ($key === '') continue; // I metakey WordPress possono contenere underscore, trattini, punti e due punti. $key = preg_replace('/[^A-Za-z0-9_\-\.\:]/', '', $key); if ($key === '') continue; $out[$key] = true; } return array_keys($out); } protected function get_be_to_fe_meta_blacklist() { $list = get_option($this->be_to_fe_meta_blacklist_option_name(), []); return $this->normalize_meta_blacklist_input($list); } protected function is_be_to_fe_meta_blocked($key) { $key = (string)$key; if ($key === '') return false; return in_array($key, $this->get_be_to_fe_meta_blacklist(), true); } protected function be_to_fe_status_mode_option_name() { return 'sv_bridge_be_to_fe_status_mode'; } protected function be_to_fe_selected_statuses_option_name() { return 'sv_bridge_be_to_fe_selected_statuses'; } protected function allowed_bridge_post_statuses() { $statuses = get_post_stati([], 'names'); if (!is_array($statuses) || empty($statuses)) { $statuses = ['publish', 'future', 'draft', 'pending', 'private']; } // Stati tecnici/di sistema che non devono mai creare o aggiornare articoli sul FE. $blocked = ['auto-draft', 'inherit', 'trash']; $out = []; foreach ($statuses as $status) { $status = sanitize_key((string)$status); if ($status === '' || in_array($status, $blocked, true)) continue; $out[$status] = $status; } foreach (['publish', 'future', 'draft', 'pending', 'private'] as $default_status) { if (!isset($out[$default_status])) $out[$default_status] = $default_status; } return array_values($out); } protected function normalize_status_list_input($raw) { if (is_array($raw)) { $parts = $raw; } else { $parts = preg_split('/[\r\n,;]+/', (string)$raw); } $allowed = array_flip($this->allowed_bridge_post_statuses()); $out = []; foreach ((array)$parts as $status) { $status = sanitize_key((string)$status); if ($status === '' || !isset($allowed[$status])) continue; $out[$status] = true; } return array_keys($out); } protected function get_be_to_fe_status_mode() { $mode = get_option($this->be_to_fe_status_mode_option_name(), 'publish_only'); $mode = sanitize_key((string)$mode); if (!in_array($mode, ['all', 'publish_only', 'selected'], true)) $mode = 'all'; return $mode; } protected function get_be_to_fe_selected_statuses() { $list = get_option($this->be_to_fe_selected_statuses_option_name(), ['publish']); $list = $this->normalize_status_list_input($list); return !empty($list) ? $list : ['publish']; } protected function get_be_to_fe_syncable_statuses() { $mode = $this->get_be_to_fe_status_mode(); if ($mode === 'publish_only') return ['publish']; if ($mode === 'selected') return $this->get_be_to_fe_selected_statuses(); return $this->allowed_bridge_post_statuses(); } protected function is_post_status_allowed_for_be_to_fe($status) { $status = sanitize_key((string)$status); if ($status === '' || in_array($status, ['auto-draft', 'inherit', 'trash'], true)) return false; return in_array($status, $this->get_be_to_fe_syncable_statuses(), true); } protected function cleanup_be_queue_after_status_rule_change() { $removed = 0; $queue = $this->get_queue(); if (empty($queue)) return 0; $new_queue = []; foreach ((array)$queue as $item) { $post_id = !empty($item['post_id']) ? (int)$item['post_id'] : 0; if (!$post_id) continue; $post = get_post($post_id); if (!$post || !$this->is_syncable_post_for_frontend($post)) { update_post_meta($post_id, self::META_QUEUE_STATUS, 'blocked_status_removed_from_queue'); update_post_meta($post_id, self::META_LAST_DIAG, 'Coda BE → FE rimossa: lo stato articolo non è consentito dalle impostazioni di sync stati.'); $this->add_article_log($post_id, 'BE→FE', 'Coda rimossa per stato non consentito', [ 'post_status' => $post ? $post->post_status : 'missing', 'allowed_statuses' => implode(',', $this->get_be_to_fe_syncable_statuses()), ]); $removed++; continue; } $new_queue[] = $item; } if ($removed > 0) $this->save_queue($new_queue); return $removed; } public function add_settings_page() { add_options_page( 'SV Bridge Meta Blacklist', 'SV Bridge Meta', 'manage_options', 'sv-bridge-meta-blacklist', [$this, 'render_meta_blacklist_settings_page'] ); } protected function get_distinct_article_metakeys($limit = 300, $offset = 0, $search = '') { global $wpdb; $limit = max(50, min(1000, (int)$limit)); $offset = max(0, (int)$offset); $search = trim((string)$search); $post_types = ['post', 'page', 'product']; $placeholders = implode(',', array_fill(0, count($post_types), '%s')); $where = "WHERE p.post_type IN ($placeholders) AND pm.meta_key <> ''"; $params = $post_types; if ($search !== '') { $where .= " AND pm.meta_key LIKE %s"; $params[] = '%' . $wpdb->esc_like($search) . '%'; } $sql = "SELECT pm.meta_key, COUNT(*) AS total_rows, COUNT(DISTINCT pm.post_id) AS post_count FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id $where GROUP BY pm.meta_key ORDER BY pm.meta_key ASC LIMIT %d OFFSET %d"; $params[] = $limit; $params[] = $offset; $rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); return is_array($rows) ? $rows : []; } protected function count_distinct_article_metakeys($search = '') { global $wpdb; $search = trim((string)$search); $post_types = ['post', 'page', 'product']; $placeholders = implode(',', array_fill(0, count($post_types), '%s')); $where = "WHERE p.post_type IN ($placeholders) AND pm.meta_key <> ''"; $params = $post_types; if ($search !== '') { $where .= " AND pm.meta_key LIKE %s"; $params[] = '%' . $wpdb->esc_like($search) . '%'; } $sql = "SELECT COUNT(*) FROM ( SELECT pm.meta_key FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id $where GROUP BY pm.meta_key ) AS svb_keys"; return (int)$wpdb->get_var($wpdb->prepare($sql, $params)); } protected function cleanup_be_queue_after_meta_blacklist_change($old_list, $new_list) { $old_list = $this->normalize_meta_blacklist_input($old_list); $new_list = $this->normalize_meta_blacklist_input($new_list); $newly_blocked = array_values(array_diff($new_list, $old_list)); if (empty($newly_blocked)) return 0; $removed = 0; $queue = $this->get_queue(); $new_queue = []; foreach ((array)$queue as $item) { $post_id = !empty($item['post_id']) ? (int)$item['post_id'] : 0; if (!$post_id) continue; $keep = true; $post = get_post($post_id); if ($post) { $remote_title_hash = (string)get_post_meta($post_id, self::META_REMOTE_TITLE_HASH, true); $remote_content_hash = (string)get_post_meta($post_id, self::META_REMOTE_CONTENT_HASH, true); $remote_excerpt_hash = (string)get_post_meta($post_id, self::META_REMOTE_EXCERPT_HASH, true); $non_meta_diff = false; if ($remote_title_hash && !hash_equals($remote_title_hash, md5((string)get_post_field('post_title', $post_id, 'raw')))) $non_meta_diff = true; if ($remote_content_hash && !hash_equals($remote_content_hash, md5((string)get_post_field('post_content', $post_id, 'raw')))) $non_meta_diff = true; if ($remote_excerpt_hash && !hash_equals($remote_excerpt_hash, md5((string)get_post_field('post_excerpt', $post_id, 'raw')))) $non_meta_diff = true; if (!$non_meta_diff) { $payload = [ 'entity' => 'post', 'action' => 'upsert', 'data' => $this->build_post($post_id), ]; $filtered_hash = $this->build_payload_hash($payload); update_post_meta($post_id, self::META_LAST_HASH, $filtered_hash); update_post_meta($post_id, self::META_QUEUE_STATUS, 'blocked_meta_removed_from_queue'); update_post_meta($post_id, self::META_LAST_DIAG, 'Coda BE → FE rimossa dopo blacklist metakey: ' . implode(', ', array_slice($newly_blocked, 0, 12)) . '.'); $this->add_article_log($post_id, 'BE→FE', 'Coda rimossa per metakey bloccato', ['metakey' => implode(',', array_slice($newly_blocked, 0, 12))]); $removed++; $keep = false; } } if ($keep) $new_queue[] = $item; } if ($removed > 0) $this->save_queue($new_queue); return $removed; } public function render_meta_blacklist_settings_page() { if (!current_user_can('manage_options')) wp_die('Permessi insufficienti'); $option = $this->be_to_fe_meta_blacklist_option_name(); $saved = $this->get_be_to_fe_meta_blacklist(); $notice = ''; if (!empty($_POST['sv_bridge_save_meta_blacklist'])) { check_admin_referer('sv_bridge_save_be_meta_blacklist'); $old_saved = $saved; $manual = isset($_POST['sv_bridge_meta_blacklist_manual']) ? wp_unslash($_POST['sv_bridge_meta_blacklist_manual']) : ''; $checked = isset($_POST['sv_bridge_meta_blacklist_checked']) ? (array)wp_unslash($_POST['sv_bridge_meta_blacklist_checked']) : []; $new = array_merge($this->normalize_meta_blacklist_input($manual), $this->normalize_meta_blacklist_input($checked)); $new = array_values(array_unique($new)); sort($new, SORT_NATURAL | SORT_FLAG_CASE); update_option($option, $new, false); $saved = $new; $old_status_mode = $this->get_be_to_fe_status_mode(); $old_statuses = $this->get_be_to_fe_selected_statuses(); $status_mode = isset($_POST['sv_bridge_be_to_fe_status_mode']) ? sanitize_key(wp_unslash($_POST['sv_bridge_be_to_fe_status_mode'])) : 'all'; if (!in_array($status_mode, ['all', 'publish_only', 'selected'], true)) $status_mode = 'all'; $selected_statuses = isset($_POST['sv_bridge_be_to_fe_selected_statuses']) ? (array)wp_unslash($_POST['sv_bridge_be_to_fe_selected_statuses']) : []; $selected_statuses = $this->normalize_status_list_input($selected_statuses); if (empty($selected_statuses)) $selected_statuses = ['publish']; update_option($this->be_to_fe_status_mode_option_name(), $status_mode, false); update_option($this->be_to_fe_selected_statuses_option_name(), $selected_statuses, false); $removed = $this->cleanup_be_queue_after_meta_blacklist_change($old_saved, $new); $removed_status = 0; if ($old_status_mode !== $status_mode || maybe_serialize($old_statuses) !== maybe_serialize($selected_statuses)) { $removed_status = $this->cleanup_be_queue_after_status_rule_change(); } $notice = 'Impostazioni SV Bridge salvate.' . ($removed > 0 ? ' Code rimosse/fermate per metakey bloccati: ' . (int)$removed . '.' : '') . ($removed_status > 0 ? ' Code rimosse/fermate per stato non consentito: ' . (int)$removed_status . '.' : ''); } $show_scan = !empty($_GET['sv_bridge_show_metakeys']); $search = isset($_GET['sv_bridge_meta_search']) ? sanitize_text_field(wp_unslash($_GET['sv_bridge_meta_search'])) : ''; $per_page = 300; $paged = isset($_GET['sv_bridge_meta_paged']) ? max(1, (int)$_GET['sv_bridge_meta_paged']) : 1; $offset = ($paged - 1) * $per_page; $rows = $show_scan ? $this->get_distinct_article_metakeys($per_page, $offset, $search) : []; $total_keys = $show_scan ? $this->count_distinct_article_metakeys($search) : 0; $total_pages = $show_scan ? max(1, (int)ceil($total_keys / $per_page)) : 1; $page_url = admin_url('options-general.php?page=sv-bridge-meta-blacklist'); echo '
'; echo '

SV Bridge Meta Blacklist

'; if ($notice) echo '

' . esc_html($notice) . '

'; echo '

Da qui blocchi i metakey che questo backend non deve mandare al frontend durante la sync BE → FE.

'; echo '

Esempio: se inserisci post_views_count o _post_views_count, quei valori non verranno più inviati al FE dal BE.

'; echo '
'; wp_nonce_field('sv_bridge_save_be_meta_blacklist'); $status_mode = $this->get_be_to_fe_status_mode(); $selected_statuses = $this->get_be_to_fe_selected_statuses(); $all_statuses = $this->allowed_bridge_post_statuses(); echo '

Modalità invio articoli BE → FE: stati da sincronizzare

'; echo '

Decidi quali articoli del backend possono entrare in coda ed essere inviati al frontend. auto-draft, trash e inherit restano sempre esclusi per evitare bozze orfane o stati tecnici.

'; echo '
'; echo '

Impostazione attiva: ' . esc_html($status_mode === 'publish_only' ? 'solo publish' : ($status_mode === 'selected' ? 'solo stati selezionati' : 'tutti gli stati editoriali')) . ' — stati effettivi: ' . esc_html(implode(', ', $this->get_be_to_fe_syncable_statuses())) . '

'; echo '

'; echo '

'; echo '
'; foreach ($all_statuses as $status) { echo ''; } echo '
'; echo '

'; echo '

Se cambi questa impostazione, eventuali code BE → FE con stati non più consentiti vengono rimosse automaticamente.

'; echo '
'; echo '

Metakey bloccati BE → FE

'; echo ''; echo '

Puoi separarli anche con virgole. Quando salvi, le code BE → FE legate solo a metakey appena bloccati vengono fermate dove possibile.

'; echo '

Mostra tutti i metakey presenti negli articoli

'; if ($show_scan) { echo '

Metakey trovati negli articoli di questo sito

'; echo '

Lettura diretta da postmeta per post, pagine e prodotti. Totale metakey distinti trovati: ' . (int)$total_keys . '.

'; echo '

'; echo 'Cerca '; if ($search !== '') echo 'Reset ricerca'; echo '

'; if (empty($rows)) { echo '

Nessun metakey trovato.

'; } else { echo '
'; echo ''; foreach ($rows as $row) { $key = isset($row['meta_key']) ? (string)$row['meta_key'] : ''; if ($key === '') continue; $checked = in_array($key, $saved, true) ? ' checked' : ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
BloccaMetakeyArticoliRighe meta
' . esc_html($key) . '' . (int)($row['post_count'] ?? 0) . '' . (int)($row['total_rows'] ?? 0) . '
'; if ($total_pages > 1) { echo '

'; if ($paged > 1) echo '← Precedenti '; echo 'Pagina ' . (int)$paged . ' di ' . (int)$total_pages . ''; if ($paged < $total_pages) echo ' Successivi →'; echo '

'; } } } echo '

'; echo '
'; echo '
'; } public function add_cron_interval($schedules) { // Conservato per compatibilità. Non viene più creato un cron ricorrente ogni minuto. return $schedules; } public function ensure_cron() { /* * Migrazione automatica dal vecchio cron ricorrente ogni minuto. * Il worker BE -> FE viene ora creato solo quando la coda contiene lavoro. */ $installed = (string)get_option(self::SCHEDULER_OPTION, ''); if ($installed !== self::SCHEDULER_VERSION) { wp_clear_scheduled_hook(self::CRON_HOOK); update_option(self::SCHEDULER_OPTION, self::SCHEDULER_VERSION, false); } $queue = $this->get_queue(); if (!empty($queue) && !wp_next_scheduled(self::CRON_HOOK)) { wp_schedule_single_event(time() + 10, self::CRON_HOOK); } } public function force_frontend_upload_urls($dirs) { $dirs['baseurl'] = $this->frontend_uploads_base; $dirs['url'] = $this->frontend_uploads_base . $dirs['subdir']; return $dirs; } public function force_attachment_url_to_frontend($url, $post_id) { return preg_replace('#https?://cms\\.cronachedellacampania\\.it/wp-content/uploads#i', $this->frontend_uploads_base, $url); } public function force_srcset_to_frontend($sources) { if (!is_array($sources)) return $sources; foreach ($sources as &$source) { if (!empty($source['url'])) { $source['url'] = preg_replace('#https?://cms\\.cronachedellacampania\\.it/wp-content/uploads#i', $this->frontend_uploads_base, $source['url']); } } return $sources; } protected function skip_meta_key($key) { $blocked_exact = [ '_edit_lock', '_edit_last', '_wp_old_slug', '_wp_trash_meta_status', '_wp_trash_meta_time', self::META_LAST_HASH, self::META_LAST_SENT_HASH, self::META_LAST_TRY, self::META_LAST_ERROR, self::META_LAST_RESPONSE_CODE, self::META_LAST_RESPONSE_BODY, self::META_LAST_DIAG, self::META_QUEUE_STATUS, self::META_QUEUE_AT, self::META_DELIVERED, self::META_DELIVERED_AT, self::META_FEATURED_FAIL, self::META_FEATURED_FAIL_MSG, self::META_WAITING_FEATURED_SINCE, self::META_REMOTE_POST_ID, self::META_REMOTE_STATUS, self::META_REMOTE_TITLE, self::META_REMOTE_LAST_SYNC, self::META_REMOTE_FEATURED_OK, self::META_REMOTE_LEGACY_LINKED_AT, self::META_REMOTE_LAST_VERIFIED_AT, self::META_REMOTE_CONTENT_VERIFIED, self::META_REMOTE_CONTENT_HASH, self::META_REMOTE_EXPECTED_CONTENT_HASH, self::META_REMOTE_TITLE_HASH, self::META_REMOTE_EXCERPT_HASH, self::META_REMOTE_VERIFICATION_MSG, self::META_REMOTE_POST_TYPE, self::META_REMOTE_IS_ATTACHMENT, ]; $blocked_prefix = ['_oembed_', '_sv_bridge_']; if (in_array($key, $blocked_exact, true)) return true; foreach ($blocked_prefix as $prefix) { if (strpos($key, $prefix) === 0) return true; } return false; } protected function diagnose_response($code) { if ((int)$code === 200) return 'Sync riuscita correttamente.'; if ((int)$code === 403) return 'Frontend raggiunto ma richiesta rifiutata (403 Unauthorized).'; if ((int)$code === 404) return 'Endpoint frontend non trovato.'; if ((int)$code === 500) return 'Errore interno sul frontend.'; return 'Risposta inattesa: ' . (int)$code; } protected function build_user($user_id) { $u = get_userdata($user_id); if (!$u) return []; return [ 'ID' => (int)$u->ID, 'user_login' => $u->user_login, 'user_email' => $u->user_email, 'display_name' => $u->display_name, 'first_name' => get_user_meta($u->ID, 'first_name', true), 'last_name' => get_user_meta($u->ID, 'last_name', true), 'role' => !empty($u->roles[0]) ? $u->roles[0] : 'author', ]; } protected function build_term($term_id, $taxonomy) { $t = get_term($term_id, $taxonomy); if (!$t || is_wp_error($t)) return []; return [ 'term_id' => (int)$t->term_id, 'taxonomy' => $taxonomy, 'name' => $t->name, 'slug' => $t->slug, 'description' => $t->description, 'parent' => (int)$t->parent, 'meta' => get_term_meta($t->term_id), ]; } protected function normalize_videopack_shortcodes($content) { $content = (string)$content; if ($content == '') return $content; $content = preg_replace_callback('/\[videopack([^\]]*)\]/i', function ($m) { $attrs = isset($m[1]) ? (string)$m[1] : ''; $attrs = preg_replace('/\s+id\s*=\s*(["\']).*?\1/i', '', $attrs); $attrs = preg_replace('/\s+id\s*=\s*[^\s\]]+/i', '', $attrs); $attrs = trim(preg_replace('/\s+/', ' ', $attrs)); return $attrs !== '' ? '[videopack ' . $attrs . ']' : '[videopack]'; }, $content); $content = preg_replace('/\[videopack\s+\]/i', '[videopack]', $content); return $content; } protected function maybe_normalize_backend_content($post_id, $post) { if ($this->is_normalizing_backend_content || !$post) return; $normalized = $this->normalize_videopack_shortcodes($post->post_content); if ($normalized === (string)$post->post_content) return; $this->is_normalizing_backend_content = true; wp_update_post([ 'ID' => (int)$post_id, 'post_content' => $normalized, ]); $this->is_normalizing_backend_content = false; } protected function scalar_contains_attachment_reference($value, $attachment_ids) { if (is_array($value) || is_object($value)) { foreach ((array)$value as $sub) { if ($this->scalar_contains_attachment_reference($sub, $attachment_ids)) return true; } return false; } if (is_numeric($value) && in_array((int)$value, $attachment_ids, true)) return true; if (is_string($value) && $value !== '') { foreach ($attachment_ids as $aid) { if (preg_match('/(^|[^\d])' . preg_quote((string)$aid, '/') . '([^\d]|$)/', $value)) { return true; } } } return false; } protected function key_looks_media_related($key) { return (bool)preg_match('/(thumb|thumbnail|image|img|video|videopack|poster|cover|fifu|attachment|media|player)/i', (string)$key); } protected function detect_meta_reference_hints($clean_meta, $attachment_ids) { $hints = []; $attachment_ids = array_values(array_unique(array_map('intval', (array)$attachment_ids))); if (empty($attachment_ids)) return $hints; foreach ((array)$clean_meta as $key => $vals) { $reason = []; if ($this->key_looks_media_related($key)) { $reason[] = 'media_like_key'; } foreach ((array)$vals as $raw) { $value = maybe_unserialize($raw); if ($this->scalar_contains_attachment_reference($value, $attachment_ids)) { $reason[] = 'contains_backend_attachment_id'; break; } } if (!empty($reason)) { $hints[$key] = array_values(array_unique($reason)); } } return $hints; } protected function build_attachment($attachment_id) { $a = get_post($attachment_id); if (!$a || $a->post_type !== 'attachment') return []; $a_meta = get_post_meta($attachment_id); $clean_a_meta = []; foreach ($a_meta as $k => $vals) { if ($this->skip_meta_key($k)) continue; if ($this->is_be_to_fe_meta_blocked($k)) continue; $clean_a_meta[$k] = $vals; } $attached_file = get_post_meta($attachment_id, '_wp_attached_file', true); $url = wp_get_attachment_url($attachment_id); $basename = $attached_file ? wp_basename($attached_file) : ($url ? wp_basename(parse_url($url, PHP_URL_PATH)) : ''); return [ 'ID' => (int)$a->ID, 'post_type' => 'attachment', 'post_status' => $a->post_status, 'post_mime_type' => $a->post_mime_type, 'post_title' => $a->post_title, 'post_content' => $this->normalize_videopack_shortcodes($a->post_content), 'post_excerpt' => $a->post_excerpt, 'post_name' => $a->post_name, 'post_date' => $a->post_date, 'post_date_gmt' => $a->post_date_gmt, 'guid' => $url, 'attached_file' => $attached_file, 'file_basename' => $basename, 'meta' => $clean_a_meta, ]; } protected function build_post($post_id) { $p = get_post($post_id); if (!$p) return []; $meta = get_post_meta($post_id); $clean_meta = []; foreach ($meta as $k => $vals) { if ($this->skip_meta_key($k)) continue; if ($this->is_be_to_fe_meta_blocked($k)) continue; $clean_meta[$k] = $vals; } $terms_payload = []; $taxes = get_object_taxonomies($p->post_type, 'names'); foreach ($taxes as $taxonomy) { $terms = wp_get_object_terms($p->ID, $taxonomy, ['hide_empty' => false]); if (is_wp_error($terms) || empty($terms)) continue; $terms_payload[$taxonomy] = []; foreach ($terms as $t) { $terms_payload[$taxonomy][] = $this->build_term($t->term_id, $taxonomy); } } $attachments_payload = []; $attachment_ids = []; $thumb_id = get_post_thumbnail_id($p->ID); if ($thumb_id) $attachment_ids[] = (int)$thumb_id; $attached = get_attached_media('', $p->ID); if (!empty($attached)) { foreach ($attached as $att) { $attachment_ids[] = (int)$att->ID; } } $attachment_ids = array_values(array_unique(array_filter($attachment_ids))); foreach ($attachment_ids as $aid) { $built = $this->build_attachment($aid); if (!empty($built)) $attachments_payload[] = $built; } $featured_url = $thumb_id ? wp_get_attachment_url($thumb_id) : ''; $featured_attached_file = $thumb_id ? get_post_meta($thumb_id, '_wp_attached_file', true) : ''; $featured_basename = $featured_attached_file ? wp_basename($featured_attached_file) : ($featured_url ? wp_basename(parse_url($featured_url, PHP_URL_PATH)) : ''); $normalized_content = $this->normalize_videopack_shortcodes($p->post_content); $meta_reference_hints = $this->detect_meta_reference_hints($clean_meta, $attachment_ids); return [ 'post' => [ 'ID' => (int)$p->ID, 'post_type' => $p->post_type, 'post_title' => $p->post_title, 'post_content' => $normalized_content, 'post_excerpt' => $p->post_excerpt, 'post_status' => $p->post_status, 'post_name' => $p->post_name, 'post_date' => $p->post_date, 'post_date_gmt' => $p->post_date_gmt, 'post_modified' => $p->post_modified, 'post_modified_gmt' => $p->post_modified_gmt, 'menu_order' => (int)$p->menu_order, 'comment_status' => $p->comment_status, 'ping_status' => $p->ping_status, 'post_password' => $p->post_password, 'post_parent' => (int)$p->post_parent, '_thumbnail_id' => $thumb_id ? (int)$thumb_id : 0, '_featured_image_url' => $featured_url, '_featured_file_name' => $featured_basename, '_featured_attached_file' => $featured_attached_file, ], 'author' => $this->build_user($p->post_author), 'terms' => $terms_payload, 'attachments' => $attachments_payload, 'meta' => $clean_meta, 'meta_reference_hints' => $meta_reference_hints, ]; } protected function build_payload_hash($payload) { return md5(wp_json_encode($payload)); } protected function is_syncable_post_for_frontend($post) { if (!$post || !($post instanceof WP_Post)) return false; if (wp_is_post_revision($post->ID)) return false; if (!in_array($post->post_type, ['post', 'page', 'product'], true)) return false; /* * WordPress crea post provvisori con post_status=auto-draft quando si apre * l'editor. Questi NON sono vere bozze editoriali e non devono mai arrivare * sul frontend, altrimenti restano come bozze abbandonate/orfane. */ if (in_array($post->post_status, ['auto-draft', 'inherit', 'trash'], true)) return false; if (!$this->is_post_status_allowed_for_be_to_fe($post->post_status)) return false; $title = trim((string)$post->post_title); $content = trim((string)$post->post_content); $excerpt = trim((string)$post->post_excerpt); // Evita di sincronizzare finte bozze vuote o il titolo tecnico "Auto Draft". if ($post->post_status === 'draft' && $title === '' && $content === '' && $excerpt === '') return false; if ($post->post_status === 'draft' && preg_match('/^auto\s*draft$/i', $title) && $content === '' && $excerpt === '') return false; return true; } protected function cleanup_unsyncable_post_state($post_id, $reason = 'unsyncable') { $post_id = (int)$post_id; if (!$post_id) return; $this->remove_post_from_queue($post_id); update_post_meta($post_id, self::META_QUEUE_STATUS, 'ignored_' . sanitize_key($reason)); update_post_meta($post_id, self::META_LAST_DIAG, 'Sync FE ignorata: ' . $reason . '. Nessuna bozza automatica deve essere creata sul frontend.'); delete_post_meta($post_id, self::META_LAST_ERROR); } protected function should_queue_post($post_id, $post, $payload_hash) { if (!$this->is_syncable_post_for_frontend($post)) { $this->cleanup_unsyncable_post_state($post_id, 'auto_draft_or_empty_draft'); return false; } $last_hash = (string)get_post_meta($post_id, self::META_LAST_HASH, true); update_post_meta($post_id, self::META_LAST_HASH, $payload_hash); return $last_hash !== $payload_hash; } protected function get_queue() { $queue = get_option(self::QUEUE_OPTION, []); return is_array($queue) ? $queue : []; } protected function save_queue($queue) { $queue = array_values((array)$queue); update_option(self::QUEUE_OPTION, $queue, false); // Compatibilità object cache persistente: forza la lettura aggiornata della option. if (function_exists('wp_cache_delete')) { wp_cache_delete(self::QUEUE_OPTION, 'options'); } } protected function remove_post_from_queue($post_id) { $post_id = (int) $post_id; if (!$post_id) return 0; $queue = $this->get_queue(); $new_queue = []; $removed = 0; foreach ($queue as $item) { if (!empty($item['post_id']) && (int)$item['post_id'] === $post_id) { $removed++; continue; } $new_queue[] = $item; } if ($removed > 0) { $this->save_queue($new_queue); } return $removed; } protected function queue_post($post_id, $force = false) { $post_id = (int) $post_id; if (!$post_id) return; $post = get_post($post_id); if (!$this->is_syncable_post_for_frontend($post)) { $this->cleanup_unsyncable_post_state($post_id, 'auto_draft_or_empty_draft'); return; } $queue = $this->get_queue(); $replacement = ['post_id' => $post_id, 'queued_at' => time()]; $new_queue = []; $inserted = false; foreach ($queue as $item) { if (!empty($item['post_id']) && (int)$item['post_id'] === $post_id) { if (!$inserted) { $new_queue[] = $replacement; $inserted = true; } continue; } $new_queue[] = $item; } if (!$inserted) $new_queue[] = $replacement; $this->save_queue($new_queue); update_post_meta($post_id, self::META_QUEUE_STATUS, 'queued'); update_post_meta($post_id, self::META_QUEUE_AT, current_time('mysql')); delete_post_meta($post_id, self::META_LAST_ERROR); $this->add_article_log($post_id, 'BE→FE', 'Messa in coda modifica BE', ['stato' => get_post_status($post_id)]); // Worker one-shot: nessun polling quando la coda è vuota. if (!wp_next_scheduled(self::CRON_HOOK)) { wp_schedule_single_event(time() + 5, self::CRON_HOOK); } } protected function dequeue_first() { $queue = $this->get_queue(); if (empty($queue)) return null; $item = array_shift($queue); $this->save_queue($queue); return $item; } protected function acquire_lock($post_id, $ttl = 120) { $key = self::LOCK_PREFIX . (int)$post_id; if (get_transient($key)) return false; set_transient($key, 1, $ttl); return true; } protected function release_lock($post_id) { delete_transient(self::LOCK_PREFIX . (int)$post_id); } protected function expected_frontend_hashes_from_data($data) { $data = is_array($data) ? $data : []; return [ 'post_content_md5' => md5(isset($data['post_content']) ? (string)$data['post_content'] : ''), 'post_title_md5' => md5(isset($data['post_title']) ? (string)$data['post_title'] : ''), 'post_excerpt_md5' => md5(isset($data['post_excerpt']) ? (string)$data['post_excerpt'] : ''), ]; } protected function remote_hashes_match($remote, $expected) { if (!is_array($remote) || !is_array($expected)) return false; foreach (['post_content_md5', 'post_title_md5', 'post_excerpt_md5'] as $key) { if (!isset($remote[$key]) || !isset($expected[$key])) return false; if (!hash_equals((string)$expected[$key], (string)$remote[$key])) return false; } return true; } protected function remote_bridge_payload_was_received($remote, $expected) { if (!is_array($remote) || !is_array($expected)) return false; $map = [ 'post_content_md5' => ['bridge_expected_content_md5', 'incoming_content_md5', 'last_be_content_md5'], 'post_title_md5' => ['bridge_expected_title_md5', 'incoming_title_md5', 'last_be_title_md5'], 'post_excerpt_md5' => ['bridge_expected_excerpt_md5', 'incoming_excerpt_md5', 'last_be_excerpt_md5'], ]; foreach ($map as $expected_key => $remote_keys) { if (empty($expected[$expected_key])) return false; $matched = false; foreach ($remote_keys as $remote_key) { if (isset($remote[$remote_key]) && hash_equals((string)$expected[$expected_key], (string)$remote[$remote_key])) { $matched = true; break; } } if (!$matched) return false; } return true; } protected function save_remote_status_from_response($post_id, $remote = [], $context = 'sync', $expected_hashes = []) { $post_id = (int)$post_id; if (!$post_id || !is_array($remote)) return; if (isset($remote['post_id'])) update_post_meta($post_id, self::META_REMOTE_POST_ID, (int)$remote['post_id']); if (isset($remote['post_status'])) update_post_meta($post_id, self::META_REMOTE_STATUS, sanitize_key((string)$remote['post_status'])); if (isset($remote['status'])) update_post_meta($post_id, self::META_REMOTE_STATUS, sanitize_key((string)$remote['status'])); if (isset($remote['title'])) update_post_meta($post_id, self::META_REMOTE_TITLE, wp_strip_all_tags((string)$remote['title'])); if (isset($remote['post_type'])) update_post_meta($post_id, self::META_REMOTE_POST_TYPE, sanitize_key((string)$remote['post_type'])); if (isset($remote['is_attachment'])) update_post_meta($post_id, self::META_REMOTE_IS_ATTACHMENT, !empty($remote['is_attachment']) ? 1 : 0); if (isset($remote['post_content_md5'])) update_post_meta($post_id, self::META_REMOTE_CONTENT_HASH, sanitize_text_field((string)$remote['post_content_md5'])); if (isset($remote['post_title_md5'])) update_post_meta($post_id, self::META_REMOTE_TITLE_HASH, sanitize_text_field((string)$remote['post_title_md5'])); if (isset($remote['post_excerpt_md5'])) update_post_meta($post_id, self::META_REMOTE_EXCERPT_HASH, sanitize_text_field((string)$remote['post_excerpt_md5'])); if (!empty($expected_hashes['post_content_md5'])) update_post_meta($post_id, self::META_REMOTE_EXPECTED_CONTENT_HASH, sanitize_text_field((string)$expected_hashes['post_content_md5'])); if (!empty($expected_hashes)) { $verified = $this->remote_hashes_match($remote, $expected_hashes); $payload_received = $this->remote_bridge_payload_was_received($remote, $expected_hashes); /* * Differenza importante: * - verified = l'hash reale del post FE coincide al 100% con il payload BE. * - payload_received = il FE ha ricevuto e memorizzato il payload BE, ma qualche hook/plugin del FE * può avere normalizzato il contenuto salvato, producendo un hash finale diverso. * * In questo secondo caso NON dobbiamo lasciare l'articolo in coda infinita: la consegna BE -> FE è riuscita. */ update_post_meta($post_id, self::META_REMOTE_CONTENT_VERIFIED, ($verified || $payload_received) ? 1 : 0); if ($verified) { $msg = 'OK: titolo, contenuto ed excerpt salvati sul FE coincidono con il payload BE.'; } elseif ($payload_received) { $msg = 'OK: il FE ha ricevuto il payload BE. Nota: l\'hash finale del contenuto FE è diverso, probabilmente per normalizzazioni/hook del frontend, ma la sync non resta in coda.'; } else { $msg = 'AVVISO: il FE ha risposto HTTP 200, ma almeno un hash reale tra titolo/contenuto/excerpt non coincide e il FE non ha restituito gli hash di payload ricevuto.'; } update_post_meta($post_id, self::META_REMOTE_VERIFICATION_MSG, $msg); } if (isset($remote['last_sync'])) update_post_meta($post_id, self::META_REMOTE_LAST_SYNC, sanitize_text_field((string)$remote['last_sync'])); if (isset($remote['featured_ok'])) update_post_meta($post_id, self::META_REMOTE_FEATURED_OK, !empty($remote['featured_ok']) ? 1 : 0); if (isset($remote['featured_synced_ok'])) update_post_meta($post_id, self::META_REMOTE_FEATURED_OK, !empty($remote['featured_synced_ok']) ? 1 : 0); if (!empty($remote['legacy_linked_at'])) { update_post_meta($post_id, self::META_REMOTE_LEGACY_LINKED_AT, sanitize_text_field((string)$remote['legacy_linked_at'])); } elseif (!empty($remote['legacy_linked'])) { if (!get_post_meta($post_id, self::META_REMOTE_LEGACY_LINKED_AT, true)) { update_post_meta($post_id, self::META_REMOTE_LEGACY_LINKED_AT, current_time('mysql')); } } update_post_meta($post_id, self::META_REMOTE_LAST_VERIFIED_AT, current_time('mysql')); $remote_id = get_post_meta($post_id, self::META_REMOTE_POST_ID, true); $remote_sync = get_post_meta($post_id, self::META_REMOTE_LAST_SYNC, true); $legacy = get_post_meta($post_id, self::META_REMOTE_LEGACY_LINKED_AT, true); $diag = 'Frontend aggiornato correttamente (' . $context . ').'; if ($remote_id) $diag .= ' ID FE: ' . $remote_id . '.'; if ($remote_sync) $diag .= ' Ultimo sync FE: ' . $remote_sync . '.'; if ($legacy) $diag .= ' Articolo legacy agganciato: ' . $legacy . '.'; $verified = get_post_meta($post_id, self::META_REMOTE_CONTENT_VERIFIED, true); if ($verified !== '') $diag .= ((int)$verified ? ' Modifica contenuto verificata via hash.' : ' ATTENZIONE: modifica contenuto NON verificata via hash.'); if ((int)get_post_meta($post_id, self::META_REMOTE_IS_ATTACHMENT, true)) $diag .= ' ATTENZIONE: il FE ha risposto con un attachment, non con un articolo.'; update_post_meta($post_id, self::META_LAST_DIAG, $diag); } protected function mark_post_delivered($post_id, $context = 'remote_check') { $this->remove_post_from_queue($post_id); update_post_meta($post_id, self::META_DELIVERED, 1); if (!get_post_meta($post_id, self::META_DELIVERED_AT, true)) { update_post_meta($post_id, self::META_DELIVERED_AT, current_time('mysql')); } update_post_meta($post_id, self::META_QUEUE_STATUS, 'sent'); update_post_meta($post_id, self::META_REMOTE_LAST_VERIFIED_AT, current_time('mysql')); delete_post_meta($post_id, self::META_LAST_ERROR); update_post_meta($post_id, self::META_LAST_DIAG, 'Verifica frontend OK (' . $context . ').'); } protected function hard_close_queue_as_sent($post_id, $context = 'frontend_confirmed', $remote = []) { $post_id = (int) $post_id; if (!$post_id) return false; // Pulizia forte: elimina tutte le occorrenze residue nella option queue. $removed = $this->remove_post_from_queue($post_id); update_post_meta($post_id, self::META_DELIVERED, 1); if (!get_post_meta($post_id, self::META_DELIVERED_AT, true)) { update_post_meta($post_id, self::META_DELIVERED_AT, current_time('mysql')); } update_post_meta($post_id, self::META_QUEUE_STATUS, 'sent'); update_post_meta($post_id, self::META_REMOTE_LAST_VERIFIED_AT, current_time('mysql')); delete_post_meta($post_id, self::META_LAST_ERROR); if (is_array($remote)) { if (isset($remote['post_id'])) update_post_meta($post_id, self::META_REMOTE_POST_ID, (int)$remote['post_id']); if (isset($remote['post_status'])) update_post_meta($post_id, self::META_REMOTE_STATUS, sanitize_key((string)$remote['post_status'])); if (isset($remote['status'])) update_post_meta($post_id, self::META_REMOTE_STATUS, sanitize_key((string)$remote['status'])); if (isset($remote['last_sync'])) update_post_meta($post_id, self::META_REMOTE_LAST_SYNC, sanitize_text_field((string)$remote['last_sync'])); if (isset($remote['featured_ok'])) update_post_meta($post_id, self::META_REMOTE_FEATURED_OK, !empty($remote['featured_ok']) ? 1 : 0); if (isset($remote['featured_synced_ok'])) update_post_meta($post_id, self::META_REMOTE_FEATURED_OK, !empty($remote['featured_synced_ok']) ? 1 : 0); } $diag = 'Coda chiusa forzatamente: frontend confermato (' . sanitize_text_field((string)$context) . ').'; if ($removed > 0) $diag .= ' Rimossi ' . (int)$removed . ' residui dalla queue.'; update_post_meta($post_id, self::META_LAST_DIAG, $diag); return true; } public function cleanup_stale_delivered_queue_items() { if (get_transient('sv_bridge_be_cleanup_queue_gate')) return; set_transient('sv_bridge_be_cleanup_queue_gate', 1, 5 * MINUTE_IN_SECONDS); $queue = $this->get_queue(); if (empty($queue)) return; $changed = false; $new_queue = []; $seen = []; $limit = 200; $i = 0; foreach ($queue as $item) { $i++; if ($i > $limit) { $new_queue[] = $item; continue; } $pid = !empty($item['post_id']) ? (int)$item['post_id'] : 0; if (!$pid) { $changed = true; continue; } // Elimina duplicati nella queue. if (isset($seen[$pid])) { $changed = true; continue; } $seen[$pid] = true; $remote_id = (int)get_post_meta($pid, self::META_REMOTE_POST_ID, true); $delivered = (bool)get_post_meta($pid, self::META_DELIVERED, true); $http_code = (int)get_post_meta($pid, self::META_LAST_RESPONSE_CODE, true); $remote_status = (string)get_post_meta($pid, self::META_REMOTE_STATUS, true); $remote_sync = (string)get_post_meta($pid, self::META_REMOTE_LAST_SYNC, true); // Caso reale del tuo log: FE confermato, HTTP 200, ma meta queue rimasta queued. if ($remote_id && ($delivered || ($http_code >= 200 && $http_code < 300)) && ($remote_status || $remote_sync)) { $this->hard_close_queue_as_sent($pid, 'cleanup stale queue'); $changed = true; continue; } $new_queue[] = $item; } if ($changed) { $this->save_queue($new_queue); } } protected function verify_remote_post_exists($post_id, $post_type = 'post') { $status_endpoint = $this->status_endpoint(); $secret = $this->secret(); if (!$status_endpoint || !$secret || !$post_id) return false; $response = wp_remote_get(add_query_arg([ 'origin_post_id' => (int)$post_id, 'post_type' => sanitize_key($post_type ?: 'post'), 't' => time(), // evita cache aggressive/proxy su endpoint status ], $status_endpoint), [ 'timeout' => 15, 'headers' => [ 'x-bridge-key' => $secret, 'Accept' => 'application/json', 'Cache-Control' => 'no-cache', ], ]); if (is_wp_error($response)) { return false; } $code = (int) wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $json = json_decode($body, true); if ($code < 200 || $code >= 300 || !is_array($json) || empty($json['success']) || empty($json['exists'])) { return false; } /* * Non blocchiamo più la riconciliazione solo perché il FE segnala featured_error: * l'articolo può essere già presente e pubblicato. L'errore immagine resta salvato * come diagnostica, ma non deve lasciare residui infiniti in coda. */ return [ 'post_id' => !empty($json['post_id']) ? (int)$json['post_id'] : 0, 'post_status' => !empty($json['post_status']) ? (string)$json['post_status'] : '', 'title' => !empty($json['title']) ? (string)$json['title'] : '', 'received_at' => !empty($json['received_at']) ? (string)$json['received_at'] : '', 'last_sync' => !empty($json['last_sync']) ? (string)$json['last_sync'] : '', 'featured_ok' => isset($json['featured_ok']) ? (bool)$json['featured_ok'] : false, 'featured_error' => !empty($json['featured_error']), 'featured_error_msg' => !empty($json['featured_error_msg']) ? (string)$json['featured_error_msg'] : '', 'legacy_linked' => !empty($json['legacy_linked']), 'legacy_linked_at' => !empty($json['legacy_linked_at']) ? (string)$json['legacy_linked_at'] : '', 'post_type' => !empty($json['post_type']) ? (string)$json['post_type'] : '', 'is_attachment' => !empty($json['is_attachment']), 'post_content_md5' => !empty($json['post_content_md5']) ? (string)$json['post_content_md5'] : '', 'post_title_md5' => !empty($json['post_title_md5']) ? (string)$json['post_title_md5'] : '', 'post_excerpt_md5' => !empty($json['post_excerpt_md5']) ? (string)$json['post_excerpt_md5'] : '', 'post_modified' => !empty($json['post_modified']) ? (string)$json['post_modified'] : '', ]; } protected function maybe_reconcile_with_frontend($post_id, $post_type = 'post') { $remote = $this->verify_remote_post_exists($post_id, $post_type); if (!$remote) return false; $payload = $this->build_post($post_id); $expected_hashes = !empty($payload['post']) ? $this->expected_frontend_hashes_from_data($payload['post']) : []; /* * La sola esistenza del post sul FE non basta per chiudere la coda: * lo stato remoto deve coincidere con quello reale del backend. * In particolare, un publish BE non può essere riconciliato come draft FE. * Il controllo avviene solo quando serve una riconciliazione, quindi non aggiunge scansioni. */ $expected_status = sanitize_key((string)get_post_status($post_id)); $remote_status = !empty($remote['post_status']) ? sanitize_key((string)$remote['post_status']) : ''; if ($expected_status && $remote_status && $expected_status !== $remote_status) { $this->save_remote_status_from_response($post_id, $remote, 'verifica status mismatch', $expected_hashes); update_post_meta($post_id, self::META_LAST_ERROR, 'Status mismatch FE: atteso ' . $expected_status . ', trovato ' . $remote_status); update_post_meta($post_id, self::META_QUEUE_STATUS, 'error'); update_post_meta($post_id, self::META_LAST_DIAG, 'Riconciliazione non chiusa: il frontend esiste ma ha stato ' . $remote_status . ', mentre il backend richiede ' . $expected_status . '. Verrà ritentato il normale invio BE → FE.'); $this->add_article_log($post_id, 'BE→FE', 'Riconciliazione rifiutata per stato diverso', [ 'stato_be' => $expected_status, 'stato_fe' => $remote_status, ]); return false; } $this->hard_close_queue_as_sent($post_id, 'remote_status_status_ok', $remote); $this->save_remote_status_from_response($post_id, $remote, 'verifica status coerente', $expected_hashes); if (!empty($remote['received_at'])) { update_post_meta($post_id, self::META_DELIVERED_AT, $remote['received_at']); } update_post_meta($post_id, self::META_LAST_RESPONSE_CODE, 209); update_post_meta($post_id, self::META_LAST_RESPONSE_BODY, wp_json_encode([ 'success' => true, 'reconciled' => true, 'remote_post_id' => $remote['post_id'], 'remote_status' => $remote['post_status'], 'featured_ok' => $remote['featured_ok'], 'featured_error' => !empty($remote['featured_error']), 'legacy_linked' => !empty($remote['legacy_linked']), 'legacy_linked_at' => !empty($remote['legacy_linked_at']) ? $remote['legacy_linked_at'] : '', ])); return true; } protected function send($entity, $action, $data, $extra = [], $log_post_id = 0) { $endpoint = $this->endpoint(); $secret = $this->secret(); if (!$endpoint || !$secret) { if ($log_post_id) { update_post_meta($log_post_id, self::META_LAST_ERROR, 'Endpoint o secret mancanti'); update_post_meta($log_post_id, self::META_LAST_DIAG, 'Controlla SV_BRIDGE_FE_ENDPOINT e SV_BRIDGE_SECRET'); update_post_meta($log_post_id, self::META_LAST_TRY, current_time('mysql')); update_post_meta($log_post_id, self::META_QUEUE_STATUS, 'error'); } return false; } $payload = array_merge(['entity' => $entity, 'action' => $action, 'data' => $data], $extra); if ($log_post_id) { $this->add_article_log($log_post_id, 'BE→FE', 'Invio HTTP al frontend', [ 'azione' => $action, 'stato_be_inviato' => isset($data['post_status']) ? (string)$data['post_status'] : '', 'titolo' => isset($data['post_title']) ? wp_trim_words(wp_strip_all_tags((string)$data['post_title']), 8, '…') : '', ]); } $expected_hashes = ($entity === 'post' && $action !== 'delete' && is_array($data)) ? $this->expected_frontend_hashes_from_data($data) : []; $response = wp_remote_post($endpoint, [ 'method' => 'POST', 'timeout' => 25, 'headers' => [ 'Content-Type' => 'application/json', 'x-bridge-key' => $secret, 'Cache-Control' => 'no-cache', ], 'body' => wp_json_encode($payload), ]); if (is_wp_error($response)) { if ($log_post_id) { update_post_meta($log_post_id, self::META_LAST_ERROR, $response->get_error_message()); update_post_meta($log_post_id, self::META_LAST_DIAG, 'wp_remote_post fallita'); update_post_meta($log_post_id, self::META_LAST_TRY, current_time('mysql')); update_post_meta($log_post_id, self::META_QUEUE_STATUS, 'error'); $post = get_post($log_post_id); if ($post && $this->maybe_reconcile_with_frontend($log_post_id, $post->post_type)) { return true; } } return false; } $code = (int)wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $json = json_decode($body, true); if ($log_post_id) { $this->add_article_log($log_post_id, 'FE→BE', 'Risposta HTTP dal frontend', [ 'http' => $code, 'success' => is_array($json) && !empty($json['success']) ? '1' : '0', 'stato_fe' => is_array($json) ? (string)($json['status'] ?? ($json['post_status'] ?? '')) : '', 'desired_status_fe' => is_array($json) ? (string)($json['desired_status'] ?? '') : '', 'id_fe' => is_array($json) ? (string)($json['post_id'] ?? '') : '', ]); } if ($log_post_id) { update_post_meta($log_post_id, self::META_LAST_TRY, current_time('mysql')); update_post_meta($log_post_id, self::META_LAST_RESPONSE_CODE, $code); update_post_meta($log_post_id, self::META_LAST_RESPONSE_BODY, wp_strip_all_tags($body)); update_post_meta($log_post_id, self::META_LAST_DIAG, $this->diagnose_response($code)); } if ($code >= 200 && $code < 300) { if ($log_post_id && is_array($json) && !empty($expected_hashes) && !$this->remote_hashes_match($json, $expected_hashes)) { /* * Prima questo caso veniva trattato come errore e rimesso in coda. * È il motivo degli articoli "In coda" pur essendo già pubblicati sul frontend. * Da ora lo trattiamo come warning: salviamo la diagnostica, ma la consegna resta OK * se il FE ha restituito HTTP 200 e ID post valido. */ update_post_meta($log_post_id, self::META_LAST_DIAG, 'Frontend aggiornato correttamente con avviso hash: il FE ha ricevuto il post, ma l\'hash reale finale non coincide al 100%. La coda viene comunque chiusa per evitare retry infiniti.'); $this->save_remote_status_from_response($log_post_id, $json, 'sync hash warning', $expected_hashes); } if ($log_post_id && is_array($json) && $entity === 'post' && $action !== 'delete' && is_array($data)) { $expected_status = !empty($data['post_status']) ? sanitize_key((string)$data['post_status']) : ''; $remote_status = ''; if (isset($json['status'])) $remote_status = sanitize_key((string)$json['status']); if (!$remote_status && isset($json['post_status'])) $remote_status = sanitize_key((string)$json['post_status']); if ($expected_status && $remote_status && $expected_status !== $remote_status) { update_post_meta($log_post_id, self::META_LAST_ERROR, 'Status mismatch FE: atteso ' . $expected_status . ', ricevuto ' . $remote_status); update_post_meta($log_post_id, self::META_QUEUE_STATUS, 'status_mismatch'); update_post_meta($log_post_id, self::META_LAST_DIAG, 'Il frontend ha ricevuto il contenuto, ma lo stato non coincide. Atteso dal BE: ' . $expected_status . '. Stato reale FE: ' . $remote_status . '. La sync resta KO per evitare pubblicazioni o bozze errate.'); $this->save_remote_status_from_response($log_post_id, $json, 'sync status mismatch', $expected_hashes); return false; } } if ($log_post_id && is_array($json) && !empty($json['success']) && !empty($json['post_id'])) { // Il FE ha accettato e salvato il post: chiusura forte della coda, anche se gli hash sono warning. $this->hard_close_queue_as_sent($log_post_id, 'sync confirmed by FE', $json); } if ($log_post_id && is_array($json) && !empty($json['featured_required']) && empty($json['featured_synced_ok'])) { $local_post = get_post($log_post_id); update_post_meta($log_post_id, self::META_FEATURED_FAIL, 1); update_post_meta($log_post_id, self::META_FEATURED_FAIL_MSG, 'Il frontend non ha ancora una featured image valida. Lo stato editoriale del backend NON viene modificato.'); update_post_meta($log_post_id, self::META_QUEUE_STATUS, 'image_failed'); update_post_meta($log_post_id, self::META_LAST_DIAG, 'FE ricevuto ma featured non pronta. Il BE resta nello stato canonico ' . ($local_post ? $local_post->post_status : 'sconosciuto') . '; nessuna retrocessione automatica a draft.'); $this->add_article_log($log_post_id, 'BE→FE', 'Featured FE non pronta: stato BE preservato', [ 'stato_be' => $local_post ? $local_post->post_status : 'missing', 'stato_fe' => (string)($json['status'] ?? ''), ]); $this->save_remote_status_from_response($log_post_id, $json, 'sync image warning', $expected_hashes); return false; } if ($log_post_id) { $this->mark_post_delivered($log_post_id, 'sync'); if (is_array($json)) { $this->save_remote_status_from_response($log_post_id, $json, 'sync', $expected_hashes); } delete_post_meta($log_post_id, self::META_FEATURED_FAIL); delete_post_meta($log_post_id, self::META_FEATURED_FAIL_MSG); } return true; } if ($log_post_id) { update_post_meta($log_post_id, self::META_LAST_ERROR, 'HTTP ' . $code); update_post_meta($log_post_id, self::META_QUEUE_STATUS, 'error'); $post = get_post($log_post_id); if ($post && $this->maybe_reconcile_with_frontend($log_post_id, $post->post_type)) { return true; } } return false; } protected function send_full_post($post_id, $action = 'upsert') { $post = get_post($post_id); if (!$this->is_syncable_post_for_frontend($post)) { $this->cleanup_unsyncable_post_state($post_id, 'auto_draft_or_empty_draft'); return true; } $payload = $this->build_post($post_id); if (empty($payload['post'])) return false; $hash = $this->build_payload_hash($payload); $ok = $this->send('post', $action, $payload['post'], [ 'author' => $payload['author'], 'terms' => $payload['terms'], 'attachments' => $payload['attachments'], 'meta' => $payload['meta'], 'meta_reference_hints' => !empty($payload['meta_reference_hints']) ? $payload['meta_reference_hints'] : [], ], $post_id); if ($ok) { $this->remove_post_from_queue($post_id); update_post_meta($post_id, self::META_LAST_SENT_HASH, $hash); } return $ok; } protected function max_featured_wait_seconds() { if (defined('SV_BRIDGE_FEATURED_WAIT_SECONDS')) { return max(60, (int) SV_BRIDGE_FEATURED_WAIT_SECONDS); } return 10 * MINUTE_IN_SECONDS; } protected function require_featured_for_frontend($post) { if (!$post || $post->post_status !== 'publish') return false; if (!in_array($post->post_type, ['post'], true)) return false; if (defined('SV_BRIDGE_REQUIRE_FEATURED_IMAGE') && !SV_BRIDGE_REQUIRE_FEATURED_IMAGE) return false; return true; } protected function has_backend_featured_image_ready($post_id) { $thumb_id = (int) get_post_thumbnail_id($post_id); if ($thumb_id > 0) { return true; } // Fallback per eventuali plugin tipo FIFU: se c'è un URL esterno immagine, non blocchiamo il sync. $fifu_keys = ['fifu_image_url', '_fifu_image_url', '_external_featured_image', 'external_featured_image']; foreach ($fifu_keys as $key) { $url = trim((string) get_post_meta($post_id, $key, true)); if ($url !== '' && filter_var($url, FILTER_VALIDATE_URL)) { return true; } } return false; } protected function hold_post_waiting_for_featured($post_id, $post) { if (!$this->require_featured_for_frontend($post)) return false; if ($this->has_backend_featured_image_ready($post_id)) { delete_post_meta($post_id, self::META_WAITING_FEATURED_SINCE); return false; } $first_wait = (int) get_post_meta($post_id, self::META_WAITING_FEATURED_SINCE, true); if (!$first_wait) { $first_wait = time(); update_post_meta($post_id, self::META_WAITING_FEATURED_SINCE, $first_wait); } if ((time() - $first_wait) < $this->max_featured_wait_seconds()) { $this->queue_post($post_id, true); update_post_meta($post_id, self::META_QUEUE_STATUS, 'waiting_image'); update_post_meta($post_id, self::META_LAST_DIAG, 'In attesa immagine in evidenza prima di inviare al frontend.'); update_post_meta($post_id, self::META_FEATURED_FAIL_MSG, 'Il post è pubblicato sul backend ma la featured image non è ancora pronta. Sync FE rinviato.'); return true; } update_post_meta($post_id, self::META_QUEUE_STATUS, 'image_missing'); update_post_meta($post_id, self::META_FEATURED_FAIL, 1); update_post_meta($post_id, self::META_FEATURED_FAIL_MSG, 'Post pubblicato sul backend senza immagine in evidenza: invio al frontend bloccato.'); update_post_meta($post_id, self::META_LAST_DIAG, 'Invio FE bloccato: immagine in evidenza ancora assente dopo il tempo massimo di attesa.'); return true; } public function on_thumbnail_meta_changed($meta_id, $post_id, $meta_key, $meta_value) { if ($meta_key !== '_thumbnail_id') return; $post = get_post($post_id); if (!$post || wp_is_post_revision($post_id)) return; if (!in_array($post->post_type, ['post', 'page', 'product'], true)) return; delete_post_meta($post_id, self::META_WAITING_FEATURED_SINCE); delete_post_meta($post_id, self::META_FEATURED_FAIL); delete_post_meta($post_id, self::META_FEATURED_FAIL_MSG); delete_post_meta($post_id, self::META_LAST_SENT_HASH); delete_post_meta($post_id, self::META_LAST_HASH); $this->queue_post($post_id, true); $this->process_single_post($post_id); } public function on_save_post($post_id, $post, $update) { if (!$post || wp_is_post_revision($post_id)) return; if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if ($post->post_type === 'revision') return; if ($this->is_normalizing_backend_content) return; if ($this->is_receiving_frontend_sync) return; if (!$this->is_syncable_post_for_frontend($post)) { $this->cleanup_unsyncable_post_state($post_id, 'auto_draft_or_empty_draft'); return; } $this->maybe_normalize_backend_content($post_id, $post); $post = get_post($post_id); if (!$post) return; $payload = $this->build_post($post_id); if (empty($payload['post'])) return; $hash = $this->build_payload_hash($payload); $queue_status = (string)get_post_meta($post_id, self::META_QUEUE_STATUS, true); $delivered = (bool)get_post_meta($post_id, self::META_DELIVERED, true); $needs_force_retry = in_array($queue_status, ['queued', 'error', 'processing'], true) || !$delivered; if ($needs_force_retry) { delete_post_meta($post_id, self::META_LAST_SENT_HASH); $this->queue_post($post_id, true); return; } if ($this->should_queue_post($post_id, $post, $hash)) { $this->queue_post($post_id); } } protected function process_single_post($post_id) { $post_id = (int)$post_id; if (!$post_id) return false; $post = get_post($post_id); if (!$post) return false; if (!$this->is_syncable_post_for_frontend($post)) { $this->cleanup_unsyncable_post_state($post_id, 'auto_draft_or_empty_draft'); return true; } if ($this->hold_post_waiting_for_featured($post_id, $post)) { return false; } if (!$this->acquire_lock($post_id)) return false; update_post_meta($post_id, self::META_QUEUE_STATUS, 'processing'); $ok = $this->send_full_post($post_id, 'upsert'); if (!$ok) { $status = (string)get_post_meta($post_id, self::META_QUEUE_STATUS, true); if (in_array($status, ['error', 'processing'], true)) { $this->queue_post($post_id, true); } } $this->release_lock($post_id); return $ok; } public function process_queue() { if (get_transient('sv_bridge_be_queue_worker_lock')) return; set_transient('sv_bridge_be_queue_worker_lock', 1, 90); try { $item = $this->dequeue_first(); if (empty($item['post_id'])) return; $post_id = (int)$item['post_id']; $this->add_article_log($post_id, 'BE→FE', 'Processo coda BE→FE avviato'); $this->process_single_post($post_id); } finally { delete_transient('sv_bridge_be_queue_worker_lock'); } // Un articolo per processo PHP. Se resta coda, parte un processo nuovo. $queue = $this->get_queue(); if (!empty($queue) && !wp_next_scheduled(self::CRON_HOOK)) { wp_schedule_single_event(time() + 10, self::CRON_HOOK); } } protected function send_trash_to_frontend($post_id, $context = 'trash') { $post_id = (int) $post_id; $post = get_post($post_id); if (!$post || wp_is_post_revision($post_id)) return false; if (!in_array($post->post_type, ['post', 'page', 'product'], true)) return false; /* * Evita doppio invio nella stessa richiesta: wp_trash_post, transition_post_status * e before_delete_post possono scattare quasi insieme. */ $lock_key = 'sv_bridge_be_trash_sync_' . $post_id; if (get_transient($lock_key)) return false; set_transient($lock_key, 1, 60); return $this->send('post', 'delete', [ 'ID' => $post_id, 'post_type' => $post->post_type, 'post_status' => 'trash', 'delete_mode' => 'trash', 'source_status' => $post->post_status, 'deleted_at' => current_time('mysql'), 'delete_context' => sanitize_key((string) $context), ], [], $post_id); } public function on_transition_from_auto_draft_to_real_status($new_status, $old_status, $post) { if ($old_status !== 'auto-draft') return; if (!$post || empty($post->ID)) return; if (!in_array($new_status, ['draft', 'pending', 'private', 'future', 'publish'], true)) return; if (!$this->is_syncable_post_for_frontend($post)) return; /* * Quando WordPress trasforma l'auto-draft in una bozza reale, azzeriamo * gli hash tecnici creati nella fase provvisoria e sincronizziamo solo * la bozza vera, mantenendo lo stesso origin_post_id BE -> FE. */ delete_post_meta($post->ID, self::META_LAST_HASH); delete_post_meta($post->ID, self::META_LAST_SENT_HASH); delete_post_meta($post->ID, self::META_LAST_ERROR); $this->remove_post_from_queue($post->ID); $this->queue_post($post->ID, true); $this->process_single_post($post->ID); } public function on_trash_post($post_id) { $this->send_trash_to_frontend($post_id, 'wp_trash_post'); } public function on_transition_to_trash($new_status, $old_status, $post) { if ($new_status !== 'trash') return; if (!$post || empty($post->ID)) return; $this->send_trash_to_frontend((int) $post->ID, 'transition_post_status'); } public function on_delete_post($post_id) { /* * Fallback: anche se l'articolo viene eliminato definitivamente dal backend, * sul frontend lo spostiamo comunque nel cestino e non lo cancelliamo hard. */ $this->send_trash_to_frontend($post_id, 'before_delete_post'); } public function add_observability_page() { add_management_page('SV Bridge Log BE', 'SV Bridge Log BE', 'manage_options', 'sv-bridge-log-be', [$this, 'render_observability_page']); } protected function get_global_log_24h() { $rows = get_option(self::GLOBAL_LOG_OPTION, []); if (!is_array($rows)) $rows = []; $cutoff = time() - DAY_IN_SECONDS; $rows = array_values(array_filter($rows, function($row) use ($cutoff) { return !empty($row['ts']) && (int)$row['ts'] >= $cutoff; })); return array_slice($rows, 0, 500); } protected function append_global_log($post_id, $direction, $event, $details = []) { $rows = $this->get_global_log_24h(); array_unshift($rows, [ 'ts' => time(), 'time' => current_time('mysql'), 'post_id' => (int)$post_id, 'direction' => sanitize_text_field((string)$direction), 'event' => sanitize_text_field((string)$event), 'details' => is_array($details) ? $details : [], ]); $rows = array_slice($rows, 0, 500); if (get_option(self::GLOBAL_LOG_OPTION, null) === null) add_option(self::GLOBAL_LOG_OPTION, $rows, '', false); else update_option(self::GLOBAL_LOG_OPTION, $rows, false); } public function cleanup_global_log_24h() { if (get_transient(self::LOG_CLEANUP_TRANSIENT)) return; set_transient(self::LOG_CLEANUP_TRANSIENT, 1, 15 * MINUTE_IN_SECONDS); update_option(self::GLOBAL_LOG_OPTION, $this->get_global_log_24h(), false); } public function handle_clear_global_log() { if (!current_user_can('manage_options')) wp_die('Permessi insufficienti'); check_admin_referer('sv_bridge_clear_be_log'); update_option(self::GLOBAL_LOG_OPTION, [], false); wp_safe_redirect(admin_url('tools.php?page=sv-bridge-log-be&cleared=1')); exit; } public function maybe_kick_queue_on_admin() { if (!is_admin() || wp_doing_ajax()) return; if (get_transient('sv_bridge_be_admin_kick')) return; $queue = $this->get_queue(); if (empty($queue)) return; set_transient('sv_bridge_be_admin_kick', 1, 30); $next = wp_next_scheduled(self::CRON_HOOK); if (!$next || $next > time() + 30) wp_schedule_single_event(time() + 1, self::CRON_HOOK); if ((!defined('DISABLE_WP_CRON') || !DISABLE_WP_CRON) && function_exists('spawn_cron')) spawn_cron(time()); } protected function publication_badge_data($post_id) { $be = sanitize_key((string)get_post_status($post_id)); $fe = sanitize_key((string)get_post_meta($post_id, self::META_REMOTE_STATUS, true)); if ($fe === 'publish') return ['FE PUBBLICATO', '#00a32a']; if ($be === 'publish' && $fe === 'draft') return ['FE BOZZA ⚠', '#d63638']; if ($fe === 'draft') return ['FE BOZZA', '#646970']; if ($fe === 'future') return ['FE PROGRAMMATO', '#dba617']; if ($fe === 'pending') return ['FE PENDING', '#dba617']; if ($fe === 'private') return ['FE PRIVATO', '#7e57c2']; return ['FE STATO ?', '#646970']; } protected function render_remote_publication_badge($post_id) { list($label, $color) = $this->publication_badge_data($post_id); echo '' . esc_html($label) . ''; } public function handle_check_fe_post() { if (!current_user_can('edit_posts')) wp_die('Permessi insufficienti'); $post_id = isset($_GET['post_id']) ? (int)$_GET['post_id'] : 0; check_admin_referer('sv_bridge_check_fe_post_' . $post_id); $post = $post_id ? get_post($post_id) : null; if (!$post) wp_die('Articolo non trovato'); $remote = $this->verify_remote_post_exists($post_id, $post->post_type); if ($remote) { $payload = $this->build_post($post_id); $expected = !empty($payload['post']) ? $this->expected_frontend_hashes_from_data($payload['post']) : []; $this->save_remote_status_from_response($post_id, $remote, 'controllo manuale stato FE', $expected); } $remote_status = sanitize_key((string)get_post_meta($post_id, self::META_REMOTE_STATUS, true)); if ($remote_status !== sanitize_key((string)$post->post_status)) { delete_post_meta($post_id, self::META_LAST_SENT_HASH); $this->queue_post($post_id, true); $this->add_article_log($post_id, 'BE→FE', 'Controllo manuale: stato FE incoerente, reinvio accodato', ['stato_be'=>$post->post_status,'stato_fe'=>$remote_status]); } else { $this->add_article_log($post_id, 'BE→FE', 'Controllo manuale: stato FE coerente', ['stato'=>$remote_status]); } $url = get_edit_post_link($post_id, 'url'); wp_safe_redirect(add_query_arg('sv_bridge_checked', '1', $url ?: admin_url('edit.php'))); exit; } public function render_observability_page() { if (!current_user_can('manage_options')) return; $rows = $this->get_global_log_24h(); $queue = $this->get_queue(); echo '

SV Bridge · Log Backend → Frontend

'; echo '

Registro rolling delle ultime 24 ore, massimo 500 eventi. Nessuna scansione continua del database.

'; $next = wp_next_scheduled(self::CRON_HOOK); echo '

Coda attuale: ' . count($queue) . ' · Prossimo worker: ' . esc_html($next ? date_i18n('Y-m-d H:i:s',$next) : 'non pianificato') . '

'; if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) echo '

DISABLE_WP_CRON è attivo: serve un cron server esterno oppure la coda può restare queued.

'; $clear = wp_nonce_url(admin_url('admin-post.php?action=sv_bridge_clear_be_log'), 'sv_bridge_clear_be_log'); echo '

Svuota log adesso

'; echo ''; foreach ($rows as $row) { $details=[]; foreach ((array)($row['details']??[]) as $k=>$v) if (is_scalar($v)) $details[]=$k . ': ' . $v; $id=(int)($row['post_id']??0); $link=$id?get_edit_post_link($id):''; echo ''; } if (empty($rows)) echo ''; echo '
OraIDFlussoEventoDettagli
' . esc_html($row['time']??'-') . '' . ($link?'#'.$id.'':'-') . '' . esc_html($row['direction']??'-') . '' . esc_html($row['event']??'-') . '' . esc_html(implode(' · ',$details)) . '
Nessun evento nelle ultime 24 ore.
'; } protected function add_article_log($post_id, $direction, $event, $details = []) { $post_id = (int)$post_id; if (!$post_id) return; $logs = get_post_meta($post_id, self::META_ARTICLE_LOG, true); if (!is_array($logs)) $logs = []; $clean_details = []; foreach ((array)$details as $k => $v) { if (is_scalar($v)) $clean_details[sanitize_key((string)$k)] = sanitize_text_field((string)$v); } array_unshift($logs, [ 'time' => current_time('mysql'), 'site' => 'BE', 'direction' => sanitize_text_field((string)$direction), 'event' => sanitize_text_field((string)$event), 'details' => $clean_details, ]); $cutoff = time() - DAY_IN_SECONDS; $logs = array_values(array_filter($logs, function($row) use ($cutoff) { $ts = !empty($row['time']) ? strtotime((string)$row['time']) : 0; return $ts && $ts >= $cutoff; })); $logs = array_slice($logs, 0, 80); update_post_meta($post_id, self::META_ARTICLE_LOG, $logs); $this->append_global_log($post_id, $direction, $event, $clean_details); } public function render_article_log_metabox($post) { if (!current_user_can('manage_options')) { echo '

Registro visibile solo agli amministratori.

'; return; } $logs = get_post_meta($post->ID, self::META_ARTICLE_LOG, true); if (!is_array($logs) || empty($logs)) { echo '

Nessuna operazione registrata per questo articolo.

'; return; } echo '
'; echo ''; echo ''; foreach ($logs as $row) { $details = []; if (!empty($row['details']) && is_array($row['details'])) { foreach ($row['details'] as $k => $v) $details[] = $k . ': ' . $v; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
OraSitoFlussoOperazioneDettagli
' . esc_html($row['time'] ?? '-') . '' . esc_html($row['site'] ?? 'BE') . '' . esc_html($row['direction'] ?? '-') . '' . esc_html($row['event'] ?? '-') . '' . esc_html(implode(' · ', $details)) . '
'; } public function add_metabox() { add_meta_box('sv-bridge-status', 'SV Bridge', [$this, 'render_metabox'], ['post', 'page', 'product'], 'side', 'high'); if (current_user_can('manage_options')) { add_meta_box('sv-bridge-article-log', 'Registro modifiche BE/FE', [$this, 'render_article_log_metabox'], ['post', 'page', 'product'], 'normal', 'default'); } } protected function get_badge_state($post_id) { $post = get_post($post_id); $queue_status = (string)get_post_meta($post_id, self::META_QUEUE_STATUS, true); $delivered = (bool)get_post_meta($post_id, self::META_DELIVERED, true); $img_fail = (bool)get_post_meta($post_id, self::META_FEATURED_FAIL, true); $http_code = (int)get_post_meta($post_id, self::META_LAST_RESPONSE_CODE, true); $last_try = (string)get_post_meta($post_id, self::META_LAST_TRY, true); $remote_status = (string)get_post_meta($post_id, self::META_REMOTE_STATUS, true); $remote_legacy = (string)get_post_meta($post_id, self::META_REMOTE_LEGACY_LINKED_AT, true); $last_update_from_fe = (string)get_post_meta($post_id, self::META_LAST_UPDATE_FROM_FE_AT, true); $verified = get_post_meta($post_id, self::META_REMOTE_CONTENT_VERIFIED, true); $is_attachment = (int)get_post_meta($post_id, self::META_REMOTE_IS_ATTACHMENT, true); if ($queue_status === 'synced_from_frontend' || $last_update_from_fe) return ['MOD FE', '#00a32a']; if ($img_fail || in_array($queue_status, ['image_missing', 'image_failed'], true)) return ['IMG KO', '#b32d2e']; if ($queue_status === 'waiting_image') return ['IMG', '#dba617']; if (in_array($queue_status, ['queued', 'processing'], true)) return ['MOD BE', '#dba617']; if ($is_attachment) return ['ERR', '#d63638']; if ($queue_status === 'status_mismatch') return ['ST KO', '#d63638']; if ($queue_status === 'content_mismatch' || $verified === '0') return ['KO', '#d63638']; if ($delivered && $remote_status === 'publish') return ['P su FE', '#00a32a']; if ($delivered && $remote_legacy) return ['LEG FE', '#7e57c2']; if ($delivered && (int)$verified === 1) return ['FE OK', '#00a32a']; if ($delivered && $http_code >= 200 && $http_code < 300) { if ($post && $post->post_status === 'draft') return ['B su FE', '#7e57c2']; return ['FE OK', '#00a32a']; } if ($last_try && ($http_code >= 400 || $queue_status === 'error')) return ['ERR', '#d63638']; if ($post && $post->post_status === 'draft' && $delivered) return ['BOZZA', '#646970']; return ['NO FE', '#646970']; } public function output_admin_list_css() { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen) return; $allowed = ['edit-post', 'edit-page', 'edit-product', 'post', 'page', 'product']; if (!in_array($screen->id, $allowed, true)) return; echo ''; } public function add_admin_column($columns) { $columns['sv_bridge_delivery'] = 'Frontend'; return $columns; } protected function get_backend_queue_item_for_post($post_id) { $post_id = (int)$post_id; foreach ($this->get_queue() as $item) { if (!empty($item['post_id']) && (int)$item['post_id'] === $post_id) return $item; } return []; } protected function get_backend_pending_change_rows($post_id) { $post_id = (int)$post_id; $post = get_post($post_id); $rows = []; if (!$post) return $rows; $queue_status = (string)get_post_meta($post_id, self::META_QUEUE_STATUS, true); $remote_status = (string)get_post_meta($post_id, self::META_REMOTE_STATUS, true); $remote_id = (string)get_post_meta($post_id, self::META_REMOTE_POST_ID, true); $remote_title_hash = (string)get_post_meta($post_id, self::META_REMOTE_TITLE_HASH, true); $remote_content_hash = (string)get_post_meta($post_id, self::META_REMOTE_CONTENT_HASH, true); $remote_excerpt_hash = (string)get_post_meta($post_id, self::META_REMOTE_EXCERPT_HASH, true); $last_error = (string)get_post_meta($post_id, self::META_LAST_ERROR, true); $diag = (string)get_post_meta($post_id, self::META_LAST_DIAG, true); $last_update_from_fe = (string)get_post_meta($post_id, self::META_LAST_UPDATE_FROM_FE_AT, true); $last_fe_post_id = (string)get_post_meta($post_id, self::META_LAST_FE_POST_ID, true); /* Caso MOD FE: il backend deve mostrare da edit.php cosa è stato ricevuto dal FE. */ if ($queue_status === 'synced_from_frontend' || $last_update_from_fe) { $rows[] = 'Direzione: frontend → backend'; $rows[] = 'Ultima modifica ricevuta dal FE: ' . ($last_update_from_fe ?: 'non disponibile'); if ($last_fe_post_id) $rows[] = 'Articolo FE sorgente: #' . $last_fe_post_id; $changed_fields = get_post_meta($post_id, self::META_LAST_FE_CHANGED_FIELDS, true); $changed_meta = get_post_meta($post_id, self::META_LAST_FE_SYNCED_META_KEYS, true); $changed_tax = get_post_meta($post_id, self::META_LAST_FE_SYNCED_TAXONOMIES, true); if (is_array($changed_fields) && !empty($changed_fields)) { $rows[] = 'Campi aggiornati sul BE: ' . implode(', ', array_slice($changed_fields, 0, 8)); } if (is_array($changed_tax) && !empty($changed_tax)) { $rows[] = 'Tassonomie aggiornate: ' . implode(', ', array_slice($changed_tax, 0, 8)); } if (is_array($changed_meta) && !empty($changed_meta)) { $rows[] = 'Metakey aggiornate: ' . implode(', ', array_slice($changed_meta, 0, 10)); } if (count($rows) <= 3) { $rows[] = 'Modifica FE applicata al BE. Nessuna azione necessaria, salvo nuova coda BE → FE.'; } if ($diag) $rows[] = wp_trim_words($diag, 18, '...'); return array_values(array_unique(array_filter($rows))); } $rows[] = 'Direzione: backend → frontend'; $rows[] = 'Stato coda: ' . ($queue_status ?: 'non impostato'); if ($remote_id) $rows[] = 'Articolo FE collegato: #' . $remote_id; if ($remote_status && $remote_status !== $post->post_status) $rows[] = 'Stato da aggiornare: ' . $remote_status . ' → ' . $post->post_status; if ($remote_title_hash && md5((string)$post->post_title) !== $remote_title_hash) $rows[] = 'Titolo da aggiornare sul FE'; if ($remote_content_hash && md5((string)$post->post_content) !== $remote_content_hash) $rows[] = 'Contenuto da aggiornare sul FE'; if ($remote_excerpt_hash && md5((string)$post->post_excerpt) !== $remote_excerpt_hash) $rows[] = 'Excerpt da aggiornare sul FE'; if (in_array($queue_status, ['waiting_image', 'image_missing', 'image_failed'], true)) { $rows[] = 'Immagine in evidenza/media da sincronizzare o correggere'; } $item = $this->get_backend_queue_item_for_post($post_id); if (!empty($item['queued_at'])) $rows[] = 'In coda dal: ' . date_i18n('Y-m-d H:i:s', (int)$item['queued_at']); if ($last_error) $rows[] = 'Ultimo errore: ' . wp_trim_words($last_error, 18, '...'); if ($diag && count($rows) <= 3) $rows[] = wp_trim_words($diag, 18, '...'); if (count($rows) <= 3 && in_array($queue_status, ['queued', 'processing'], true)) { $rows[] = 'Payload modificato: possibili modifiche a metakey, tassonomie, contenuto normalizzato o featured image.'; } return array_values(array_unique(array_filter($rows))); } protected function render_bridge_badge_popup($label, $color, $badge_class = '', $rows = [], $post_id = 0) { $class = 'sv-bridge-badge ' . sanitize_html_class((string)$badge_class); $style = 'background:' . esc_attr($color) . ';'; if (empty($rows)) { echo '' . esc_html($label) . ''; return; } echo '
'; echo '' . esc_html($label) . ''; echo '
'; echo '
Modifiche in coda
'; echo ''; if ($post_id && current_user_can('edit_post', $post_id)) { $queue_status = (string)get_post_meta((int)$post_id, self::META_QUEUE_STATUS, true); if (in_array($queue_status, ['queued', 'processing', 'waiting_image', 'content_mismatch', 'image_failed', 'image_missing', 'error'], true)) { $url = wp_nonce_url(admin_url('admin-post.php?action=sv_bridge_resend_post&post_id=' . (int)$post_id), 'sv_bridge_resend_post_' . (int)$post_id); echo '

Forza coda BE → FE

'; } } echo '
'; echo '
'; } public function render_admin_column($column, $post_id) { if ($column !== 'sv_bridge_delivery') return; list($label, $color) = $this->get_badge_state($post_id); $queue_status = (string) get_post_meta($post_id, self::META_QUEUE_STATUS, true); $delivered_at = (string) get_post_meta($post_id, self::META_DELIVERED_AT, true); $remote_id = (string) get_post_meta($post_id, self::META_REMOTE_POST_ID, true); $remote_sync = (string) get_post_meta($post_id, self::META_REMOTE_LAST_SYNC, true); $verified = get_post_meta($post_id, self::META_REMOTE_CONTENT_VERIFIED, true); $remote_legacy = (string) get_post_meta($post_id, self::META_REMOTE_LEGACY_LINKED_AT, true); $remote_type = (string) get_post_meta($post_id, self::META_REMOTE_POST_TYPE, true); $is_attachment = (int) get_post_meta($post_id, self::META_REMOTE_IS_ATTACHMENT, true); $diag = (string) get_post_meta($post_id, self::META_LAST_DIAG, true); $badge_class = 'neutral'; if ($is_attachment || $queue_status === 'content_mismatch' || $verified === '0' || in_array($queue_status, ['error', 'image_failed', 'image_missing'], true)) { $badge_class = 'err'; } elseif (in_array($queue_status, ['queued', 'processing', 'waiting_image'], true)) { $badge_class = 'warn'; } elseif ($remote_legacy) { $badge_class = 'legacy'; } elseif ($queue_status === 'sent' || $verified === '1') { $badge_class = 'ok'; } elseif ($queue_status) { $badge_class = 'info'; } echo '
'; $popup_rows = []; $last_update_from_fe = (string) get_post_meta($post_id, self::META_LAST_UPDATE_FROM_FE_AT, true); if (in_array($queue_status, ['queued', 'processing', 'waiting_image', 'content_mismatch', 'image_failed', 'image_missing', 'error', 'synced_from_frontend'], true) || $last_update_from_fe) { $popup_rows = $this->get_backend_pending_change_rows($post_id); } $this->render_bridge_badge_popup($label, $color, $badge_class, $popup_rows, $post_id); $this->render_remote_publication_badge($post_id); echo '
'; if ($remote_id) { echo '
FE #' . esc_html($remote_id) . '
'; } if ($remote_type) { echo '
Tipo: ' . esc_html($remote_type); if ($is_attachment) echo ' ATTACHMENT'; echo '
'; } if ($remote_sync) { echo '
Sync: ' . esc_html($remote_sync) . '
'; } elseif ($delivered_at) { echo '
Consegnato: ' . esc_html($delivered_at) . '
'; } if ($verified !== '') { echo '
Content: '; echo ((int) $verified === 1) ? 'OK' : 'NO'; echo '
'; } if ($remote_legacy) { echo '
Legacy: OK
'; } if (!$remote_id && $diag) { echo '
' . esc_html(wp_trim_words($diag, 12, '...')) . '
'; } echo '
'; echo '
'; } public function render_metabox($post) { $last_try = get_post_meta($post->ID, self::META_LAST_TRY, true); $http_code = get_post_meta($post->ID, self::META_LAST_RESPONSE_CODE, true); $error = get_post_meta($post->ID, self::META_LAST_ERROR, true); $response = get_post_meta($post->ID, self::META_LAST_RESPONSE_BODY, true); $diag = get_post_meta($post->ID, self::META_LAST_DIAG, true); $img_fail = get_post_meta($post->ID, self::META_FEATURED_FAIL, true); $img_msg = get_post_meta($post->ID, self::META_FEATURED_FAIL_MSG, true); $queue_status = get_post_meta($post->ID, self::META_QUEUE_STATUS, true); $delivered_at = get_post_meta($post->ID, self::META_DELIVERED_AT, true); $remote_id = get_post_meta($post->ID, self::META_REMOTE_POST_ID, true); $remote_status = get_post_meta($post->ID, self::META_REMOTE_STATUS, true); $remote_title = get_post_meta($post->ID, self::META_REMOTE_TITLE, true); $remote_last_sync = get_post_meta($post->ID, self::META_REMOTE_LAST_SYNC, true); $remote_featured_ok = get_post_meta($post->ID, self::META_REMOTE_FEATURED_OK, true); $remote_legacy = get_post_meta($post->ID, self::META_REMOTE_LEGACY_LINKED_AT, true); $remote_verified = get_post_meta($post->ID, self::META_REMOTE_LAST_VERIFIED_AT, true); $remote_content_verified = get_post_meta($post->ID, self::META_REMOTE_CONTENT_VERIFIED, true); $remote_verification_msg = get_post_meta($post->ID, self::META_REMOTE_VERIFICATION_MSG, true); $remote_content_hash = get_post_meta($post->ID, self::META_REMOTE_CONTENT_HASH, true); $remote_expected_hash = get_post_meta($post->ID, self::META_REMOTE_EXPECTED_CONTENT_HASH, true); $remote_post_type = get_post_meta($post->ID, self::META_REMOTE_POST_TYPE, true); $remote_is_attachment = get_post_meta($post->ID, self::META_REMOTE_IS_ATTACHMENT, true); $last_update_from_fe = get_post_meta($post->ID, self::META_LAST_UPDATE_FROM_FE_AT, true); $last_fe_sync_status = get_post_meta($post->ID, self::META_LAST_FE_SYNC_STATUS, true); $last_fe_post_id = get_post_meta($post->ID, self::META_LAST_FE_POST_ID, true); $endpoint = $this->endpoint(); $secret_present = $this->secret() ? 'Sì' : 'No'; list($badge, $color) = $this->get_badge_state($post->ID); echo '

Stato: ' . esc_html($badge) . '

'; echo '

Pubblicazione reale FE:
'; $this->render_remote_publication_badge($post->ID); echo '

'; echo '

Queue: ' . esc_html($queue_status ?: '-') . '

'; if ($last_update_from_fe) { echo '

Modifica ricevuta dal FE:
' . esc_html($last_update_from_fe) . '
Stato FE→BE: ' . esc_html($last_fe_sync_status ?: '-') . ' · ID FE: ' . esc_html($last_fe_post_id ?: '-') . '

'; } echo '

Consegnato al frontend:
' . esc_html($delivered_at ?: '-') . '

'; echo '
'; echo '

Conferma reale FE:
'; echo 'ID FE: ' . esc_html($remote_id ?: '-') . '
'; echo 'Tipo FE: ' . esc_html($remote_post_type ?: '-') . '' . ((int)$remote_is_attachment ? ' ATTACHMENT' : '') . '
'; echo 'Stato FE: ' . esc_html($remote_status ?: '-') . '
'; echo 'Ultimo sync FE: ' . esc_html($remote_last_sync ?: '-') . '
'; echo 'Verificato BE: ' . esc_html($remote_verified ?: '-') . '
'; echo 'Featured FE: ' . esc_html($remote_featured_ok !== '' ? ((int)$remote_featured_ok ? 'OK' : 'NO') : '-') . '
'; echo 'Verifica modifica content: ' . esc_html($remote_content_verified !== '' ? ((int)$remote_content_verified ? 'OK' : 'NO') : '-') . ''; echo '

'; echo '

Prova reale modifica:
' . esc_html($remote_verification_msg ?: '-') . '

'; echo '

Hash content BE/FE:
BE: ' . esc_html($remote_expected_hash ?: '-') . '
FE: ' . esc_html($remote_content_hash ?: '-') . '

'; if ($remote_legacy) { echo '

Articolo vecchio agganciato al FE
' . esc_html($remote_legacy) . '

'; } if ($remote_title) { echo '

Titolo FE ricevuto:
' . esc_html($remote_title) . '

'; } echo '

Endpoint FE:
' . esc_html($endpoint ?: 'NON DEFINITO') . '

'; echo '

Secret presente: ' . esc_html($secret_present) . '

'; echo '

Ultimo tentativo:
' . esc_html($last_try ?: 'Mai') . '

'; echo '

HTTP code:
' . esc_html($http_code ?: '-') . '

'; echo '

Errore tecnico:
' . esc_html($error ?: 'Nessuno') . '

'; echo '

Diagnosi:
' . nl2br(esc_html($diag ?: 'Nessuna diagnosi disponibile')) . '

'; echo '

Ultima risposta FE:
' . esc_html($response ?: '-') . '

'; if ($img_fail) { echo '

Problema immagine in evidenza:
' . esc_html($img_msg ?: 'Immagine non sincronizzata') . '

'; } $url = wp_nonce_url(admin_url('admin-post.php?action=sv_bridge_resend_post&post_id=' . (int)$post->ID), 'sv_bridge_resend_post_' . (int)$post->ID); echo '

Metti in coda / reinvia ora

'; $check_url = wp_nonce_url(admin_url('admin-post.php?action=sv_bridge_check_fe_post&post_id=' . (int)$post->ID), 'sv_bridge_check_fe_post_' . (int)$post->ID); echo '

Controlla stato FE / ripara coda

'; echo '

Il sistema evita invii duplicati quando il payload non è cambiato.

'; } public function handle_manual_resend() { if (!current_user_can('edit_posts')) wp_die('Permessi insufficienti'); $post_id = isset($_GET['post_id']) ? (int)$_GET['post_id'] : 0; check_admin_referer('sv_bridge_resend_post_' . $post_id); if (!$post_id || !get_post($post_id)) { wp_safe_redirect(admin_url('edit.php')); exit; } $post = get_post($post_id); if ($post && $this->maybe_reconcile_with_frontend($post_id, $post->post_type)) { $redirect = get_edit_post_link($post_id, 'url'); if (!$redirect) $redirect = admin_url('post.php?post=' . $post_id . '&action=edit'); $redirect = add_query_arg('sv_bridge_resend', 'verified', $redirect); wp_safe_redirect($redirect); exit; } delete_post_meta($post_id, self::META_LAST_SENT_HASH); $this->queue_post($post_id, true); $this->add_article_log($post_id, 'BE→FE', 'Forzatura manuale coda dal popup/metabox', ['utente' => wp_get_current_user()->user_login]); $redirect = get_edit_post_link($post_id, 'url'); if (!$redirect) $redirect = admin_url('post.php?post=' . $post_id . '&action=edit'); $redirect = add_query_arg('sv_bridge_resend', 'queued', $redirect); wp_safe_redirect($redirect); exit; } } if (!wp_next_scheduled('sv_bridge_be_process_queue') && function_exists('wp_get_schedules') && !isset(wp_get_schedules()['minute'])) { add_filter('cron_schedules', function($schedules) { $schedules['minute'] = ['interval' => 60, 'display' => 'Every Minute']; return $schedules; }); } new SV_Bridge_BE_Sender_Optimized(); Pio esposito https://www.cronachedellacampania.it/tag/pio-esposito/ Le ultime notizie LIVE dalla Campania Wed, 15 Oct 2025 17:22:43 +0000 it-IT hourly 1 https://www.cronachedellacampania.it/wp-content/uploads/2026/04/cropped-cronache_logo_rotondo_2026-dimensioni-medie-32x32.png Pio esposito https://www.cronachedellacampania.it/tag/pio-esposito/ 32 32 112696088 Golden Boy 2025: Yildiz, Pio Esposito e Leoni tra i 25 finalisti https://www.cronachedellacampania.it/2025/10/golden-boy-2025/ https://www.cronachedellacampania.it/2025/10/golden-boy-2025/#comments Wed, 15 Oct 2025 17:42:16 +0000 Torino – L’Italia alza la testa nella corsa al Golden Boy 2025, il premio che celebra ogni anno il miglior calciatore Under 21 d’Europa. Nell’elenco dei 25 finalisti spiccano tre nomi che rappresentano il futuro del calcio italiano: Kenan Yildiz della Juventus, Francesco Pio Esposito dell’Inter e Giovanni Leoni, difensore del...]]>

Torino – L’Italia alza la testa nella corsa al Golden Boy 2025, il premio che celebra ogni anno il miglior calciatore Under 21 d’Europa. Nell’elenco dei 25 finalisti spiccano tre nomi che rappresentano il futuro del calcio italiano: Kenan Yildiz della Juventus, Francesco Pio Esposito dell’Inter e Giovanni Leoni, difensore del Liverpool.

La lista è stata svelata alla Badia di Sant’Andrea, sulla collina degli Erzelli di Genova, durante la presentazione ufficiale dell’“European Golden Boy – Absolute Best 2025”. L’elenco include i 20 giocatori con il punteggio più alto nel Golden Boy Football Benchmark Index – un sistema che misura le prestazioni dei giovani talenti europei sulla base di dati oggettivi come minutaggio, rendimento in club e Nazionale e valore di mercato – a cui si aggiungono cinque “wild card” scelte tra le posizioni dalla 21 alla 100.

A contendersi il trono, oltre ai tre italiani, ci saranno nomi pesanti come Pau Cubarsì del Barcellona, Desiré Doué e Warren Zaire-Emery del PSG, e i gioielli del Real Madrid Arda Güler, Dean Huijsen e Franco Mastantuono. Francia e Inghilterra dominano la lista con cinque candidati ciascuna, seguite da Spagna, Italia, Turchia e Portogallo.

Con 9 giocatori, la Premier League è il campionato più rappresentato, davanti a Liga, Ligue 1 e Primeira Liga. Paris Saint-Germain e Real Madrid vantano il maggior numero di finalisti – tre a testa – a conferma della loro supremazia nella formazione di giovani talenti. Il valore complessivo dei 25 candidati supera 1,25 miliardi di euro, un dato che testimonia la ricchezza e il potenziale della nuova generazione calcistica.

Nel corso dell’evento sono state annunciate anche le 10 finaliste del Golden Girl, il riconoscimento dedicato alla miglior calciatrice Under 21 d’Europa, presentate da Marta Carissimi, responsabile del settore femminile del Genoa. A impreziosire la serata gli interventi di Patrick Vieira, allenatore del club rossoblù, e del CEO Andrés Blázquez, che hanno sottolineato l’importanza delle strutture e dei modelli di sviluppo per far crescere i campioni del domani. La giuria internazionale composta da 50 giornalisti europei decreterà il vincitore a novembre, durante una conferenza stampa in cui saranno annunciati anche gli altri premi speciali.

]]>
https://www.cronachedellacampania.it/2025/10/golden-boy-2025/feed/ 1 797602
Pio Esposito si racconta: “Stagione molto positiva per me, ma non mi monto la testa” https://www.cronachedellacampania.it/2025/07/pio-esposito-si-racconta/ https://www.cronachedellacampania.it/2025/07/pio-esposito-si-racconta/#respond Tue, 01 Jul 2025 17:13:31 +0000 https://www.cronachedellacampania.it/?p=780936 C’è chi parla di numeri, chi di talento e chi di futuro. Pio Francesco Esposito, invece, parla di lavoro e di fiducia. Ospite del format Box to Box in onda sui canali di Che Fatica La Vita Da Bomber, il giovane attaccante si è raccontato senza filtri, mostrando la consapevolezza...]]>

C’è chi parla di numeri, chi di talento e chi di futuro. Pio Francesco Esposito, invece, parla di lavoro e di fiducia. Ospite del format Box to Box in onda sui canali di Che Fatica La Vita Da Bomber, il giovane attaccante si è raccontato senza filtri, mostrando la consapevolezza di chi sa da dove è partito e quanto conti ogni singolo passo.

“È stata una stagione molto positiva, sia a livello individuale che di squadra. E sono due cose che, nel mio ruolo, vanno di pari passo. Quando la squadra gira, è più facile fare gol e mettersi in mostra”, ha spiegato Esposito. Non dimentica però il punto di partenza, quel primo anno tra i professionisti in cui l’unico obiettivo era restare a galla.

“Giocavamo per non retrocedere, e non riuscivo a esprimermi al meglio. Ora ho fiducia, e tutto diventa più naturale”. Il cambiamento è stato graduale ma profondo: dal peso della pressione alla leggerezza ritrovata grazie al lavoro quotidiano. E senza mai perdere la bussola: “Sto facendo meglio, ma non ho motivo di montarmi la testa. Faccio semplicemente ciò che amo. Mi alleno, do tutto, e se va bene… tanto meglio”.

In un panorama che tende troppo in fretta a bruciare giovani promesse, le parole di Esposito hanno il peso raro dell’equilibrio. L’ambizione c’è, la fame pure, ma prima di tutto viene la concretezza. E per chi lo osserva da vicino, la sensazione è che il meglio debba ancora arrivare.

]]>
https://www.cronachedellacampania.it/2025/07/pio-esposito-si-racconta/feed/ 0 780936
Il sogno di Pio Esposito: “Gol al River? E’ successo tutto velocemente, non ho ancora realizzato” https://www.cronachedellacampania.it/2025/06/il-sogno-di-pio-esposito/ https://www.cronachedellacampania.it/2025/06/il-sogno-di-pio-esposito/#comments Thu, 26 Jun 2025 09:06:58 +0000 https://www.cronachedellacampania.it/?p=780178 Lo ha vissuto come un sogno, uno di quelli che ti scuotono l’anima ma di cui ti rendi conto solo dopo, quando il cuore rallenta e il boato si spegne. Pio Esposito ha ancora negli occhi l’attimo in cui ha gonfiato la rete contro il River Plate, al Mondiale per...]]>

Lo ha vissuto come un sogno, uno di quelli che ti scuotono l’anima ma di cui ti rendi conto solo dopo, quando il cuore rallenta e il boato si spegne. Pio Esposito ha ancora negli occhi l’attimo in cui ha gonfiato la rete contro il River Plate, al Mondiale per club, ma confessa che non è riuscito a realizzare davvero cosa sia successo: “È stato tutto così veloce. Quando ho segnato non ci credevo, mi sono girato e ho visto Lautaro e gli altri correre verso di me. Lì ho capito che era reale”, ha detto ai microfoni di Inter Tv, col volto ancora acceso dall’adrenalina.

Per il giovane attaccante nerazzurro è il secondo sogno realizzato in pochi giorni, un’accelerazione che lo sta proiettando nell’élite del calcio con la naturalezza di chi ha talento ma anche fame e spalle larghe. “Sono orgogliosissimo, ringrazio il mister per la fiducia e tutti i compagni per come mi hanno accolto. Sì, mi sono gasato, eccome: queste sono le partite che preferisco, dove si lotta, dove posso usare le mie armi migliori come protezione palla e sponde”.

Esposito racconta anche il momento esatto del gol, un’azione letta e vissuta con la lucidità di un attaccante maturo: “Ho visto Peter fare una grande giocata, ho capito che la palla sarebbe arrivata veloce. L’ho stoppata e orientata subito sul destro. A tu per tu ho deciso di aprire il tiro, è andata bene”. E poi, come un vero interista, guarda già avanti: “Siamo pronti a battagliare ancora, vogliamo passare il turno e vincere sempre”.

]]>
https://www.cronachedellacampania.it/2025/06/il-sogno-di-pio-esposito/feed/ 1 780178
Inter, la favola dello stabiese Pio Esposito: il gol decisivo contro il River Plate al Mondiale per Club porta i nerazzurri agli ottavi https://www.cronachedellacampania.it/2025/06/inter-pio-esposito/ https://www.cronachedellacampania.it/2025/06/inter-pio-esposito/#comments Thu, 26 Jun 2025 08:56:01 +0000 https://www.cronachedellacampania.it/?p=780175 Ha solo 19 anni ma gioca, lotta e parla come un veterano. Lo stabiese Pio Esposito si è presentato al grande pubblico internazionale nel modo più rumoroso possibile, da titolare con la maglia dell’Inter nella sua prima vera uscita “tra i grandi”. E lo ha fatto lasciando un’impronta che a...]]>

Ha solo 19 anni ma gioca, lotta e parla come un veterano. Lo stabiese Pio Esposito si è presentato al grande pubblico internazionale nel modo più rumoroso possibile, da titolare con la maglia dell’Inter nella sua prima vera uscita “tra i grandi”. E lo ha fatto lasciando un’impronta che a Milano – e non solo – ha fatto girare parecchie teste.

Contro l’Argentina, in una sfida fisica, sporca, in cui molti talenti si sono sciolti, Pio è rimasto in piedi, lottando come un guerriero e segnando come un attaccante vero. Il suo gol ha zittito l’ardore dei sudamericani e messo in chiaro che lui non è solo una promessa, ma una certezza in costruzione. Mentre tutti aspettavano l’argentino Mastantuono – acquistato dal Real Madrid per 63 milioni – a prendersi la scena è stato proprio lui, il più giovane della famiglia Esposito.

Nel dopopartita, emozionato ma lucido, il centravanti nerazzurro ha raccontato: «Sapevo che sarebbe arrivata l’occasione, ho immaginato quel gol mille volte nei sogni. È stata la giornata più emozionante della mia vita». Accanto a lui, in platea, c’era suo fratello Sebastiano ad ascoltarlo, testimone di una serata che potrebbe diventare un punto di svolta per la carriera del ragazzo cresciuto nella cantera dell’Inter.

Il club vuole tenerlo stretto e punta su di lui per costruire l’attacco del futuro, ma sul tavolo ci sono due strade: un prestito in Serie A – Fiorentina e Bologna lo seguono con attenzione – oppure la permanenza in nerazzurro, per giocarsi il posto accanto alla ThuLa, magari contendendo minuti a Bonny. Cristian Chivu, che lo conosce da quando aveva 13 anni, lo vuole valutare bene nel ritiro estivo prima di decidere con la società il percorso ideale. Agosto sarà il mese della scelta.

]]>
https://www.cronachedellacampania.it/2025/06/inter-pio-esposito/feed/ 1 780175
Lo stabiese Pio Esposito: “Inter il mio sogno, pronto a dare tutto” https://www.cronachedellacampania.it/2025/06/pio-esposito-inter/ https://www.cronachedellacampania.it/2025/06/pio-esposito-inter/#comments Wed, 25 Jun 2025 09:36:45 +0000 https://www.cronachedellacampania.it/?p=779977 Da Castellammare di Stabia a Seattle, passando per undici anni di sudore e passione nelle giovanili dell’Inter. Francesco Pio Esposito ha vissuto la notte che aspettava da una vita: il debutto in prima squadra. È successo nella semifinale del Mondiale per Club contro gli Urawa Red Diamonds, quando il tecnico...]]>

Da Castellammare di Stabia a Seattle, passando per undici anni di sudore e passione nelle giovanili dell’Inter. Francesco Pio Esposito ha vissuto la notte che aspettava da una vita: il debutto in prima squadra.

È successo nella semifinale del Mondiale per Club contro gli Urawa Red Diamonds, quando il tecnico nerazzurro lo ha chiamato in campo al posto del fratello Sebastiano. Un simbolo, un segnale, un momento che profuma di destino. “Sono orgoglioso, un sogno che rincorro da quando ero bambino e indossavo questa maglia nelle giovanili”, racconta Pio con gli occhi lucidi e la voce decisa di chi non si accontenta.

Adesso il pensiero va alla sfida contro il River Plate. E lui vuole esserci, anche solo per un minuto, pronto a lasciare ogni goccia di sudore sul prato: “Darò tutto, sempre. Ogni allenamento, ogni palla. Il gol? Un altro sogno da rincorrere”. Con lui, c’è Sebastiano. Compagni di stanza, fratelli sul campo, alleati nella vita. “Ci confrontiamo, non parliamo solo di calcio, ma lui mi dà consigli utili: ci è già passato, sa cosa vuol dire esordire con questa maglia”.

E mentre studia i movimenti del capitano Lautaro Martinez, di cui dice di voler apprendere tutto, dalla grinta alla tecnica, Pio sa bene cosa gli chiede il mister. “Chivu mi ha detto di restare me stesso: occupare l’area, proteggere palla, dare profondità. È il mio gioco, è il mio modo di aiutare i compagni”. Ora testa al River, alla squadra argentina che corre, picchia e non regala nulla: “Siamo pronti a giocarcela fino in fondo, vogliamo andare avanti e ce la metteremo tutta. Con il cuore e con la maglia”.

]]>
https://www.cronachedellacampania.it/2025/06/pio-esposito-inter/feed/ 1 779977
Vico Equense accende i riflettori: al via il Social World Film Festival 2025 https://www.cronachedellacampania.it/2025/06/vico-equense-accende-i-riflettori-al-via-il-social-world-film-festival-2025/ https://www.cronachedellacampania.it/2025/06/vico-equense-accende-i-riflettori-al-via-il-social-world-film-festival-2025/#comments Mon, 23 Jun 2025 08:25:32 +0000 https://www.cronachedellacampania.it/?p=779668 È partita il 22 giugno, nella suggestiva cornice di Vico Equense, la 15ª edizione del Social World Film Festival. Sotto la direzione di Giuseppe Alessio Nuzzo, l’evento – in programma fino al 29 giugno – rende omaggio ad Alida Valli, icona senza tempo del cinema. La sua carriera e l’impronta...]]>

È partita il 22 giugno, nella suggestiva cornice di Vico Equense, la 15ª edizione del Social World Film Festival. Sotto la direzione di Giuseppe Alessio Nuzzo, l’evento – in programma fino al 29 giugno – rende omaggio ad Alida Valli, icona senza tempo del cinema.

La sua carriera e l’impronta artistica sono celebrate mediante una retrospettiva curata da Rai Teche, accompagnata da una mostra fotografica realizzata in collaborazione con la Fondazione Centro Sperimentale di Cinematografia – Cineteca Nazionale.

L’edizione di quest’anno si distingue come la più internazionale di sempre, con ospiti di grande prestigio.

Tra i titoli di punta spicca il ritorno di Abel Ferrara con Turn in the Wound, documentario sull’Ucraina insignito del riconoscimento Film sociale dell’anno. L’opera, intensa e profondamente attuale, applaudita alla Berlinale, verrà presentata per la prima volta in Italia proprio al Social World Film Festival: una scelta che conferma la volontà della rassegna di dare spazio a un cinema capace di scuotere le coscienze e aprire riflessioni globali.

“Il suo documentario sull’Ucraina è stato considerato molto forte durante l’anteprima stampa a Roma: in quell’occasione disse che ‘siamo ormai nella terza guerra mondiale’, e questo prima ancora che si sapessero degli attacchi degli Stati Uniti,” ha commentato il direttore del festival Giuseppe Alessio Nuzzo, intervenendo ieri pomeriggio durante la conferenza stampa ospitata nella prestigiosa cornice del Castello Giusso.

Attesissimo ospite della scena internazionale: Giancarlo Esposito, celebre per il suo ruolo in Breaking Bad, insignito del riconoscimento di miglior attore internazionale. Spazio anche all’intrattenimento con una serata speciale targata Disney incentrata sulla figura di Captain America e sul potenziale impatto sociale dei grandi blockbuster.

Ad impreziosire ulteriormente il festival sarà la presenza di Ben Stiller, scelto come padrino di questa edizione e presentato in anteprima durante la conferenza stampa di Napoli. Non mancheranno i protagonisti del cinema italiano: Edoardo Leo e Maurizio De Giovanni riceveranno il Premio alla Carriera, mentre Sergio Rubini celebrerà proprio a Vico Equense i suoi 40 anni di attività artistica.

Ad inaugurare ufficialmente la Mostra Internazionale del Cinema Sociale, diretta da Giuseppe Alessio Nuzzo, La stanza accanto, il nuovo atteso film di Pedro Almodóvar, al Cinema Aequa.

La cerimonia del taglio del nastro si è invece tenuta all’Arena Fellini, nel suggestivo chiostro delle SS. Trinità e Paradiso, accompagnata dall’emozionante concerto CineSax, eseguito dalla Duæ Siciliæ Saxophone Ensemble del Conservatorio San Pietro a Majella di Napoli. «Un’orchestra di 15 elementi, composta esclusivamente da sassofoni, che ci allieterà con un concerto di musica da film completamente eseguita in sax.

L’abbiamo ribattezzata CineSax ed è un’idea originale che ci piaceva portare al Social World Film Festival», ha spiegato il direttore del festival Giuseppe Alessio Nuzzo durante la conferenza stampa.

Ospite della prima giornata è stato Francesco Panarella, noto per il suo ruolo nella serie Mare Fuori, che in serata ha introdotto la proiezione fuori concorso del cortometraggio Interference, di cui è protagonista.

Nel film interpreta un giovane alle prese con la depressione, che cerca di evaderla attraverso dirette streaming: una via di fuga che si rivela presto dannosa, a causa dei giudizi negativi ricevuti online. Nel pomeriggio, l’attore si è anche intrattenuto con il pubblico più giovane partecipando a una coinvolgente masterclass presso la Sala Lollobrigida del Cinema Aequa.

Durante l’incontro con i giovani al festival, l’attore Francesco Panarella ha condiviso con sincerità il proprio percorso, raccontando come la passione per la recitazione sia nata dal desiderio di trasformare la vulnerabilità in forza. Ha parlato delle difficoltà affrontate da ragazzo, del rapporto con la fama e del valore della pazienza e dell’empatia nel mestiere dell’attore.

Con uno stile ironico e diretto, ha sottolineato l’importanza di restare autentici, di trattare tutti con rispetto sul set e di usare la visibilità per generare qualcosa di buono: “Se riesco a far sentire meno solo anche solo uno spettatore, per me è una vittoria.”

Il festival dedica ampio spazio ai registi in concorso nelle sezioni Focus e Documentari, che presentano opere intense e profondamente radicate nel sociale. Da Fermata “Le Madonelle”di Lorenzo Cammisa, ispirato al massacro di Ponticelli, al documentario NoteItaliane di Adelmo Togliani, omaggio al compositore E. A. Mario; passando per Terramara di Simone Romano, denuncia poetica sul caporalato nella Piana del Fucino, fino a Nottefonda di Giuseppe Miale di Mauro, racconto intimo e struggente sulla solitudine e il lutto.

Storie diverse, ma unite da uno sguardo consapevole e necessario sul nostro tempo.
Tra grandi nomi internazionali, testimonianze toccanti e storie che non temono di guardare negli angoli più scomodi dell’esistenza, il festival costruisce un vero e proprio ponte tra spettacolo e realtà consapevole. E siamo solo all’inizio di una settimana che si preannuncia carica di emozioni, riflessioni e nuove scoperte.

Il programma completo della manifestazione, e le modalità di partecipazione agli eventi, sono reperibili al seguente link: https://socialfestival.com/it/programma

]]>
https://www.cronachedellacampania.it/2025/06/vico-equense-accende-i-riflettori-al-via-il-social-world-film-festival-2025/feed/ 1 779668
Taormina Film festival : premiato il talento napoletano di Giovanni Esposito ed Alessandro Siani https://www.cronachedellacampania.it/2025/06/taormina-film-festival-premiato-il-talento-napoletano-di-giovanni-esposito-ed-alessandro-siani/ https://www.cronachedellacampania.it/2025/06/taormina-film-festival-premiato-il-talento-napoletano-di-giovanni-esposito-ed-alessandro-siani/#comments Sat, 14 Jun 2025 17:46:48 +0000 https://www.cronachedellacampania.it/?p=778431 Nel suggestivo scenario del Teatro Antico di Taormina, il 12 giugno scorso, si è svolta uno dei momenti più attesi della 71ª edizione del Taormina Film Festival (10–14 giugno 2025). Quando il sipario si è alzato al suggestivo Teatro Antico per la 71ª edizione del Taormina Film Festival, l’estate ha...]]>

Nel suggestivo scenario del Teatro Antico di Taormina, il 12 giugno scorso, si è svolta uno dei momenti più attesi della 71ª edizione del Taormina Film Festival (10–14 giugno 2025).

Quando il sipario si è alzato al suggestivo Teatro Antico per la 71ª edizione del Taormina Film Festival, l’estate ha trovato il suo apice. Ma è stata la scintilla partenopea di Alessandro Siani e Giovanni Esposito a rubare la scena, conquistando applausi e premi con cuore e passione.

Durante la cerimonia sono stati conferiti prestigiosi riconoscimenti a due figure simbolo del panorama napoletano:

Alessandro Siani — Taormina Best Performance Award

Alessandro Siani, noto attore, regista e comico, ha ritirato il Taormina Best Performance Award in riconoscimento della sua carismatica interpretazione e dell’arte dell’improvvisazione. In un video pubblicato da La Sicilia, Siani ha scherzato sul fatto che “stasera dovrò fare qualcosa – improvvisare rende una performance completa”. Il premio valorizza il suo contributo al cinema e al teatro, conferendo ulteriore lustro alla sua carriera.

Giovanni Esposito — Taormina Award per “Benvenuti in casa Esposito”

Giovanni Esposito, attore poliedrico napoletano e interprete principale del nuovo spettacolo Benvenuti in casa Esposito diretto da Alessandro Siani, ha ricevuto il Taormina Award per “Benvenuti in casa Esposito”, un riconoscimento dedicato ai progetti teatrali originali . Esposito, noto per le sue interpretazioni televisive e teatrali, ha consolidato ulteriormente la sua reputazione come figura di spicco delle arti e della cultura italiana

Parole dal palco

Tra risate, emozione e intenzioni future, Siani ha confessato che “quella sera avrebbe improvvisato qualcosa”, invitando il pubblico a rilassarsi e sospendere ogni aspettativa.

Mentre Giovanni  Esposito,  ha ringraziato colleghi e spettatori, sottolineando quanto sia speciale raccontare storie che parlano di casa, affetti e autenticità umana.


Perché questa premiazione ha colpito al cuore

  • Doppio orgoglio partenopeo: due artisti napoletani premiati a un evento internazionale, simbolo di un riscatto culturale.

  • Il teatro che parla al cuore: Benvenuti in casa Esposito è l’esempio di una narrazione che mescola leggerezza e sentimento, facendo rispecchiare ognuno di noi.

  • Un festival che unisce mondi diversi: grandi nomi internazionali e voci italiane emergenti insieme per lasciare il segno.


Nella foto Giovanni Esposito ed Alessandro Siani al Taormina Film Festival 2025

Contesto dell’evento

Il festival, diretto da Tiziana Rocca, ha visto la partecipazione di numerose star internazionali – da Martin Scorsese a Michael Douglas, Catherine Deneuve e Monica Bellucci – e della folta schiera di nomi italiani, tra cui Luisa Ranieri e Salvatore Esposito . Tra i premi individuali consegnati in quella stessa serata figuravano riconoscimenti accademici e artistici di prestigio: da Scorsese (Lifetime Achievement) a Douglas, Deneuve, Bellucci, fino ai premi musicali e speciali.

La cerimonia all’Antico Teatro ha unito la magia del teatro greco-romano con il glamour del cinema internazionale, offrendo una vetrina unica alla creatività italiana.


 Riflessioni finali

La doppia premiazione di Siani e Esposito rappresenta un riconoscimento al talento e alla capacità di rinnovare la cultura teatrale e cinematografica partenopea. Siani, ambasciatore di comicità e sensibilità, ed Esposito, interprete autentico del tessuto sociale e familiare, sono stati celebrati nel cuore di Taormina, suggellando un momento di orgoglio per il teatro e il cinema italiani.

]]>
https://www.cronachedellacampania.it/2025/06/taormina-film-festival-premiato-il-talento-napoletano-di-giovanni-esposito-ed-alessandro-siani/feed/ 1 778431
Capri rivoluziona l’accoglienza turistica: funicolare prioritaria per ospiti hotel e nuovi Info Point https://www.cronachedellacampania.it/2025/06/capri-rivoluziona-laccoglienza-turistica/ https://www.cronachedellacampania.it/2025/06/capri-rivoluziona-laccoglienza-turistica/#comments Sun, 08 Jun 2025 07:15:11 +0000 https://www.cronachedellacampania.it/?p=777367 Capri  – L’isola di Capri lancia nuove misure per migliorare l’esperienza dei visitatori. Dal 1° giugno è attivo un servizio potenziato di accoglienza e informazione turistica al porto, frutto della collaborazione tra l’Assessorato al Turismo del Comune di Capri, Federalberghi Isola di Capri e la società Porto Turistico di Capri....]]>

Capri  – L’isola di Capri lancia nuove misure per migliorare l’esperienza dei visitatori. Dal 1° giugno è attivo un servizio potenziato di accoglienza e informazione turistica al porto, frutto della collaborazione tra l’Assessorato al Turismo del Comune di Capri, Federalberghi Isola di Capri e la società Porto Turistico di Capri.

Tra le novità più significative, un accesso prioritario alla funicolare per gli ospiti degli hotel e l’installazione di nuovi punti informativi.

Meno code, più informazioni: le novità a Marina Grande

Presso il porto commerciale di Marina Grande sono stati installati nuovi punti informativi con la presenza di hostess dedicate all’assistenza dei visitatori. Contestualmente, è stato implementato un sistema di canalizzazione per il trasporto pubblico, inclusa la funicolare e i taxi, con transennamenti mirati a ottimizzare i flussi nei momenti di maggiore afflusso.

La grande novità è l’introduzione di un accesso prioritario alla funicolare, che da quest’anno non sarà più riservato solo ai residenti, ma anche agli ospiti delle strutture alberghiere dell’isola. Per usufruire di questa corsia preferenziale, gli ospiti dovranno esibire una conferma di prenotazione o un biglietto fornito direttamente dal loro hotel.

Un’esperienza “più consapevole” per il turismo alberghiero

“Con questa iniziativa intendiamo intervenire concretamente per migliorare l’esperienza dei nostri ospiti nella zona dell’isola che più di tutte soffre del congestionamento, soprattutto nelle ore di punta”, ha dichiarato l’Assessore al Turismo Melania Esposito.

“Lo facciamo anche attraverso una scelta chiara che mira a tutelare in particolare gli ospiti che, soggiornando nelle strutture alberghiere dell’isola, puntano ad un’esperienza più consapevole del nostro territorio per poterne apprezzare in toto il ricco patrimonio fatto di cultura, natura, arte e bellezza”.

Lorenzo Coppola, presidente di Federalberghi Isola di Capri, ha aggiunto: “Siamo entusiasti di partecipare, concretamente, al potenziamento dei servizi di accoglienza al porto, attraverso il contributo di ragazze appositamente formate. Lo facciamo nella convinzione che il primo benvenuto sull’isola debba essere chiaro, efficiente e all’altezza dell’ospitalità caprese.”

Un passo avanti per la vivibilità di Marina Grande

Il progetto si inserisce in un più ampio programma di interventi volti a migliorare la vivibilità e l’accessibilità del borgo marinaro di Marina Grande. “A piccoli passi possiamo dire di esserci avviati nella giusta direzione. Da anni Marina Grande non vedeva così tanti interventi, e non ci fermeremo qui”, ha commentato il Consigliere Delegato Mirko Avelino, forte sostenitore dell’iniziativa.

Questo nuovo servizio rappresenta un passo concreto per ridurre l’impatto negativo del sovraffollamento e garantire una migliore qualità dell’accoglienza per tutti coloro che scelgono di visitare e soggiornare sull’isola, promettendo un’esperienza più fluida e piacevole sin dall’arrivo.

]]>
https://www.cronachedellacampania.it/2025/06/capri-rivoluziona-laccoglienza-turistica/feed/ 1 777367
Festival Internazionale del Cinema di Pompei : la scultura vivente di Domenico Sepe inaugura la serata finale https://www.cronachedellacampania.it/2025/06/festival-cinema-pompie-domenico-sepe-scultura-vivente/ https://www.cronachedellacampania.it/2025/06/festival-cinema-pompie-domenico-sepe-scultura-vivente/#comments Fri, 06 Jun 2025 13:56:26 +0000 https://www.cronachedellacampania.it/?p=777069 Tra le rovine senza tempo di Pompei, dove ogni pietra conserva il racconto di civiltà perdute, prende vita il Festival Internazionale del Cinema di Pompei. Nella sua prima edizione, questa manifestazione fonde magistralmente cinema, arte contemporanea e patrimonio archeologico. Guidato da Direttore artistico Enrico Vanzina insieme all’instancabile impegno della presidente...]]>

Tra le rovine senza tempo di Pompei, dove ogni pietra conserva il racconto di civiltà perdute, prende vita il Festival Internazionale del Cinema di Pompei. Nella sua prima edizione, questa manifestazione fonde magistralmente cinema, arte contemporanea e patrimonio archeologico.

Guidato da Direttore artistico Enrico Vanzina insieme all’instancabile impegno della presidente Annarita Borelli, il Festival ha già attirato l’attenzione per la sua capacità di esaltare Pompei come palcoscenico d’eccellenza: dalle proiezioni ai workshop, dalle sfilate di moda alle performance artistiche. Tuttavia, sarà la serata conclusiva di domenica 8 giugno, ospitata nel suggestivo Teatro Grande del Parco Archeologico, a segnare il vero culmine dell’evento, con la performance dal vivo dello scultore contemporaneo Domenico Sepe.


Performance live: la scultura vivente ispirata al Fauno danzante di Pompei

Il Teatro Grande di Pompei si trasformerà in un palcoscenico rituale, dove materia e gesto dialogheranno dal vivo. In apertura, Domenico Sepe, artista di fama nazionale e internazionale, proporrà una “scultura vivente” ispirata al celebre Fauno danzante, capolavoro classico di Pompei simbolo di energia, danza e rinascita.

  • Tecnica e spettacolo: Sepe utilizzerà argilla, leghe metalliche e pigmenti terrosi per dar forma al corpo di un modello in posa. Ogni movimento, ogni palpito del cuore, diventerà atto creativo: il corpo umano si trasformerà progressivamente in statua, in un susseguirsi di gesti plastici che cattureranno lo sguardo degli spettatori.
  • Significato simbolico: Attraverso questa performance, l’artista celebra la resilienza dell’arte: il Fauno – spirito di vita nella Pompei antica – ritorna, rievocato in un rito contemporaneo, a ricordarci che anche dalle macerie può nascere la bellezza.

Grazie all’ottima risonanza ottenuta sui social nelle settimane precedenti, l’hashtag #SculturaViventePompei è già trend tra gli appassionati di arte, turismo culturale e cinefili: un’assicurazione virtuale che l’evento sarà al centro dell’attenzione internazionale.


I premi scultorei di Domenico Sepe: bronzo, lava e rinascita artistica

Non solo performance: Domenico Sepe ha firmato anche le opere-premio che verranno consegnate ai protagonisti del Festival. Due varianti, entrambe ricche di riferimenti simbolici, hanno catturato l’interesse dei collezionisti e dei critici d’arte:

  1. Premio in effetto bronzo
    • Rappresenta il Fauno che emerge dalla pietra lavica del Vesuvio, come metafora della forza creativa che sgorga dall’abisso della memoria.
    • Questo riconoscimento è già nelle mani di Marco Risi, che con il suo film Fortapàsc ha lasciato un segno indelebile nella storia del cinema italiano.
  2. Bassorilievo della rinascita
    • Un’opera in rilievo che suggerisce, attraverso linee fluide e contrasti di superfici, il concetto di “rinascita artistica”.
    • Sarà consegnato al termine della serata ai vincitori delle categorie principali della competizione cinematografica.

Questi premi, espressione di una scultura contemporanea che dialoga con il contesto archeologico, costituiscono un ponte ideale tra passato e presente, enfatizzando la capacità del Festival Internazionale del Cinema di Pompei di unire mondi lontani nel segno della creatività.


Ospiti d’eccezione e cerimonia di premiazione: da Neri Parenti a Luca Ward

La diretta della serata finale sarà affidata al noto attore e conduttore Sergio Assisi, che, con garbo e simpatia, guiderà il pubblico attraverso momenti di cinema, spettacolo e musica. Non mancheranno gli interventi di:

  • Enrico Vanzina (Direttore Artistico), che ripercorrerà le tappe salienti del Festival e darà spazio alle curiosità sul dietro le quinte.
  • Annarita Borelli (Presidente), che illustrerà le prospettive future e i progetti culturali legati al Parco Archeologico di Pompei.

Premi alla Carriera

Sul palco saliranno due autentici pilastri del cinema e del doppiaggio italiano:

  • Neri Parenti
    • Maestro della commedia cinematografica, autore di pellicole capaci di conquistare platee intere nel corso degli ultimi tre decenni.
    • Neri Parenti riceverà da Domenico Sepe un’opera scultorea in bronzo, raffigurante il Fauno in un gesto di danza, quale riconoscimento per la sua straordinaria capacità di raccontare l’Italia attraverso la leggerezza dell’ironia.
  • Luca Ward
    • Attore e doppiatore dalla voce “epica” e inconfondibile, celebre per aver prestato la propria interpretazione vocale a grandi icone internazionali come Russell Crowe e Keanu Reeves.
    • Il premio per Luca Ward sarà un bassorilievo inedito, che sintetizza l’idea di “rinascita sonora” e il potere evocativo del suono nell’ambito cinematografico.

Ospiti, star e personalità del Festival

Durante la serata sfileranno volti noti del cinema, della televisione e della musica italiana, tra cui:

  • Gianni Amelio (regista pluripremiato), che condividerà riflessioni sul ruolo del patrimonio culturale come fonte d’ispirazione per il grande schermo.
  • Amedeo Minghi (cantautore), che offrirà alcune delle sue note emozionanti per sottolineare l’importanza della colonna sonora nel racconto filmico.
  • Alessandro Cecchi Paone (giornalista e divulgatore), introdurrà brevi segmenti di approfondimento sul tema “Cinema e archeologia”.
  • Gino Rivieccio (attore e cabarettista), condividerà aneddoti divertenti dal backstage di celebri produzioni cinematografiche.
  • Maria Sole Pollio e Susy Del Giudice (attrici emergenti), interpreteranno frammenti di dialoghi tratti da pellicole in concorso.
  • Contributi di Giovanni Esposito, Anna Gaia Sole, Salvatore Lioniello, Mery Esposito, Antonella Morea, Alessandra Ierse, Paulitti Italico, Nazareno Nicoletti, Dario Formisano, Pierfrancesco Aiello, Emilia Fanetti Scarpati, Emilio Fallarino, Donatella Zaccagnini, Romina Capuana, Fiorenza D’Antonio, Manfredi Russo.

A impreziosire l’atmosfera, il gruppo vocale Soul Food Vocalist, con le sue armonie spiritual e gospel, accompagnerà i momenti clou della serata, trasformando il Teatro Grande in un luogo di fusione tra arti contemporanee e vibrazioni ancestrali.


Opera monumentale di Domenico Sepe: l’arte che dialoga con l’archeologia

Per rendere omaggio all’essenza stessa di Pompei, il Festival esporrà un’opera monumentale inedita di Sepe all’interno del Parco Archeologico, proprio a pochi metri dal Teatro Grande.

  • Descrizione e significato: Un’installazione di grandi dimensioni, realizzata con elementi di pietra vesuviana e materiali sintetici trasparenti, che crea un effetto visivo di “emersione dal sottosuolo”.
  • Connessione con il territorio: L’opera richiama il mistero dei ritrovamenti archeologici – vasi frammentati, calchi di corpi, tracce di affreschi – rivisitandoli in chiave moderna. Diventa così un simbolo di continuità tra passato e presente, illustrando come l’arte contemporanea possa trasformare le rovine in prospettive creative.

Call to action per i visitatori

L’installazione resterà visibile fino al tramonto, invitando gli appassionati di architettura e i turisti a scattare fotografie e condividere l’esperienza sui social. L’hashtag ufficiale per chi vorrà partecipare alla conversazione digitale è #ArtePompei2025.


L’ultima notte del festival: tra mito, arte e suggestioni sensorali

La conclusione del Festival Internazionale del Cinema di Pompei sarà un’esplosione di suggestioni: la scultura vivente, i premi scultorei, gli omaggi a Parenti e Ward, i monologhi e gli interventi musicali si intrecceranno in un percorso emozionale che coinvolgerà tutti i cinque sensi.

  • Visivo: L’osservazione del Fauno prendere forma, l’installazione monumentale a cielo aperto, le luci studiati per esaltare i profili delle statue.
  • Uditivo: Le note struggenti di Minghi, i canti spiritual del coro, il doppiaggio epico di Luca Ward.
  • Tattile e cinestetico: Il pubblico potrà avvicinarsi allo spazio scenico, percepire la texture dell’argilla e della pietra durante la performance di Sepe, come un invito a sentire con mano la nascita dell’opera.
  • Emotivo: I racconti di Amelio sul valore della memoria, i sorrisi di Parenti che rievocano l’importanza del sorriso nel cinema, le parole intrise di passione di Borelli e Vanzina sul futuro culturale di Pompei.

Un’esperienza multimediale e interattiva

Per chi non potesse partecipare di persona, il Festival ha predisposto una diretta streaming sui canali ufficiali, accompagnata da una realtà aumentata che permetterà agli spettatori da remoto di “entrare” idealmente all’interno del Parco Archeologico e di ammirare l’opera monumentale di Sepe in 3D.

Chi seguirà l’evento online potrà interagire con un chatbot dedicato, ricevere informazioni sui film in concorso e partecipare a un sondaggio in tempo reale per votare la “Performance d’arte preferita”.


I numeri del successo: turismo culturale e risonanza mediatica

Il Festival Internazionale del Cinema di Pompei non è soltanto un evento artistico, ma un volano per turismo culturale e promozione integrata del territorio campano.

  • Partecipazione record: Oltre 2.500 presenze nelle prime tre giornate, con spettatori provenienti da tutta Italia e dall’Europa, affascinati dall’idea di unire visita archeologica e cinema d’autore.
  • Coinvolgimento digitale: Più di 10.000 interazioni sui social (like, condivisioni, commenti) per i contenuti legati a Domenico Sepe e alle performance in programma.
  • Collaborazioni istituzionali: Partnership con il MiC (Ministero della Cultura), la Regione Campania, il Parco Archeologico di Pompei e diversi istituti universitari, tra cui l’Università degli Studi di Napoli Federico II e la Seconda Università degli Studi di Napoli, per workshop di sceneggiatura e restauri virtuali.

Conclusioni: Pompei, crocevia di arte, cinema e identità culturale

La serata del 8 giugno 2025 al Teatro Grande rappresenta l’essenza stessa di ciò che il Festival Internazionale del Cinema di Pompei intende trasmettere: un dialogo aperto tra il mito e la modernità, tra le rovine millenarie e la creatività contemporanea.

  • Domenico Sepe, con la sua scultura vivente e i premi in bronzo e lava, ha saputo infondere un’insolita vitalità alle pietre del passato, trasformando il processo creativo in un gesto collettivo.
  • Neri Parenti e Luca Ward, ricevendo il Premio alla Carriera, hanno coronato storie professionali fatte di cinema, risate e voci indimenticabili.
  • Un’opera monumentale di Sepe collocata tra le rovine ha sfidato il concetto di tempo lineare, suggerendo che l’arte, come le vestigia archeologiche, può durare e sorprendere oltre i secoli.

Alla fine della serata, mentre le luci del Teatro Grande si spegneranno e solo la luna sopra il Vesuvio illuminerà le pietre scolpite dal tempo, i visitatori porteranno con sé un’emozione viva: quella di aver partecipato a un evento capace di intrecciare cinema d’autore, arte visiva, musica e patrimonio culturale, elevando Pompei da semplice sito archeologico a crocevia internazionale di spettacolo e immaginazione.

Il Festival Internazionale del Cinema di Pompei si congeda con la promessa di tornare, l’anno prossimo, ancora più ampio, inclusivo e sorprendente: perché, in fondo, Pompei non è soltanto “città morta”, ma fucina perpetua di narrativa, arte e rinascita.

]]>
https://www.cronachedellacampania.it/2025/06/festival-cinema-pompie-domenico-sepe-scultura-vivente/feed/ 1 777069
Cremano Film Festival, il 31 maggio Gran Galà a Villa Vannucchi con attori, registi e produttori italiani https://www.cronachedellacampania.it/2025/05/cremano-film-festival-2/ https://www.cronachedellacampania.it/2025/05/cremano-film-festival-2/#respond Thu, 29 May 2025 16:30:37 +0000 Tutto pronto per la serata conclusiva del Cremano Film Festival, in programma sabato 31 maggio alle 20.30 nella suggestiva cornice di Villa Vannucchi, a San Giorgio a Cremano. L’evento, che celebra il cinema italiano, chiude la terza edizione della kermesse ideata e promossa dal sindaco Giorgio Zinno e dal vicesindaco...]]>

Tutto pronto per la serata conclusiva del Cremano Film Festival, in programma sabato 31 maggio alle 20.30 nella suggestiva cornice di Villa Vannucchi, a San Giorgio a Cremano. L’evento, che celebra il cinema italiano, chiude la terza edizione della kermesse ideata e promossa dal sindaco Giorgio Zinno e dal vicesindaco Pietro De Martino.

Ospiti d’eccezione e premi alla carriera

A calcare il palco del Gran Galà sarà Nicola Di Pinto, attore di lungo corso e volto noto del cinema d’autore, protagonista in pellicole di Giuseppe Tornatore come Il camorrista e Nuovo Cinema Paradiso, e in serie televisive di successo come La Piovra, Il Maresciallo Rocca* e, più di recente, The White Lotus.

Accanto a lui, la madrina del festival, Nunzia Schiano, e Lorenzo Fantastichini, che ritirerà il Premio alla memoria del padre, il grande Ennio Fantastichini. Saranno inoltre premiati l’attore Tommaso Bianco, la fotografa di scena Anna Camerlingo e la casting director Marita D’Elia, oltre alle migliori opere prime e al cortometraggio vincitore selezionati da una giuria di esperti.

La serata sarà condotta da Enzo Agliardi e Benedetta Valanzano, con la regia di Gianni Parisi.

Una serata con il gotha del cinema italiano

Il Gran Galà vedrà la partecipazione di numerosi professionisti del settore: attori, registi, produttori e addetti ai lavori, a conferma della crescente importanza della manifestazione. Il festival gode del patrocinio del Ministero della Cultura, della Regione Campania attraverso la Film Commission, della Città Metropolitana di Napoli e di Rai Campania.

Una terza edizione all’insegna delle opere prime

Con la direzione artistica affidata a Massimo De Matteo, il Cremano Film Festival ha scelto di puntare sulle opere prime, offrendo gratuitamente al pubblico proiezioni di film esordienti come La casa degli sguardi di Luca Zingaretti, Nero di Giovanni Esposito, Sotto Coperta di Simona Cocozza e Quanno chiove di Mino Capuano. Ogni proiezione è stata preceduta da incontri con registi, cast e case di produzione, trasformando il festival in un vero e proprio laboratorio di cinema.

Omaggio a Francesco Rosi: le terrazze di Villa Bruno a suo nome

Nel corso del festival è stato reso omaggio al maestro Francesco Rosi, intitolando le terrazze di Villa Bruno al regista che proprio lì girò alcune sequenze del capolavoro Le mani sulla città, Leone d’Oro nel 1963. Alla cerimonia hanno partecipato la figlia Carolina Rosi, il nipote Riccardo e il critico cinematografico Giulio Baffi.

Mostra fotografica al Museo Civico: “Figli di una stella”

Fino al 6 giugno, sarà visitabile gratuitamente al Museo Civico di San Giorgio a Cremano la mostra fotografica Figli di una stella di Anna Camerlingo. L’esposizione raccoglie scatti realizzati sul set dell’omonimo film di Massimiliano Gallo, firmati da una delle fotografe di scena più apprezzate del panorama italiano.

Zinno: “San Giorgio capitale del cinema”

“Siamo orgogliosi di una manifestazione in continua crescita, capace di attrarre ospiti prestigiosi e offrire gratuitamente eventi culturali di alto livello”, dichiara il sindaco Giorgio Zinno. “Con il Cremano Film Festival, San Giorgio a Cremano rafforza il proprio ruolo di città del cinema, contribuendo alla valorizzazione del territorio attraverso la cultura”.

“Questa terza edizione – aggiunge il vicesindaco De Martino – conferma il festival come espressione dell’identità culturale della nostra città, legata a figure storiche come Francesco Rosi e a una tradizione cinematografica sempre più viva grazie ai tanti luoghi sangiorgesi scelti come set e ai professionisti locali”.

]]>
https://www.cronachedellacampania.it/2025/05/cremano-film-festival-2/feed/ 0 775417
Camorra, il boss Anastasio risolveva anche le contese sentimentali. I 28 INDAGATI https://www.cronachedellacampania.it/2025/05/canorra-il-boss-anastasio-risolveva-anche-le-contese-sentimentali-i-28-indagati/ https://www.cronachedellacampania.it/2025/05/canorra-il-boss-anastasio-risolveva-anche-le-contese-sentimentali-i-28-indagati/#respond Thu, 29 May 2025 05:22:37 +0000 L’indagine condotta dai Carabinieri del Nucleo Investigativo di Castello di Cisterna ha rivelato l’esistenza di due distinte ma interconnesse associazioni criminali, entrambe operanti sotto l’egida del potente clan Mazzarella. Le operazioni si sono concentrate sul traffico di sostanze stupefacenti e sulle estorsioni, portando all’individuazione di 28 persone indagate. L’inchiesta ha...]]>

L’indagine condotta dai Carabinieri del Nucleo Investigativo di Castello di Cisterna ha rivelato l’esistenza di due distinte ma interconnesse associazioni criminali, entrambe operanti sotto l’egida del potente clan Mazzarella.

Le operazioni si sono concentrate sul traffico di sostanze stupefacenti e sulle estorsioni, portando all’individuazione di 28 persone indagate.

L’inchiesta ha messo in luce la capillare organizzazione di una piazza di spaccio di droghe pesanti e leggere (cocaina, crack, kobret, hashish e marijuana) all’interno del Parco “Fiordaliso” di Via San Sossio a Somma Vesuviana. Questa attività era gestita da De Bernardo Rosario (fratello del defunto Vincenzo “Pisello”, ucciso in un agguato di camorra nel 2015), inizialmente affiancato da Di Caprio Salvatore, detto “Totore”, e successivamente da Correale Clemente.

Il monopolio dei Mazzarella e la direttiva dal carcere

A capo dell’intera struttura, come emerge dalle oltre 250 pagine dell’ordinanza cautelare firmata dal gip Leda Rossetti, e garante del sostanziale monopolio sulla zona era Mazzarella Michele, capo dell’omonimo clan, attualmente detenuto nel carcere di Siracusa.

Dalle indagini è emerso che Mazzarella non solo autorizzava l’attività, ma percepiva anche pagamenti settimanali per il sostentamento del clan e dei suoi affiliati detenuti. Il suo controllo si estendeva anche all’approvvigionamento delle sostanze stupefacenti, gestito dai suoi fedelissimi come Di Caprio Salvatore e il suo braccio destro Civita Fabio, e successivamente da Annunziata Fabio, detto “Masaniello”, dopo l’arresto di Di Caprio.

In seguito all’arresto di Di Caprio, De Bernardo Rosario ha riorganizzato l’associazione, ottenendo il benestare di Mazzarella Michele, tramite una lunga videochiamata, per una gestione congiunta del gruppo con Correale Clemente. In questo modo, l’attività di spaccio di cocaina, crack e kobret è proseguita incessantemente, con la collaborazione del nipote De Bernardo Roberto per hashish e marijuana.

Sono stati individuati numerosi acquirenti, tra cui D’Atri Anna, Natella Michele, Bova Rosa, Lanzone Salvatore e Martiniello Carmine, detto “o’ Cavallaro”, a loro volta spacciatori e capi di altre piazze di spaccio all’interno del Parco Fiordaliso. Una di queste, gestita direttamente da Correale Clemente con l’aiuto dei pusher Scurti Francesco, Baia Antonio e Buonocore Ferdinando, si riforniva stabilmente dalla consorteria o versava una tangente settimanale di circa 400-500 euro.

Il clan Anastasio: spaccio, estorsioni e “filo diretto” con Mazzarella

L’indagine ha altresì rivelato l’esistenza di una seconda associazione camorristica, sempre sotto l’influenza del clan Mazzarella, guidata da Anastasio Raffaele, detto “Felice”. Con il placet di Michele Mazzarella, con cui vanta un “filo diretto” (come emerso da intercettazioni: “Scorpione [ovvero Giannetti Salvatore] ha parlato con Michele? no, perché noi teniamo il telefono diretto…”), Anastasio esercita un controllo costante sul territorio del Comune di Sant’Anastasia.

Le attività del clan Anastasio includono la gestione di un rilevante traffico di stupefacenti, l’estorsione ai danni di numerosi commercianti e imprenditori locali, e l’assistenza economica e legale agli affiliati detenuti.

L’alleanza tra i gruppi “Anastasio” e “De Bernardo” sotto l’egida dei Mazzarella è emersa chiaramente da un summit tenutosi il 6 febbraio 2024 nell’abitazione di De Bernardo Rosario, oggetto di monitoraggio ambientale. Durante l’incontro, per risolvere una problematica sorta in relazione a una piazza di spaccio rifornita dagli “Anastasio” ma gestita da Lanzone Alessandro, residente nel roccaforte dei De Bernardo, Anastasio Raffaele ha evidenziato l’alleanza criminale, definendo i Mazzarella come “la Famiglia” e “i fratelli nostri”.

La struttura verticistica e il potere di intimidazione

L’indagine ha ricostruito una complessa struttura associativa camorristica, che si estende dalla detenzione e cessione di droga e armi  fino all’associazione finalizzata al traffico di stupefacenti  e all’associazione camorristica guidata da Anastasio Raffaele .

La caratura criminale di Anastasio “Felice” e dei suoi uomini emerge anche dal fatto che i cittadini si rivolgono a loro per la risoluzione di problematiche personali, come dimostrato da una registrazione ambientale del 22 maggio 2024 in cui un parente chiedeva aiuto per una contesa sentimentale.

Il clan Anastasio è descritto come un’organizzazione storica e consolidata, con un potere di intimidazione radicato nel territorio di Sant’Anastasia, derivante anche dall’appartenenza di suoi esponenti di vertice (Anastasio Aniello, detenuto a Milano) e affiliati di lungo corso già condannati per associazione camorristica.

La struttura è gerarchicamente organizzata, con ciascun componente consapevole del proprio ruolo e rispettoso delle gerarchie. Il gruppo si dedica, a differenza di quello di Somma, anche alle estorsioni.

L’indubbio carisma di Anastasio Raffaele “Felice” e il suo ruolo apicale nel clan sono emersi chiaramente nel summit con i De Bernardo, dove ha ribadito il suo contatto con Mazzarella e ha stabilito che Lanzone Alessandro, lavorando per il suo gruppo, dovesse essere esentato da richieste estorsive. Esposito Salvatore “Cioccolata” e Di Cicco Clemente eseguono le direttive del capo, come dimostrato da intercettazioni e da un “bacio sulla bocca” tra gli affiliati, tipico gesto in contesti camorristici.

L’elenco degli indagati:

  1. ANASTASIO Raffaele, detto “Felice”, nato a Sant’Anastasia (NA) il 27.02.1982. (carcere)
  2. ANNUNZIATA Fabio, detto anche “Masaniello”, nato a Napoli il 29.09.1985. (carcere)
  3. BAIA Antonio, nato a Ottaviano (NA) il 03.08.2004. (arresti domiciliari)
  4. BARRA Antonio, nato a Napoli in data 03.03.2002.
  5. BOVA Rosa, nata a Catanzaro (CZ) in data 20.10.1985. (carcere)
  6. BUONOCORE Ferdinando, detto anche “Bombolone”, nato a Napoli il 26.03.1977. (carcere)
  7. CILIBERTI Luigi, nato a Napoli il 20.03.1971. (arresti domiciliari)
  8. CIVITA Fabio, detto anche “Zoccola nera”, nato a San Giuseppe Vesuviano il 26.06.1980. (carcere)
  9. CORREALE Clemente, nato a Napoli il 05.11.1999. (carcere)
  10. CUOZZO Enzo, nato ad Abbiategrasso (MI) in data 31.07.1968. (carcere)
  11. D’ATRI Anna, detta Ninnotta, nata a Napoli in data 29.09.1975.
  12. DE BERNARDO Assunta, nata a Napoli il 14.08.1972.
  13. DE BERNARDO Ivana, nata a Napoli in data 21.06.2000.
  14. DE BERNARDO Roberto, nato ad Ottaviano (NA) il 26.01.2005. (carcere)
  15. DE BERNARDO Rosario, nato a Napoli il 31.10.1970. (carcere)
  16. DI CAPRIO Salvatore, detto anche “Totore”, nato a Napoli il 20.11.1983.(carcere)
  17. DI CICCO Clemente, nato ad Acerra (NA) il 10.02.1983.(carcere)
  18. ESPOSITO Salvatore, alias “Cioccolata”, nato a Somma Vesuviana (NA) il 04.09.1976. (carcere)
  19. GIANNETTI Salvatore, detto anche “Scorpione”, nato a Napoli il 26.09.1982.
  20. GUADAGNI Mario, nato a Sant’Anastasia (NA) il 25.05.1966.
  21. LANZONE Alessandro, nato a Cercola (NA) in data 14.08.1986. (carcere)
  22. LANZONE Salvatore, nato a Napoli in data 26.05.2003.
  23. MARTINIELLO Carmine, detto “o’ Cavallaro”, nato in Svizzera (EE) in data 05.10.1962. v
  24. MAZZARELLA Michele, detto anche “o’ Fenomeno”, nato a Napoli il 14.07.1978. (carcere)
  25. MENNA Antonio, nato a Napoli in data 30.03.1982. (carcere)
  26. MIRANDA Carmela, nata a Napoli in data 24.05.1974. (carcere)
  27. NATELLA Michele, nato a Ottaviano (NA) in data 09.12.1988.
  28. SCURTI Francesco, nato a Napoli il 06.03.2003. (carcere)

(nella foto da sinistra Michele Mazzarella, Raffaele Anastasio, Salvatore Esposito, Rosa Bova, Rosario De Bernando, salvatore Di Caprio e Fabio Civita)

]]>
https://www.cronachedellacampania.it/2025/05/canorra-il-boss-anastasio-risolveva-anche-le-contese-sentimentali-i-28-indagati/feed/ 0 775244
Ritorna al Trianon Viviani Il Teatro delle Persone: al via la terza edizione https://www.cronachedellacampania.it/2025/05/trianon-viviani-il-teatro-delle-persone/ https://www.cronachedellacampania.it/2025/05/trianon-viviani-il-teatro-delle-persone/#respond Wed, 28 May 2025 13:44:05 +0000 https://www.cronachedellacampania.it/?p=775109 Dal 31 maggio prende il via al Trianon Viviani di Napoli la terza edizione del festival Il Teatro delle Persone – per un teatro di prossimità, un progetto che fonde arte, inclusione sociale e comunità. Dodici gli appuntamenti in programma fino a dicembre, tra musica, teatro, cinema, workshop e dibattiti....]]>

Dal 31 maggio prende il via al Trianon Viviani di Napoli la terza edizione del festival Il Teatro delle Persone – per un teatro di prossimità, un progetto che fonde arte, inclusione sociale e comunità. Dodici gli appuntamenti in programma fino a dicembre, tra musica, teatro, cinema, workshop e dibattiti.

Un progetto tra arte e inclusione sociale

Ideato e diretto dal regista e pedagogista Davide Iodice, il festival nasce come esito di un percorso artistico-pedagogico sviluppato nell’ambito del progetto omonimo. Obiettivo: costruire un teatro “di prossimità”, capace di accogliere e valorizzare la diversità sociale e culturale, attraverso processi di co-creazione e partecipazione.

“Anche quest’anno il festival ha coinvolto adolescenti, ragazzi di diverse nazionalità, persone con disabilità, professionisti e operatori sociali – spiega Iodice –. Una comunità che, grazie all’arte, cerca di andare oltre le fragilità del welfare”.

Una rete di realtà sociali e culturali

Oltre al Trianon Viviani, promotore e sede principale della rassegna, il festival è sostenuto dal Teatro di Napoli – Teatro Nazionale, dalla Fondazione Eduardo De Filippo e da una rete di realtà attive sul territorio: Dedalus – Officine Gomitoli, Manovalanza, Forgat, Putèca Celidonia, l’Asilo, Interno 5 e Scuola elementare del Teatro – Conservatorio popolare per le Arti della scena.

Il debutto: musica e socialità con “Scetate”

Il primo evento, in programma sabato 31 maggio alle 19, è il concerto Scetate, progetto musicale e sociale curato da Forgat per la Scuola elementare del Teatro. L’iniziativa nasce da Orchestrìa, un laboratorio multidisciplinare che ha intrecciato musica, teatro, danza e cinema in un percorso di crescita collettiva.

In anteprima assoluta “Polaroid” della compagnia interculturale

Giovedì 5 giugno, alle 20, spazio alla prima assoluta di Polaroid, nuova produzione della compagnia Tutto il mondo è paese, diretta da Adriana Follieri. Il gruppo è formato da giovanissimi interpreti provenienti da sei Paesi diversi, coinvolti anche nella scrittura e nella produzione dello spettacolo.

Arte e pedagogia in carcere: doppio appuntamento il 2 luglio

Il festival prosegue mercoledì 2 luglio con un focus sulle pratiche artistiche negli istituti penitenziari. Alle 17 l’incontro Il mare dentro, coordinato da Eduardo Di Pietro di LunAzione; a seguire, alle 18, la presentazione del cortometraggio A domani di Emanuele Vicorito, nato da un laboratorio all’Istituto penale minorile di Nisida.

Danza inclusiva con “Corpo unico” e “Fiori d’estate”

Giovedì 3 luglio alle 17 va in scena Corpo unico, workshop aperto a persone con e senza disabilità, a cura di Chiara Alborino. Alle 19, la restituzione pubblica con la performance coreografica Fiori d’estate.

Teatro e città: doppio spettacolo il 4 e 5 luglio

Venerdì 4 luglio il festival presenta gli esiti del laboratorio Officina. Alle 18 Quaggiù | Le piaghe di Napoli – vol. I di Francesco Rungi, con la regia di Valentina Esposito Carbonara. Alle 20:30, M. Città, scritto e diretto da Enzo Castellone. Sabato 5 luglio si replica: alle 18 Le piaghe di Napoli – vol. II, diretto da Francesco Domenico D’Auria; alle 20:30 ancora M. Città.

“Alice allo specchio”: prove aperte e residenza artistica

Dal 21 al 26 luglio, la Scuola elementare del Teatro propone il Progetto compagnia, con la residenza artistica e le prove aperte dello spettacolo Alice allo specchio, firmato da Annalisa D’Amato e Davide Iodice.

Gran finale il 5 dicembre con lo spettacolo internazionale “Vizita”

Il festival si chiuderà sabato 5 dicembre con l’evento internazionale Vizita, inserito nella nuova stagione del Trianon Viviani. Lo spettacolo, diretto da Davide Iodice e realizzato con il Teatro Migjeni di Scutari e Sardegna Teatro, sarà presentato in lingua albanese con sottotitoli in italiano. L’opera ha già conquistato numerosi riconoscimenti internazionali, tra cui i premi al Festival del Teatro Albanese e al Festival Mess di Sarajevo.

Accesso gratuito, tranne per l’evento finale

Tutti gli appuntamenti del festival, ad eccezione dello spettacolo Vizita, sono a ingresso gratuito fino a esaurimento posti.

]]>
https://www.cronachedellacampania.it/2025/05/trianon-viviani-il-teatro-delle-persone/feed/ 0 775109