Merge pull request 'Fix #208: Totmannschalter für den Nextcloud-Import' (#212) from fds2610-bot/Steuerkram:main into main

Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/212
This commit is contained in:
fds2610 2026-09-21 07:28:18 +02:00
commit f67e3b86f4
14 changed files with 222 additions and 5 deletions

View file

@ -2,6 +2,18 @@
--- ---
## 2026-09-20 – Analyse prüft das Importskript (#209)
- **`scripts/analyse-nextcloud.sh`:** neuer Abschnitt 6 prüft, ob `scripts/container/nextcloud-import.php` im Container existiert und syntaktisch ladbar ist (`php -l`, ohne Ausführung), und nennt den Pfad, den der Host-Cronjob verwenden muss. Fehlt das Skript, endet die Analyse mit Exit 1 – auch bei erfolgreicher Verbindung. Ein `--dry-run`-Schalter für den Import wurde nicht ergänzt.
- **`tests/75_AnalyseNextcloudSkriptTest.php`:** prüft die Existenzabfrage und dass das Importskript nie ausgeführt wird.
## 2026-09-20 – Überwachung des Nextcloud-Imports (#208)
- **`migrations/096_nextcloud_watchdog.sql`** (neu): `company_settings.nextcloud_last_run` (Zeitstempel des letzten Laufs) und `nextcloud_alarm_stunden` (Schwelle, Default 24, 0 = aus).
- **`src/Services/NextcloudWatchdog.php`** (neu): Totmannschalter. `NextcloudImportService::import()` setzt nach erfolgreichem PROPFIND den Zeitstempel – auch bei stillen Läufen ohne Ereignis. Ist er älter als die Schwelle (oder nie gesetzt, bei konfigurierter Nextcloud), erscheint eine Warnung oben auf `/ereignisse` und ein ⚠ am Nav-Eintrag „Ereignisse".
- **Einstellungen:** Feld „Import-Überwachung" im Nextcloud-Abschnitt.
- **`tests/101_NextcloudWatchdogTest.php`** (neu): Schwellenlogik gegen künstlich alte Zeitstempel.
## 2026-09-20 – Gezahlte Vorsteuer als Betriebsausgabe in der EÜR (EÜR-Zeile 57 / Kz 185) ## 2026-09-20 – Gezahlte Vorsteuer als Betriebsausgabe in der EÜR (EÜR-Zeile 57 / Kz 185)
- **`migrations/095_vorsteuerkonten_betriebsausgabe.sql`** (neu): Die Konten 1571/1574/1576 (abziehbare Vorsteuer) lagen als `Zahlungskonto` im Kontenrahmen und wurden vom `EuerCalculator` daher ignoriert – die auf ihnen gebuchte Vorsteuer tauchte in der EÜR nirgends auf. Da die vereinnahmte USt seit Migration 076 als Betriebseinnahme (Zeile 17) und die ans Finanzamt gezahlte USt seit Migration 077 als Betriebsausgabe (Zeile 58) geführt werden, war die Bruttomethode unvollständig und der ausgewiesene Gewinn um die gezahlte Vorsteuer zu hoch. Alle drei Konten sind jetzt `Betriebsausgabe` mit `euer_zeile = '57'`. Auch 1574 (Vorsteuer aus ig. Erwerb) muss mit: sein Gegenkonto 1774 ist seit Migration 076 Betriebseinnahme, bei Reverse-Charge zählte also nur die Erwerbsteuer als Einnahme – die Buchung war entgegen der Absicht nicht ergebnisneutral. - **`migrations/095_vorsteuerkonten_betriebsausgabe.sql`** (neu): Die Konten 1571/1574/1576 (abziehbare Vorsteuer) lagen als `Zahlungskonto` im Kontenrahmen und wurden vom `EuerCalculator` daher ignoriert – die auf ihnen gebuchte Vorsteuer tauchte in der EÜR nirgends auf. Da die vereinnahmte USt seit Migration 076 als Betriebseinnahme (Zeile 17) und die ans Finanzamt gezahlte USt seit Migration 077 als Betriebsausgabe (Zeile 58) geführt werden, war die Bruttomethode unvollständig und der ausgewiesene Gewinn um die gezahlte Vorsteuer zu hoch. Alle drei Konten sind jetzt `Betriebsausgabe` mit `euer_zeile = '57'`. Auch 1574 (Vorsteuer aus ig. Erwerb) muss mit: sein Gegenkonto 1774 ist seit Migration 076 Betriebseinnahme, bei Reverse-Charge zählte also nur die Erwerbsteuer als Einnahme – die Buchung war entgegen der Absicht nicht ergebnisneutral.

View file

@ -14,7 +14,7 @@ Vollständige Kontenliste: [kontenrahmen.md](kontenrahmen.md)
- SHA-256 Integritätsprüfung je Datei - SHA-256 Integritätsprüfung je Datei
- Automatischer Weißrand-Zuschnitt bei JPEG/PNG via GD - Automatischer Weißrand-Zuschnitt bei JPEG/PNG via GD
- KI-gestützter Kontierungsvorschlag (optional, siehe [konfiguration.md](konfiguration.md)) - KI-gestützter Kontierungsvorschlag (optional, siehe [konfiguration.md](konfiguration.md))
- Nextcloud-Import via WebDAV (optional) - Nextcloud-Import via WebDAV (optional); Überwachung: Warnung in Ereignisse/Nav, wenn kein Lauf mehr stattfand (Schwelle einstellbar, Standard 24 Std.)
- Belege können nicht gelöscht werden, solange Buchungen darauf verweisen (GoBD) - Belege können nicht gelöscht werden, solange Buchungen darauf verweisen (GoBD)
--- ---

View file

@ -96,7 +96,7 @@ sudo usermod -aG docker $USER
### Verbindung analysieren ### Verbindung analysieren
Meldet der Import „PROPFIND fehlgeschlagen" oder findet er keine Dateien, grenzt `scripts/analyse-nextcloud.sh` die Ursache ein. Das Skript wird auf dem Host aufgerufen (nicht im Container) und arbeitet rein lesend – es importiert, verschiebt und löscht nichts. Das Passwort wird nie im Klartext ausgegeben. Meldet der Import „PROPFIND fehlgeschlagen" oder findet er keine Dateien, grenzt `scripts/analyse-nextcloud.sh` die Ursache ein. Das Skript wird auf dem Host aufgerufen (nicht im Container) und arbeitet rein lesend – es importiert, verschiebt und löscht nichts. Das Passwort wird nie im Klartext ausgegeben. Zum Schluss prüft die Analyse, ob `scripts/container/nextcloud-import.php` im Container vorhanden und ladbar ist (ohne es auszuführen), und nennt den Pfad, den der Host-Cronjob verwenden muss.
```bash ```bash
./scripts/analyse-nextcloud.sh # Konfiguration, DNS, Verbindungsversuch ./scripts/analyse-nextcloud.sh # Konfiguration, DNS, Verbindungsversuch

View file

@ -0,0 +1,6 @@
-- Totmannschalter für den Nextcloud-Import (Issue #208).
-- nextcloud_last_run: Zeitstempel des letzten Importlaufs (auch ohne Dateiaktivität)
-- nextcloud_alarm_stunden: Schwelle in Stunden, 0 = Überwachung aus
ALTER TABLE company_settings
ADD COLUMN IF NOT EXISTS nextcloud_last_run DATETIME NULL,
ADD COLUMN IF NOT EXISTS nextcloud_alarm_stunden SMALLINT UNSIGNED NOT NULL DEFAULT 24;

View file

@ -7,13 +7,16 @@
# Schlägt der fehl, grenzt das Script die Ursache über Gegenproben ein: # Schlägt der fehl, grenzt das Script die Ursache über Gegenproben ein:
# ohne IP-Pinning, nur IPv4, nur IPv6. # ohne IP-Pinning, nur IPv4, nur IPv6.
# #
# Zuletzt wird geprüft, ob das Importskript im Container existiert und ladbar
# ist (der Cronjob muss denselben Pfad verwenden).
#
# Rein lesend – es werden keine Belege importiert, verschoben oder gelöscht. # Rein lesend – es werden keine Belege importiert, verschoben oder gelöscht.
# Das Passwort wird nie im Klartext ausgegeben. # Das Passwort wird nie im Klartext ausgegeben.
# #
# Usage: # Usage:
# ./analyse-nextcloud.sh [--verbose] # ./analyse-nextcloud.sh [--verbose]
# #
# Exit-Code: 0 = Verbindung steht, 1 = Konfigurations- oder Verbindungsfehler # Exit-Code: 0 = Verbindung steht und Importskript vorhanden, 1 = Konfigurations- oder Verbindungsfehler
# ════════════════════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════════════════════
set -euo pipefail set -euo pipefail
@ -145,6 +148,42 @@ function isOk(array $r): bool
return $r['errno'] === 0 && $r['code'] >= 200 && $r['code'] < 300; return $r['errno'] === 0 && $r['code'] >= 200 && $r['code'] < 300;
} }
/**
* Prüft, ob der Importlauf selbst startbar ist. Die Analyse läuft über die
* Standardeingabe und braucht das Importskript nicht – ein verschobenes oder
* fehlendes Skript bliebe sonst unbemerkt, obwohl der Cronjob ins Leere läuft.
* Das Skript wird nur auf Syntax geprüft (php -l), nicht ausgeführt.
*/
$checkImportScript = static function () use ($section, $green, $red, $bold): bool {
$section('6. Importskript (Cronjob-Ziel)');
$rel = 'scripts/container/nextcloud-import.php';
$path = '/var/www/html/' . $rel;
if (!is_file($path)) {
echo ' ' . $red('✗') . ' ' . $rel . ' fehlt im Container.' . PHP_EOL;
echo ' Der Cronjob findet das Skript nicht und der Import läuft nie.' . PHP_EOL;
echo ' Auch prüfen, ob der Bind-Mount ./scripts:/var/www/html/scripts den Image-Inhalt verdeckt.' . PHP_EOL;
return false;
}
echo ' ' . $green('✓') . ' ' . $rel . ' vorhanden' . PHP_EOL;
$output = [];
exec(escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($path) . ' 2>&1', $output, $code);
if ($code !== 0) {
echo ' ' . $red('✗') . ' Das Skript ist nicht ladbar: ' . implode(' ', $output) . PHP_EOL;
return false;
}
echo ' ' . $green('✓') . ' Syntax gültig' . PHP_EOL;
echo PHP_EOL . ' ' . $bold('Hinweis:') . ' Der Host-Cronjob muss genau diesen Pfad verwenden:' . PHP_EOL;
echo ' docker compose exec -T web php ' . $rel . PHP_EOL;
echo ' Mit `crontab -l` auf dem Host abgleichen.' . PHP_EOL;
return true;
};
// ── 1. Konfiguration ──────────────────────────────────────────────────────── // ── 1. Konfiguration ────────────────────────────────────────────────────────
$section('1. Konfiguration (Einstellungen → Nextcloud-Belegimport)'); $section('1. Konfiguration (Einstellungen → Nextcloud-Belegimport)');
@ -272,6 +311,7 @@ if (!isOk($result)) {
echo ' Netzwerkverbindung, Firewall und TLS-Zertifikat der Nextcloud prüfen.' . PHP_EOL; echo ' Netzwerkverbindung, Firewall und TLS-Zertifikat der Nextcloud prüfen.' . PHP_EOL;
} }
$checkImportScript();
exit(1); exit(1);
} }
@ -328,6 +368,13 @@ if ($done !== null) {
} }
} }
$importOk = $checkImportScript();
if (!$importOk) {
echo PHP_EOL . ' ' . $red('Die Verbindung steht, aber der Importlauf ist nicht startbar.') . PHP_EOL . PHP_EOL;
exit(1);
}
echo PHP_EOL . ' ' . $green('Die Verbindung zur Nextcloud funktioniert.') . PHP_EOL . PHP_EOL; echo PHP_EOL . ' ' . $green('Die Verbindung zur Nextcloud funktioniert.') . PHP_EOL . PHP_EOL;
exit(0); exit(0);
PHPEOF PHPEOF

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Steuerkram\Controllers; namespace Steuerkram\Controllers;
use Steuerkram\Models\EventLog; use Steuerkram\Models\EventLog;
use Steuerkram\Services\NextcloudWatchdog;
use Steuerkram\View; use Steuerkram\View;
class EventController extends BaseController class EventController extends BaseController
@ -23,6 +24,7 @@ class EventController extends BaseController
'events' => $events, 'events' => $events,
'filter' => $filter, 'filter' => $filter,
'channels' => EventLog::channels(), 'channels' => EventLog::channels(),
'watchdog' => NextcloudWatchdog::warning(),
'eventsHtml' => $this->renderView('ereignisse/_body', [ 'eventsHtml' => $this->renderView('ereignisse/_body', [
'events' => $events, 'events' => $events,
]), ]),

