Upload via GUI (267 Dateien)
This commit is contained in:
@@ -90,4 +90,24 @@ $summary['permissions'] = [];
|
||||
foreach (['reply_ticket', 'post_note', 'close_ticket', 'assign_ticket', 'transfer_ticket', 'delete_ticket'] as $__perm) {
|
||||
$summary['permissions'][$__perm] = has_permission($staff, $__perm, (int) $ticket['department_id']);
|
||||
}
|
||||
// Signaturen zur Auswahl beim Antworten (eigene / Abteilung / keine) als Klartext mit Hinweis auf Bilder
|
||||
$sigOptions = reply_signature_options($pdo, $staff, (int) $ticket['department_id']);
|
||||
$sigEntry = fn(string $html) => ['available' => $html !== '', 'text' => api_plain_text($html), 'html' => $html, 'hasImage' => stripos($html, '<img') !== false, 'imageUrl' => api_first_image_url($html)];
|
||||
$summary['replySignatures'] = [
|
||||
'default' => $sigOptions['default'],
|
||||
'own' => $sigEntry($sigOptions['own']),
|
||||
'department' => $sigEntry($sigOptions['dept']) + ['name' => $sigOptions['deptName']],
|
||||
];
|
||||
// Zusatzfelder des Tickets (vom Kunden im Formular ausgefüllt), in der Reihenfolge des Formulars
|
||||
$cfStmt = $pdo->prepare(
|
||||
'SELECT cf.label, cf.field_type, v.value FROM ticket_field_values v
|
||||
JOIN custom_fields cf ON cf.id = v.custom_field_id
|
||||
WHERE v.ticket_id = ? ORDER BY cf.sort_order, cf.label'
|
||||
);
|
||||
$cfStmt->execute([(int) $ticket['id']]);
|
||||
$summary['customFields'] = array_map(fn($r) => [
|
||||
'label' => (string) $r['label'],
|
||||
'type' => (string) $r['field_type'],
|
||||
'value' => $r['field_type'] === 'checkbox' ? ($r['value'] ? 'Ja' : 'Nein') : (string) $r['value'],
|
||||
], $cfStmt->fetchAll());
|
||||
api_json(['ticket' => $summary]);
|
||||
|
||||
@@ -9,10 +9,12 @@ $number = trim((string) ($body['number'] ?? ''));
|
||||
$type = (string) ($body['type'] ?? '');
|
||||
$message = (string) ($body['message'] ?? '');
|
||||
$setStatus = isset($body['setStatus']) ? (string) $body['setStatus'] : null;
|
||||
// Optional: welche Signatur angehängt wird (own = eigene, dept = Abteilung, none = keine); ohne Angabe eigene, ersatzweise Abteilung
|
||||
$signatureChoice = isset($body['signature']) && in_array($body['signature'], ['own', 'dept', 'none'], true) ? (string) $body['signature'] : null;
|
||||
if ($number === '') {
|
||||
api_error(422, 'TICKET_NUMBER_REQUIRED', 'Eine Ticketnummer ist erforderlich.');
|
||||
}
|
||||
$pdo = get_db();
|
||||
$ticket = api_find_ticket($pdo, $staff, $number);
|
||||
$threadId = api_add_ticket_message($pdo, $staff, $ticket, $type, $message, $setStatus);
|
||||
$threadId = api_add_ticket_message($pdo, $staff, $ticket, $type, $message, $setStatus, $signatureChoice);
|
||||
api_json(['threadId' => $threadId], 201);
|
||||
|
||||
@@ -19,7 +19,7 @@ $pdo = get_db();
|
||||
|
||||
const RULE_FIELDS = ['subject' => 'Betreff', 'message' => 'Nachricht', 'requester_email' => 'Absender-E-Mail', 'requester_name' => 'Absender-Name', 'source' => 'Kanal (web/email)'];
|
||||
const RULE_OPERATORS = ['contains' => 'enthält', 'not_contains' => 'enthält nicht', 'equals' => 'ist genau', 'starts_with' => 'beginnt mit', 'ends_with' => 'endet mit'];
|
||||
const RULE_ACTIONS = ['set_department' => 'Abteilung setzen', 'set_priority' => 'Priorität setzen', 'set_help_topic' => 'Hilfe-Thema setzen', 'assign_staff' => 'Mitarbeiter zuweisen', 'add_tag' => 'Tag hinzufügen', 'auto_reply' => 'Auto-Antwort senden', 'reject' => 'Ticket verwerfen (z.B. Spam)'];
|
||||
const RULE_ACTIONS = ['set_department' => 'Abteilung setzen', 'set_priority' => 'Priorität setzen', 'set_help_topic' => 'Hilfe-Thema setzen', 'assign_staff' => 'Mitarbeiter zuweisen', 'add_tag' => 'Tag hinzufügen', 'auto_reply' => 'Auto-Antwort senden', 'close' => 'Ticket schließen', 'reject' => 'Ticket verwerfen (z.B. Spam)'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$departments = departments_for_select($pdo, array_map('intval', $pdo->query("SELECT action_value FROM ticket_rule_actions WHERE action_type = 'set_department'")->fetchAll(PDO::FETCH_COLUMN)));
|
||||
@@ -54,7 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
'assign_staff' => $staffById[$value] ?? null,
|
||||
'auto_reply' => $cannedById[$value] ?? null,
|
||||
'add_tag' => $value,
|
||||
'reject' => null,
|
||||
'reject', 'close' => null,
|
||||
default => null,
|
||||
};
|
||||
$actionsOut[] = ['type' => $type, 'value' => $value, 'valueLabel' => $label];
|
||||
@@ -138,8 +138,9 @@ if ($action === 'save') {
|
||||
foreach ($rawActions as $a) {
|
||||
$type = (string) ($a['type'] ?? '');
|
||||
if (!isset(RULE_ACTIONS[$type])) continue;
|
||||
$value = $type === 'reject' ? '1' : trim((string) ($a['value'] ?? ''));
|
||||
if ($type !== 'reject' && $value === '') continue;
|
||||
$noValue = in_array($type, ['reject', 'close'], true);
|
||||
$value = $noValue ? '1' : trim((string) ($a['value'] ?? ''));
|
||||
if (!$noValue && $value === '') continue;
|
||||
$actionsToSave[] = ['type' => $type, 'value' => $value];
|
||||
}
|
||||
if (!$actionsToSave) api_error(422, 'ACTIONS_REQUIRED', 'Bitte mindestens eine Aktion festlegen.');
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# Verhindert die Ausführung von hochgeladenen Dateien in diesem Verzeichnis
|
||||
<FilesMatch "\.(php|php3|php4|php5|php7|phtml|phar|pl|py|cgi|asp|aspx|jsp|sh|bash|exe|htaccess|htpasswd|env)$">
|
||||
Require all denied
|
||||
# Hochgeladene Logos: nur Bilder ausliefern, nie Programmcode ausführen
|
||||
<FilesMatch "\.(php|php3|php4|php5|php7|php8|phtml|phar|pl|py|cgi|asp|aspx|jsp|sh|bash|exe|htaccess|htpasswd|env|html?|xhtml|shtml|xml|js|mjs)$">
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
php_flag engine off
|
||||
# "php_flag" nur setzen, wenn PHP als Apache-Modul läuft. Unter FPM/CGI führt die Zeile sonst zu einem Serverfehler (HTTP 500) für jede Datei,
|
||||
# auch für das Logo.
|
||||
<IfModule mod_php.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
<IfModule mod_php7.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
<IfModule mod_php8.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
# Ein SVG-Logo darf im Bild-Tag laufen, aber nicht als eigene Seite Skripte ausführen
|
||||
<IfModule mod_headers.c>
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
Header set Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; sandbox"
|
||||
</IfModule>
|
||||
|
||||
@@ -6,6 +6,93 @@
|
||||
* zusätzlich über sanitize_staff_html() bereinigt – dieses Skript ist nur für Komfort,
|
||||
* keine Sicherheitsgrenze.
|
||||
*/
|
||||
(function () {
|
||||
/**
|
||||
* Bilder im Editor skalieren: Klick auf ein Bild zeigt einen Rahmen mit Anfasser (ziehen mit Maus oder Finger) und Schnellwahl
|
||||
* 25 / 50 / 75 % und Original. Chrome und Edge bieten das bei contenteditable nicht von selbst, Firefox hat eigene Anfasser,
|
||||
* die hier abgeschaltet werden, damit es nur eine Bedienung gibt. Die Breite wird als Attribut und als Stil gesetzt
|
||||
* (E-Mail-Programme wie Outlook beachten das Attribut), die Höhe folgt automatisch.
|
||||
*/
|
||||
window.attachImageResize = function (editable) {
|
||||
if (!editable || editable.dataset.imgResize) return;
|
||||
editable.dataset.imgResize = '1';
|
||||
try { document.execCommand('enableObjectResizing', false, false); } catch (e) { /* nicht überall verfügbar */ }
|
||||
|
||||
let current = null;
|
||||
const box = document.createElement('div');
|
||||
box.style.cssText = 'position:absolute;display:none;z-index:9999;border:2px solid #0f6f6a;box-sizing:border-box;pointer-events:none';
|
||||
const bar = document.createElement('div');
|
||||
bar.style.cssText = 'position:absolute;left:-2px;top:-40px;display:flex;gap:4px;align-items:center;pointer-events:auto;background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:3px;box-shadow:0 2px 8px rgba(0,0,0,.18);white-space:nowrap';
|
||||
const info = document.createElement('span');
|
||||
info.style.cssText = 'font:12px system-ui,sans-serif;color:#475569;padding:0 6px';
|
||||
const handle = document.createElement('div');
|
||||
handle.title = 'Ziehen zum Skalieren';
|
||||
handle.style.cssText = 'position:absolute;right:-9px;bottom:-9px;width:16px;height:16px;background:#0f6f6a;border:2px solid #fff;border-radius:50%;cursor:nwse-resize;pointer-events:auto;touch-action:none;box-shadow:0 1px 4px rgba(0,0,0,.3)';
|
||||
box.appendChild(bar);
|
||||
box.appendChild(handle);
|
||||
document.body.appendChild(box);
|
||||
|
||||
function changed() { editable.dispatchEvent(new Event('input', { bubbles: true })); }
|
||||
function maxWidth() { return Math.max(40, editable.clientWidth - 24); }
|
||||
function place() {
|
||||
if (!current || !document.body.contains(current)) { hide(); return; }
|
||||
const r = current.getBoundingClientRect();
|
||||
box.style.display = 'block';
|
||||
box.style.left = (r.left + window.scrollX) + 'px';
|
||||
box.style.top = (r.top + window.scrollY) + 'px';
|
||||
box.style.width = r.width + 'px';
|
||||
box.style.height = r.height + 'px';
|
||||
bar.style.top = r.top + window.scrollY > 44 ? '-40px' : (r.height + 8) + 'px'; // am Seitenanfang unter dem Bild statt darüber
|
||||
info.textContent = Math.round(r.width) + ' px';
|
||||
}
|
||||
function hide() { current = null; box.style.display = 'none'; }
|
||||
function setWidth(w) {
|
||||
if (!current) return;
|
||||
w = Math.max(24, Math.min(Math.round(w), maxWidth()));
|
||||
current.style.width = w + 'px';
|
||||
current.style.height = 'auto';
|
||||
current.style.maxWidth = '100%';
|
||||
current.setAttribute('width', String(w));
|
||||
current.removeAttribute('height');
|
||||
place();
|
||||
changed();
|
||||
}
|
||||
|
||||
[['25 %', 0.25], ['50 %', 0.5], ['75 %', 0.75], ['Original', 1]].forEach(function (p) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.textContent = p[0];
|
||||
b.style.cssText = 'font:12px system-ui,sans-serif;padding:3px 8px;border:1px solid #cbd5e1;border-radius:4px;background:#f8fafc;cursor:pointer';
|
||||
b.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
||||
b.addEventListener('click', function () { if (current) setWidth((current.naturalWidth || current.width) * p[1]); });
|
||||
bar.appendChild(b);
|
||||
});
|
||||
bar.appendChild(info);
|
||||
|
||||
handle.addEventListener('pointerdown', function (e) {
|
||||
if (!current) return;
|
||||
e.preventDefault();
|
||||
handle.setPointerCapture(e.pointerId);
|
||||
const startX = e.clientX, startW = current.getBoundingClientRect().width;
|
||||
function move(ev) { setWidth(startW + (ev.clientX - startX)); }
|
||||
function up() { handle.removeEventListener('pointermove', move); handle.removeEventListener('pointerup', up); handle.removeEventListener('pointercancel', up); }
|
||||
handle.addEventListener('pointermove', move);
|
||||
handle.addEventListener('pointerup', up);
|
||||
handle.addEventListener('pointercancel', up);
|
||||
});
|
||||
|
||||
editable.addEventListener('click', function (e) {
|
||||
if (e.target && e.target.tagName === 'IMG') { current = e.target; place(); } else { hide(); }
|
||||
});
|
||||
document.addEventListener('mousedown', function (e) {
|
||||
if (current && e.target !== current && !box.contains(e.target)) hide();
|
||||
});
|
||||
editable.addEventListener('keyup', function () { if (current) place(); });
|
||||
editable.addEventListener('scroll', place);
|
||||
window.addEventListener('scroll', place, true);
|
||||
window.addEventListener('resize', place);
|
||||
};
|
||||
})();
|
||||
(function () {
|
||||
function initRichText(textarea) {
|
||||
if (!textarea || textarea.dataset.richtextReady) return;
|
||||
@@ -126,6 +213,7 @@
|
||||
wrap.appendChild(toolbar);
|
||||
wrap.appendChild(editable);
|
||||
wrap.appendChild(textarea);
|
||||
if (window.attachImageResize) window.attachImageResize(editable);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ return [
|
||||
// Datenbankverbindung
|
||||
'db' => [
|
||||
// 'mysql' für den Produktivbetrieb, 'sqlite' NUR für lokale Tests
|
||||
'driver' => 'sqlite',
|
||||
'driver' => 'mysql',
|
||||
|
||||
// --- MySQL-Einstellungen (nur relevant wenn driver = mysql) ---
|
||||
'host' => 'localhost',
|
||||
@@ -48,6 +48,17 @@ return [
|
||||
'url' => 'http://localhost:8000',
|
||||
'admin_email' => 'support@example.com',
|
||||
'timezone' => 'Europe/Berlin',
|
||||
|
||||
// Reverse-Proxy (hier Cloudflare): Nur Anfragen von diesen Adressen dürfen die Besucher-IP
|
||||
// und HTTPS über X-Forwarded-* melden. Aktuelle Liste: https://www.cloudflare.com/ips/
|
||||
'trusted_proxies' => [
|
||||
'173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22',
|
||||
'141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20',
|
||||
'197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13',
|
||||
'104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
|
||||
'2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32',
|
||||
'2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32',
|
||||
],
|
||||
],
|
||||
|
||||
// Ausgehende E-Mails (Benachrichtigungen an Kunden/Mitarbeiter)
|
||||
|
||||
@@ -316,6 +316,7 @@ function fetch_emails_for_account(PDO $pdo, array $account, bool $verbose): arra
|
||||
log_activity($pdo, $ticketId, null, 'Ticket-Regel angewendet: ' . implode(', ', $ruleResult['matched_rules']));
|
||||
}
|
||||
apply_auto_reply($pdo, $ticketId, $ruleResult['auto_reply']);
|
||||
$closedByRule = apply_rule_close($pdo, $ticketId, $ruleResult);
|
||||
|
||||
$deptStmt = $pdo->prepare('SELECT name, email FROM departments WHERE id = ?');
|
||||
$deptStmt->execute([$defaultDeptId]);
|
||||
@@ -328,9 +329,11 @@ function fetch_emails_for_account(PDO $pdo, array $account, bool $verbose): arra
|
||||
'department_id' => $defaultDeptId,
|
||||
'department_name' => $dept['name'] ?? null,
|
||||
];
|
||||
notify_new_ticket_channels($ticketForNotify, $dept['email'] ?? null);
|
||||
notify_staff_new_ticket($ticketForNotify);
|
||||
webhook_dispatch($pdo, 'ticket.created', $ticketId);
|
||||
if (!$closedByRule) {
|
||||
notify_new_ticket_channels($ticketForNotify, $dept['email'] ?? null);
|
||||
notify_staff_new_ticket($ticketForNotify);
|
||||
webhook_dispatch($pdo, 'ticket.created', $ticketId);
|
||||
}
|
||||
|
||||
handle_after_fetch($mbox, $emailNum, $afterFetch, $archiveFolder);
|
||||
$processed++;
|
||||
|
||||
@@ -72,7 +72,7 @@ function run_process_backups(PDO $pdo, bool $verbose = false, bool $force = fals
|
||||
|
||||
// Sofort prüfen, ob sich das Backup wiederherstellen lässt (ohne etwas zu verändern)
|
||||
require_once __DIR__ . '/../includes/backup_restore.php';
|
||||
$check = backup_verify_and_record($result['path']);
|
||||
$check = $result['verify'] ?? backup_verify_and_record($result['path']); // create_backup() prüft jedes Backup automatisch
|
||||
$log[] = date('Y-m-d H:i:s') . ($check['ok'] ? ' – Prüfung: Backup ist vollständig und wiederherstellbar.' : ' – WARNUNG, Backup nicht wiederherstellbar: ' . implode(' ', $check['errors']));
|
||||
|
||||
// Letztes Backup-Datum speichern
|
||||
@@ -140,6 +140,10 @@ function run_process_backups(PDO $pdo, bool $verbose = false, bool $force = fals
|
||||
}
|
||||
}
|
||||
|
||||
// Noch nicht oder lange nicht geprüfte Backups nachholen (zum Beispiel ältere oder von der NAS geholte)
|
||||
$late = backup_verify_pending(3);
|
||||
if ($late['checked']) $log[] = date('Y-m-d H:i:s') . ' – ' . $late['checked'] . ' weitere(s) Backup(s) geprüft' . ($late['bad'] ? ', davon ' . $late['bad'] . ' NICHT wiederherstellbar' : '') . '.';
|
||||
|
||||
set_setting('cron_process_backups_last_run', date('Y-m-d H:i:s'));
|
||||
$log[] = date('Y-m-d H:i:s') . ' – Fertig.';
|
||||
|
||||
|
||||
@@ -1016,6 +1016,133 @@ function run_migrations(PDO $pdo, bool $verbose = false): array
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 0033: Eigenes Ticket-Formular pro Abteilung (Felder + Texte, unabhängig vom Standardformular).
|
||||
if (!isset($done['php_0033_department_forms'])) {
|
||||
try {
|
||||
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$colsOf = function (string $table) use ($pdo, $drv): array {
|
||||
$cols = [];
|
||||
foreach ($pdo->query($drv === 'sqlite' ? "PRAGMA table_info($table)" : "SHOW COLUMNS FROM $table")->fetchAll() as $col) {
|
||||
$cols[] = $drv === 'sqlite' ? ($col['name'] ?? '') : ($col['Field'] ?? '');
|
||||
}
|
||||
return $cols;
|
||||
};
|
||||
if (!in_array('department_id', $colsOf('custom_fields'), true)) $pdo->exec("ALTER TABLE custom_fields ADD COLUMN department_id INT NULL");
|
||||
$dc = $colsOf('departments');
|
||||
if (!in_array('own_form', $dc, true)) $pdo->exec("ALTER TABLE departments ADD COLUMN own_form TINYINT(1) NOT NULL DEFAULT 0");
|
||||
if (!in_array('form_config', $dc, true)) $pdo->exec("ALTER TABLE departments ADD COLUMN form_config TEXT NULL");
|
||||
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0033_department_forms']);
|
||||
$result['applied']++;
|
||||
if ($verbose) $result['log'][] = ' [OK] php_0033_department_forms';
|
||||
} catch (Throwable $e) {
|
||||
$result['errors'][] = 'php_0033_department_forms: ' . $e->getMessage();
|
||||
if ($verbose) $result['log'][] = ' [FAIL] php_0033_department_forms - ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 0034: Abschnitt eines Abteilungsfeldes im Ticket-Formular (Kontaktdaten, Anliegen, eigene Abschnitte).
|
||||
if (!isset($done['php_0034_field_sections'])) {
|
||||
try {
|
||||
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$cols = [];
|
||||
foreach ($pdo->query($drv === 'sqlite' ? "PRAGMA table_info(custom_fields)" : "SHOW COLUMNS FROM custom_fields")->fetchAll() as $col) {
|
||||
$cols[] = $drv === 'sqlite' ? ($col['name'] ?? '') : ($col['Field'] ?? '');
|
||||
}
|
||||
if (!in_array('form_section', $cols, true)) $pdo->exec("ALTER TABLE custom_fields ADD COLUMN form_section VARCHAR(100) NULL");
|
||||
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0034_field_sections']);
|
||||
$result['applied']++;
|
||||
if ($verbose) $result['log'][] = ' [OK] php_0034_field_sections';
|
||||
} catch (Throwable $e) {
|
||||
$result['errors'][] = 'php_0034_field_sections: ' . $e->getMessage();
|
||||
if ($verbose) $result['log'][] = ' [FAIL] php_0034_field_sections - ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 0035: Feldtyp "Radio-Auswahl" (MySQL: ENUM erweitern; SQLite kennt keine ENUMs).
|
||||
if (!isset($done['php_0035_field_type_radio'])) {
|
||||
try {
|
||||
if ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') {
|
||||
$pdo->exec("ALTER TABLE custom_fields MODIFY COLUMN field_type ENUM('text','textarea','select','checkbox','number','date','list','radio') NOT NULL DEFAULT 'text'");
|
||||
}
|
||||
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0035_field_type_radio']);
|
||||
$result['applied']++;
|
||||
if ($verbose) $result['log'][] = ' [OK] php_0035_field_type_radio';
|
||||
} catch (Throwable $e) {
|
||||
$result['errors'][] = 'php_0035_field_type_radio: ' . $e->getMessage();
|
||||
if ($verbose) $result['log'][] = ' [FAIL] php_0035_field_type_radio - ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 0036: Breite eines Formularfeldes (volle Breite, Hälfte, Drittel, zwei Drittel).
|
||||
if (!isset($done['php_0036_field_width'])) {
|
||||
try {
|
||||
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$cols = [];
|
||||
foreach ($pdo->query($drv === 'sqlite' ? "PRAGMA table_info(custom_fields)" : "SHOW COLUMNS FROM custom_fields")->fetchAll() as $col) {
|
||||
$cols[] = $drv === 'sqlite' ? ($col['name'] ?? '') : ($col['Field'] ?? '');
|
||||
}
|
||||
if (!in_array('form_width', $cols, true)) $pdo->exec("ALTER TABLE custom_fields ADD COLUMN form_width VARCHAR(10) NULL");
|
||||
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0036_field_width']);
|
||||
$result['applied']++;
|
||||
if ($verbose) $result['log'][] = ' [OK] php_0036_field_width';
|
||||
} catch (Throwable $e) {
|
||||
$result['errors'][] = 'php_0036_field_width: ' . $e->getMessage();
|
||||
if ($verbose) $result['log'][] = ' [FAIL] php_0036_field_width - ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 0037: Anordnung der Radio-Optionen (nebeneinander oder untereinander).
|
||||
if (!isset($done['php_0037_radio_inline'])) {
|
||||
try {
|
||||
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$cols = [];
|
||||
foreach ($pdo->query($drv === 'sqlite' ? "PRAGMA table_info(custom_fields)" : "SHOW COLUMNS FROM custom_fields")->fetchAll() as $col) {
|
||||
$cols[] = $drv === 'sqlite' ? ($col['name'] ?? '') : ($col['Field'] ?? '');
|
||||
}
|
||||
if (!in_array('radio_inline', $cols, true)) $pdo->exec("ALTER TABLE custom_fields ADD COLUMN radio_inline TINYINT(1) NOT NULL DEFAULT 0");
|
||||
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0037_radio_inline']);
|
||||
$result['applied']++;
|
||||
if ($verbose) $result['log'][] = ' [OK] php_0037_radio_inline';
|
||||
} catch (Throwable $e) {
|
||||
$result['errors'][] = 'php_0037_radio_inline: ' . $e->getMessage();
|
||||
if ($verbose) $result['log'][] = ' [FAIL] php_0037_radio_inline - ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 0038: Hinweistext (mit Links) zu einem Formularfeld, z. B. für die Zustimmung zu Nutzerbedingungen.
|
||||
if (!isset($done['php_0038_field_help_text'])) {
|
||||
try {
|
||||
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$cols = [];
|
||||
foreach ($pdo->query($drv === 'sqlite' ? "PRAGMA table_info(custom_fields)" : "SHOW COLUMNS FROM custom_fields")->fetchAll() as $col) {
|
||||
$cols[] = $drv === 'sqlite' ? ($col['name'] ?? '') : ($col['Field'] ?? '');
|
||||
}
|
||||
if (!in_array('help_text', $cols, true)) $pdo->exec("ALTER TABLE custom_fields ADD COLUMN help_text TEXT NULL");
|
||||
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0038_field_help_text']);
|
||||
$result['applied']++;
|
||||
if ($verbose) $result['log'][] = ' [OK] php_0038_field_help_text';
|
||||
} catch (Throwable $e) {
|
||||
$result['errors'][] = 'php_0038_field_help_text: ' . $e->getMessage();
|
||||
if ($verbose) $result['log'][] = ' [FAIL] php_0038_field_help_text - ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 0039: Regel-Aktion "Ticket schließen" (MySQL: ENUM erweitern; SQLite kennt keine ENUMs).
|
||||
if (!isset($done['php_0039_rule_close'])) {
|
||||
try {
|
||||
if ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') {
|
||||
$pdo->exec("ALTER TABLE ticket_rule_actions MODIFY COLUMN action_type
|
||||
ENUM('set_department','set_priority','set_help_topic','assign_staff','add_tag','reject','auto_reply','close') NOT NULL");
|
||||
}
|
||||
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0039_rule_close']);
|
||||
$result['applied']++;
|
||||
if ($verbose) $result['log'][] = ' [OK] php_0039_rule_close';
|
||||
} catch (Throwable $e) {
|
||||
$result['errors'][] = 'php_0039_rule_close: ' . $e->getMessage();
|
||||
if ($verbose) $result['log'][] = ' [FAIL] php_0039_rule_close - ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user