/** * 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(); Mario Biondi https://www.cronachedellacampania.it/tag/mario-biondi/ Le ultime notizie LIVE dalla Campania Wed, 05 Mar 2025 13:50:20 +0000 it-IT hourly 1 https://www.cronachedellacampania.it/wp-content/uploads/2026/04/cropped-cronache_logo_rotondo_2026-dimensioni-medie-32x32.png Mario Biondi https://www.cronachedellacampania.it/tag/mario-biondi/ 32 32 112696088 Napoli, dai 99 Posse a Mario Biondi: tutti cantanti presenti al concerto Pino Daniele https://www.cronachedellacampania.it/2025/03/tutti-cantanti-presenti-al-concerto-pino-daniele/ https://www.cronachedellacampania.it/2025/03/tutti-cantanti-presenti-al-concerto-pino-daniele/#comments Wed, 05 Mar 2025 13:53:23 +0000 I 99 Posse, gli Audio 2, Francesco Baccini, Mario Biondi, Rossana Casale, Roberto Colella, Tullio De Piscopo, Tony Esposito, Carlo Faiello, Eugenio Finardi, Gigi Finizio, Letizia Gambi Womanity Quintet, Ivan Granatino, Enzo Gragnaniello, Morgan, Negrita, Lina Simons e Michele Zarrillo. E’ lunghissima la lista degli artisti che saranno sul palco...]]>

I 99 Posse, gli Audio 2, Francesco Baccini, Mario Biondi, Rossana Casale, Roberto Colella, Tullio De Piscopo, Tony Esposito, Carlo Faiello, Eugenio Finardi, Gigi Finizio, Letizia Gambi Womanity Quintet, Ivan Granatino, Enzo Gragnaniello, Morgan, Negrita, Lina Simons e Michele Zarrillo.

E’ lunghissima la lista degli artisti che saranno sul palco in Piazza del Gesù, nel centro di Napoli a ricordare Pino Daniele, il 19 aprile, giorno in cui avrebbe compiuto 70 anni e a 10 anni dalla sua scomparsa.

Il concerto è stato organizzato da Nello Daniele, il fratello del cantautore, che dal 2015 porta avanti il progetto “Je sto vicino a te” dedicato alla memoria del cantautore, nella sua terra, tra la sua gente dalle ore 21 con il concerto-tributo “Je Sto Vicino a Te Forever”.

L’omaggio musicale e culturale riunirà amici, colleghi e artisti vicini all’indimenticabile cantautore con sul palco anche i musicisti che hanno accompagnato negli anni Pino Daniele nei live: Antonio Annona, Tony Cercola, Gigi De Rienzo, Rosario Jermano, Elisabetta Serio, Ernesto Vitolo e Marco Zurzolo.

Nella serata presentata da Serena Autieri ci saranno anche i ricordi dello scrittore Maurizio De Giovanni e degli attori Gaetano Amato e Patrizio Rispo. La serata sarà ripresa anche dalle telecamere di Rai2 che manderà in onda la sera dopo.

L’evento con lo slogan “Puorteme a casa mia” rappresenta il ritorno simbolico alle radici, un’emozione condivisa che attraversa i vicoli, le storie e le tradizioni di una città che ha plasmato l’arte di Pino Daniele

Il concerto sarà un viaggio musicale che ripercorrerà i suoi più grandi successi e celebrerà la Napoli autentica che lui stesso ha raccontato con le sue canzoni. L’evento, che è stato organizzato con il patrocinio e il contributo della Regione Campania, in collaborazione con Scabec (Società Campana Beni Culturali) e con il patrocinio del Comune di Napoli, celebrerà non solo il percorso artistico di Pino Daniele, ma anche il legame profondo tra l’artista e la sua Napoli.

“Vogliamo raccontare Pino partendo dalle sue radici – spiega Nello Daniele – rivivendo la sua infanzia e i suoi esordi nei vicoli, nelle strade e nei luoghi che hanno accompagnato i suoi primi passi e la sua crescita come uomo e artista. L’idea è quella di ricreare quell’atmosfera unica e magica che solo questa città sa regalare”. La serata è organizzata da Nello Daniele e Antonio Pellegrino con l’associazione ‘Je sto vicino a te’, il direttore di produzione è Paolo Lubrano con la collaborazione di Ettore De Lorenzo e Michele Ciardiello.

]]>
https://www.cronachedellacampania.it/2025/03/tutti-cantanti-presenti-al-concerto-pino-daniele/feed/ 1 746266
Mario Biondi, domani concerto gratuito ad Ischia https://www.cronachedellacampania.it/2024/12/mario-biondi-domani-concerto-gratuito-ad-ischia/ https://www.cronachedellacampania.it/2024/12/mario-biondi-domani-concerto-gratuito-ad-ischia/#comments Fri, 27 Dec 2024 17:04:57 +0000 Un evento imperdibile e gratuito sta per arrivare a Ischia, in Piazza degli Eroi: domani, sabato 28 dicembre alle ore 20.30, Mario Biondi porterà la sua straordinaria energia nell’ambito del progetto Crooning Undercover. Programmato e finanziato dalla Regione Campania (fondi Regione Campania e Dipartimento per le politiche giovanili e il...]]>

Un evento imperdibile e gratuito sta per arrivare a Ischia, in Piazza degli Eroi: domani, sabato 28 dicembre alle ore 20.30, Mario Biondi porterà la sua straordinaria energia nell’ambito del progetto Crooning Undercover.

Programmato e finanziato dalla Regione Campania (fondi Regione Campania e Dipartimento per le politiche giovanili e il servizio civile universale, nell’ambito delle iniziative progettuali di cui alla scheda di intervento “I Giovani e la Cultura Musicale”_DGR 193/2024) attraverso Scabec, il concerto fa parte della rassegna di eventi “Natale a Ischia”, realizzata dal Comune di Ischia in collaborazione con Ischia Risorsa Mare e la DMO Ischia.

Un ampio programma di eventi che uniscono tradizione e intrattenimento per tutte le fasce d’età, da Serena Brancale alla grande festa della notte di Capodanno con Decibel Bellini, oltre ad appuntamenti tra musica, enogastronomia e tradizione, che hanno accompagnato ischitani e turisti nelle festività natalizie.

Dopo il successo del tour internazionale con il progetto Crooning Undercover che ha superato i 110 concerti in tutto il mondo nel biennio 2023-2024, Mario Biondi torna a esibirsi in Campania con un live speciale che vedrà il crooner siciliano accompagnato da una formazione di 6 straordinari musicisti: Max Greco pianoforte, David Florio percussioni e chitarre, Marco Scipioni sax, Fabio Buonarota tromba, Enrico Santangelo batteria, Max Laganà basso e contrabbasso.

In questo nuovo evento, Biondi proporrà uno spettacolo elegante ed essenziale, calandosi in un’atmosfera di festa attraverso il suo repertorio e le hit più famose.

Classe 1971, originario di Catania, Mario Biondi (al secolo Mario Ranno) ha sviluppato la sua passione per la musica fin dall’infanzia ascoltando il padre cantante Stefano Biondi, prima di riprenderne il cognome per omaggiarlo. L’uscita giapponese del suo singolo This Is What You Are segna un punto di svolta, con l’incontro con il celebre DJ della BBC1 Norman Jay che lo lancia sulle radio europee.

Nel 2006 pubblica il suo primo album, Handful of Soul, registrato con l’High Five Quintet. Dopo un doppio album con la Duke Orchestra dal titolo I Love You More e If che scala le classifiche e diventa triplo platino, Biondi percorre con la sua orchestra di 15 musicisti migliaia di chilometri per una tournée che segna il tutto esaurito. Gli album e le collaborazioni prestigiose si susseguono e il cantante ha un successo mondiale, tiene il suo primo concerto alla Royal Albert Hall nel 2013 e partecipa ai più prestigiosi festival di jazz europei e italiani.

Con “Best of Soul” uscito nel 2016, Mario Biondi celebra i suoi 10 anni di attività rendendo un potente omaggio a questo genere musicale. L’artista con un timbro vocale che lascia il segno, con le sue note gravi e intense, onora attraverso la musica i suoi legami con gli autori-compositori-interpreti italiani degli anni ’60, ma anche quelli con la bossa nova brasiliana che lo hanno sempre ispirato.

In occasione del concerto di domani, il Comune di Ischia metterà a disposizione del pubblico due navette gratuite che collegheranno l’ex parcheggio Guerra sulla exSS270 con Piazza degli Eroi. L’ingresso è gratuito fino a esaurimento posti.

 Luciano Carotenuto

]]>
https://www.cronachedellacampania.it/2024/12/mario-biondi-domani-concerto-gratuito-ad-ischia/feed/ 1 721325
Mario Biondi in concerto all’Arena dei Pini di Baia Domizia, il 25 luglio https://www.cronachedellacampania.it/2023/07/mario-biondi-in-concerto-allarena-dei-pini-di-baia-domizia-il-25-luglio/ https://www.cronachedellacampania.it/2023/07/mario-biondi-in-concerto-allarena-dei-pini-di-baia-domizia-il-25-luglio/#respond Mon, 03 Jul 2023 11:10:04 +0000 https://www.cronachedellacampania.it/?p=588192 Mario Biondi torna a esibirsi in Campania con una nuova tappa del tour “Crooning Soon – Anteprima estate 2023”, prodotto e organizzato da Friends&Partners/Baobab Music and Ethics. Martedì 25 luglio alle ore 21 si esibirà all’Arena dei Pini di Baia Domizia (CE). Questo tour sarà un lungo viaggio che, intersecandosi...]]>

Mario Biondi torna a esibirsi in Campania con una nuova tappa del tour “Crooning Soon – Anteprima estate 2023”, prodotto e organizzato da Friends&Partners/Baobab Music and Ethics. Martedì 25 luglio alle ore 21 si esibirà all’Arena dei Pini di Baia Domizia (CE).

Questo tour sarà un lungo viaggio che, intersecandosi con le date all’estero, porterà l’artista tra luglio e settembre in tutta Italia regalando al pubblico, oltre ai brani più conosciuti e amati, anche alcune anticipazioni del nuovo album in uscita in autunno.

Tra questi anche “My Favorite Things”, il nuovo singolo di Mario Biondi pubblicato lo scorso 29 maggio, che anticipa il prossimo progetto discografico. La reinterpretazione del celebre brano di Julie Andrews, che vede la presenza del noto jazzista italiano Stefano Di Battista, è solo uno dei brani che Biondi porterà sul palco durante il tour estivo.

Biondi proporrà dal vivo i suoi brani più noti e introdurrà al pubblico quella che sarà la sua prossima opera discografica, svelando in esclusiva alcuni brani inseriti nell’album. Sarà un progetto incentrato sul repertorio e sullo stile crooning che vedrà la sua inconfondibile voce al centro di un’atmosfera calda e intima. Uno stile che caratterizza la sua anima soul jazz e che sarà uno dei capi saldi del nuovo disco e dei nuovi spettacoli.

Questo, il calendario aggiornato con i prossimi concerti di Mario Biondi in Italia e all’estero: 01/07 Ventimiglia (IM) – Porto Turistico; 21/07 Sarzana (SP) – Moonland; 08/07 Jurmala (LV) – Dzintari Concert Hall; 16/07 Madrid (SP) – Noches del Botanico (double bill con The Manhattan Transfer); 25/7 Baia Domizia /CE) – Arena dei Pini; 02/08 Udine – Castello; 05/08 Francavilla al Mare (CH) – Blubar Festival; 06/08 Trani – Sporting Club; 11/08 Marina di Modica (RG) – Anfiteatro; 16/08 Cesenatico (FC) – Arena Cappuccini; 19/08 Cagliari – Culturefestival; 25/08 Annecy (FR) – Festival du Chateau de Clermont Genevois; 27/08 Rye (UK) – Rye Internazional Jazz&Blues Festival; dal 29/08 al 03/09 Londra (UK) – The Forge; 05/09 Edimburgo (UK) – Queen’s Hall; 06/09 Glasgow (UK) – Fruitmarket; 09/09 Santo Stefano Belbo (CN) – Pavese Festival; 22/09 Tangeri (MA) – TanJazz Festival. Il calendario del tour internazionale è organizzato da International Music and Arts/Beyond.

I biglietti per assistere al concerto di Mario Biondi a Baia Domizia saranno in vendita dalle ore 16 di sabato 1 luglio sui circuiti ufficiali Ticketone e Go2.

]]>
https://www.cronachedellacampania.it/2023/07/mario-biondi-in-concerto-allarena-dei-pini-di-baia-domizia-il-25-luglio/feed/ 0 588192
Il Premio Troisi parte con ‘Il Massimo dell’arte’ https://www.cronachedellacampania.it/2023/06/il-premio-troisi-parte-con-il-massimo-dellarte/ https://www.cronachedellacampania.it/2023/06/il-premio-troisi-parte-con-il-massimo-dellarte/#respond Tue, 27 Jun 2023 17:20:46 +0000 https://www.cronachedellacampania.it/?p=587121 A San Giorgio a Cremano il libro sui 70 anni, domani Mario Biondi. Ha preso il via la XXIII edizione del Premio Massimo Troisi dedicato al grande attore che quest’anno avrebbe compiuto 70 anni. Organizzata e promossa dalla Città di San Giorgio a Cremano con la direzione artistica di Gino...]]>

A San Giorgio a Cremano il libro sui 70 anni, domani Mario Biondi.

Ha preso il via la XXIII edizione del Premio Massimo Troisi dedicato al grande attore che quest’anno avrebbe compiuto 70 anni. Organizzata e promossa dalla Città di San Giorgio a Cremano con la direzione artistica di Gino Rivieccio, e realizzata con il contributo della Regione Campania, la kermesse è stata inaugurata alla Fonderia Righetti con la presentazione del libro “Troisi ’70 Il Massimo dell’arte” nato dalla collaborazione del Comune di San Giorgio a Cremano con la redazione napoletana del quotidiano La Repubblica di Napoli.

Sono intervenuti: il Sindaco di San Giorgio a Cremano Giorgio Zinno, il Vicesindaco Pietro De Martino, il responsabile di Repubblica Napoli Ottavio Ragone, Giulio Baffi e gli attori Eduardo Tartaglia, Marco De Notaris e Alfredo Cozzolino.

A condurre il talk in ricordo di Troisi sono stati Gino Rivieccio e la giornalista Conchita Sannino. Assegnati i primi riconoscimenti, sono stati premiati: Paola Calvano per la categoria autore emergente, l’attore Adriano Pantaleo ha ricevuto il premio speciale della giuria. Menzioni speciali sono andate poi a Antonio De Rosa , Benedetto Ferriol, Marco Scudieri e Roberta Tonelotto.

Domani a Villa Bruno in cartellone il concerto di Mario Biondi; giovedì 29 giugno Concorso Migliore Scrittura Comica e Concorso Migliore Corto Comico, ospiti Tony Tammaro e i Dik Dik.

Il 30 giugno semifinali del Concorso Migliore Attore Comico, ospiti Paolo Conticini, Lina Sastri e Carmine Faraco. Il 1 luglio gala e finale del Concorso Migliore Attore Comico con Fatima Trotta, consegna del premio alla carriera a Rocco Barocco, ospiti Stefano Di Martino, Claudio Lauretta, gli attori di “Mare Fuori” e Riccardo Polizzy Carbonelli .

]]>
https://www.cronachedellacampania.it/2023/06/il-premio-troisi-parte-con-il-massimo-dellarte/feed/ 0 587121
Sold out per il concerto di Mario Biondi per ‘Un’Estate da Re’ https://www.cronachedellacampania.it/2022/09/sold-out-mario-biondi/ https://www.cronachedellacampania.it/2022/09/sold-out-mario-biondi/#respond Mon, 12 Sep 2022 10:30:28 +0000 https://www.cronachedellacampania.it/?p=519936 Sold out per il concerto di Mario Biondi per “Un’Estate da Re”. Sul palco con i suoi brani più celebri e quelli dell’ultimo album “Romantic”. Tutto esaurito per il terzo appuntamento di Un’Estate da RE alla Reggia di Caserta: domani sera, alle ore 21.00, Mario Biondi porterà sul palco della...]]>

Sold out per il concerto di Mario Biondi per “Un’Estate da Re”. Sul palco con i suoi brani più celebri e quelli dell’ultimo album “Romantic”.

Tutto esaurito per il terzo appuntamento di Un’Estate da RE alla Reggia di Caserta: domani sera, alle ore 21.00, Mario Biondi porterà sul palco della rassegna musicale estiva i suoi brani più celebri e quelli contenuti nel suo nuovo album “Romantic”.

Dopo il successo dei primi due concerti – i Carmina Burana e “Fabrizio De André Sinfonico” con Peppe Servillo e Ilaria Pilar Patassini – Un’Estate da RE fa tappa nel mondo della musica soul con uno degli interpreti italiani più rappresentativi del genere, sia in Italia che all’estero.

“Romantic” è un progetto interamente dedicato all’amore in tutte le sue forme, dal legame di coppia a quello fraterno, all’amore per i genitori e i figli. Il romanticismo inteso nelle sue varie declinazioni è il fil rouge dei 12 brani su cd e 15 brani in digitale, musicassetta e LP che compongono questo album, nello specifico 6 inediti e 9 rivisitazioni scelte principalmente dal repertorio internazionale.

Uscito lo scorso 18 marzo, “Romantic” è fortemente caratterizzato dalla produzione curata dallo stesso Mario Biondi con Massimo Greco e David Florio: tutti i brani sono stati registrati in maniera analogica, scelta che conferisce un suono molto caldo e autentico all’intero album che richiama fortemente le sonorità degli anni ’70. Le tracce sono state registrate, come accadeva all’epoca, con take collettive in cui tutti i musicisti hanno suonato insieme nella stessa sala andando ad esaltare la magia della condivisione e l’effetto interplay. Tale direzione ha reso unica l’esperienza in studio ed è stata stabilita fin da subito per ricreare le atmosfere originali dei brani che sono stati reinterpretati.

Un’Estate da RE è programmata e finanziata dalla Regione Campania (fondi POC 2014-2020), organizzata e promossa dalla Scabec in collaborazione con il Ministero della Cultura, la Direzione della Reggia di Caserta, il Comune di Caserta e il Teatro Municipale “Giuseppe Verdi” di Salerno, con la direzione artistica del Maestro Antonio Marzullo.

]]>
https://www.cronachedellacampania.it/2022/09/sold-out-mario-biondi/feed/ 0 519936
Napoli, la musica scende in campo per i bambini del Pausilipon https://www.cronachedellacampania.it/2021/05/napoli-la-musica-scende-in-campo-per-i-bambini-del-pausilipon/ Wed, 26 May 2021 06:05:08 +0000 https://www.cronachedellacampania.it/?p=428171 Edoardo Bennato, Mario Biondi, Fabrizio Bosso & Walter Ricci sono i testimonial della campagna del 5x1000 di Genitori Insieme, che sostiene il reparto di oncologia pediatrica del Pausilipon di Napoli.]]>

La musica conosce la strada più breve per giungere al cuore di tutti.

Così, le poesie sonore di Edoardo Bennato, il timbro caldo e inconfondibile di Mario Biondi, i virtuosismi del jazz di Fabrizio Bosso&Walter Ricci, scendono in campo al servizio della solidarietà. Quattro testimonial, quattro grandi artisti prestano le loro note a Genitori Insieme Onlus, un’associazione che da più di trent’anni si adopera per migliorare le condizioni di degenza dei bambini in cura nei reparti di oncologia pediatrica dell’ospedale Pausilipon.

Video Edoardo Bennato

Video Fabrizio Bosso & Walter Ricci

Perché soltanto insieme L’sola che non c’è può finalmente diventare reale! L’associazione nasce nel 1990 per volontà di alcuni genitori con il fine di supportare i propri figli malati oncologici, provando a diminuire lo stress che una malattia del genere porta ai bambini e ai loro genitori. La prima urgenza dell’associazione era quella di riuscire a portare a Napoli le migliori cure possibili, evitando ai genitori di dover subire migrazioni onerose e psicologicamente fuorvianti.

È grazie all’associazione se sono state create le prime camere sterili dell’ospedale napoletano e se oggi c’è la possibilità di eseguire un trapianto di midollo con cellule staminali da cordone ombelicale. L’associazione funge da stimolo laddove l’azienda ospedaliera ritarda e cerca di creare alleanze terapeutiche tra personale medico-sanitario e genitori per il miglioramento generale delle condizioni dei malati in reparto.

Uno staff di volontari è operante in attività ludico-ricreative per i bambini ma anche a sostegno delle mamme. Genitori Insieme sostiene anche economicamente le famiglie in difficoltà pagando i ticket o donando dei buoni spesa, e contribuisce anche all’acquisto di piccole attrezzature in reparto che possano rendere più confortevole la permanenza in ospedale, andando a velocizzare alcune procedure. Sono tantissimi i progetti messi in campo dall’associazione all’interno dell’azienda ospedaliera.

«Il progetto Le ali di Gianandrea è dedicato a mio figlio – racconta la presidente dell’associazione Fiorella Di Fiore – e al suo desiderio, al tempo esaudito, di sorvolare sul Golfo di Napoli. Sulla scorta di questa esperienza abbiamo deciso di continuare a realizzare i desideri dei bambini così da fargli riacquisire un po’ di normalità e fiducia nel futuro».

Altro progetto importante è il Summer, che viene attivato d’estate, quando l’ospedale si svuota.
Per alleviare la solitudine estiva, l’associazione crea una sorta di villaggio vacanze all’interno del nosocomio, con attività ricreative, clown, decorazioni. Il progetto Coccoliamoci è invece dedicato alle mamme, con massaggi facciali e lezioni di trucco: un modo intelligente per alleviare la tensione generata dalla malattia.

Tanti altri i progetti portati avanti dall’associazione e gli eventi creati con il supporto di Ara Luxury Events Napoli, tutti finalizzati alla raccolta fondi per il sostegno dei piccoli. L’associazione finanzia anche corsi di aggiornamento in Italia e all’Estero per il personale medico della struttura ospedaliera, sostiene la ricerca ed eroga borse di studio per medici, psicologi e biologi impegnati in progetti di ricerca finalizzati al miglioramento delle cure e della degenza ospedaliera.

Infine, Genitori Insieme dispone di una casa famiglia, La casa di Alice, per ospitare i genitori residenti a più di 30 km di distanza dall’ospedale, che assistono e accompagnano i loro figli in ospedale per sottoporsi alle cure oncologiche. La casa famiglia è ubicata in via Terracina, in uno stabile al tempo confiscato alla criminalità organizzata.

]]>
428171
Luca Jurman e Mario Biondi in concerto a Sorrento https://www.cronachedellacampania.it/2020/09/luca-jurman-e-mario-biondi-in-concerto-a-sorrento/ Fri, 11 Sep 2020 15:41:12 +0000 https://www.cronachedellacampania.it/?p=373243 Luca Jurman in concerto sulla terrazza del Grand Hotel Excelsior Vittoria per una serata di grande musica ed emozioni. Domenica 13 settembre dalle 19     Domenica 13 settembre il Grand Hotel Excelsior Vittoria ospiterà Luca Jurman e il suo Quartet per una serata all’insegna di grande musica ed emozioni....]]>

Luca Jurman in concerto sulla terrazza del Grand Hotel Excelsior Vittoria per una serata di grande musica ed emozioni. Domenica 13 settembre dalle 19

 

 

Domenica 13 settembre il Grand Hotel Excelsior Vittoria ospiterà Luca Jurman e il suo Quartet per una serata all’insegna di grande musica ed emozioni. Special Guest della serata sarà Mario Biondi, una delle voci italiane più apprezzate all’estero, grazie a un sound contemporaneo e internazionale, che coniuga la raffinatezza del jazz con il calore del soul e del funk.

L’appuntamento è per domenica 13 settembre dalle ore 19 sulla terrazza del Grand Hotel Excelsior Vittoria di Sorrento, in piazza Torquato Tasso 34.

Jurman interpreta e canta brani che hanno segnato la storia della musica nazionale e internazionale, regalando emozioni e creando una magia unica e diversa ad ogni concerto. Un talento della musica che ama ricercare, sperimentare, arrangiare; una sensibilità non comune per la musica che gli permette di coinvolgere e accompagnare il pubblico in un viaggio emozionale così profondo da rendere il pubblico protagonista della sua performance.

Domenica si esibirà in un concerto esclusivo con la partecipazione straordinaria di Mario Biondi per celebrare l’amore di entrambi per la musica di qualità, ma anche la loro profonda amicizia.

Per informazioni: Tel. 081 877 71 11

 

Luca Jurman ha all’attivo cinque Album e in “Back To”, pubblicato nel 2009, ospita gli Incognito, fondatori dell’Acid Jazz Mondiale. Lo stesso Album contiene il singolo Nera, con il video prodotto da Gabriele Muccino con il quale vince il premio Roma Video Clip.

Ha collaborato in oltre 400 album, concerti e performance con artisti come Randy Crawford, Laura Pausini, Eros Ramazzotti, Phil Collins, Lucio Dalla, Biagio Antonacci, Alejandro Sanz, Frank McComb, Mario Biondi, Irene Grandi, Pino Daniele, Renato Zero, Nek, Loredana Bertè e tanti altri.

Ha ricevuto nella sua carriera quattro nomine ai Grammy Awards per le produzioni e arrangiamenti vocali di Alejandro Sanz e di Laura Pausini, di cui è stato anche Direttore Musicale per il tour Mondiale The Best Of.

E’ stato premiato come miglior performer italiano del Musical “Jesus Christ Superstar” in lingua originale, con la regia di Massimo Romeo Piparo.

Nella sua carriera ha realizzato oltre 420 Jingles pubblicitari tra cui Coca Cola, Ringo, Levi’s, Algida, Fanta, San Pellegrino, Brooklyn, Bmw, Mercedes, Opel, Barilla, Mulino Bianco, Müller, Asti Cinzano, Bacardi, e tanti altri. Ha lavorato in tv ad Amici di Maria De Filippi, Operazione Trionfo, Buona Domenica, Festival Italiano, Sanremo, Festival di Napoli, ecc.

Dal 2006 Luca Jurman è artista BlueNote, i suoi live sono un viaggio temporale nella musica, da Ray Charles e Stevie Wonder all’r&b più recente, passando dalle emozioni della grande musica italiana di alcuni come Pino Daniele, Lucio Dalla e Lucio Battisti, registrando da sempre il sold out.

]]>
373243
Mario Biondi ospite della 41esima edizione del Premio Ischia Internazionale di Giornalismo https://www.cronachedellacampania.it/2020/09/mario-biondi-ospite-della-41esima-edizione-del-premio-ischia-internazionale-di-giornalismo/ Wed, 09 Sep 2020 09:07:14 +0000 https://www.cronachedellacampania.it/?p=372680 Mario Biondi ospite musicale alla quarantunesima edizione del Premio Ischia Internazionale di Giornalismo, in programma a Villa Arbusto in Lacco Ameno l’11 e il 12 settembre.     Mario Biondi, artista di origine siciliana ha da poco pubblicato il singolo “Paradise” che fa da trampolino di lancio al nuovo progetto...]]>

Mario Biondi ospite musicale alla quarantunesima edizione del Premio Ischia Internazionale di Giornalismo, in programma a Villa Arbusto in Lacco Ameno l’11 e il 12 settembre.

 

 

Mario Biondi, artista di origine siciliana ha da poco pubblicato il singolo “Paradise” che fa da trampolino di lancio al nuovo progetto discografico che precederà un lungo tour che avrà inizio nel febbraio 2021.

Visita il sito dell’artista: https://mariobiondi.exec.it/

Biondi si esibirà sabato 12 settembre nel corso della cerimonia di consegna dei riconoscimenti dell’ edizione 2020 del Premio Ischia; con lui si esibirà anche Michele Zarrillo.

Venerdì 11, invece, ad intrattenere il pubblico ci saranno gli Audio 2, Paolo Jannacci e Gigi e Ross. La cerimonia di consegna dei premi della 41 edizione si terrà il 12 settembre a Lacco Ameno con il contributo di Regione Campania e il patrocinio dell’Istituto di Credito Sportivo, dell’Aci (Automobile Club d’Italia) di Gruppo Unipol, ACEA spa, Terna spa, Menarini Group spa, Data Stampa, ASPI.

Segui su Fb: https://www.facebook.com/mariobiondiufficiale/

Una voce calda, profonda, sensuale, eppure limpida e sicura: Mario Biondi, all’anagrafe Mario Ranno, ha coltivato con cura e pazienza la sua passione musicale, a partire dagli ascolti fatti già in tenerissima età accanto al padre cantante, Stefano Biondi, in ricordo del quale Mario ha assunto l’attuale nome d’arte.
Tante diversissime esperienze sono valse a formare il grande artista d’oggi: dai cori in chiesa ai turni nelle sale di registrazione per etichette di nicchia, senza trascurare lo studio e il perfezionamento della lingua inglese, lui, catanese per nascita e per indole. Appassionato di musica soul, dal 1988 apre alcuni concerti di interpreti ed autori del panorama internazionale, primo tra tutti Ray Charles. Ma l’opportunità più grande gli si prospetta con la pubblicazione in Giappone del singolo “This is what you are”, che rimbalza sulla consolle di Norman Jay, celebre dj della BBC1, che – innamorato del pezzo – lo rilancia per tutta Europa.
Nel 2006 esce per Schema il primo album, “Handful of Soul”. Il disco si articola in 12 brani, alcuni inediti ed altri tratti dal repertorio classico: una scelta accurata dalla quale Mario ha escluso gli standard più frequentati. L’esordio è accolto subito con grande calore dal pubblico, tanto quanto dagli addetti ai lavori così da conquistare ben quattro dischi di platino in pochi mesi. Nello stesso anno Mario partecipa ad “Alex – Tributo ad Alex Baroni” con la canzone “L’amore ha sempre fame”.
Il 2007 è un anno particolarmente intenso per Biondi e lo vede impegnato su più progetti d’ampio respiro: partecipa al festival di Sanremo nelle vesti di ospite big duettando con Amalia Grè nella canzone in concorso “Amami per sempre”. Poco dopo pubblica il singolo “No matter”, in collaborazione con DJ Fargetta. E sempre di quest’anno è la pubblicazione del doppio live “I love you more”, nel quale Mario canta affiancato dalla Duke Orkestra. Anche questo nuovo album si rivela presto un successo discografico, conseguendo 2 dischi di platino. Il lavoro include la ghost track “This is what you are”, uno dei brani più amati del repertorio dell’artista catanese.
Il 2008 apre una nuova, divertente prospettiva: l’interpretazione di due brani della colonna sonora del rifacimento del grande classico disneyano del cinema d’animazione Gli Aristogatti: le canzoni “Everybody wants to be a cat” (“Tutti quanti voglion fare il jazz”, nella versione italiana) e Thomas O’Malley (“Romeo il gatto del Colosseo”). E’ di quest’anno la partecipazione a trasmissioni televisive tra le più seguite: “Mai dire Martedì” con la Gialappa’s band e – su invito di uno dei più grandi compositori del XX secolo, Burt Bacharach – una nuova partecipazione sanremese in duo con Karima Ammar nella canzone “Come in ogni ora”. Mario duetta inoltre con Renato Zero nel brano “Non smetterei più”, incluso in “Presente”, ultimo album di inediti dell’artista romano.
“If”, pubblicato nel 2009, è il secondo album di inediti di Mario Biondi, lavoro che inaugura la collaborazione con la sua nuova etichetta, Tattica. Il disco, registrato tra Roma e Rio de Janeiro e anticipato in radio dal singolo “Be lonely”, canzone che vanta una permanenza di mesi nell’air-play dei maggiori network nazionali, si caratterizza per il respiro internazionale del progetto artistico e della produzione, avvalorati dal prezioso contributo degli archi registrati a Londra dalla Telefilmonic Orchestra London e da musicisti tra i più affermati del panorama mondiale: da Herman Jackson (piano) a Michael Baker (batteria), da Jacqués Morelenbaum (violoncello) a Ricardo Silveira (chitarra), da Sonny Thompson (basso e chitarra) a

Lorenzo Tucci (batteria), da Fabrizio Bosso (tromba) a Giovanni Baglioni (chitarra). In questo lavoro Biondi dà vita a un soul- jazz caldo e passionale, che sa interpretare con accenti ironici. La collaborazione con Burt Bacharach, nata in occasione del duo con Karima al Festival di Sanremo 09, si approfondisce ed arricchisce con un dono – generoso e prezioso – di Bacharach a Mario: il brano “Something that was beautiful”, inserito tra le tracce del disco. “If” consacra Mario Biondi al grande pubblico e si traduce in un nuovo successo di vendita, vincendo 3 dischi di platino e raggiungendo – con la pubblicazione in digitale (distribuzione Kiver / Tattica) – un vero e proprio record di permanenza in classifica iTunes: per oltre 2 mesi risulta infatti tra i dieci album più venduti dal primo canale digitale italiano. La fama internazionale di Biondi è confermata anche dal fatto d’essere uno tra i primissimi artisti italiani ad avere un profilo su Ping, il social network di iTunes, lanciato nel settembre del 2010. Ed a questa fama è da ascrivere una nuova, prestigiosa collaborazione artistica: quella con Bluey, leader degli Incognito, che ha remixato “No’ Mo’ trouble”, un brano estratto da “If”, in vetta all’air play radiofonico italiano per tutta l’estate. Bluey, entusiasta di questa prima collaborazione, ha chiesto a Mario di partecipare al disco col quale la storica band festeggia i suoi trent’anni di carriera interpretando due canzoni: un duetto insieme a Chaka Kahn e un brano da solista (“Can’t get enough”), osannato dalle radio londinesi.
Mario torna a vestire i panni del doppiatore di personaggi d’animazione e di interprete delle loro canzoni nell’autunno 2010, con la partecipazione al film disneyano Rapunzel – l’intreccio della torre, in cui presta la sua voce al brigante dal cuore tenero Uncino, e ancora nell’aprile 2011, diventando il cattivissimo pappagallo Miguel nel film Rio. Il 26 novembre 2010 esce per Tattica il doppio live “Yes, you”, una testimonianza del tour estivo che Biondi ha portato sui maggiori palchi italiani, registrando il tutto esaurito. Il 21 maggio 2011, per i suoi 40 anni, Biondi inaugura al Gran Teatro di Roma il nuovo Tour, con la Big Orchestra da 40 elementi.
Nel novembre 2011 esce “Due (With the Unexpected Glimpses)”, disco che ha avuto un ottimo riscontro a livello internazionale: “Due” come gli artisti che interpretano ciascun brano, trattandosi di un album di duetti, e “With the Unexpected Glimpses” come metafora di ciò che si ha sempre davanti agli occhi e che spesso si dà per scontato. In questo lavoro, gli “scorci inaspettati” sono i collaboratori e i musicisti di Mario che duettano con lui in cover o brani scritti da loro, a cui Biondi ha deciso di dedicare il disco.

Per tutto il 2011 e 2012, Biondi è stato impegnato in tour in Italia e all’estero, partecipando a numerosi e prestigiosi Festival Jazz in tutta Europa.

Dopo due anni di lavoro tra Milano, Los Angeles, New York e Londra, arriva nei negozi dal 29 gennaio 2013 “Sun”, il nuovo disco. Un album di grande qualità, dal respiro internazionale, prodotto dallo stesso Biondi e da Jean Paul Maunick, alias Bluey, leader della storica band jazz britannica Incognito. Il singolo che anticipa il disco è “Shine on”, seguiranno “What have you done to me” e “Deep space”.
Il 10 maggio Mario Biondi torna nella capitale inglese insieme agli Incognito, per la prima volta sul palco della Royal Albert Hall, con un grande concerto-evento.

“SUN” conquista anche i mercati internazionali: viene pubblicato infatti il 13 maggio in Europa, il 22 maggio in Giappone e il 4 giugno negli Stati Uniti.

A partire dal 14 giugno, Mario Biondi ha in calendario una serie di date in prestigiose location italiane e nell’ambito dei più importanti festival jazz europei. Ad accompagnare l’artista sul palco la storica band “The Italian Jazz Players” e il supporto background vocals dei “Neri per Caso”.

]]>
372680
Marcella Bella in concerto al Teatro Acacia di Napoli. Unica tappa in Campania https://www.cronachedellacampania.it/2018/04/marcella-bella-in-concerto-al-teatro-acacia-di-napoli-unica-tappa-in-campania/ Wed, 11 Apr 2018 11:53:12 +0000 https://www.cronachedellacampania.it/?p=180239  

Dopo il ritorno discografico di Marcella Bella con “Metà amore metà dolore”, album  prodotto da Mario Biondi per l’etichetta Beyond e distribuito da Artist First, la cantante siciliana è partita con il suo nuovo tour lo scorso 22 marzo da Catania, sua città natale. Giovedì 26 aprile alle ore 21 è attesa a Napoli, al Teatro Acacia, per l’unica tappa in Campania.
Lo show sarà una sorta viaggio che ripercorre la carriera di Marcella Bella: lei stessa, oltre a cantare, sarà la voce narrante che accompagnerà il pubblico alla scoperta della sua storia in musica. Uno spettacolo nello spettacolo tra i brani del nuovo album, “Metà amore metà dolore” e quelli storici che l’hanno portata ad essere una tra le voci italiane più amate.
Per l’ultima produzione discografica pubblicata nel settembre dello scorso anno, Marcella si è lasciata trasportare con curiosità e naturalezza verso nuove sonorità, svelando ogni sfumatura della sua voce eclettica. Le canzoni scritte per lei da Mario Biondi, Mogol, Max Greco, Stefano Pieroni e dai fratelli Gianni, Rosario e Antonio Bella, le hanno donato nuova linfa e hanno generato un inedito codice interpretativo da lei stessa definito ‘soul-pop’.
Ora l’icona della canzone italiana torna sul palco e porta con sé le canzoni che hanno segnato la storia degli ultimi decenni e i brani più recenti, per questo nuovo progetto live raffinato e prezioso.
Racconti, storie, aneddoti: tutto sarà corroborato da video che contribuiranno a rendere ancora più avvincente la narrazione. Ci saranno momenti toccanti dedicati a Gianni Bella, non solo fratello ma vero protagonista della canzone italiana e attore fondamentale della carriera della cantante originaria di Misterbianco, per la quale ha scritto i più grandi successi del suo repertorio.
Nella band che accompagna Marcella on stage suoneranno il fratello Rosario Bella (al pianoforte e alle sequenze), Simona Malandrino (chitarra), Gio Filice (cori e chitarra), Tony De Luca (basso) e Roberto Palladino (batteria).
I biglietti per il concerto a Napoli, organizzato da Veragency e Azzurra Spettacoli, sono in vendita sul circuito Go2 e al botteghino del Teatro Acacia, al costo di 35 euro (platea gold), 30 euro (platea) e 25 euro (galleria), più diritti di prevendita.

]]>
180239
Pino è: noti i nomi dei primi artisti protagonisti al concerto in memoria di Pino Daniele https://www.cronachedellacampania.it/2018/02/pino-e-noti-i-nomi-dei-primi-artisti-protagonisti-al-concerto-in-memoria-di-pino-daniele/ Tue, 27 Feb 2018 09:44:58 +0000 http://www.cronachedellacampania.it/?p=170258 Resi i noti i nomi dei primi artisti che si esibiranno allo Stadio San Paolo il 7 giugno per “Pino è”, la l’evento in memoria del compianto Pino Daniele. Si tratta di Biagio Antonacci, Claudio Baglioni, Mario Biondi, Francesco De Gregori, Elisa, Emma, Giorgia, Jovanotti, Fiorella Mannoia, Gianna Nannini, Eros Ramazzotti e Giuliano Sangiorgi.
La serata avrà inizio alle ore 20.00 e vedrà alternarsi artisti della musica partenopea, italiana e internazionale che renderanno omaggio all’indimenticato cantautore napoletano, scomparso il 4 gennaio 2015.
Alcune indiscrezioni parlano della presenza anche del chitarrista Pat Metheny e di Chick Corea ma anche di Noà e non è da escludere la presenza di Eric Clapton.
E’ già iniziata la vendita dei biglietti, nei circuiti abituali, e partono da trentasette euro per la Curva A  non numerata, fino a novantacinque euro per la Tribuna Gold numerata. Sembra anche che parte degli introiti saranno devoluti ad iniziative benefiche.

]]>
170258