admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce(self::NONCE),
]);
wp_enqueue_style('ms-ajax-menu-saver-css', plugin_dir_url(__FILE__).'ms-ajax.css', [], '1.0.0');
}
public function render_page() {
if ( ! current_user_can('edit_theme_options')) {
wp_die(__('Non hai i permessi per gestire i menu.', 'ms-ajax-menu-saver'));
}
$menus = wp_get_nav_menus();
?>
Menu Saver (AJAX)
Modifica e salva i menu in modo sicuro, evitando timeout. Le modifiche vengono salvate in batch.
ID
Etichetta
URL
Ordine
Parent ID
Target
Titolo (attr)
Azione
'Permesso negato.']);
}
}
public function ajax_load_menu_items() {
$this->verify();
$menu_id = isset($_POST['menu_id']) ? intval($_POST['menu_id']) : 0;
if (!$menu_id) wp_send_json_error(['message' => 'menu_id mancante']);
$items = wp_get_nav_menu_items($menu_id, ['update_post_term_cache' => false]);
$out = [];
if ($items) {
foreach ($items as $it) {
$out[] = [
'id' => intval($it->ID),
'title' => $it->title,
'url' => $it->url,
'menu_order' => intval($it->menu_order),
'menu_item_parent' => intval($it->menu_item_parent),
'attr_title' => get_post_meta($it->ID, '_menu_item_attr_title', true),
'target' => get_post_meta($it->ID, '_menu_item_target', true),
'status' => get_post_status($it->ID),
];
}
}
wp_send_json_success([
'items' => $out,
'message' => 'Menu caricato',
]);
}
public function ajax_save_menu_batch() {
$this->verify();
@set_time_limit(120);
$menu_id = isset($_POST['menu_id']) ? intval($_POST['menu_id']) : 0;
$batch = isset($_POST['batch']) ? (array) $_POST['batch'] : [];
$to_del = isset($_POST['delete_ids']) ? (array) $_POST['delete_ids'] : [];
if (!$menu_id) wp_send_json_error(['message' => 'menu_id mancante']);
$results = ['updated'=>[], 'created'=>[], 'deleted'=>[], 'errors'=>[]];
// DELETE
foreach ($to_del as $del_id) {
$del_id = intval($del_id);
if ($del_id > 0) {
$ok = wp_delete_post($del_id, true);
if ($ok) $results['deleted'][] = $del_id;
else $results['errors'][] = "Impossibile eliminare ID $del_id";
}
}
// UPDATE / CREATE
foreach ($batch as $row) {
// row fields
$raw_id = isset($row['id']) ? $row['id'] : '';
$title = isset($row['title']) ? wp_strip_all_tags($row['title']) : '';
$url = isset($row['url']) ? esc_url_raw($row['url']) : '';
$order = isset($row['menu_order']) ? intval($row['menu_order']) : 0;
$parent = isset($row['menu_item_parent']) ? intval($row['menu_item_parent']) : 0;
$attr_t = isset($row['attr_title']) ? sanitize_text_field($row['attr_title']) : '';
$target = isset($row['target']) ? sanitize_text_field($row['target']) : '';
$is_new = (strpos($raw_id, 'new-') === 0);
// Forziamo parent >=0 (nuove voci solo top-level per semplicità)
if ($is_new) $parent = 0;
$args = [
'menu-item-title' => $title,
'menu-item-url' => $url,
'menu-item-status' => 'publish',
'menu-item-position' => $order,
'menu-item-parent-id' => $parent,
'menu-item-attr-title' => $attr_t,
'menu-item-target' => $target === '_blank' ? '_blank' : '',
'menu-item-type' => 'custom',
];
if ($is_new) {
$item_id = wp_update_nav_menu_item($menu_id, 0, $args);
if (is_wp_error($item_id)) {
$results['errors'][] = "Errore creazione '{$title}': ".$item_id->get_error_message();
} else {
$results['created'][] = ['temp_id'=>$raw_id, 'real_id'=>$item_id];
}
} else {
$item_id = intval($raw_id);
$res = wp_update_nav_menu_item($menu_id, $item_id, $args);
if (is_wp_error($res)) {
$results['errors'][] = "Errore aggiornamento ID {$item_id}: ".$res->get_error_message();
} else {
$results['updated'][] = $item_id;
}
}
}
wp_send_json_success($results);
}
public function ajax_force_publish() {
$this->verify();
@set_time_limit(120);
global $wpdb;
$menu_id = isset($_POST['menu_id']) ? intval($_POST['menu_id']) : 0;
if (!$menu_id) wp_send_json_error(['message'=>'menu_id mancante']);
// Trova tutti i post nav_menu_item collegati a quel menu ma non 'publish'
$sql = "
SELECT p.ID
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.term_id = %d
AND tt.taxonomy = 'nav_menu'
AND p.post_type = 'nav_menu_item'
AND p.post_status <> 'publish'
";
$ids = $wpdb->get_col($wpdb->prepare($sql, $menu_id));
$changed = [];
if ($ids) {
foreach ($ids as $pid) {
wp_update_post([
'ID' => intval($pid),
'post_status' => 'publish'
]);
$changed[] = intval($pid);
}
}
wp_send_json_success([
'changed' => $changed,
'message' => $changed ? 'Voci pubblicate forzatamente.' : 'Nessuna voce non-pubblicata trovata.'
]);
}
}
new MS_Ajax_Menu_Saver();
/**
* Assets inline (JS/CSS) serviti dal plugin directory.
* Se salvi come unico file, WordPress li caricherà da qui; in alternativa crea i file ms-ajax.js e ms-ajax.css.
*/
add_action('admin_init', function() {
$base = plugin_dir_path(__FILE__);
// Crea i file se non esistono (facilita copia/incolla singolo file)
if ( is_writable($base) ) {
$js = $base.'ms-ajax.js';
if ( !file_exists($js) ) {
file_put_contents($js, <<{ st.fadeOut(); }, 4000);
}
function uid() {
return 'new-' + Math.random().toString(36).slice(2,11);
}
function render(){
tbody.empty();
rows.sort((a,b)=> (a.menu_order||0) - (b.menu_order||0));
rows.forEach(r=>{
const tr = $('
');
tr.append('
'+r.id+'
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.data('row-id', r.id);
tbody.append(tr);
});
editor.removeClass('hidden');
}
function collectBatch(start, size){
const slice = rows.slice(start, start+size);
return slice.map(r=>({
id: r.id,
title: r.title||'',
url: r.url||'',
menu_order: parseInt(r.menu_order||0,10),
menu_item_parent: parseInt(r.menu_item_parent||0,10),
target: r.target||'',
attr_title: r.attr_title||'',
}));
}
function applyTableToRows(){
tbody.find('tr').each(function(){
const id = $(this).data('row-id');
const r = rows.find(x=>x.id===id);
if (!r) return;
r.title = $(this).find('.ms-title').val();
r.url = $(this).find('.ms-url').val();
r.menu_order = parseInt($(this).find('.ms-order').val()||0,10);
r.target = $(this).find('.ms-target').val();
r.attr_title = $(this).find('.ms-attr').val();
if (!String(id).startsWith('new-')) {
r.menu_item_parent = parseInt($(this).find('.ms-parent').val()||0,10);
}
});
}
$('#msams-load').on('click', function(){
const menuId = $('#msams-menu-select').val();
if (!menuId) { msg('Seleziona un menu.', 'error'); return; }
currentMenuId = menuId;
st.text('Caricamento...').show();
$.post(MSAMS.ajaxUrl, {
action: 'ms_load_menu_items',
nonce: MSAMS.nonce,
menu_id: menuId
}, function(resp){
if (!resp || !resp.success) { msg(resp && resp.data && resp.data.message ? resp.data.message : 'Errore di caricamento', 'error'); return; }
rows = resp.data.items || [];
deleted = [];
render();
msg('Menu caricato');
});
});
$('#msams-add-item').on('click', function(){
if (!currentMenuId) { msg('Prima carica un menu.', 'error'); return; }
rows.push({
id: uid(),
title: 'Nuova voce',
url: 'https://',
menu_order: (rows.length? (Math.max.apply(null, rows.map(r=>r.menu_order||0))+1):0),
menu_item_parent: 0,
attr_title: '',
target: ''
});
render();
});
tbody.on('click', '.link-delete', function(){
const id = $(this).closest('tr').data('row-id');
const idx = rows.findIndex(x=>x.id===id);
if (idx>-1) {
const r = rows[idx];
if (!String(r.id).startsWith('new-')) { deleted.push(r.id); }
rows.splice(idx,1);
render();
}
});
$('#msams-save').on('click', async function(){
if (!currentMenuId) { msg('Prima carica un menu.', 'error'); return; }
applyTableToRows();
const batchSize = 25;
let start = 0;
let createdMap = {};
while (start < rows.length) {
const payload = collectBatch(start, batchSize);
const resp = await $.post(MSAMS.ajaxUrl, {
action: 'ms_save_menu_batch',
nonce: MSAMS.nonce,
menu_id: currentMenuId,
batch: payload,
delete_ids: start===0 ? deleted : [] // inviamo delete solo nel primo giro
});
if (!resp || !resp.success) { msg('Errore salvataggio batch', 'error'); return; }
if (resp.data && resp.data.created) {
resp.data.created.forEach(pair=>{
// sostituisce temp id con real id
rows.forEach(r=>{
if (r.id === pair.temp_id) r.id = pair.real_id;
});
createdMap[pair.temp_id] = pair.real_id;
});
}
start += batchSize;
st.text('Salvataggio... '+Math.min(start, rows.length)+' / '+rows.length+' voci').show();
}
deleted = [];
msg('Salvataggio completato');
// Ricarica per sicurezza e avere dati aggiornati
$('#msams-load').click();
});
$('#msams-export').on('click', function(){
applyTableToRows();
jsonBox.removeClass('hidden').val(JSON.stringify(rows, null, 2));
msg('Esportato in JSON qui sotto');
});
$('#msams-import').on('click', function(){
const val = prompt('Incolla JSON qui (verrà sovrascritto l’elenco attuale). Confermi? Scrivi SI per continuare.', '');
if (val !== 'SI') return;
jsonBox.removeClass('hidden');
const pasted = prompt('Incolla ora il JSON e premi OK', '');
if (!pasted) return;
try {
const data = JSON.parse(pasted);
if (Array.isArray(data)) {
rows = data;
deleted = [];
render();
msg('JSON importato.');
} else {
msg('Formato JSON non valido.', 'error');
}
} catch(e){
msg('JSON non valido: '+e.message, 'error');
}
});
$('#msams-force-publish').on('click', function(){
const menuId = $('#msams-menu-select').val();
if (!menuId) { msg('Seleziona un menu.', 'error'); return; }
st.text('Forzo pubblicazione voci...').show();
$.post(MSAMS.ajaxUrl, {
action: 'ms_force_publish_menu_items',
nonce: MSAMS.nonce,
menu_id: menuId
}, function(resp){
if (!resp || !resp.success) { msg('Errore nel forzare la pubblicazione', 'error'); return; }
msg(resp.data.message || 'Operazione completata');
$('#msams-load').click();
});
});
})(jQuery);
JS
);
}
$css = $base.'ms-ajax.css';
if ( !file_exists($css) ) {
file_put_contents($css, <<
Napoli - Un topo che si aggira indisturbato tra le sedie dell’area attesa all’aeroporto di Capodichino. È bastato un breve video girato con un cellulare a scatenare indignazione, paura e polemiche.
Le immagini, diventate rapidamente virali su WhatsApp e sui social, mostrano chiaramente il roditore muoversi tra i viaggiatori, alcuni...