View file

@ -42,7 +42,7 @@ class Setting
invoice_nr_prefix=?, beleg_nr_prefix=?, anlage_nr_prefix=?, customer_nr_prefix=?, invoice_nr_prefix=?, beleg_nr_prefix=?, anlage_nr_prefix=?, customer_nr_prefix=?,
payment_days_default=?, test_mode=?, ki_vorschlag_aktiv=?, payment_days_default=?, test_mode=?, ki_vorschlag_aktiv=?,
nextcloud_url=?, nextcloud_user=?, nextcloud_pass=?, nextcloud_url=?, nextcloud_user=?, nextcloud_pass=?,
nextcloud_folder=?, nextcloud_done=?, nextcloud_folder=?, nextcloud_done=?, nextcloud_alarm_stunden=?,
mail_smtp_host=?, mail_smtp_port=?, mail_smtp_encryption=?, mail_smtp_host=?, mail_smtp_port=?, mail_smtp_encryption=?,
mail_smtp_user=?, mail_smtp_pass=?, mail_smtp_user=?, mail_smtp_pass=?,
mail_from_name=?, mail_from_email=?, mail_from_name=?, mail_from_email=?,
@ -80,6 +80,7 @@ class Setting
!empty($data['nextcloud_pass']) ? Crypto::encrypt($data['nextcloud_pass']) : null, !empty($data['nextcloud_pass']) ? Crypto::encrypt($data['nextcloud_pass']) : null,
($data['nextcloud_folder'] ?? '') ?: '/Belege/Import', ($data['nextcloud_folder'] ?? '') ?: '/Belege/Import',
($data['nextcloud_done'] ?? '') ?: null, ($data['nextcloud_done'] ?? '') ?: null,
max(0, min(8760, (int)($data['nextcloud_alarm_stunden'] ?? 24))),
($data['mail_smtp_host'] ?? '') ?: '', ($data['mail_smtp_host'] ?? '') ?: '',
(int)($data['mail_smtp_port'] ?? 587), (int)($data['mail_smtp_port'] ?? 587),
in_array($data['mail_smtp_encryption'] ?? '', ['tls', 'ssl', 'none']) ? $data['mail_smtp_encryption'] : 'tls', in_array($data['mail_smtp_encryption'] ?? '', ['tls', 'ssl', 'none']) ? $data['mail_smtp_encryption'] : 'tls',

