false, 'message' => $msg] + $extra, $code); } $pdo = DBHandlerSelect::getInstance()->getConnection(); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $userId = (int) $iduserlogin; $action = $_REQUEST['action'] ?? ''; $isPost = $_SERVER['REQUEST_METHOD'] === 'POST'; const NL_MAX_JSON = 2 * 1024 * 1024; // 2 MB di blocchi bastano e avanzano /* ------------------------------------------------------------------------- * Funzioni di supporto * ---------------------------------------------------------------------- */ function draftRow(PDO $pdo, int $id): array { $st = $pdo->prepare("SELECT * FROM newsletter_drafts WHERE iddraft = ? AND deleted_at IS NULL"); $st->execute([$id]); $d = $st->fetch(PDO::FETCH_ASSOC); if (!$d) fail('Newsletter non trovata.', 404); return $d; } function draftPayload(array $d): array { $wpStatus = null; if (!empty($d['wp_email_id']) && NewsletterWp::isConfigured()) { try { $wpStatus = NewsletterWp::statuses([(int) $d['wp_email_id']])[(int) $d['wp_email_id']] ?? 'deleted'; } catch (Throwable $e) { $wpStatus = null; } } return [ 'id' => (int) $d['iddraft'], 'title' => $d['title'], 'subject' => $d['subject'], 'preheader' => $d['preheader'], 'blocks' => json_decode($d['blocks'] ?: '[]', true) ?: [], 'settings' => NewsletterRenderer::settings(json_decode($d['settings'] ?: '{}', true) ?: []), 'revision' => (int) $d['revision'], 'updated_at' => $d['updated_at'], 'wp_email_id' => $d['wp_email_id'] !== null ? (int) $d['wp_email_id'] : null, 'wp_pushed_at' => $d['wp_pushed_at'], 'wp_status' => $wpStatus, ]; } /** Legge e valida blocchi + impostazioni inviati dall'editor. */ function readContent(): array { $blocksRaw = (string) ($_POST['blocks'] ?? '[]'); $settingsRaw = (string) ($_POST['settings'] ?? '{}'); if (strlen($blocksRaw) > NL_MAX_JSON) fail('La newsletter è troppo grande.', 413); $blocks = json_decode($blocksRaw, true); $settings = json_decode($settingsRaw, true); if (!is_array($blocks) || !is_array($settings)) fail('Dati non validi.', 422); $clean = []; foreach ($blocks as $b) { if (!is_array($b) || !in_array($b['type'] ?? '', NewsletterRenderer::BLOCK_TYPES, true)) continue; $b['id'] = preg_replace('/[^a-z0-9]/i', '', (string) ($b['id'] ?? '')) ?: bin2hex(random_bytes(6)); $clean[] = $b; } return [ 'title' => mb_substr(trim((string) ($_POST['title'] ?? '')), 0, 255), 'subject' => mb_substr(trim((string) ($_POST['subject'] ?? '')), 0, 255), 'preheader' => mb_substr(trim((string) ($_POST['preheader'] ?? '')), 0, 255), 'blocks' => $clean, 'settings' => $settings, ]; } function addVersion(PDO $pdo, array $d, string $note, int $userId): void { $pdo->prepare("INSERT INTO newsletter_draft_versions (iddraft, revision, subject, preheader, blocks, settings, note, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())") ->execute([(int) $d['iddraft'], (int) $d['revision'], $d['subject'], $d['preheader'], $d['blocks'], $d['settings'], mb_substr($note, 0, 100), $userId]); // Tiene le ultime 200 versioni per newsletter $pdo->prepare("DELETE FROM newsletter_draft_versions WHERE iddraft = ? AND idversion NOT IN ( SELECT idversion FROM (SELECT idversion FROM newsletter_draft_versions WHERE iddraft = ? ORDER BY idversion DESC LIMIT 200) keep )")->execute([(int) $d['iddraft'], (int) $d['iddraft']]); } function createDraft(PDO $pdo, int $userId, string $title, string $subject, string $preheader, array $blocks, array $settings, ?int $wpId = null): int { $pdo->prepare("INSERT INTO newsletter_drafts (title, subject, preheader, blocks, settings, revision, wp_email_id, created_by, updated_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, NOW(), NOW())") ->execute([$title, $subject, $preheader, json_encode($blocks, JSON_UNESCAPED_UNICODE), json_encode($settings, JSON_UNESCAPED_UNICODE), $wpId, $userId, $userId]); return (int) $pdo->lastInsertId(); } function baseUrl(): string { if (!empty($_ENV['NEWSLETTER_BASE_URL'])) return rtrim($_ENV['NEWSLETTER_BASE_URL'], '/'); $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; $dir = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/')), '/'); return ($https ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . $dir; } /* ------------------------------------------------------------------------- * Azioni * ---------------------------------------------------------------------- */ try { switch ($action) { case 'load': out(['success' => true, 'draft' => draftPayload(draftRow($pdo, (int) ($_GET['id'] ?? 0)))]); case 'save': if (!$isPost) fail('Metodo non consentito.', 405); $id = (int) ($_POST['id'] ?? 0); $rev = (int) ($_POST['revision'] ?? -1); $force = ($_POST['force'] ?? '') === '1'; $c = readContent(); $sql = "UPDATE newsletter_drafts SET title = ?, subject = ?, preheader = ?, blocks = ?, settings = ?, revision = revision + 1, updated_by = ?, updated_at = NOW() WHERE iddraft = ? AND deleted_at IS NULL" . ($force ? '' : ' AND revision = ?'); $params = [ $c['title'], $c['subject'], $c['preheader'], json_encode($c['blocks'], JSON_UNESCAPED_UNICODE), json_encode($c['settings'], JSON_UNESCAPED_UNICODE), $userId, $id ]; if (!$force) $params[] = $rev; $st = $pdo->prepare($sql); $st->execute($params); $d = draftRow($pdo, $id); if ($st->rowCount() === 0) { fail('Questa newsletter è stata salvata da un\'altra parte (un\'altra scheda o un altro computer).', 409, [ 'conflict' => true, 'revision' => (int) $d['revision'], 'updated_at' => $d['updated_at'], ]); } // Versione automatica ogni 10 minuti di lavoro $st = $pdo->prepare("SELECT MAX(created_at) FROM newsletter_draft_versions WHERE iddraft = ?"); $st->execute([$id]); $last = $st->fetchColumn(); if (!$last || strtotime($last) < time() - 600) { addVersion($pdo, $d, 'Automatica', $userId); } out(['success' => true, 'revision' => (int) $d['revision'], 'updated_at' => $d['updated_at']]); case 'versions': $st = $pdo->prepare("SELECT v.idversion, v.revision, v.note, v.created_at, v.subject, TRIM(CONCAT(COALESCE(u.first_name, ''), ' ', COALESCE(u.last_name, ''))) AS author FROM newsletter_draft_versions v LEFT JOIN auth_users u ON u.id = v.created_by WHERE v.iddraft = ? ORDER BY v.idversion DESC LIMIT 200"); $st->execute([(int) ($_GET['id'] ?? 0)]); out(['success' => true, 'versions' => $st->fetchAll(PDO::FETCH_ASSOC)]); case 'version_html': $st = $pdo->prepare("SELECT * FROM newsletter_draft_versions WHERE idversion = ?"); $st->execute([(int) ($_GET['idversion'] ?? 0)]); $v = $st->fetch(PDO::FETCH_ASSOC); if (!$v) fail('Versione non trovata.', 404); if (ob_get_length() !== false) ob_end_clean(); header('Content-Type: text/html; charset=utf-8'); echo NewsletterRenderer::renderDocument(json_decode($v['blocks'] ?: '[]', true) ?: [], json_decode($v['settings'] ?: '{}', true) ?: [], $v['subject'], $v['preheader'], true); exit; case 'snapshot': if (!$isPost) fail('Metodo non consentito.', 405); $d = draftRow($pdo, (int) ($_POST['id'] ?? 0)); addVersion($pdo, $d, trim((string) ($_POST['note'] ?? '')) ?: 'Salvata a mano', $userId); out(['success' => true]); case 'restore': if (!$isPost) fail('Metodo non consentito.', 405); $id = (int) ($_POST['id'] ?? 0); $d = draftRow($pdo, $id); $st = $pdo->prepare("SELECT * FROM newsletter_draft_versions WHERE idversion = ? AND iddraft = ?"); $st->execute([(int) ($_POST['idversion'] ?? 0), $id]); $v = $st->fetch(PDO::FETCH_ASSOC); if (!$v) fail('Versione non trovata.', 404); $pdo->beginTransaction(); addVersion($pdo, $d, 'Prima del ripristino', $userId); $pdo->prepare("UPDATE newsletter_drafts SET subject = ?, preheader = ?, blocks = ?, settings = ?, revision = revision + 1, updated_by = ?, updated_at = NOW() WHERE iddraft = ?") ->execute([$v['subject'], $v['preheader'], $v['blocks'], $v['settings'], $userId, $id]); $pdo->commit(); out(['success' => true, 'draft' => draftPayload(draftRow($pdo, $id))]); case 'preview': if (!$isPost) fail('Metodo non consentito.', 405); $c = readContent(); $real = NewsletterRenderer::renderDocument($c['blocks'], $c['settings'], $c['subject'], $c['preheader']); out([ 'success' => true, 'html' => NewsletterRenderer::renderDocument($c['blocks'], $c['settings'], $c['subject'], $c['preheader'], true), 'kb' => (int) round(strlen($real) / 1024), 'checks' => NewsletterRenderer::checks($c['blocks'], $c['subject'], $c['preheader'], $real), ]); case 'upload_image': if (!$isPost) fail('Metodo non consentito.', 405); $f = $_FILES['image'] ?? null; if (!$f || ($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $why = [ UPLOAD_ERR_INI_SIZE => 'il file supera il limite del server (upload_max_filesize)', UPLOAD_ERR_FORM_SIZE => 'il file è troppo grande', UPLOAD_ERR_PARTIAL => 'caricamento interrotto', UPLOAD_ERR_NO_FILE => 'nessun file ricevuto', UPLOAD_ERR_NO_TMP_DIR => 'manca la cartella temporanea sul server', UPLOAD_ERR_CANT_WRITE => 'il server non riesce a scrivere il file', ][$f['error'] ?? UPLOAD_ERR_NO_FILE] ?? 'errore sconosciuto'; fail('Caricamento non riuscito: ' . $why . '.'); } if ($f['size'] > 15 * 1024 * 1024) fail('Immagine troppo grande (massimo 15 MB).'); $info = @getimagesize($f['tmp_name']); $mime = $info['mime'] ?? ''; $ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp'][$mime] ?? null; if (!$ext) fail('Formato non supportato: usa JPG, PNG, GIF o WebP.'); $dirRel = 'assets/uploads/newsletter/' . date('Y/m'); $dirAbs = __DIR__ . '/' . $dirRel; if (!is_dir($dirAbs) && !@mkdir($dirAbs, 0755, true)) { fail('Impossibile creare la cartella ' . $dirRel . ': controlla i permessi di assets/uploads.', 500); } if (!is_writable($dirAbs)) fail('La cartella ' . $dirRel . ' non è scrivibile: controlla i permessi.', 500); $name = date('Ymd_His') . '_' . bin2hex(random_bytes(4)); [$w, $h] = $info; $note = ''; // Si può ottimizzare? Serve GD con il formato giusto $reader = ['image/jpeg' => 'imagecreatefromjpeg', 'image/png' => 'imagecreatefrompng', 'image/webp' => 'imagecreatefromwebp'][$mime] ?? null; $canResize = $mime !== 'image/gif' && $reader && function_exists($reader) && function_exists('imagecreatetruecolor'); if (!$canResize) { // GIF (forse animata) oppure server senza GD: salviamo l'originale if ($mime !== 'image/gif' && $f['size'] > 3 * 1024 * 1024) { fail('Il server non può ridimensionare le immagini (manca la libreria GD) e questa supera 3 MB: riducila prima di caricarla.'); } $file = $name . '.' . $ext; if (!move_uploaded_file($f['tmp_name'], $dirAbs . '/' . $file)) fail('Impossibile salvare l\'immagine.', 500); if ($mime !== 'image/gif') $note = 'Caricata senza ottimizzazione (sul server manca la libreria GD).'; } else { try { @ini_set('memory_limit', '512M'); // le foto dal telefono sono grandi $src = @$reader($f['tmp_name']); if (!$src) fail('Immagine non leggibile o danneggiata.'); // Foto dal telefono: raddrizza secondo l'orientamento EXIF if ($mime === 'image/jpeg' && function_exists('exif_read_data')) { $o = (int) (@exif_read_data($f['tmp_name'])['Orientation'] ?? 1); $rot = [3 => 180, 6 => -90, 8 => 90][$o] ?? 0; if ($rot) { $src = imagerotate($src, $rot, 0); $w = imagesx($src); $h = imagesy($src); } } $nw = min($w, 1200); // il doppio della larghezza email: nitida sugli schermi retina $nh = max(1, (int) round($h * $nw / $w)); // PNG con trasparenza resta PNG, il resto diventa JPG (molto più leggero) $keepPng = false; if ($mime === 'image/png') { $ct = ord((string) file_get_contents($f['tmp_name'], false, null, 25, 1)); $keepPng = in_array($ct, [4, 6], true); } $dst = imagecreatetruecolor($nw, $nh); if ($keepPng) { imagealphablending($dst, false); imagesavealpha($dst, true); } else { imagefill($dst, 0, 0, imagecolorallocate($dst, 255, 255, 255)); } imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $w, $h); $file = $name . ($keepPng ? '.png' : '.jpg'); $ok = $keepPng ? imagepng($dst, $dirAbs . '/' . $file, 9) : imagejpeg($dst, $dirAbs . '/' . $file, 82); imagedestroy($src); imagedestroy($dst); if (!$ok) fail('Impossibile salvare l\'immagine ottimizzata.', 500); [$w, $h] = [$nw, $nh]; } catch (Throwable $e) { error_log('newsletter_api [upload_image]: ' . $e->getMessage()); fail('Ottimizzazione non riuscita: ' . $e->getMessage(), 500); } } out([ 'success' => true, 'url' => baseUrl() . '/' . $dirRel . '/' . $file, 'width' => $w, 'height' => $h, 'kb' => (int) round(filesize($dirAbs . '/' . $file) / 1024), 'note' => $note, ]); case 'send_test': if (!$isPost) fail('Metodo non consentito.', 405); $d = draftRow($pdo, (int) ($_POST['id'] ?? 0)); $to = trim((string) ($_POST['to'] ?? '')); if (!filter_var($to, FILTER_VALIDATE_EMAIL)) fail('Indirizzo email non valido.'); $p = draftPayload($d); $html = NewsletterRenderer::renderDocument($p['blocks'], $p['settings'], $p['subject'], $p['preheader'], true); $clean = fn($v) => trim((string) $v, "\" \t\n\r\0\x0B"); $mail = new \PHPMailer\PHPMailer\PHPMailer(true); $mail->isSMTP(); $mail->Host = $clean($_ENV['MAIL_HOST'] ?? ''); $mail->SMTPAuth = true; $mail->Username = $_ENV['MAIL_USERNAME'] ?? ''; $mail->Password = $_ENV['MAIL_PASSWORD'] ?? ''; $mail->SMTPSecure = $clean($_ENV['MAIL_ENCRYPTION'] ?? 'ssl'); $mail->Port = (int) ($_ENV['MAIL_PORT'] ?? 465); $mail->Timeout = 20; $mail->CharSet = 'UTF-8'; $mail->setFrom($clean($_ENV['MAIL_FROM_ADDRESS'] ?? ''), $clean($_ENV['MAIL_FROM_NAME'] ?? 'YogaSoul')); $mail->addAddress($to); $mail->isHTML(true); $mail->Subject = '[PROVA] ' . ($p['subject'] ?: '(senza oggetto)'); $mail->Body = $html; $mail->AltBody = str_replace('{name}', 'Maria', NewsletterRenderer::renderText($p['blocks'], $p['preheader'])); $mail->send(); out(['success' => true, 'message' => 'Prova inviata a ' . $to . '.']); case 'publish': if (!$isPost) fail('Metodo non consentito.', 405); $id = (int) ($_POST['id'] ?? 0); $d = draftRow($pdo, $id); $p = draftPayload($d); $html = NewsletterRenderer::renderDocument($p['blocks'], $p['settings'], $p['subject'], $p['preheader']); $checks = NewsletterRenderer::checks($p['blocks'], $p['subject'], $p['preheader'], $html); $errors = array_filter($checks, fn($c) => $c[0] === 'error'); if ($errors && ($_POST['confirm'] ?? '') !== '1') { out(['success' => false, 'needs_confirm' => true, 'checks' => $checks, 'message' => 'Ci sono problemi da controllare.']); } if (trim($p['subject']) === '') fail("Manca l'oggetto: in WordPress non si può inviare senza."); $text = NewsletterRenderer::renderText($p['blocks'], $p['preheader']); [$wpId, $created] = NewsletterWp::push($p['wp_email_id'], $p['subject'], $p['preheader'], $html, $text, $p['settings']); $pdo->prepare("UPDATE newsletter_drafts SET wp_email_id = ?, wp_pushed_at = NOW() WHERE iddraft = ?")->execute([$wpId, $id]); addVersion($pdo, $d, 'Pubblicata su The Newsletter', $userId); out([ 'success' => true, 'wp_id' => $wpId, 'created' => $created, 'admin_url' => NewsletterWp::adminUrl(), 'message' => $created ? "Creata in The Newsletter (n. $wpId). Il pubblico è impostato come l'ultima newsletter: controllalo prima di inviare." : "Aggiornata in The Newsletter (n. $wpId).", ]); case 'create': if (!$isPost) fail('Metodo non consentito.', 405); $title = trim((string) ($_POST['title'] ?? '')) ?: 'Newsletter del ' . date('d/m/Y'); $id = createDraft($pdo, $userId, $title, '', '', NewsletterRenderer::starterBlocks(), NewsletterRenderer::defaultSettings()); out(['success' => true, 'id' => $id]); case 'duplicate': if (!$isPost) fail('Metodo non consentito.', 405); $d = draftRow($pdo, (int) ($_POST['id'] ?? 0)); $blocks = json_decode($d['blocks'] ?: '[]', true) ?: []; foreach ($blocks as &$b) $b['id'] = bin2hex(random_bytes(6)); unset($b); $id = createDraft($pdo, $userId, 'Copia di ' . $d['title'], $d['subject'], $d['preheader'], $blocks, json_decode($d['settings'] ?: '{}', true) ?: []); out(['success' => true, 'id' => $id]); case 'delete': if (!$isPost) fail('Metodo non consentito.', 405); $pdo->prepare("UPDATE newsletter_drafts SET deleted_at = NOW(), updated_by = ? WHERE iddraft = ?") ->execute([$userId, (int) ($_POST['id'] ?? 0)]); out(['success' => true]); case 'list_wp': out(['success' => true, 'emails' => NewsletterWp::listEmails(40)]); case 'import_wp': if (!$isPost) fail('Metodo non consentito.', 405); $wpId = (int) ($_POST['wp_id'] ?? 0); $imp = NewsletterWp::import($wpId); // Se in WordPress non è ancora partita, la colleghiamo: "Pubblica" aggiornerà quella $status = NewsletterWp::statuses([$wpId])[$wpId] ?? ''; $id = createDraft( $pdo, $userId, $imp['subject'] ?: 'Importata da WordPress', $imp['subject'], $imp['preheader'], $imp['blocks'], $imp['settings'], $status === 'new' ? $wpId : null ); out(['success' => true, 'id' => $id, 'linked' => $status === 'new']); default: fail('Azione sconosciuta.', 404); } } catch (\PHPMailer\PHPMailer\Exception $e) { fail('Invio non riuscito: ' . $e->getMessage(), 502); } catch (PDOException $e) { if ($pdo->inTransaction()) $pdo->rollBack(); error_log('newsletter_api [' . $action . '] DB: ' . $e->getMessage()); fail('Errore del database. Riprova; se continua controlla il log.', 500); } catch (RuntimeException $e) { fail($e->getMessage(), 409); } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); error_log('newsletter_api [' . $action . ']: ' . $e->getMessage() . ' @' . $e->getFile() . ':' . $e->getLine()); fail('Errore del server. Riprova; se continua controlla il log.', 500); }