/**
* 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 '
';
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 '
Ora ID Flusso Evento Dettagli ';
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 '' . esc_html($row['time']??'-') . ' ' . ($link?'#'.$id.' ':'-') . ' ' . esc_html($row['direction']??'-') . ' ' . esc_html($row['event']??'-') . ' ' . esc_html(implode(' · ',$details)) . ' ';
}
if (empty($rows)) echo 'Nessun evento nelle ultime 24 ore. ';
echo '
';
}
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 'Ora Sito Flusso Operazione Dettagli ';
foreach ($logs as $row) {
$details = [];
if (!empty($row['details']) && is_array($row['details'])) {
foreach ($row['details'] as $k => $v) $details[] = $k . ': ' . $v;
}
echo '';
echo '' . esc_html($row['time'] ?? '-') . ' ';
echo '' . esc_html($row['site'] ?? 'BE') . ' ';
echo '' . esc_html($row['direction'] ?? '-') . ' ';
echo '' . esc_html($row['event'] ?? '-') . ' ';
echo '' . esc_html(implode(' · ', $details)) . ' ';
echo ' ';
}
echo '
';
}
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 ' ';
}
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 '
';
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();
Giuseppe de carolis
https://www.cronachedellacampania.it/tag/giuseppe-de-carolis/
Le ultime notizie LIVE dalla Campania
Thu, 28 Jan 2021 13:04:55 +0000
it-IT
hourly
1
https://www.cronachedellacampania.it/wp-content/uploads/2026/04/cropped-cronache_logo_rotondo_2026-dimensioni-medie-32x32.png
Giuseppe de carolis
https://www.cronachedellacampania.it/tag/giuseppe-de-carolis/
32
32
112696088 -
La Corte Appello di Napoli: troppi processi finiscono con prescrizione
https://www.cronachedellacampania.it/2021/01/la-corte-appello-di-napoli-troppi-processi-finiscono-con-prescrizione/
Thu, 28 Jan 2021 13:35:37 +0000
https://www.cronachedellacampania.it/?p=404164
La Corte Appello di Napoli: troppi processi finiscono con prescrizione. Nel distretto della Corte di Appello di Napoli resta molto alta la percentuale di processi che si conclude con la prescrizione. L’allarme è stato lanciato dal presidente della Corte d’Appello di Napoli, Giuseppe De Carolis, anticipando la relazione in vista...]]>
La Corte Appello di Napoli: troppi processi finiscono con prescrizione.
Nel distretto della Corte di Appello di Napoli resta molto alta la percentuale di processi che si conclude con la prescrizione. L’allarme è stato lanciato dal presidente della Corte d’Appello di Napoli, Giuseppe De Carolis, anticipando la relazione in vista della cerimonia di inaugurazione dell’anno giudiziario. I procedimenti penali definiti in appello con declaratoria di estinzione del reato per prescrizione sono stati 3.626, pari al 39,6%, mentre negli altri uffici del Distretto sono stati complessivamente 67 per il dibattito collegiale (pari al 4,9%), 2.565 per il dibattimento monocratico (pari al 9,6%), 2.562 per le sezioni gip-gup (pari al 4,4%) e 2.302 per le procure della Repubblica (pari al 2,4%).
]]>
404164
-
Mancano cancellieri e magistrati, l’allarme all’inaugurazione dell’Anno Giudiziario a Napoli
https://www.cronachedellacampania.it/2019/01/mancano-cancellieri-e-magistrati-lallarme-allinaugurazione-dellanno-giudiziario-a-napoli/
Sat, 26 Jan 2019 10:56:13 +0000
https://www.cronachedellacampania.it/?p=243384
“Tutti gli uffici presentano carenze di organico, specialmente di personale amministrativo, nonostante l’entrata in servizio di nuovi assistenti giudiziari”. Questo l’allarme lanciato dal presidente della Corte d’Appello di Napoli, Giuseppe de Carolis, nel giorno dell’inaugurazione dell’Anno Giudiziario del Distretto di Napoli. “Si auspica che il ministero provveda al piu’ presto...]]>
“Tutti gli uffici presentano carenze di organico, specialmente di personale amministrativo, nonostante l’entrata in servizio di nuovi assistenti giudiziari”. Questo l’allarme lanciato dal presidente della Corte d’Appello di Napoli, Giuseppe de Carolis, nel giorno dell’inaugurazione dell’Anno Giudiziario del Distretto di Napoli. “Si auspica che il ministero provveda al piu’ presto allo scorrimento della graduatoria e a nuove assunzione, perche’ la pianta organica del personale di Corte d’Appello e’ rimasta invariata nonostante la riforma del giudice primo”. Ancora e’ allarme per la carenza di magistrati e per i consiglieri di Corte d’Appello: “in mancanza di riforme si puo’ affermare che la Corte non potra’ svolgere il propio compito in maniera corretta”. La grande sofferenza e’ in corte d’Appello che e’ diventato un imbuto del sistema Giudiziario del Distretto “che non e’ in grado di smaltire in tempi ragionali l’enorme mole di processi che arrivano. La conseguenza e’ la vanificazione del lavoro svolto perche’ una volta emessa la sentenza poi il fascicolo si accumula negli armadi”. Nel settore Civile la modifica dell’assetto tabellare ha consentito di dare inizio a una graduale perequazione dei carichi tra tutte le sezione e una generalizzata diminuzione del tempo di attesa di definizione. Nel settore penale la priorita’ e’ stata la celebrazione dei processi per omicidio in Corte d’Assise. “Ma anche qui si soffre per la carenza di personale con addirittura ben 15 magistrati in meno che andrebbero a comporre 5 sezioni”. A Napoli Nord e’ vero allarme: “Un ufficio in perenne sofferenza che anche quest’Anno sia nel penale che nel civile ha avuto una sopravvenienza di nuovi procedimenti di circa 40mila fascicoli. La pianta organica e’ inadeguata e tutti gli allarmi lanciati dal presidente del Tribunale sono stati inascoltati.La coesione tra magistratura e avvocatura sono i componenti essenziali di questo Distretto che hanno il comune obiettivo della giustizia, soprattutto in questo momento in cui il dibattito rischia di essere fuorviato dalla massiccia diffusione dei social network, che ricordano in qualche modo le folle in tumulto di manzoniana memorie a tendono a polarizzare le opinioni secondo una logica di tifoseria che nuoce alla serenita’ di giudizio generando attacchi violenti contro i giudici sui social”. Lo sottolinea nella sua relazione Giuseppe de Carolis, presidente della Corte d’Appello del Distretto di Napoli, nel corso dell’inaugurazione dell’Anno Giudiziario. “E’ pur vero che la lentezza dei tempi dei processi rimane il principale problema e genera frustrazione nelle parti coinvolte nel processo penale perche’ ad esempio nel processo penale non garantisce la certezza della pena in tempi ragionevoli per i colpevoli e non garantisce abbastanza gli innocenti che rimangono esposti per troppo tempo a una gogna mediatica. Ma e’ altrettanto vero che l’accelerazione del processo non puo’ significare una compressione delle garanzie processuali”. Infine l’autocritica: perche’ talvolta il rischio di delegittimazione della giurisdizione puo’ derivare talvolta anche da comportamenti non corretti posti in essere dai magistrati. Si tratta per fortuna di casi isolati”, ha concluso.
]]>
243384
-
Anno Giudiziario, il procuratore generale di Napoli: Fare affari con la camorra non conviene’
https://www.cronachedellacampania.it/2019/01/anno-giudiziario-il-procuratore-generale-di-napoli-fare-affari-con-la-camorra-non-conviene/
Thu, 24 Jan 2019 12:00:42 +0000
https://www.cronachedellacampania.it/?p=242861
“Fare gli affari con la camorra non è conveniente perché o si viene arrestati o si finisce in bancarotta, prima o poi si viene scoperti”. Lo ha detto Luigi Riello, procuratore generale della Corte dei Conti di Napoli, durante la conferenza stampa di presentazione dell’anno giudiziario che si terrà sabato....]]>
“Fare gli affari con la camorra non è conveniente perché o si viene arrestati o si finisce in bancarotta, prima o poi si viene scoperti”. Lo ha detto Luigi Riello, procuratore generale della Corte dei Conti di Napoli, durante la conferenza stampa di presentazione dell’anno giudiziario che si terrà sabato. “È fondamentale i fili invisibili che esistono tra una società apparentemente per bene e la camorra” , ha spiegato. E poi ha aggiunto: “La camorra, quando viene colpita, reagisce in maniera scomposta, ma il messaggio deve essere chiaro: questi fili vanno recisi. C’è maggiore fluidità nella camorra e gli assetti strutturati di una volta sono cambiati. Napoli, dunque, resta una città difficile da analizzare e difficile da schematizzare, ma il lavoro della magistratura e il controllo territorio sortiscono effetti importanti. Tuttavia, neanche il tempo di rallegrarci che sono avvenuti fatti difficilmente leggibili”, sottolinea Riello. Forte resta la “commistione sotterranea tra criminalita’ e persone insospettabili, e cioe’ notabili, politici imprenditori e liberi professionisti – spiega – ci sono fili non visibili nelle statistiche e che dobbiamo recidere. Questi fili sono stati recisi perché le indagini della Dda di Napoli hanno ottenuto risultati importanti, ma persiste la volontà e la vocazione imprenditrice da parte camorra che trasforma la violenza in forza economica. Non cerchiamo eroi tra i cittadini, ma le due città apparentemente separate si incontrano: merci contraffatti, uso di stupefacenti, prostituzione, credito illegale, parcheggi abusivi e contrabbando”.
Aumento pendenza procedimenti penali
Pendenza dei procedimenti penali in aumento mentre diminuiscono quelli civili. È quanto emerge dalla relazione presentata, questa mattina a Napoli, dal procuratore generale Luigi Riello e dal presidente della Corte d’appello Giuseppe de Carolis di Prossedi’. La carenza di organico è l’elemento che ha portato all’aumento nel settore penale, dove per le sezioni ordinarie c’è stato un aumento del 9,2%.”ci sono 15 magistrati in meno e quindi immaginate quanto questo possa pesare”, ha spiegato De Carolis di Prossedi’.
“Allarme prescrizione”
Circa 3mila processi ogni anno nel Distretto di Corte d’Appello di Napoli vengono estinti per prescrizione. Il 33 per cento dei reati si perdono in secondo grado. “Mancano 15 magistrati e se pensiamo che c’e’ un solo un ausiliario idoneo a spingere i carrelli con i fascicoli che viaggiano da una parte all’altra del tribunale. Questo rende chiara l’emergenza”, ha spiegato Giuseppe de Carolis, presidente della Corte d’Appello di Napoli incontrando la stampa a pochi giorni dall’inaugurazione del nuovo anno giudiziario. Per il procuratore generale, Luigi Riello, neanche la “sospensione della prescrizione potrebbe servire, dato che corre il rischio di aumentare processi lumaca. Vanno modificate tante cose, come ad esempio le letture di atti processuali, sanzioni alternative al carcere effettive e dissuasive e stanziamenti straordinari per il settore Giustizia”. La Corte d’Appello del Distretto di Napoli resta al secondo posto in Italia per numero di processi definiti, subito dopo Roma. E’ record con 11.187 procedimenti penali definiti, mentre ne sono arrivati 15mila che sono “difficili da gestire con organico non adeguato”, spiega de Carolis. Sono stati definiti 95 processi in Corte d’Assise, “che e’ il nostro codice rosso”. Nel settore civile, si e’ passati da 49 mila processi a 45mila pendenti. I tribunali civili del Distretto hanno in totale 208mila pendenze. A Napoli Nord la “situazione e’ drammatica”, con 25mila processi definiti e “una sopravvenienza enorme con risorse inferiori e inadeguate”, dice ancora De Carolis. Sono diminuite le pendenze per i processi di protezione internazionale, ma sono ancora a quota 3.300. Nel settore penale, aumenta la pendenza in Corte d’appello nonostante mole di sopravvenienza di 15mila nuovi processi. Aumentano le pendenze per i minorenni, i cui processi sono passati da 104 a 145. Oltre alla definizione dei processi per omicidio, l’altra priorita’, per De Carolis, e’ l’esecuzione: “Tra il 2017 e il 2018 sono stati emessi piu di 8mila estratti esecutivi. Nel primo semestre sono stati 2.999”. Infine, sull’andamento dei reati si evidenzia un calo nell’anno solare 2018, passando da 131mila a 128mila. Gli omicidi sono scesi da 35 a 21 e solo 8 sono legati alla camorra rispetto ai 22 dell’anno precendente. Diminuiti in modo massiccio gli incendi boschivi, passati da 53 a 8. Vi sono ancora 4700 rapine, 660 casi di usura, 46 le associazioni. Ancora alti i numeri dei reati legati allo spaccio con 2mila reati commessi. Aumentati i furti con 68mila casi nel solo 2018 e i delitti informatici passati da 303 a 521.
Grave carenza dei servizi sociali
Sul fronte della lotta alla criminalità minorile “abbiamo denunciato la carenza di servizi sociali, di operatori sociali. Su questo profilo non sono stati fatti passi da gigante e questo è molto grave”. Ha detto ancora il procuratore generale di Napoli, Luigi Riello. E poi ha aggiunto: “La giustizia lenta è uno dei cancri della giustizia italiana, ma la risposta del Tribunale per i minorenni sugli episodi sconvolgenti dello scorso anno è stata molto rapida. Sono stati accesi i riflettori su questa realtà, è venuto a Napoli il Csm che ha svolto un plenum qui. Noi abbiamo denunciato soprattutto la carenza di servizi sociali e su questo profilo non sono stati fatti passi da gigante e questo molto grave”. Riello ha ribadito che “la risposta repressiva dello Stato c’è stata, anche con norme più dissuasive, ma senza una bonifica sociale vera con forze e fondi straordinari non andiamo da nessuna parte”.
Camorra: meno omicidi ma la situazione resta grave
Resta “grave” la situazione con riguardo alla criminalità organizzata a Napoli e provincia “nonostante la rilevante riduzione degli omicidi di camorra”. Ha invece spiegato il presidente della Corte d’Appello di Napoli, Giuseppe De Carolis di Prossedi, nel corso di un incontro con la stampa in vista della cerimonia di inaugurazione dell’anno giudiziario che si terrà sabato mattina al Maschio Angioino. La criminalità organizzata, ha spiegato De Carolis citando la Questura di Napoli, “ha assunto in città e provincia negli ultimi tempi delle caratteristiche di maggiore fluidità, con sodalizi criminali composti anche da elementi molto giovani che mutano assetto, consistenza e obiettivi nel volgere di pochi mesi e si scontrano tra loro con metodi violenti e colpi di ‘stese’ per il controllo delle piazze di spaccio e talvolta anche per contendersi minime estensioni di territorio, senza la capacità di misurare il rapporto tra benefici e costi delle proprie azioni criminali, come dimostrano anche i preoccupanti episodi verificatisi di recente di esplosioni di ordigni nel centro storico di Napoli e soprattutto di Afragola, che hanno creato un notevole allarme sociale”.
]]>
242861
-
Giustizia di prossimità e Campi Flegrei, convegno del Sindacato Forense Puteolano a Pozzuoli
https://www.cronachedellacampania.it/2018/06/giustizia-di-prossimita-e-campi-flegrei-convegno-del-sindacato-forense-puteolano-a-pozzuoli/
Thu, 21 Jun 2018 14:09:02 +0000
https://www.cronachedellacampania.it/?p=194715
“Giustizia di prossimità. Campi flegrei e la rilevanza degli uffici giudiziari periferici nell’attuale geografia giudiziaria”. Si discuterà di questo al convegno organizzato dal Sindacato Forense Puteolano con il patrocinio dell’Ordine degli Avvocati di Napoli e del Comune di Pozzuoli, venerdì 22 giugno (a partire dalle ore 16), negli spazi del Villaggio del Fanciullo di via Campi Flegrei, 12 a Pozzuoli (Napoli). I lavori, moderati dall’avv. Maria Cristallino (segretario del Sindacato Forense), saranno introdotti dal prof. Nicola Rascio, ordinario di Procedura Civile alla Federico II di Napoli. Seguiranno i saluti del dr. Giuseppe De Carolis, presidente della Corte D’Appello di Napoli, e del dr. Ettore Ferrara, presidente del Tribunale di Napoli; quindi gli interventi dell’avv. Maurizio Bianco, presidente del Consiglio dell’Ordine degli Avvocati di Napoli; dell’avv. Vincenzo Crasto, giudice di pace e componente del consiglio giudiziario Sezione Autonoma e dell’avv. Franco Capasso, presidente dell’Associazione Fract Major. Le conclusioni saranno affidate a Fernando Bocchini, professore di Diritto Privato alla Federico II di Napoli nonché professore Straordinario (nella stessa materia) all’Università Pegaso.
]]>
194715
-
Camorra, il presidente della Corte di appello: ‘Giovani leve nei clan e stese in aumento’
https://www.cronachedellacampania.it/2018/01/camorra-il-presidente-della-corte-di-appello-giovani-leve-nei-clan-e-stese-in-aumento/
Thu, 25 Jan 2018 18:16:59 +0000
http://www.cronachedellacampania.it/?p=164505
“Nonostante l’obiettiva e significativa riduzione dei delitti più gravi e in particolare di quelli predatori, che hanno maggiore impatto sull’opinione pubblica”, nel Distretto della Corte d’Appello di Napoli “restano alti l’allarme sociale e la percezione di insicurezza dei cittadini”. Lo ha dichiarato il presidente della Corte d’Appello di Napoli, Giuseppe...]]>
“Nonostante l’obiettiva e significativa riduzione dei delitti più gravi e in particolare di quelli predatori, che hanno maggiore impatto sull’opinione pubblica”, nel Distretto della Corte d’Appello di Napoli “restano alti l’allarme sociale e la percezione di insicurezza dei cittadini”. Lo ha dichiarato il presidente della Corte d’Appello di Napoli, Giuseppe De Carolis, citando in particolare “l’allarme sociale che resta alto per la presenza nel territorio del Distretto, soprattutto nelle province di Napoli e Caserta, di numerose organizzazioni camorriste, spesso in feroce lotta tra loro per assicurarsi il predominio nella gestione delle attività illecite e, in particolare, il traffico di sostanze stupefacenti e le estorsioni in danno di attività commerciali e imprenditoriali e gli ingenti profitti economici che ne derivano”. De Carolis ha sottolineato inoltre un aspetto particolarmente importante: “Nelle file delle organizzazioni camorriste militano sempre più giovani, attirati dal miraggio di facili e immediati guadagni, che mirano a raggiungere posizioni apicali nell’ambito delle organizzazioni criminali, approfittando talvolta dell’arresto dei vecchi capi da parte delle forze dell’ordine”. Si è “ulteriormente diffuso” inoltre “il pericoloso fenomeno delle cosiddette ‘stese’, scorribande eseguite in sella a motocicli sparando all’impazzata, finalizzate a ostentare il controllo del territorio sia nei confronti degli abitanti della zona sia nei confronti delle organizzazioni rivali, che talvolta hanno causato anche il ferimento di persone innocenti che per caso si trovavano affacciate a un balcone”. Nella conferenza stampa che anticipa l’inaugurazione dell’anno giudiziario il presidente della Corte di Appello di Napoli Giuseppe De Carolis de Prossedi e dal procuratore generale Luigi Riello hanno fatto il punto sull’incidenza dei reati nel distretto napoletano. Diminuiscono i reati predatori: mille rapine in meno, sensibile riduzione di omicidi e furti, ma resta alta la percezione di insicurezza da parte dei cittadini. Per quanto riguarda i numeri, particolare rilievo e’ stato dato all’aumento del 4 per cento delle pendenze nel settore penale. Criticita’ presenta la situazione della Corte di assise di appello, dove pur avendo definito 104 processi ne sono sopravvenuti 121, conseguenza soprattutto della gran parte di procedimenti conclusi in primo grado davanti al gup con rito abbreviato. Per tale motivo De Carolis ha chiesto l’invio a Napoli di altri magistrati e l’istituzione di una quinta sezione della Corte di assise di appello. Dato positivo la percentuale di processi conclusi in prescrizione (dal 39 al 27,6 per cento).
Nel settore civile riduzione delle pendenze sia in Appello sia davanti ai tribunali del distretto. Diminuisce dell’8 per cento il numero complessivo dei reati commessi nelle aree del distretto di Napoli. Una particolare attenzione al fenomeno della criminalita’ minorile, dove a fronte di una complessiva diminuzione dei reati si assiste a un aumento della gravita’ degli illeciti, come emerge dalle contestazioni di associazione per delinquere e droga, mentre per quanto riguarda gli episodi di bullismo la novita’ e’ costituita dal fatto che diversi casi si sono registrati anche in zone ”che venivano ritenute sicure” e non piu’ soltanto nelle periferie degradate. De Carolis ha evidenziato, a tale proposito, l’assenza in citta’ ”di zone franche” mentre Riello ha parlato di ”salto di qualita’ della criminalita’ minorile”. Quello delle baby gang e’ un problema, ha detto il pg, ”che si deve affrontare sul piano della bonifica sociale”. ”Sarebbe stupido rispondere solo sul piano repressivo. Fermezza e recupero non sono termini antitetici” ha affermato sottolineando l’importanza delle misure alternative al carcere ed a misure di sostegno alle famiglie in difficoltà. Si tratta di un fenomeno non nuovo – ha dichiarato il presidente della Corte di appello – ma che ha assunto negli ultimi tempi connotazioni di particolare gravita’, quasi una sorta di evoluzione criminale del bullismo, che non puo’ essere sottovalutato ma deve essere oggetto di grande attenzione”. La diminuzione dei reati dei minori e’ legata anche al fatto che ”sono diminuiti i minori in quanto a Napoli, come nel resto d’Italia, si e’ alzata l’eta’ media della popolazione a causa dell’abbassamento della natalita”’. Tra i punti fondamentali ”molto importante in funzione preventiva e’ la lotta alla dispersione scolastica”. Meno 47 per cento gli omicidi volontari (da 63 a 33 di cui 26 legati alla criminalita’ organizzata), diminuiscono le rapine, le estorsioni, i furti, le ricettazioni, le violenze sessuali, i casi di sfruttamento della prostituzione. Aumentano invece i reati relativi alle sostanze stupefacenti, da 1805 a 2011. De Carolis e Riello hanno evidenziato il fenomeno dell’abusivismo edilizio ”incoraggiato da politiche dissennate”, come ha osservato il pg. I magistrati hanno chiamato in causa la scarsa collaborazione e l’inerzia dei sindaci e delle amministrazioni locali, che mettono a dormire le pratiche di condono e non chiedono i contributi per eseguire le demolizioni. Sulla questione dell’abusivismo e delle demolizioni e’ stato costituito una task force, coordinata dall’avvocato generale Antonio Gialanella. Numeri rilevanti anche sul fronte della criminalita’ organizzata, dove si registra l’esecuzione di 121 ordini di custodia cautelare, il sequestro di 870 milioni di euro e la cattura di 55 latitanti.
]]>
164505