View file

@ -115,6 +115,8 @@ class NextcloudImportService
return $result; return $result;
} }
NextcloudWatchdog::markRun();
$result['details'][] = count($files) . ' Datei(en) im Import-Ordner gefunden.'; $result['details'][] = count($files) . ' Datei(en) im Import-Ordner gefunden.';
foreach ($files as $file) { foreach ($files as $file) {

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Steuerkram\Services;
use Steuerkram\Database;
/**
* Totmannschalter für den Nextcloud-Import (Issue #208).
*
* Jeder Importlauf, der die Nextcloud erreicht, hinterlässt einen Zeitstempel
* (company_settings.nextcloud_last_run) – auch wenn er nichts zu tun hatte und
* deshalb kein Ereignis schreibt. Liegt dieser Zeitstempel länger zurück als
* die eingestellte Schwelle (nextcloud_alarm_stunden, 0 = aus), gilt der Import
* als ausgeblieben.
*/
class NextcloudWatchdog
{
public const DEFAULT_HOURS = 24;
public static function markRun(): void
{
Database::execute('UPDATE company_settings SET nextcloud_last_run = NOW() WHERE id = 1');
}
/**
* Liefert eine Warnung, wenn der Import überfällig ist, sonst null.
*/
public static function warning(?int $now = null): ?string
{
$row = Database::fetchOne(
'SELECT nextcloud_url, nextcloud_last_run, nextcloud_alarm_stunden
FROM company_settings WHERE id = 1'
) ?: [];
return self::evaluate(
(string)($row['nextcloud_url'] ?? ''),
$row['nextcloud_last_run'] ?? null,
(int)($row['nextcloud_alarm_stunden'] ?? self::DEFAULT_HOURS),
$now ?? time()
);
}
/**
* Reine Schwellenlogik. Ohne konfigurierte Nextcloud oder mit Schwelle 0
* gibt es nie eine Warnung.
*/
public static function evaluate(string $url, ?string $lastRun, int $thresholdHours, int $now): ?string
{
if ($url === '' || $thresholdHours <= 0) {
return null;
}
if ($lastRun === null || $lastRun === '') {
return 'Der Nextcloud-Import ist noch nie gelaufen. Cronjob prüfen'
. ' (oder Überwachung in den Einstellungen abschalten).';
}
$ts = strtotime($lastRun);
if ($ts === false || $now - $ts <= $thresholdHours * 3600) {
return null;
}
$stunden = intdiv($now - $ts, 3600);
return sprintf(
'Der Nextcloud-Import ist seit %s (vor %d Std.) nicht mehr gelaufen, Schwelle: %d Std.'
. ' Cronjob prüfen.',
date('d.m.Y H:i', $ts), $stunden, $thresholdHours
);
}
}

View file

@ -27,6 +27,10 @@ $levelLabels = [
.switch input:checked + .switch-track::before { transform: translateX(18px); } .switch input:checked + .switch-track::before { transform: translateX(18px); }
</style> </style>
<?php if (!empty($watchdog)): ?>
<div class="alert alert-warning"><?= $h($watchdog) ?></div>
<?php endif; ?>
<div class="card"> <div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;padding:.75rem 1rem;border-bottom:1px solid var(--color-border)"> <div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;padding:.75rem 1rem;border-bottom:1px solid var(--color-border)">
<form method="get" action="/ereignisse" id="events-filter" style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap"> <form method="get" action="/ereignisse" id="events-filter" style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">

View file

@ -89,6 +89,9 @@ function navActive(string $prefix, string $current): string {
<a href="/ereignisse" class="<?= navActive('/ereignisse', $path) ?>"> <a href="/ereignisse" class="<?= navActive('/ereignisse', $path) ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22V12M12 12L8 16M12 12l4 4"/><circle cx="12" cy="5" r="3"/><path d="M5.5 8.5A7 7 0 0 0 5 12"/><path d="M18.5 8.5A7 7 0 0 1 19 12"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22V12M12 12L8 16M12 12l4 4"/><circle cx="12" cy="5" r="3"/><path d="M5.5 8.5A7 7 0 0 0 5 12"/><path d="M18.5 8.5A7 7 0 0 1 19 12"/></svg>
Ereignisse Ereignisse
<?php if (\Steuerkram\Services\NextcloudWatchdog::warning() !== null): ?>
<span class="text-danger" title="Nextcloud-Import überfällig">&#9888;</span>
<?php endif; ?>
</a> </a>
<?php if (str_starts_with($path, '/ereignisse')): ?> <?php if (str_starts_with($path, '/ereignisse')): ?>
<a href="/ereignisse?level=error" class="<?= ($_GET['level'] ?? '') === 'error' ? 'active' : '' ?>" style="padding-left:2.25rem;font-size:.875rem"> <a href="/ereignisse?level=error" class="<?= ($_GET['level'] ?? '') === 'error' ? 'active' : '' ?>" style="padding-left:2.25rem;font-size:.875rem">

View file

@ -343,6 +343,11 @@
value="<?= $h($settings['nextcloud_done'] ?? '') ?>" value="<?= $h($settings['nextcloud_done'] ?? '') ?>"
placeholder="/Belege/Importiert"> placeholder="/Belege/Importiert">
</div> </div>
<div class="form-group">
<label>Import-Überwachung <span class="text-muted">(Stunden ohne Lauf bis zur Warnung, 0 = aus)</span></label>
<input type="number" name="nextcloud_alarm_stunden" min="0" max="8760"
value="<?= (int)($settings['nextcloud_alarm_stunden'] ?? 24) ?>">
</div>
<div class="form-hint"> <div class="form-hint">
Import manuell starten: Import manuell starten:
<code>docker compose exec web php scripts/container/nextcloud-import.php</code> <code>docker compose exec web php scripts/container/nextcloud-import.php</code>

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
use Steuerkram\Services\NextcloudWatchdog;
/**
* Test 101: Totmannschalter für den Nextcloud-Import (Issue #208)
*/
class NextcloudWatchdogTest extends TestCase
{
protected string $testName = 'Test 101: Nextcloud-Import-Überwachung';
public function run(): void
{
$now = strtotime('2026-09-20 12:00:00');
$url = 'https://cloud.example.com';
$this->assert(
NextcloudWatchdog::evaluate($url, '2026-09-20 11:55:00', 24, $now) === null,
'Frischer Lauf: keine Warnung'
);
$this->assert(
NextcloudWatchdog::evaluate($url, '2026-09-19 12:00:00', 24, $now) === null,
'Genau an der Schwelle: keine Warnung'
);
$alt = NextcloudWatchdog::evaluate($url, '2026-08-30 12:00:00', 24, $now);
$this->assertNotEmpty($alt, 'Drei Wochen alter Lauf: Warnung');
$this->assert(str_contains((string)$alt, '30.08.2026'), 'Warnung nennt den letzten Lauf');
$this->assert(
NextcloudWatchdog::evaluate($url, '2026-08-30 12:00:00', 0, $now) === null,
'Schwelle 0 schaltet die Überwachung ab'
);
$this->assert(
NextcloudWatchdog::evaluate('', null, 24, $now) === null,
'Ohne konfigurierte Nextcloud keine Warnung'
);
$this->assertNotEmpty(
NextcloudWatchdog::evaluate($url, null, 24, $now),
'Konfiguriert, aber nie gelaufen: Warnung'
);
// Zeitstempel in der Datenbank
\Steuerkram\Database::execute("UPDATE company_settings SET nextcloud_url = ?, nextcloud_alarm_stunden = 24, nextcloud_last_run = NULL WHERE id = 1", [$url]);
$this->assertNotEmpty(NextcloudWatchdog::warning(), 'DB: ohne Zeitstempel Warnung');
NextcloudWatchdog::markRun();
$this->assert(NextcloudWatchdog::warning() === null, 'DB: nach markRun() keine Warnung');
\Steuerkram\Database::execute("UPDATE company_settings SET nextcloud_last_run = NOW() - INTERVAL 3 DAY WHERE id = 1");
$this->assertNotEmpty(NextcloudWatchdog::warning(), 'DB: 3 Tage alter Zeitstempel löst Warnung aus');
}
}

View file

@ -37,9 +37,21 @@ class AnalyseNextcloudSkriptTest extends TestCase
// Verweise auf NextcloudImportService im Kommentar sind erwünscht (die cURL-Optionen // Verweise auf NextcloudImportService im Kommentar sind erwünscht (die cURL-Optionen
// müssen dort synchron gehalten werden) – ausgeführt werden darf der Import nicht. // müssen dort synchron gehalten werden) – ausgeführt werden darf der Import nicht.
$this->assert( $this->assert(
!str_contains($content, 'nextcloud-import.php') && !str_contains($content, 'new NextcloudImportService'), !str_contains($content, 'new NextcloudImportService'),
'Skript startet keinen Importlauf' 'Skript startet keinen Importlauf'
); );
// Das Importskript wird nie ausgeführt – nur per Syntaxprüfung (php -l) angefasst.
$this->assert(
!preg_match('/\b(passthru|shell_exec|system|proc_open|include|require)\b[^;\n]*nextcloud-import/', $content)
&& str_contains($content, "' -l '"),
'Skript führt das Importskript nicht aus, sondern prüft nur die Syntax'
);
// Die Analyse muss die Existenz des Importskripts im Container prüfen.
$this->assert(
str_contains($content, 'scripts/container/nextcloud-import.php') && str_contains($content, 'is_file($path)'),
'Skript prüft, ob scripts/container/nextcloud-import.php im Container existiert'
);
// Das Passwort dient ausschließlich als cURL-Zugangsdatum. In Ausgaben darf // Das Passwort dient ausschließlich als cURL-Zugangsdatum. In Ausgaben darf
// es nur als Länge (strlen) auftauchen, nie im Klartext. // es nur als Länge (strlen) auftauchen, nie im Klartext.