Compare commits

...

35 Commits

Author SHA1 Message Date
Terranom674
b86e066e2a Fix NC wizard syntax and preserve fallback state 2026-08-18 09:10:55 +02:00
Terranom674
d995f6889b Make legacy cutover compatible with current connector credential format 2026-08-18 09:05:49 +02:00
Terranom674
b6fc2879a0 Remove stale hard-coded connector user agent version 2026-08-18 09:05:11 +02:00
Terranom674
9fa75f844a Bump Bratonien Tools to 0.9.4.2 2026-08-18 09:04:31 +02:00
Terranom674
2f1d6cd832 Refresh current development status for 0.9.4.2 2026-08-18 09:04:15 +02:00
Terranom674
fc6f39445e Refresh README for 0.9.4 connector architecture 2026-08-18 09:03:51 +02:00
Terranom674
6a04b33f0c Allow API-only native NC connector activation 2026-08-18 09:03:06 +02:00
Terranom674
4194cf9e3b Simplify NC wizard for end users and branch technical questions 2026-08-18 09:02:33 +02:00
Terranom674
95a64fabc5 Consolidate intelligent NC connection wizard flow 2026-08-18 09:01:17 +02:00
Terranom674
50feeccd45 Allow storage root mappings and improve connector validation 2026-08-18 09:00:16 +02:00
Terranom674
d9d6968d5a Bump Bratonien Tools to 0.9.4.1 2026-08-18 08:54:14 +02:00
Terranom674
4a6765f53e NC polling: update run timestamps in place without reloading admin page 2026-08-18 08:53:58 +02:00
Terranom674
928c270319 NC status endpoint: expose last and next run labels without page reload 2026-08-18 08:53:32 +02:00
Terranom674
812df537c3 Release consolidated NC connector hardening 0.9.3.43 2026-08-18 08:49:34 +02:00
Terranom674
2f8885f377 Clean connector-owned state when deleting connections 2026-08-18 08:49:05 +02:00
Terranom674
d9c9a88a66 Validate NC manifest SQL view identifiers 2026-08-18 08:48:11 +02:00
Terranom674
53ab96e73b Validate NC runtime SQL view identifiers 2026-08-18 08:47:51 +02:00
Terranom674
6d278c2d0d Make NC shadow tree replacement rollback-safe 2026-08-18 08:47:37 +02:00
Terranom674
73a13a87d3 Remove wizard secrets from browser storage 2026-08-18 08:47:15 +02:00
Terranom674
e9ed89555a Consolidate NC connector wizard for end users 2026-08-18 08:46:57 +02:00
Terranom674
e5a03b13bd Allow validated wizard API commit and return connection id 2026-08-18 08:45:55 +02:00
Terranom674
98a4f49b26 Fix consolidated wizard syntax 2026-08-18 08:45:37 +02:00
Terranom674
b0f5c3d571 Consolidate NC wizard state, validation and API commit 2026-08-18 08:44:41 +02:00
Terranom674
43d01d0450 Bump Bratonien Tools to 0.9.3.42 2026-08-18 08:34:35 +02:00
Terranom674
38a8e76fee Fix Nextcloud OCS success handling in connection wizard 2026-08-18 08:34:19 +02:00
Terranom674
a38035fc92 Bump version to 0.9.3.41 2026-08-18 08:28:19 +02:00
Terranom674
39776d82a3 Preserve NC wizard input state across validation errors 2026-08-18 08:28:06 +02:00
Terranom674
cce875c965 Bump version to 0.9.3.40 2026-08-18 08:24:26 +02:00
Terranom674
ae6707f3cd Make Nextcloud wizard probe HTTPS and HTTP automatically 2026-08-18 08:24:07 +02:00
Terranom674
19abb98f23 Bump version to 0.9.3.39 2026-08-18 08:20:25 +02:00
Terranom674
d307f8437f NC polling: do not reload while wizard is open 2026-08-18 08:20:12 +02:00
Terranom674
97cb0b808f Bump version to 0.9.3.38 2026-08-18 08:15:07 +02:00
Terranom674
bfda6680f8 Prevent duplicate POST actions on page reload 2026-08-18 08:14:47 +02:00
Terranom674
0f05760735 Bump Bratonien Tools to 0.9.3.37 2026-08-18 08:10:45 +02:00
Terranom674
beff765612 Fix Smarty parsing in NC Connector dialog script 2026-08-18 08:10:23 +02:00
16 changed files with 1245 additions and 2276 deletions

View File

@@ -1,122 +1,121 @@
# Aktueller Entwicklungsstand
Stand: 17.08.2026
Stand: 18.08.2026
## Plugin
- Aktuelle Plugin-Version: **0.9.3.17**
- Aktueller Entwicklungsblock: **NC Connector Verbindungsverwaltung / laufende Optimierung**
- NC Connector ist Feature 10 und noch nicht vollständig abgeschlossen.
- Solange dieser Optimierungsblock läuft, bleibt die Version im Bereich `0.9.3.x`.
- Aktuelle Plugin-Version: **0.9.4.2**
- Aktueller Entwicklungsblock: **NC Connector Endnutzer-Assistent und Konsolidierung**
- `0.9.3.43` markiert den abgeschlossenen vorherigen Meilenstein.
- `0.9.4.x` ist der neue Entwicklungsblock.
## Aktueller GitHub-Stand
## Produktiver Stand
Der lokale NC Connector ist für den aktuellen Bratonien-Einsatz im bisherigen Login-Weg End-to-End funktionsfähig. Mit `0.9.3.17` beginnt der Umbau auf API-first für die Piwigo-Synchronisierung.
Der lokale NC Connector ist im aktuellen Bratonien-Einsatz End-to-End funktionsfähig:
Bereits erfolgreich umgesetzt und getestet:
- Nextcloud-Freigaben `folder` und `file` werden verarbeitet.
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert.
- Einzeldateifreigaben werden als echte Piwigo-Orphans ohne Albumzuordnung registriert.
- Entfernte Freigaben werden aus Piwigo entfernt, Originaldateien bleiben erhalten.
- Neue physische Connector-Alben werden privat angelegt.
- API-first-Synchronisierung ist produktiv für Piwigo **16.4.0** freigegeben.
- Piwigo-Album- und Fotoinformationen werden nach dem direkten Sync aktualisiert.
- Der gemeinsame systemd-Timer verarbeitet aktive Verbindungen regelmäßig.
- Nextcloud-Freigaben vom Typ `folder` und `file` werden aus der Source-View gelesen.
- Ordnerfreigaben werden als Verzeichnisbaum im Shadow Tree gespiegelt.
- Einzeldateifreigaben werden als Symlink direkt im Galerie-Root angelegt und als echte Piwigo-Orphans ohne Albumzuordnung registriert.
- Entfernte Einzeldateifreigaben werden beim normalen Connector-Lauf aus Shadow Tree und Piwigo entfernt.
- Das Nextcloud-Original bleibt bei Löschvorgängen unangetastet.
- Connector-getriggerte neue physische Alben werden standardmäßig privat angelegt.
- Der gemeinsame systemd-Timer `bratonien-nc-connector.timer` läuft produktiv.
## 0.9.4.1
Neu in `0.9.3.17`:
Die Statusaktualisierung im Admin-UI wurde von Seiten-Reload auf gezielte DOM-Aktualisierung umgestellt. Der Poller aktualisiert nur noch **Letzter Lauf** und **Nächster Lauf**. Offene Dialoge, Details und der Assistent bleiben dadurch erhalten.
- die Piwigo-API ist als bevorzugter Synchronisierungsweg vorgesehen;
- ein erfolgreich geprüfter API-Key wird verschlüsselt in der Piwigo-Konfiguration gespeichert;
- produktive API-Synchronisierung ist ausdrücklich an die freigegebene Piwigo-Version **16.4.0** gebunden;
- neuer Webservice `bratonien.nc.syncProductive` delegiert den freigegebenen produktiven Dateisync an Piwigos Core-Synchronisierung;
- `runtime/lib/piwigo-sync.php` versucht zuerst die API und fällt nur bei nicht nutzbarer API auf Benutzername/Passwort zurück;
- der bisherige Benutzername/Passwort-Zugang kann als verschlüsselter Fallback gespeichert oder gelöscht werden;
- ein Benutzername/Passwort-Fallback kann außerdem einmalig manuell ausgeführt werden, ohne die Zugangsdaten zu speichern;
- neue lokale Verbindungen werden intern als `api-first` angelegt; ein dauerhaft gespeicherter Login-Fallback ist technisch nicht mehr zwingend, sofern ein API-Zugang vorhanden ist.
## 0.9.4.2
### Verbindungsassistent
Der Assistent wurde erneut aus Endnutzersicht konsolidiert:
1. Nextcloud-Adresse + Benutzer + Passwort.
2. Automatischer Web-Scan über HTTP/HTTPS und OCS.
3. Der Nutzer sieht zunächst keine komplette Datenbank-Technikmaske.
4. Für den Datenzugriff werden zuerst nur Lese-Benutzer und Passwort abgefragt.
5. Host = Nextcloud-Host, Port 5432 und Datenbank `nextcloud` werden nur als automatischer Prüfversuch verwendet und nicht als sicher erkannt dargestellt.
6. Scheitert dieser Versuch, fragt der Assistent gezielt die abweichende Datenbank-Adresse, Port und Datenbank ab.
7. Storage-Mounts werden nur abgefragt, wenn sie nicht sicher aus vorhandenen Zuordnungen übernommen werden können.
8. Danach folgen Showcase-Benutzer, Piwigo-API-Test/Skip und Fallback.
9. Erst der letzte Schritt legt die Verbindung dauerhaft an.
Fehler leeren die Wizard-Eingaben nicht. Geheimnisse bleiben während des Wizards serverseitig in der PHP-Sitzung und werden nicht im Browser-Web-Storage persistiert.
### Storage-Mappings
Das Storage-Format erlaubt jetzt einen leeren `source_prefix`. Damit kann ein kompletter Storage direkt auf einen lokalen Mount zeigen. Die frühere Fehlermeldung
`Storage-Zeilen muessen das Format storage_id | source_prefix | local_mount verwenden.`
tritt bei einem legitimen Root-Mapping nicht mehr auf.
### Native Aktivierung
`nc-connector-install.php` unterstützt jetzt auch API-only-Verbindungen ohne dauerhaft gespeicherten Login-Fallback. Ein Fallback ist nur nötig, wenn keine nutzbare API gespeichert ist.
### Dokumentation
README und CURRENT_STATUS wurden auf den aktuellen `0.9.4.x`-Stand gebracht und beschreiben jetzt den realen API-first-Sync, den Wizard, die Statusaktualisierung ohne Reload und die aktuelle Runtime-Architektur.
## Architektur NC Connector
- Nextcloud bleibt die einzige dauerhafte Quelle der Originalbilder.
- Piwigo erhält nur die benötigte Verzeichnis-/Symlink-Struktur und erzeugt daraus seine Derivate und Cache-Dateien.
- Der Connector arbeitet mit einem PostgreSQL-Leser und den Views `piwigo_showcase_sources` und `piwigo_showcase_activity`.
- Laufzeitdaten liegen unter `/var/lib/bratonien-tools/nc-connector/...`.
- Verbindungs-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/connection-*.conf`.
- Der Shadow Tree enthält Verzeichnisse und Symlinks, keine Kopien der Originaldateien.
- Alle aktivierten Verbindungen werden über `runtime/run-all.sh` gemeinsam verarbeitet.
- Nextcloud ist Quelle der Originalbilder.
- Piwigo erhält einen Shadow Tree aus Verzeichnissen und Symlinks.
- PostgreSQL-Reader und die Views `piwigo_showcase_sources` / `piwigo_showcase_activity` liefern Quellen und Aktivität.
- Runtime-Konfigurationen: `/etc/bratonien-tools/nc-connector/connection-*.conf`
- State: `/var/lib/bratonien-tools/nc-connector/connection-ID`
- Öffentlicher Admin-Status: Piwigo `_data/bratonien-tools/nc-connector-status/`
- Runner: `runtime/run-all.sh`
- Einzelverbindung: `runtime/sync.sh`
## Nextcloud-Freigabemodell
## Piwigo-Synchronisierung
Der Connector unterstützt komplette Ordner und einzelne Bilder.
- `folder` -> Verzeichnisstruktur spiegeln und über Piwigos Dateisynchronisierung als physische Albumstruktur einlesen;
- `file` -> Symlink direkt im Galerie-Root und anschließende Registrierung als Piwigo-Orphan.
Für einzelne Dateien wird kein künstlicher Unterordner erzeugt.
## Activity-Gate
Das Activity-Gate berücksichtigt neben dem Nextcloud-Aktivitätsstand auch die Signatur der aktuell sichtbaren Freigaben. Weiterhin vorhanden sind Quiet-Time, maximale Wartezeit, periodischer Full-Sync und verbindungsspezifischer State.
## Piwigo-Synchronisation
### API-first
Der normale Runtime-Lauf verwendet ab `0.9.3.17` folgende Priorität:
Priorität:
1. gespeicherter Piwigo-API-Key;
2. bei API-Fehler, fehlender Freigabe für die installierte Piwigo-Version oder nicht vorhandener API: gespeicherter Benutzername/Passwort-Fallback;
3. ohne nutzbaren API- und Fallback-Zugang wird der Piwigo-Sync mit einer klaren Fehlermeldung abgebrochen.
2. bei nicht nutzbarer API gespeicherter Benutzername/Passwort-Fallback;
3. ohne beide Wege Abbruch mit Fehlerstatus.
Nach einem Piwigo-Update wird die produktive API nicht automatisch für die neue Version freigeschaltet. Erst nach einem Kompatibilitätstest wird die Versionsfreigabe im Plugin angehoben. Bis dahin bleibt nur der Login-Fallback zulässig.
Produktive API-Methoden:
### Physische Alben
- `bratonien.nc.syncProductive`
- `bratonien.nc.syncOrphans`
`bratonien.nc.syncProductive` ist aktuell nur für Piwigo **16.4.0** freigegeben. Der Endpoint läuft als Administrator-Webservice und verwendet für den eigentlichen Dateisync Piwigos Core-Synchronisierung. Neue Connector-Alben werden dabei privat angelegt.
`bratonien.nc.syncProductive` ist auf Piwigo 16.4.0 versionsgebunden. Der direkte Sync verwendet Bratonien-eigene Logik auf Basis der Piwigo-Core-Funktionen, nicht das Rendern oder Fernsteuern einer Admin-Seite.
### Root-Dateien / Orphans
## Activity Gate und Shadow Tree
`bratonien.nc.syncOrphans` verarbeitet direkte Dateien im Galerie-Root separat. Neue Root-Dateien werden als Piwigo-Orphans ohne Albumzuordnung registriert. Entfernte Root-Dateien werden nur aus Piwigo entfernt; das Nextcloud-Original bleibt unberührt.
Das Activity Gate berücksichtigt:
## Erfolgreiche Live-Tests bis 0.9.3.16
- Aktivitätsstand;
- Share-Fingerprint;
- Quiet-Time;
- Max-Wartezeit;
- periodischen Full-Sync;
- Reparaturbedarf bei beschädigter lokaler Struktur.
Bestätigt wurden:
Der Shadow-Tree-Wechsel besitzt einen Rollback auf den vorherigen Baum, falls das Umschalten fehlschlägt.
- Ordnerfreigaben werden korrekt eingelesen;
- Einzeldateifreigaben werden als `file` erkannt und im Galerie-Root verlinkt;
- Orphan-Anlage und Orphan-Löschung funktionieren;
- physische neue Connector-Alben werden privat angelegt;
- der automatische Timer kann regulär laufen.
## Sicherheit
## Offener Test für 0.9.3.17
- Nextcloud-Originale werden nicht gelöscht.
- Connector-Zugangsdaten werden verschlüsselt gespeichert.
- Wizard-Secrets bleiben serverseitig.
- Storage-Pfade werden validiert.
- SQL-View-Namen werden validiert.
- Piwigo-API ist versionsgebunden.
- Mutierende Admin-Aktionen verwenden Post/Redirect/Get, sodass Browser-Reloads sie nicht erneut ausführen.
- Self-Updates sind an einen konkreten Commit und SHA-256 gebunden.
Vor Abschluss des API-first-Blocks muss noch live geprüft werden:
## Noch offen
1. Plugin-Update auf `0.9.3.17`;
2. API-Key im NC Connector prüfen und speichern;
3. produktiver Lauf verwendet `bratonien.nc.syncProductive` erfolgreich;
4. Ordner- und Orphan-Sync bleiben unverändert korrekt;
5. gespeicherter Login-Fallback übernimmt bei absichtlich nicht nutzbarer API;
6. einmaliger Fallback funktioniert ohne Speicherung;
7. gelöschter Fallback wird vom Runtime-Lauf nicht mehr verwendet.
## Sicherheit / Betriebsmodell
- Nextcloud bleibt Eigentümer der Originaldateien.
- Der Connector löscht keine Originaldateien aus den Storage-Mounts.
- API- und Login-Zugangsdaten werden verschlüsselt gespeichert und nicht in das Repository geschrieben.
- Die API hat Vorrang, ist aber streng versionsgebunden.
- Benutzername/Passwort ist nur Fallback und kann dauerhaft, einmalig oder gar nicht verwendet werden.
- Neue Connector-Alben werden privat angelegt.
## Bekannte offene Punkte
Der NC Connector bleibt im Entwicklungsblock `0.9.3.x`. Nach erfolgreichem API-first-Livetest folgen insbesondere:
- Bereinigung und Vereinfachung der Verbindungsverwaltung im Admin-UI;
- Remote-Nextcloud-Adapter;
- Entscheidung über verbleibende Legacy-/Migrationshilfen;
- weitere Komfortfunktionen für Status, Diagnose und Administration.
- vollständiger Remote-Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff;
- weitere Bereinigung alter Legacy-Migrationshelfer, sobald sie nicht mehr benötigt werden;
- weitere Endnutzer-Optimierung des Assistenten auf Basis realer Fehler- und Installationspfade.
## Repository-Fallback
Während der Entwicklung bleibt GitHub das führende Repository. Zusätzlich existiert auf dem privaten Gitea-System ein Pull-Mirror als Ausfall-/Fallback-Ebene.
GitHub bleibt das führende Repository. Das private Gitea-System dient weiterhin als Mirror/Fallback.

391
README.md
View File

@@ -1,303 +1,228 @@
# Piwigo Bratonien Tools
Modular aufgebautes Piwigo-Plugin mit erweiterten Werkzeugen fuer Administration, Bildverarbeitung, Nextcloud-Anbindung und Frontend-Funktionen.
Modulares Piwigo-Plugin r Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo.
Das Projekt ist aus der Bratonien-Piwigo-Installation entstanden, wird aber bewusst so entwickelt, dass die einzelnen Funktionen moeglichst neutral und auch ausserhalb dieser Installation nutzbar bleiben.
Aktuelle Plugin-Version: **0.9.4.2**
Aktuelle Plugin-Version: **0.9.3.16**
## Grundprinzip
## Funktionsumfang
Bratonien Tools erweitert Piwigo um Funktionen, die in der Bratonien-Installation benötigt werden, hält die einzelnen Module aber möglichst unabhängig voneinander.
### NC Connector
Für die Administration gilt: Der normale Nutzer sieht Aufgaben, Entscheidungen und Ergebnisse. Technische Interna werden automatisch ermittelt oder bleiben hinter ausdrücklich technischen Einstellungen verborgen.
Der NC Connector verwaltet Nextcloud-Quellen fuer Piwigo und fuehrt den regelmaessigen Abgleich mit einer Plugin-eigenen Sync-Runtime aus.
## NC Connector
Aktuell vorhanden:
Der NC Connector synchronisiert ausdrücklich freigegebene Nextcloud-Inhalte mit Piwigo.
- mehrere Connector-Verbindungen im eigenen Datenmodell
- lokale Verbindungen direkt in Bratonien Tools anlegen
- verschluesselte Speicherung von PostgreSQL- und Piwigo-Sync-Zugangsdaten
- lokaler Nextcloud-Adapter ueber PostgreSQL-Reader und explizite Storage-Mounts
- Verifikation von PostgreSQL-Verbindung, Source-View, Activity-View und Storage-Mounts
- native Aktivierung einer verifizierten Verbindung ohne vorherige Legacy-Installation
- eigener State pro Verbindung unter `/var/lib/bratonien-tools/nc-connector/connection-ID`
- gemeinsame Runtime-Konfiguration unter `/etc/bratonien-tools/nc-connector/`
- Multi-Connection-Runner unter `runtime/run-all.sh`
- gemeinsame Zeitsteuerung ueber `bratonien-nc-connector.timer`
- Verbindungen koennen kontrolliert deaktiviert und danach geloescht werden
- Anzeige von Timer-Status, letztem Lauf, letztem Ergebnis und naechstem geplanten Lauf
- Plugin-eigene Sync-Runtime unter `runtime/`
- Activity-Gate mit Quiet-Time, Max-Wartezeit, Share-Fingerprint und periodischem Full-Sync
- Shadow Tree fuer Piwigo ohne Kopie der Originaldateien
- Unterstuetzung von Nextcloud-Freigaben fuer komplette Ordner und einzelne Dateien
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert
- neu importierte Connector-Alben werden direkt als privat angelegt
- einzelne Dateifreigaben werden direkt im Galerie-Root verlinkt und als echte Piwigo-Orphans registriert
- entfernte Einzeldateifreigaben werden beim naechsten Lauf wieder aus Piwigo entfernt, ohne das Original zu loeschen
- Originalbilder bleiben an ihrer Nextcloud-/Storage-Quelle
- lokale Piwigo-Derivate und Caches bleiben davon getrennt und koennen jederzeit neu erzeugt werden
### Datenmodell
Die Runtime baut aus den freigegebenen Nextcloud-Quellen einen Piwigo-kompatiblen Shadow Tree. Dateien werden dabei nicht in eine zweite permanente Originalbibliothek kopiert, sondern ueber Symlinks auf die vorhandenen Storage-Mounts referenziert.
- Nextcloud bleibt die Quelle der Originaldateien.
- Originalbilder werden nicht in eine zweite dauerhafte Bibliothek kopiert.
- Ein Shadow Tree erzeugt die für Piwigo benötigte Verzeichnisstruktur mit Symlinks.
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert.
- Einzeldateifreigaben werden im Galerie-Root verlinkt und als echte Piwigo-Orphans ohne Albumzuordnung registriert.
- Entfernte Freigaben werden aus Shadow Tree und Piwigo entfernt, das Nextcloud-Original bleibt erhalten.
- Neu importierte physische Connector-Alben werden privat angelegt.
#### Freigabemodell
### Verbindungsassistent
Der Connector unterscheidet zwei Quelltypen:
Neue Verbindungen werden bevorzugt mit einem Endnutzer-Assistenten angelegt.
- `folder` - komplette Ordnerfreigabe; der Verzeichnisbaum wird gespiegelt und ueber Piwigos normale Dateisynchronisierung als Albumstruktur eingelesen
- `file` - einzelne Dateifreigabe; die Datei wird direkt als Symlink im Galerie-Root angelegt und separat als Piwigo-Orphan ohne Albumzuordnung registriert
Ablauf:
Fuer Einzeldateien wird kein kuenstlicher Unterordner erzeugt. Entfernt Nextcloud eine solche Freigabe, entfernt der Connector den Root-Symlink und anschliessend nur den zugehoerigen Piwigo-Datenbankeintrag. Das Original bleibt unangetastet.
1. Nextcloud-Adresse, Benutzer und Passwort eingeben.
2. Der Assistent prüft HTTP/HTTPS, Nextcloud-Status und Anmeldung.
3. Vorhandene passende Connector-Daten werden automatisch wiederverwendet, wenn sie sicher zugeordnet werden können.
4. Für den Nextcloud-Datenzugriff wird zunächst nur der eingerichtete Lese-Benutzer mit Passwort abgefragt. Der übliche PostgreSQL-Weg wird automatisch geprüft.
5. Nur wenn diese Prüfung scheitert, werden abweichende Datenbank-Adresse, Port und Datenbankname gezielt abgefragt.
6. Nicht automatisch zuordenbare Storage-Mounts werden einzeln bestätigt.
7. Danach wird der Nextcloud-Benutzer gewählt, dessen Freigaben verwendet werden sollen. Empfohlen wird ein eigener `showcase`-Benutzer.
8. Piwigo-API-Zugang testen oder bewusst überspringen.
9. Optionaler bzw. bei übersprungener API verpflichtender Login-Fallback.
10. Erst der Abschluss legt die Verbindung dauerhaft an.
#### Neuinstallation
Wizard-Geheimnisse werden während der Einrichtung serverseitig in der Sitzung gehalten. Ein fehlgeschlagener Test leert die Eingaben nicht. Ein API-Test verändert die dauerhafte API-Konfiguration erst beim erfolgreichen Abschluss des Assistenten.
Eine frische Piwigo-Installation benoetigt keinen vorher vorhandenen `piwigo-sync` mehr.
Die vollständige technische Maske bleibt weiterhin über **Ohne Assistent anlegen** sowie bei bestehenden Verbindungen unter **Technische Einstellungen** erreichbar.
Der vorgesehene Ablauf ist:
### Lokaler Adapter
1. lokale Nextcloud-Verbindung in Bratonien Tools anlegen
2. PostgreSQL, Views und Storage-Mounts pruefen
3. die angezeigte Root-Aktivierung ausfuehren
4. der Installer legt Runtime-Konfiguration, Secrets, State-Verzeichnis sowie systemd-Service und -Timer an
5. vor der Aktivierung wird ein echter Lauf mit der Plugin-Runtime getestet
Der derzeit produktive Adapter verwendet:
Der gemeinsame Service verarbeitet alle aktivierten `connection-*.conf`-Dateien nacheinander. Dadurch koennen mehrere Verbindungen mit einer gemeinsamen Zeitsteuerung betrieben werden.
- PostgreSQL-Reader mit minimalen Leserechten;
- Source-View `piwigo_showcase_sources`;
- Activity-View `piwigo_showcase_activity`;
- explizite Storage-Zuordnungen auf bereits vorhandene lokale Mounts;
- Plugin-eigene Runtime und getrennten State pro Verbindung.
#### Migration bestehender Installationen
Ein Storage-Mapping besteht aus `storage_id`, einem optionalen `source_prefix` und dem lokalen Mount. Ein leerer Prefix ist zulässig und bedeutet, dass der Mount direkt dem Storage-Root entspricht.
Fuer bestehende Installationen, die zuvor den separaten `piwigo-sync` aus `Proxmox_Scripts` verwendet haben, existiert weiterhin ein kontrollierter Migrationsweg:
### Activity Gate
- bestehende Verbindung einmalig importieren
- Connector-Kopie unabhaengig verifizieren
- kontrollierten Cutover vorbereiten
- Connector-Timer aktivieren und Legacy-Timer deaktivieren
- Runtime vom bisherigen `/opt/piwigo-sync` auf die Plugin-eigene Runtime umstellen
- Legacy-Bestand entfernen
- den verbliebenen Laufzeit-State anschliessend mit `nc-connector-normalize.php` in die native Struktur unter `/var/lib/bratonien-tools/nc-connector/` ueberfuehren
Der regelmäßige Lauf berücksichtigt:
Nach abgeschlossener Migration werden `/opt/piwigo-sync`, `/etc/piwigo-sync`, `piwigo-sync.service` und `piwigo-sync.timer` nicht mehr benoetigt.
- Nextcloud-Aktivität;
- Signatur der sichtbaren Freigaben;
- Quiet-Time;
- maximale Wartezeit;
- periodischen Full-Sync;
- notwendigen Reparaturlauf bei beschädigtem Shadow Tree oder fehlenden Piwigo-Alben.
### Bildcache
### Piwigo-Synchronisierung
- Piwigo-Bildcache gezielt leeren
- vorhandene Bildderivate neu erzeugen
- Cache-Aufbau als Worker-Prozess starten und abbrechen
- Worker-Einstellungen verwalten
- Originalbilder bleiben unangetastet
Der produktive Lauf ist API-first:
### Wasserzeichenverwaltung
1. `bratonien.nc.syncProductive` für physische Alben und Bilder;
2. `bratonien.nc.syncOrphans` für Root-Dateien;
3. Benutzername/Passwort nur als optionaler Fallback.
Erweiterte Wasserzeichenverwaltung als Alternative zur einfachen Piwigo-Standardkonfiguration.
Die produktive direkte API-Synchronisierung ist aktuell ausdrücklich für **Piwigo 16.4.0** freigegeben. Bei einer anderen Piwigo-Version wird sie nicht stillschweigend als kompatibel angenommen.
Unter anderem vorhanden:
Der direkte produktive Sync führt außerdem die für den normalen Piwigo-Import relevanten Nacharbeiten aus, darunter Albuminformationen, globale Ränge, Bildinformationen, Pfade und Cache-Invalidierung. Er ruft dafür keine Admin-Seite fernsteuernd auf.
- eigene Wasserzeichendateien
- Wasserzeichenprofile
- Standardprofil
- albumbezogene Regeln
- getrennte Einstellungen fuer Positionierung und Verarbeitung
- eigener Runtime-Filter fuer Piwigo-Bildderivate
- Aktivierung und Deaktivierung der erweiterten Wasserzeichen-Engine
### Runtime
Beim Aktivieren wird die bisherige Piwigo-Wasserzeichenkonfiguration gesichert und nicht dauerhaft ueberschrieben. Beim Deaktivieren kann sie wiederhergestellt werden.
Aktive Verbindungen werden über einen gemeinsamen systemd-Timer verarbeitet:
### Bilddateien & Pfade
- `bratonien-nc-connector.timer`
- `bratonien-nc-connector.service`
- `runtime/run-all.sh`
- `runtime/sync.sh`
Verwaltung eigener Bilddateien fuer die Piwigo-Installation.
Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`, State-Daten unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
- Upload nach `local/bratonien/assets/`
- Vorschau vorhandener Dateien
- Anzeige von Dateiname, Pfad, Abmessungen und Dateigroesse
- Dateien loeschen
- konfigurierbare PHP-Uploadgrenzen ueber `.user.ini`
- Validierung von `upload_max_filesize` und `post_max_size`
Der Shadow-Tree-Austausch besitzt einen Rollback: Scheitert der Wechsel auf den neuen Baum, wird der vorherige Galeriebaum wiederhergestellt.
### Oeffentliche Bildauswahl fuer Batch Downloader
### Statusanzeige
Erweitert Albumseiten um die Moeglichkeit, einzelne Bilder fuer einen Download auszuwaehlen.
Die Administration zeigt letzten und nächsten Lauf. Der Status wird regelmäßig im Hintergrund abgefragt. Dafür wird die Admin-Seite **nicht** neu geladen; offene Dialoge, Assistenten und aufgeklappte Bereiche bleiben erhalten.
- Auswahlmodus direkt in der Albumansicht
- einzelne Bilder markieren
- alle Bilder auswaehlen oder Auswahl aufheben
- nur die ausgewaehlten Bilder an Batch Downloader uebergeben
- vorhandene Piwigo- und Batch-Downloader-Berechtigungen bleiben wirksam
- getrennte Freigabe fuer Gaeste, registrierte Benutzer und Gruppen
### Bestehende Verbindungen
**Abhaengigkeit:** Das Piwigo-Plugin Batch Downloader muss installiert und aktiv sein.
Bestehende Verbindungen können:
Die ZIP-Erstellung wird nicht dupliziert. Bratonien Tools uebergibt lediglich die berechtigten Bild-IDs an Batch Downloader.
- umbenannt;
- geprüft;
- technisch bearbeitet, solange sie nicht aktiv sind;
- deaktiviert und anschließend gelöscht werden.
### Fortlaufende Bildtitel in der Stapelverarbeitung
Das Löschen einer Connector-Verbindung entfernt keine Nextcloud-Originale und keine Piwigo-Bilder. Erreichbare Connector-eigene Status-/State-Daten der Verbindung werden bereinigt.
Erweitert Piwigos globale Stapelverarbeitung um die Aktion **Fortlaufende Bildtitel**.
### Legacy-Migration
- arbeitet ausschliesslich mit der aktuellen Piwigo-Auswahl
- frei waehlbares Titelpraefix
- frei waehlbare Startnummer
- einstellbare Stellenzahl mit fuehrenden Nullen
- direkte Vorschau des resultierenden Titelformats
- wahlweise alle ausgewaehlten Titel ueberschreiben oder nur leere beziehungsweise typische Kamera-/Importtitel ersetzen
- Reihenfolge nach Dateiname, Aufnahmedatum oder aktueller Albumreihenfolge
- vorhandene individuelle Titel koennen gezielt geschuetzt werden
- physische Dateinamen werden nicht veraendert
Für ältere Installationen mit dem früheren separaten `piwigo-sync` existieren weiterhin Migrations-/Cutover-Helfer. Neue Installationen sollen den nativen Connector-Weg verwenden.
Beispiel: `Samt 2026 - 001`, `Samt 2026 - 002`, `Samt 2026 - 003`.
## Bildcache
Die Albumreihenfolge steht nur zur Verfuegung, wenn die Stapelverarbeitung auf genau ein Album ohne rekursive Unteralben gefiltert ist.
- Piwigo-Bildderivate gezielt leeren;
- vorhandene Bildgrößen neu erzeugen;
- Cache-Aufbau als Worker-Prozess starten und abbrechen;
- Worker-Zahl automatisch oder manuell konfigurieren;
- Originalbilder bleiben unangetastet.
### Albumzugriff verwalten
## Wasserzeichenverwaltung
In der Administrationsoberflaeche koennen Alben direkt zwischen **oeffentlich** und **privat** umgeschaltet werden.
- eigene Wasserzeichendateien;
- Wasserzeichenprofile;
- globale Regeln für öffentliche/private Alben;
- Album-Ausnahmen und Vererbung;
- Position, Transparenz und Skalierung;
- eigener Runtime-Filter für Piwigo-Derivate;
- bisherige Piwigo-Wasserzeichenkonfiguration wird beim Aktivieren gesichert.
- Albumliste mit Suchfeld
- paginierte Darstellung
- Umschalten des Zugriffs direkt aus Bratonien Tools
- Verwendung nativer Piwigo-Icons fuer den Zugriffsstatus
- beim Sperren eines Albums behaelt der aktuell handelnde Benutzer automatisch direkten Zugriff
## Bilddateien und Pfade
Der Schutz vor versehentlichem Selbstaussperren greift auch dann, wenn ein Album ueber Piwigos eigene Album-Zugriffsverwaltung von oeffentlich auf privat gesetzt wird.
- Upload nach `local/bratonien/assets/`;
- Vorschau, Abmessungen und Dateigröße;
- Löschen verwalteter Assets;
- konfigurierbare PHP-Uploadgrenzen über `.user.ini`, sofern die Serverkonfiguration dies zulässt.
### Geschuetzte Albumfreigaben
## Fotoauswahl und Batch Downloader
Private Alben koennen direkt mit Bratonien Tools geteilt werden. ShareAlbum oder ein anderes Freigabe-Plugin ist dafuer nicht erforderlich.
Bratonien Tools erweitert Albumseiten um eine öffentliche bzw. berechtigungsabhängige Bildauswahl und übergibt nur die ausgewählten Bild-IDs an das Piwigo-Plugin Batch Downloader.
- eigener individueller Freigabelink pro Freigabe
- Passwort **optional**; ohne Passwort reicht der nicht erratbare Link
- Passwoerter werden nur als Hash gespeichert
- integrierter Generator fuer sichere Passwoerter
- erzeugte Passwoerter koennen angezeigt und kopiert werden
- optionales Ablaufdatum
- optionaler Freigabe-Tag, um mehrere Freigaben desselben Albums auseinanderzuhalten
- eigener technischer Piwigo-Benutzer pro Freigabe
- der technische Benutzer erhaelt nur Zugriff auf das freigegebene private Album
- aktive Freigabelinks werden in der Administration angezeigt und koennen direkt kopiert werden
- bei aelteren, nicht rekonstruierbaren Links kann ein neuer Link erzeugt werden; der bisherige Link wird dabei ungueltig
- Freigaben koennen einzeln widerrufen werden
- beim Widerruf wird der technische Benutzer samt Sitzungen und Albumrecht entfernt
- wird das Album geloescht, werden zugehoerige Freigaben automatisch bereinigt
**Abhängigkeit:** Batch Downloader muss installiert und aktiv sein.
Freigabe-Tokens werden nicht im Klartext gespeichert. Fuer aktuelle Freigaben kann der Link deterministisch aus dem technischen Freigabebenutzer, dem Album und einem lokal gespeicherten Secret rekonstruiert werden; in der Datenbank liegt nur der Hash des Tokens.
## Fortlaufende Bildtitel
### Erweiterte Bildnavigation
Die globale Piwigo-Stapelverarbeitung erhält die Aktion **Fortlaufende Bildtitel** mit Präfix, Startnummer, Stellenzahl, Sortierung und Schutz vorhandener individueller Titel. Physische Dateinamen werden nicht verändert.
Auf der Bilddetailseite werden die vorhandenen Piwigo-Image-Maps weiterverwendet und responsiv neu aufgeteilt.
## Albumzugriff
Die Bildflaeche besteht aus vier Navigationszonen:
Alben können in Bratonien Tools zwischen öffentlich und privat umgeschaltet werden. Beim Sperren wird verhindert, dass sich der handelnde Benutzer versehentlich selbst aussperrt.
- links: vorheriges Bild
- rechts: naechstes Bild
- oben mittig: zurueck zur Vorschau beziehungsweise Albumansicht
- Mitte: PhotoSwipe / Vollbildansicht
## Geschützte Albumfreigaben
Die Zonen werden anhand der aktuell dargestellten Bildgroesse berechnet und bei Groessenaenderungen neu gesetzt.
Private Alben können über eigene Freigabelinks geteilt werden:
Fuer die sichtbaren Hover-Elemente werden neutrale CSS-Klassen bereitgestellt. Das Plugin enthaelt bewusst kein Bratonien-spezifisches Farbschema. Individuelles Branding kann ueber Theme- oder Custom-CSS erfolgen.
- optionales Passwort;
- optionales Ablaufdatum;
- eigener technischer Piwigo-Benutzer pro Freigabe;
- minimaler Albumzugriff;
- Hash-Speicherung von Passwörtern und Tokens;
- Widerruf und automatische Bereinigung beim Löschen eines Albums.
### Selbstaktualisierung
## Erweiterte Bildnavigation
Bratonien Tools kann den aktuellen Stand des GitHub-Repositories pruefen und sich aus der Administration heraus aktualisieren.
Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges Bild, nächstes Bild, Rückkehr zur Übersicht und PhotoSwipe/Vollbild.
- Versionspruefung gegen `main.inc.php` im GitHub-Repository
- Update-Pruefung wird kurzzeitig zwischengespeichert
- Updates duerfen nur vom Piwigo-Webmaster ausgefuehrt werden
- Download des aktuellen `main`-Branches als ZIP
- Pruefung auf `ZipArchive` und Schreibrechte des Plugin-Verzeichnisses
- detailliertere Downloadfehler
- Status wird nach Update-Pruefungen und Aktionen neu eingelesen
## Selbstaktualisierung
Der integrierte Updater:
- liest den Zielstand aus GitHub;
- bindet ein Update an einen konkreten Commit statt an einen während des Updates beweglichen Branch-Stand;
- prüft Version und SHA-256 des Zielstands;
- erstellt vor dem Austausch ein Backup;
- nutzt Post/Redirect/Get, damit ein Browser-Reload keine schreibende Aktion erneut ausführt;
- verlangt Webmaster-Rechte, `ZipArchive` und ausreichende Schreibrechte.
## Architektur
Das Plugin ist modular aufgebaut. Administrative Werkzeuge werden getrennt implementiert und ueber eine zentrale Registry eingebunden. Frontend-, Runtime- und Piwigo-Integrationen liegen ebenfalls in eigenen Modulen.
Wichtige Dateien und Verzeichnisse:
Wichtige Bestandteile:
- `main.inc.php` - Plugin-Einstieg, Runtime-Module und Admin-Menue
- `admin.php` - zentraler Admin-Controller
- `include/tool_registry.inc.php` - Registry der administrativen Aktionen
- `include/nc_connector.inc.php` - Connector-Datenmodell, Legacy-Import und gemeinsame Low-Level-Funktionen
- `include/nc_connector_manage.inc.php` - native Connection-Verwaltung und Verifikation
- `include/nc_connector_takeover.inc.php` - kontrollierte Legacy-Uebergabe
- `include/nc_connector_system.inc.php` - Timer- und Laufzeitstatus ueber alle aktiven Verbindungen
- `include/nc_connector_ws.inc.php` - read-only Paritaets-/Synchronisationspruefung ueber den Piwigo-Webservice
- `include/nc_orphan_ws.inc.php` - produktive Synchronisation einzelner Root-Dateien als Piwigo-Orphans
- `runtime/sync.sh` - Plugin-eigene Sync-Runtime einer Verbindung
- `runtime/run-all.sh` - gemeinsamer Runner fuer alle installierten Verbindungen
- `runtime/lib/activity_gate.py` - Activity-Gate, Share-Fingerprint und zeitgesteuerte Reconciliation
- `runtime/lib/build_manifest.py` - Aufloesung von Ordner- und Dateifreigaben auf konfigurierte Storage-Mounts
- `runtime/lib/shadow_tree.py` - Aufbau des Piwigo-kompatiblen Shadow Trees ohne Kopieren der Originale
- `runtime/lib/piwigo-db-check.php` - Konsistenzpruefung vorhandener Piwigo-Alben
- `runtime/lib/piwigo-db-sync.pl` - Ausloesen der Piwigo-Dateisynchronisierung und anschliessender Orphan-Abgleich
- `nc-connector-install.php` - native Aktivierung einer verifizierten Verbindung
- `nc-connector-disable.php` - kontrollierte Deaktivierung einer aktiven Verbindung
- `nc-connector-normalize.php` - Ueberfuehrung einer migrierten aktiven Verbindung in den nativen State- und Multi-Connection-Aufbau
- `nc-connector-migrate.php` - einmaliger Migrationshelfer fuer bestehende Legacy-Installationen
- `nc-connector-cutover-v2.php` - kontrollierter Legacy-Cutover
- `nc-connector-runtime-switch.php` - Umstellung einer aktiven Legacy-Verbindung auf die Plugin-eigene Runtime
- `nc-connector-legacy-cleanup.php` - kontrollierte Entfernung alter Sync-Scripts und systemd-Units
- `include/public_selection.inc.php` - oeffentliche Fotoauswahl und Batch-Downloader-Anbindung
- `include/picture_navigation.inc.php` - Einbindung der erweiterten Bildnavigation
- `include/batch_titles.inc.php` - fortlaufende Titelvergabe in Piwigos Stapelverarbeitung
- `include/album_lock.inc.php` - Laden und Umschalten des Album-Zugriffsstatus
- `include/album_shares.inc.php` - Albumfreigaben, Freigabetokens und Schutz vor Selbstaussperren bei privaten Alben
- `include/cache_worker_settings.inc.php` - Einstellungen fuer den Cache-Worker
- `include/dependencies.inc.php` - Abhaengigkeitspruefungen
- `include/watermark_*.inc.php` - Wasserzeichen-Engine und Runtime
- `include/self_update.inc.php` - Versionspruefung und Selbstaktualisierung
- `tools/album_rules.inc.php` - albumbezogene Regeln
- `tools/asset_manager.inc.php` - Verwaltung eigener Bilddateien
- `tools/image_cache.inc.php` - Cache-Verwaltung
- `tools/watermark*.inc.php` - administrative Wasserzeichenmodule
- `main-cache-build.php` - Cache-Aufbau im Worker-Prozess
- `main-cache-status.php` - Statusschnittstelle fuer den Cache-Worker
- `js/` - Frontend- und Admin-JavaScript
- `css/` - strukturelles Plugin-CSS
- `template/` - Admin- und Frontend-Templates
- `maintain.class.php` - Piwigo-Lifecycle
## Neues administratives Tool hinzufuegen
1. Neue Implementierung unter `tools/` anlegen.
2. Datei in `include/tool_registry.inc.php` laden.
3. Handler in `bratonien_tools_get_tools()` registrieren.
4. Benoetigte Darstellung in der gemeinsamen Administrationsoberflaeche ergaenzen.
Funktionen sollten moeglichst eigenstaendig bleiben und keine Bratonien-spezifische Gestaltung voraussetzen.
- `main.inc.php` Plugin-Einstieg und Runtime-Hooks
- `admin.php` zentraler Admin-Controller mit Post/Redirect/Get
- `include/tool_registry.inc.php` Registry administrativer Aktionen
- `include/nc_connector.inc.php` Datenmodell und gemeinsame Connector-Funktionen
- `include/nc_connector_wizard.inc.php` Endnutzer-Assistent und Connection-Bearbeitung
- `include/nc_connector_manage.inc.php` Credential-Format, Storage-Mappings, Verifikation und Löschen
- `include/nc_connector_create_api.inc.php` API-first-Verbindungserstellung
- `include/nc_connector_piwigo_api.inc.php` API-Zugang und Fallback-Verwaltung
- `include/nc_connector_system.inc.php` Timer- und Laufzeitstatus
- `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync
- `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung
- `runtime/lib/activity_gate.py` Activity Gate
- `runtime/lib/build_manifest.py` Auflösung der Nextcloud-Freigaben auf Storage-Mounts
- `runtime/lib/shadow_tree.py` atomarer Shadow Tree mit Rollback
- `runtime/lib/piwigo-sync.php` API-first-Piwigo-Sync mit Fallback
- `runtime/run-all.sh` Multi-Connection-Runner
- `runtime/sync.sh` Ablauf einer Verbindung
- `nc-connector-install.php` native Root-Aktivierung
- `nc-connector-disable.php` Deaktivierung
- `nc-connector-normalize.php` Normalisierung älterer aktiver Verbindungen
- `nc-connector-*-cleanup/switch/cutover` Legacy-Migrationshelfer
- `include/self_update.inc.php` Self-Updater
- `include/album_shares.inc.php` geschützte Albumfreigaben
- `include/public_selection.inc.php` Fotoauswahl
- `include/batch_titles.inc.php` fortlaufende Titel
- `include/watermark_*.inc.php` Wasserzeichen-Engine
- `tools/` administrative Einzelwerkzeuge
- `template/`, `js/`, `css/` Oberfläche
## Sicherheit
Administrative Aktionen pruefen Piwigo-Berechtigungen und verwenden fuer schreibende Aktionen Piwigos CSRF-Schutz.
Weitere Schutzmechanismen sind funktionsabhaengig, unter anderem:
- verschluesselte Speicherung der Connector-Zugangsdaten mit einem lokalen Connector-Secret
- PostgreSQL-Reader mit minimalen Leserechten fuer lokale Nextcloud-Verbindungen
- explizite Storage-Zuordnungen statt automatischer Freigabe beliebiger Dateipfade
- Verifikation von Datenbank, Views und Mounts vor einer Aktivierung
- Runtime-Test vor der nativen Aktivierung
- Root-only-Hilfsprogramme fuer systemd-, Secret- und State-Aenderungen
- getrennte State-Verzeichnisse pro Connector-Verbindung
- Originalbilder werden vom NC Connector nicht in eine zweite permanente Bibliothek kopiert
- Validierung von Cache- und Dateipfaden
- Filterung ausgewaehlter Bild-IDs gegen die aktuell berechtigte Bildmenge
- Nutzung der von Piwigos Stapelverarbeitung validierten Auswahl fuer die fortlaufende Titelvergabe
- gehashte Passwoerter und gehashte Freigabetokens fuer Albumfreigaben
- nicht erratbare Freigabelinks auf Basis eines lokal erzeugten Secrets
- eigene technische Benutzer mit minimalem Albumzugriff fuer Freigaben
- automatische Bereinigung widerrufener und geloeschter Albumfreigaben
- automatischer Erhalt des eigenen Zugriffs beim Umschalten eines Albums auf privat
- Connector-importierte physische Alben werden standardmaessig privat angelegt
- Beibehaltung bestehender Piwigo- und Plugin-Berechtigungen
- kontrollierte Uploadziele und Uploadgrenzen
- kein Zugriff auf Originalbilder beim Leeren des Bildcaches
- Webmaster-Pruefung, Schreibbarkeitspruefung und kontrolliertes temporaeres Arbeitsverzeichnis bei Selbstupdates
## Styling und Anpassung
Bratonien Tools soll Funktion und Gestaltung voneinander trennen. Plugin-eigene Styles dienen daher nur der technischen Darstellung und Positionierung.
Farben, Schatten, Hover-Effekte und individuelles Branding sollten ueber das aktive Piwigo-Theme oder Custom CSS umgesetzt werden.
- administrative Schreibaktionen verwenden Piwigos CSRF-Schutz;
- Connector-Zugangsdaten werden verschlüsselt gespeichert;
- Wizard-Geheimnisse werden nicht in Browser-Web-Storage persistiert;
- Storage-Mounts werden explizit zugeordnet und validiert;
- SQL-View-Namen werden vor Verwendung validiert;
- gefährliche technische Pfade sind nicht Teil des normalen Wizard-Happy-Paths;
- produktive Piwigo-API ist versionsgebunden;
- Originalbilder werden vom Connector nicht gelöscht;
- Update-Pakete werden an Commit und Hash gebunden;
- Root-Helfer prüfen CLI-/Root-Ausführung, bevor sie Systemdienste oder `/etc`-/`/var/lib`-Daten verändern.
## Entwicklungsstand
Das Plugin befindet sich weiterhin in aktiver Entwicklung. Mit Version **0.9.3.16** ist der lokale NC-Connector fuer den aktuellen Bratonien-Einsatz End-to-End funktionsfaehig: Ordner- und Einzeldateifreigaben werden erkannt, der Shadow Tree wird automatisch gepflegt, Piwigo wird synchronisiert, Einzeldateien werden als Orphans verwaltet und der regelmaessige Lauf erfolgt ueber den gemeinsamen systemd-Timer.
Als naechster groesserer Connector-Ausbauschritt bleibt der Remote-Nextcloud-Adapter offen.
`0.9.4.x` ist der aktuelle Entwicklungsblock. Der lokale NC Connector ist End-to-End produktiv im Einsatz. Der Verbindungsassistent und die Verwaltung werden weiter aus Endnutzersicht konsolidiert. Ein vollständig entfernter Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff ist noch nicht umgesetzt.

View File

@@ -15,6 +15,32 @@ $errors = array();
$self_update_override = null;
$nc_piwigo_api_test = null;
// Post/Redirect/Get: never leave a mutating Bratonien Tools action as the
// browser's current request. Otherwise a normal reload can submit the same
// action again (for example an update, delete or create action).
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && !empty($_SESSION['bratonien_tools_flash']) && is_array($_SESSION['bratonien_tools_flash']))
{
$flash = $_SESSION['bratonien_tools_flash'];
unset($_SESSION['bratonien_tools_flash']);
if (!empty($flash['messages']) && is_array($flash['messages']))
{
$messages = array_values($flash['messages']);
}
if (!empty($flash['errors']) && is_array($flash['errors']))
{
$errors = array_values($flash['errors']);
}
if (!empty($flash['self_update']) && is_array($flash['self_update']))
{
$self_update_override = $flash['self_update'];
}
if (!empty($flash['nc_piwigo_api_test']) && is_array($flash['nc_piwigo_api_test']))
{
$nc_piwigo_api_test = $flash['nc_piwigo_api_test'];
}
}
if (
$_SERVER['REQUEST_METHOD'] === 'POST'
&& empty($_POST)
@@ -59,6 +85,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bratonien_tool']))
$errors[] = $e->getMessage();
}
}
$_SESSION['bratonien_tools_flash'] = array(
'messages' => $messages,
'errors' => $errors,
'self_update' => $self_update_override,
'nc_piwigo_api_test' => $nc_piwigo_api_test,
);
$redirect_url = get_root_url().'admin.php?page=plugin-'.BRATONIEN_TOOLS_ID;
if (!headers_sent())
{
header('Location: '.$redirect_url, true, 303);
exit;
}
}
$watermark = bratonien_tools_get_watermark_data();

View File

@@ -16,13 +16,9 @@ function bratonien_tools_nc_connector_create_local_api_first()
'nc_activity_view' => 'Activity-View',
'nc_gallery_root' => 'Galerie-Pfad',
);
foreach ($required as $field => $label)
{
if (trim((string)($_POST[$field] ?? '')) === '')
{
throw new RuntimeException($label.' fehlt.');
}
if (trim((string)($_POST[$field] ?? '')) === '') throw new RuntimeException($label.' fehlt.');
}
$fallback_user = trim((string)($_POST['nc_piwigo_user'] ?? ''));
@@ -33,102 +29,67 @@ function bratonien_tools_nc_connector_create_local_api_first()
}
$api = bratonien_tools_nc_api_credentials();
if (($api['key_id'] ?? '') === '' && $fallback_user === '')
$validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1';
if (($api['key_id'] ?? '') === '' && $fallback_user === '' && !$validated_pending_api)
{
throw new RuntimeException('Es ist weder ein gespeicherter Piwigo-API-Zugang noch ein Benutzername/Passwort-Fallback vorhanden.');
throw new RuntimeException('Es ist weder ein geprüfter Piwigo-API-Zugang noch ein Benutzername/Passwort-Fallback vorhanden.');
}
$port = (int)($_POST['nc_port'] ?? 5432);
if ($port < 1 || $port > 65535)
{
throw new RuntimeException('Ungueltiger PostgreSQL-Port.');
}
if ($port < 1 || $port > 65535) throw new RuntimeException('Ungueltiger PostgreSQL-Port.');
$gallery_root = rtrim(trim((string)$_POST['nc_gallery_root']), '/');
if (strpos($gallery_root, './') === 0)
{
$piwigo_root = realpath(PHPWG_ROOT_PATH);
if ($piwigo_root === false)
{
throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgelöst werden.');
}
if ($piwigo_root === false) throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgelöst werden.');
$gallery_root = rtrim($piwigo_root, '/').'/'.ltrim(substr($gallery_root, 2), '/');
}
if ($gallery_root === '' || $gallery_root[0] !== '/')
{
throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
}
if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
$config = array(
'host' => trim((string)$_POST['nc_host']),
'port' => (string)$port,
'database' => trim((string)$_POST['nc_database']),
'user' => trim((string)$_POST['nc_user']),
'source_view' => trim((string)$_POST['nc_source_view']),
'activity_view' => trim((string)$_POST['nc_activity_view']),
'gallery_root' => $gallery_root,
'state_dir' => '',
'status_file' => '',
'quiet_seconds' => max(0, (int)($_POST['nc_quiet_seconds'] ?? 120)),
'max_wait_seconds' => max(60, (int)($_POST['nc_max_wait_seconds'] ?? 900)),
'full_sync_seconds' => max(300, (int)($_POST['nc_full_sync_seconds'] ?? 86400)),
'storages' => bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''),
'origin' => 'native',
'piwigo_auth' => 'api-first',
'host'=>trim((string)$_POST['nc_host']),
'port'=>(string)$port,
'database'=>trim((string)$_POST['nc_database']),
'user'=>trim((string)$_POST['nc_user']),
'source_view'=>trim((string)$_POST['nc_source_view']),
'activity_view'=>trim((string)$_POST['nc_activity_view']),
'gallery_root'=>$gallery_root,
'state_dir'=>'','status_file'=>'',
'quiet_seconds'=>max(0,(int)($_POST['nc_quiet_seconds'] ?? 120)),
'max_wait_seconds'=>max(60,(int)($_POST['nc_max_wait_seconds'] ?? 900)),
'full_sync_seconds'=>max(300,(int)($_POST['nc_full_sync_seconds'] ?? 86400)),
'storages'=>bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''),
'origin'=>'native','piwigo_auth'=>'api-first',
);
foreach (array(
'nextcloud_url' => 'nc_nextcloud_url',
'showcase_user' => 'nc_showcase_user',
'nextcloud_access_user' => 'nc_access_user',
'nextcloud_product' => 'nc_product',
'nextcloud_version' => 'nc_version',
) as $config_key => $post_key)
foreach (array('nextcloud_url'=>'nc_nextcloud_url','showcase_user'=>'nc_showcase_user','nextcloud_access_user'=>'nc_access_user','nextcloud_product'=>'nc_product','nextcloud_version'=>'nc_version') as $config_key=>$post_key)
{
$value = trim((string)($_POST[$post_key] ?? ''));
if ($value !== '')
{
$config[$config_key] = $value;
}
$value=trim((string)($_POST[$post_key] ?? '')); if($value!=='') $config[$config_key]=$value;
}
bratonien_tools_nc_connector_view_name($config['source_view']);
bratonien_tools_nc_connector_view_name($config['activity_view']);
$table = bratonien_tools_nc_connector_table();
bratonien_tools_nc_connector_ensure_table();
$now = date('Y-m-d H:i:s');
$connection_key = 'local-'.bin2hex(random_bytes(12));
$secret_blob = bratonien_tools_nc_connector_encrypt_credentials(
(string)$_POST['nc_db_password'],
$fallback_user,
$fallback_password
);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
$table=bratonien_tools_nc_connector_table(); bratonien_tools_nc_connector_ensure_table(); $now=date('Y-m-d H:i:s');
$connection_key='local-'.bin2hex(random_bytes(12));
$secret_blob=bratonien_tools_nc_connector_encrypt_credentials((string)$_POST['nc_db_password'],$fallback_user,$fallback_password);
$config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
if(!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
pwg_query("INSERT INTO `$table` (connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated) VALUES ('".pwg_db_real_escape_string($connection_key)."','".pwg_db_real_escape_string(trim((string)$_POST['nc_name']))."','local',0,'disabled','".pwg_db_real_escape_string($config_json)."','".pwg_db_real_escape_string($secret_blob)."','".pwg_db_real_escape_string($now)."','".pwg_db_real_escape_string($now)."')");
$id=(int)pwg_db_insert_id();
if($id<1) throw new RuntimeException('Die Connector-Verbindung konnte nicht eindeutig angelegt werden.');
$config['state_dir']='/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file']=$config['state_dir'].'/connector-status.json';
$config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
if(!is_string($config_json))
{
throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
throw new RuntimeException('Connector-Konfiguration konnte nach dem Anlegen nicht serialisiert werden.');
}
pwg_query("INSERT INTO `$table`
(connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated)
VALUES ('".pwg_db_real_escape_string($connection_key)."',
'".pwg_db_real_escape_string(trim((string)$_POST['nc_name']))."',
'local', 0, 'disabled',
'".pwg_db_real_escape_string($config_json)."',
'".pwg_db_real_escape_string($secret_blob)."',
'".pwg_db_real_escape_string($now)."',
'".pwg_db_real_escape_string($now)."')");
$id = (int)pwg_db_insert_id();
$config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id);
return array(
'message' => $fallback_user === ''
? 'Nextcloud-Verbindung wurde API-first ohne dauerhaft gespeicherten Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.'
: 'Nextcloud-Verbindung wurde API-first mit verschluesseltem Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.'
'connection_id'=>$id,
'message'=>$fallback_user==='' ? 'Nextcloud-Verbindung wurde API-first ohne dauerhaft gespeicherten Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.' : 'Nextcloud-Verbindung wurde API-first mit verschluesseltem Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.'
);
}

View File

@@ -7,304 +7,101 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_nc_connector_credentials_from_blob($blob)
{
$plain = bratonien_tools_nc_connector_decrypt_secret($blob);
if ($plain === '')
{
return array('db_password'=>'', 'piwigo_user'=>'', 'piwigo_password'=>'');
}
if ($plain === '') return array('db_password'=>'', 'piwigo_user'=>'', 'piwigo_password'=>'');
$decoded = json_decode($plain, true);
if (is_array($decoded) && isset($decoded['db_password']))
{
return array(
'db_password' => (string)($decoded['db_password'] ?? ''),
'piwigo_user' => (string)($decoded['piwigo_user'] ?? ''),
'piwigo_password' => (string)($decoded['piwigo_password'] ?? ''),
'db_password'=>(string)($decoded['db_password'] ?? ''),
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
);
}
// Backwards compatibility for migrated 0.14.x connections.
return array('db_password'=>$plain, 'piwigo_user'=>'', 'piwigo_password'=>'');
}
function bratonien_tools_nc_connector_encrypt_credentials($db_password, $piwigo_user, $piwigo_password)
{
$payload = json_encode(array(
'v' => 1,
'db_password' => (string)$db_password,
'piwigo_user' => (string)$piwigo_user,
'piwigo_password' => (string)$piwigo_password,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($payload))
{
throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
}
$payload=json_encode(array('v'=>1,'db_password'=>(string)$db_password,'piwigo_user'=>(string)$piwigo_user,'piwigo_password'=>(string)$piwigo_password),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
if(!is_string($payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
return bratonien_tools_nc_connector_encrypt_secret($payload);
}
function bratonien_tools_nc_connector_parse_storages($raw)
{
$storages = array();
$lines = preg_split('/\r\n|\r|\n/', trim((string)$raw));
foreach ($lines as $line)
$storages=array();
$lines=preg_split('/\r\n|\r|\n/',trim((string)$raw));
foreach($lines as $line)
{
$line = trim($line);
if ($line === '')
if(trim($line)==='') continue;
$parts=array_map('trim',explode('|',$line,3));
if(count($parts)!==3 || $parts[0]==='' || $parts[2]==='')
{
continue;
throw new RuntimeException('Eine Speicherzuordnung ist unvollständig. Benötigt werden Storage-ID und lokaler Speicherpfad.');
}
$parts = array_map('trim', explode('|', $line));
if (count($parts) !== 3 || $parts[0] === '' || $parts[1] === '' || $parts[2] === '')
{
throw new RuntimeException('Storage-Zeilen muessen das Format storage_id | source_prefix | local_mount verwenden.');
}
if ($parts[2][0] !== '/')
{
throw new RuntimeException('Der lokale Storage-Mount muss ein absoluter Pfad sein.');
}
$storages[] = array(
'storage_id' => $parts[0],
'source_prefix' => $parts[1],
'local_mount' => rtrim($parts[2], '/'),
if($parts[2][0]!=='/') throw new RuntimeException('Der lokale Speicherpfad muss ein absoluter Pfad sein.');
$storages[]=array(
'storage_id'=>$parts[0],
'source_prefix'=>trim($parts[1],'/'),
'local_mount'=>rtrim($parts[2],'/'),
);
}
if (!$storages)
{
throw new RuntimeException('Mindestens ein Storage muss konfiguriert werden.');
}
if(!$storages) throw new RuntimeException('Mindestens ein Speicherort muss zugeordnet werden.');
return $storages;
}
function bratonien_tools_nc_connector_create_local()
{
$required = array(
'nc_name' => 'Name',
'nc_host' => 'PostgreSQL-Host',
'nc_database' => 'Datenbank',
'nc_user' => 'Reader-Benutzer',
'nc_db_password' => 'Reader-Passwort',
'nc_source_view' => 'Source-View',
'nc_activity_view' => 'Activity-View',
'nc_gallery_root' => 'Galerie-Pfad',
'nc_piwigo_user' => 'Piwigo-Benutzer',
'nc_piwigo_password' => 'Piwigo-Passwort',
);
foreach ($required as $field => $label)
{
if (trim((string)($_POST[$field] ?? '')) === '')
{
throw new RuntimeException($label.' fehlt.');
}
}
$port = (int)($_POST['nc_port'] ?? 5432);
if ($port < 1 || $port > 65535)
{
throw new RuntimeException('Ungueltiger PostgreSQL-Port.');
}
$gallery_root = rtrim(trim((string)$_POST['nc_gallery_root']), '/');
if ($gallery_root === '' || $gallery_root[0] !== '/')
{
throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
}
$config = array(
'host' => trim((string)$_POST['nc_host']),
'port' => (string)$port,
'database' => trim((string)$_POST['nc_database']),
'user' => trim((string)$_POST['nc_user']),
'source_view' => trim((string)$_POST['nc_source_view']),
'activity_view' => trim((string)$_POST['nc_activity_view']),
'gallery_root' => $gallery_root,
'state_dir' => '',
'status_file' => '',
'quiet_seconds' => max(0, (int)($_POST['nc_quiet_seconds'] ?? 120)),
'max_wait_seconds' => max(60, (int)($_POST['nc_max_wait_seconds'] ?? 900)),
'full_sync_seconds' => max(300, (int)($_POST['nc_full_sync_seconds'] ?? 86400)),
'storages' => bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''),
'origin' => 'native',
);
bratonien_tools_nc_connector_view_name($config['source_view']);
bratonien_tools_nc_connector_view_name($config['activity_view']);
$table = bratonien_tools_nc_connector_table();
bratonien_tools_nc_connector_ensure_table();
$now = date('Y-m-d H:i:s');
$connection_key = 'local-'.bin2hex(random_bytes(12));
$secret_blob = bratonien_tools_nc_connector_encrypt_credentials(
(string)$_POST['nc_db_password'],
trim((string)$_POST['nc_piwigo_user']),
(string)$_POST['nc_piwigo_password']
);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
}
pwg_query("INSERT INTO `$table`
(connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated)
VALUES ('".pwg_db_real_escape_string($connection_key)."',
'".pwg_db_real_escape_string(trim((string)$_POST['nc_name']))."',
'local', 0, 'disabled',
'".pwg_db_real_escape_string($config_json)."',
'".pwg_db_real_escape_string($secret_blob)."',
'".pwg_db_real_escape_string($now)."',
'".pwg_db_real_escape_string($now)."')");
$id = (int)pwg_db_insert_id();
$config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id);
$required=array('nc_name'=>'Name','nc_host'=>'PostgreSQL-Host','nc_database'=>'Datenbank','nc_user'=>'Reader-Benutzer','nc_db_password'=>'Reader-Passwort','nc_source_view'=>'Source-View','nc_activity_view'=>'Activity-View','nc_gallery_root'=>'Galerie-Pfad','nc_piwigo_user'=>'Piwigo-Benutzer','nc_piwigo_password'=>'Piwigo-Passwort');
foreach($required as $field=>$label)if(trim((string)($_POST[$field]??''))==='')throw new RuntimeException($label.' fehlt.');
$port=(int)($_POST['nc_port']??5432);if($port<1||$port>65535)throw new RuntimeException('Ungueltiger PostgreSQL-Port.');
$gallery_root=rtrim(trim((string)$_POST['nc_gallery_root']),'/');if($gallery_root===''||$gallery_root[0]!=='/')throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
$config=array('host'=>trim((string)$_POST['nc_host']),'port'=>(string)$port,'database'=>trim((string)$_POST['nc_database']),'user'=>trim((string)$_POST['nc_user']),'source_view'=>trim((string)$_POST['nc_source_view']),'activity_view'=>trim((string)$_POST['nc_activity_view']),'gallery_root'=>$gallery_root,'state_dir'=>'','status_file'=>'','quiet_seconds'=>max(0,(int)($_POST['nc_quiet_seconds']??120)),'max_wait_seconds'=>max(60,(int)($_POST['nc_max_wait_seconds']??900)),'full_sync_seconds'=>max(300,(int)($_POST['nc_full_sync_seconds']??86400)),'storages'=>bratonien_tools_nc_connector_parse_storages($_POST['nc_storages']??''),'origin'=>'native');
bratonien_tools_nc_connector_view_name($config['source_view']);bratonien_tools_nc_connector_view_name($config['activity_view']);
$table=bratonien_tools_nc_connector_table();bratonien_tools_nc_connector_ensure_table();$now=date('Y-m-d H:i:s');$connection_key='local-'.bin2hex(random_bytes(12));$secret_blob=bratonien_tools_nc_connector_encrypt_credentials((string)$_POST['nc_db_password'],trim((string)$_POST['nc_piwigo_user']),(string)$_POST['nc_piwigo_password']);$config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);if(!is_string($config_json))throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
pwg_query("INSERT INTO `$table` (connection_key,name,adapter,enabled,takeover_state,config_json,secret_blob,created,updated) VALUES ('".pwg_db_real_escape_string($connection_key)."','".pwg_db_real_escape_string(trim((string)$_POST['nc_name']))."','local',0,'disabled','".pwg_db_real_escape_string($config_json)."','".pwg_db_real_escape_string($secret_blob)."','".pwg_db_real_escape_string($now)."','".pwg_db_real_escape_string($now)."')");
$id=(int)pwg_db_insert_id();$config['state_dir']='/var/lib/bratonien-tools/nc-connector/connection-'.$id;$config['status_file']=$config['state_dir'].'/connector-status.json';$config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id);
return array('message'=>'Nextcloud-Verbindung wurde angelegt. Bitte zuerst technisch pruefen und danach im LXC aktivieren.');
}
function bratonien_tools_nc_connector_verify_managed()
{
$id = isset($_POST['connection_id']) ? (int)$_POST['connection_id'] : 0;
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
$id=isset($_POST['connection_id'])?(int)$_POST['connection_id']:0;$connection=bratonien_tools_nc_connector_connection($id,true);if(!$connection)throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');if((string)$connection['adapter']!=='local')throw new RuntimeException('Diese Verifikation ist derzeit nur fuer lokale Connector-Verbindungen verfuegbar.');
$config=$connection['config'];foreach(array('host','port','database','user','source_view','activity_view') as $key)if(trim((string)($config[$key]??''))==='')throw new RuntimeException('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
$credentials=bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob']??'');if($credentials['db_password']==='')throw new RuntimeException('Fuer diese Verbindung ist kein Datenbankpasswort gespeichert.');
$checks=array();$ok=true;$source_count=null;
try{bratonien_tools_nc_connector_psql($config,$credentials['db_password'],'SELECT 1');$checks[]=array('name'=>'PostgreSQL-Verbindung','ok'=>true,'detail'=>'Reader-Anmeldung erfolgreich');}catch(Throwable $e){$checks[]=array('name'=>'PostgreSQL-Verbindung','ok'=>false,'detail'=>$e->getMessage());$ok=false;}
if($ok)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
if ((string)$connection['adapter'] !== 'local')
{
throw new RuntimeException('Diese Verifikation ist derzeit nur fuer lokale Connector-Verbindungen verfuegbar.');
try{$source_view=bratonien_tools_nc_connector_view_name($config['source_view']);$value=bratonien_tools_nc_connector_psql($config,$credentials['db_password'],'SELECT COUNT(*) FROM '.$source_view);if(!preg_match('/^\d+$/',$value))throw new RuntimeException('Source-View lieferte keinen gueltigen Zaehler.');$source_count=(int)$value;$checks[]=array('name'=>'Source-View','ok'=>true,'detail'=>$source_count.' Quelle(n) lesbar');}catch(Throwable $e){$checks[]=array('name'=>'Source-View','ok'=>false,'detail'=>$e->getMessage());$ok=false;}
try{$activity_view=bratonien_tools_nc_connector_view_name($config['activity_view']);bratonien_tools_nc_connector_psql($config,$credentials['db_password'],'SELECT 1 FROM '.$activity_view.' LIMIT 1');$checks[]=array('name'=>'Activity-View','ok'=>true,'detail'=>'View lesbar');}catch(Throwable $e){$checks[]=array('name'=>'Activity-View','ok'=>false,'detail'=>$e->getMessage());$ok=false;}
}
$storages=isset($config['storages'])&&is_array($config['storages'])?$config['storages']:array();$mount_points=bratonien_tools_nc_connector_mount_points();
if(!$storages){$checks[]=array('name'=>'Storage-Mounts','ok'=>false,'detail'=>'Keine Storage-Zuordnung gespeichert');$ok=false;}
else foreach($storages as $index=>$storage){$path=rtrim((string)($storage['local_mount']??''),'/');$exists=$path!==''&&is_dir($path);$readable=$exists&&is_readable($path);$mounted=$path!==''&&isset($mount_points[$path]);$storage_ok=$exists&&$readable&&$mounted;$storage_id=(string)($storage['storage_id']??('#'.($index+1)));$detail=$storage_id.' -> '.($path!==''?$path:'(kein Pfad)');if(!$exists)$detail.=' (Pfad fehlt)';elseif(!$readable)$detail.=' (nicht lesbar)';elseif(!$mounted)$detail.=' (kein aktiver Mount)';else$detail.=' (bereit)';$checks[]=array('name'=>'Storage '.($index+1),'ok'=>$storage_ok,'detail'=>$detail);if(!$storage_ok)$ok=false;}
$config['verification']=array('checked_at'=>date('Y-m-d H:i:s'),'ok'=>$ok,'source_count'=>$source_count,'checks'=>$checks);$table=bratonien_tools_nc_connector_table();$config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);if(!is_string($config_json))throw new RuntimeException('Verifikationsergebnis konnte nicht gespeichert werden.');$state=$ok?'verified':((string)$connection['takeover_state']==='disabled'?'disabled':'imported');$now=date('Y-m-d H:i:s');pwg_query("UPDATE `$table` SET takeover_state='".pwg_db_real_escape_string($state)."', enabled=0, config_json='".pwg_db_real_escape_string($config_json)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id);
if(!$ok)throw new RuntimeException('Connector-Verifikation ist fehlgeschlagen. Die Verbindung bleibt deaktiviert; Details stehen im NC Connector.');return array('message'=>'Connector-Verbindung wurde erfolgreich verifiziert. PostgreSQL, Views und Storage-Mounts sind erreichbar.');
}
$config = $connection['config'];
foreach (array('host','port','database','user','source_view','activity_view') as $key)
{
if (trim((string)($config[$key] ?? '')) === '')
{
throw new RuntimeException('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
}
}
$credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? '');
if ($credentials['db_password'] === '')
{
throw new RuntimeException('Fuer diese Verbindung ist kein Datenbankpasswort gespeichert.');
}
$checks = array();
$ok = true;
$source_count = null;
try
{
bratonien_tools_nc_connector_psql($config, $credentials['db_password'], 'SELECT 1');
$checks[] = array('name'=>'PostgreSQL-Verbindung', 'ok'=>true, 'detail'=>'Reader-Anmeldung erfolgreich');
}
catch (Throwable $e)
{
$checks[] = array('name'=>'PostgreSQL-Verbindung', 'ok'=>false, 'detail'=>$e->getMessage());
$ok = false;
}
if ($ok)
{
try
{
$source_view = bratonien_tools_nc_connector_view_name($config['source_view']);
$value = bratonien_tools_nc_connector_psql($config, $credentials['db_password'], 'SELECT COUNT(*) FROM '.$source_view);
if (!preg_match('/^\d+$/', $value))
{
throw new RuntimeException('Source-View lieferte keinen gueltigen Zaehler.');
}
$source_count = (int)$value;
$checks[] = array('name'=>'Source-View', 'ok'=>true, 'detail'=>$source_count.' Quelle(n) lesbar');
}
catch (Throwable $e)
{
$checks[] = array('name'=>'Source-View', 'ok'=>false, 'detail'=>$e->getMessage());
$ok = false;
}
try
{
$activity_view = bratonien_tools_nc_connector_view_name($config['activity_view']);
bratonien_tools_nc_connector_psql($config, $credentials['db_password'], 'SELECT 1 FROM '.$activity_view.' LIMIT 1');
$checks[] = array('name'=>'Activity-View', 'ok'=>true, 'detail'=>'View lesbar');
}
catch (Throwable $e)
{
$checks[] = array('name'=>'Activity-View', 'ok'=>false, 'detail'=>$e->getMessage());
$ok = false;
}
}
$storages = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array();
$mount_points = bratonien_tools_nc_connector_mount_points();
if (!$storages)
{
$checks[] = array('name'=>'Storage-Mounts', 'ok'=>false, 'detail'=>'Keine Storage-Zuordnung gespeichert');
$ok = false;
}
else
{
foreach ($storages as $index => $storage)
{
$path = rtrim((string)($storage['local_mount'] ?? ''), '/');
$exists = $path !== '' && is_dir($path);
$readable = $exists && is_readable($path);
$mounted = $path !== '' && isset($mount_points[$path]);
$storage_ok = $exists && $readable && $mounted;
$storage_id = (string)($storage['storage_id'] ?? ('#'.($index + 1)));
$detail = $storage_id.' -> '.($path !== '' ? $path : '(kein Pfad)');
if (!$exists) $detail .= ' (Pfad fehlt)';
elseif (!$readable) $detail .= ' (nicht lesbar)';
elseif (!$mounted) $detail .= ' (kein aktiver Mount)';
else $detail .= ' (bereit)';
$checks[] = array('name'=>'Storage '.($index + 1), 'ok'=>$storage_ok, 'detail'=>$detail);
if (!$storage_ok) $ok = false;
}
}
$config['verification'] = array(
'checked_at' => date('Y-m-d H:i:s'),
'ok' => $ok,
'source_count' => $source_count,
'checks' => $checks,
);
$table = bratonien_tools_nc_connector_table();
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Verifikationsergebnis konnte nicht gespeichert werden.');
}
$state = $ok ? 'verified' : ((string)$connection['takeover_state'] === 'disabled' ? 'disabled' : 'imported');
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET takeover_state='".pwg_db_real_escape_string($state)."', enabled=0, config_json='".pwg_db_real_escape_string($config_json)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id);
if (!$ok)
{
throw new RuntimeException('Connector-Verifikation ist fehlgeschlagen. Die Verbindung bleibt deaktiviert; Details stehen im NC Connector.');
}
return array('message'=>'Connector-Verbindung wurde erfolgreich verifiziert. PostgreSQL, Views und Storage-Mounts sind erreichbar.');
function bratonien_tools_nc_connector_remove_tree($path)
{
$path=rtrim((string)$path,'/');if($path===''||$path==='/'||!file_exists($path))return true;
if(is_link($path)||is_file($path))return @unlink($path);
$items=@scandir($path);if(!is_array($items))return false;
foreach($items as $item){if($item==='.'||$item==='..')continue;if(!bratonien_tools_nc_connector_remove_tree($path.'/'.$item))return false;}
return @rmdir($path);
}
function bratonien_tools_nc_connector_delete()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection)
$id=(int)($_POST['connection_id']??0);$connection=bratonien_tools_nc_connector_connection($id,false);if(!$connection)throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');if(!empty($connection['enabled'])||(string)$connection['takeover_state']==='active')throw new RuntimeException('Eine aktive Verbindung kann nicht geloescht werden. Sie muss zuerst deaktiviert werden.');
$config=is_array($connection['config'])?$connection['config']:array();
$public_status=rtrim(PHPWG_ROOT_PATH,'/').'/_data/bratonien-tools/nc-connector-status/connection-'.$id.'.json';if(is_file($public_status))@unlink($public_status);
$state_dir=rtrim((string)($config['state_dir']??''),'/');
if($state_dir!==''&&strpos($state_dir,'/var/lib/bratonien-tools/nc-connector/connection-'.$id)===0&&file_exists($state_dir))
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
bratonien_tools_nc_connector_remove_tree($state_dir);
}
if (!empty($connection['enabled']) || (string)$connection['takeover_state'] === 'active')
{
throw new RuntimeException('Eine aktive Verbindung kann nicht geloescht werden. Sie muss zuerst deaktiviert werden.');
}
$table = bratonien_tools_nc_connector_table();
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
return array('message'=>'Connector-Verbindung wurde geloescht.');
$table=bratonien_tools_nc_connector_table();pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
return array('message'=>'Connector-Verbindung und erreichbare Connector-eigene Statusdaten wurden geloescht. Nextcloud- und Piwigo-Bilder blieben unverändert.');
}

View File

@@ -6,689 +6,372 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_nc_wizard_state()
{
$state = isset($_SESSION['bratonien_nc_wizard']) && is_array($_SESSION['bratonien_nc_wizard'])
? $_SESSION['bratonien_nc_wizard']
: array();
$state = isset($_SESSION['bratonien_nc_wizard']) && is_array($_SESSION['bratonien_nc_wizard']) ? $_SESSION['bratonien_nc_wizard'] : array();
return array_merge(array(
'step' => 1,
'scan_ok' => false,
'base_url' => '',
'host_input' => '',
'username' => '',
'display_name' => '',
'version' => '',
'product' => 'Nextcloud',
'users' => array(),
'can_list_users' => false,
'showcase_user' => '',
'connection_name' => '',
'scan_message' => '',
'technical_source' => '',
'technical_complete' => false,
'db_host' => '',
'db_port' => '5432',
'db_database' => 'nextcloud',
'db_user' => '',
'db_password_set' => false,
'source_view' => 'piwigo_showcase_sources',
'activity_view' => 'piwigo_showcase_activity',
'gallery_root' => '',
'storages' => array(),
'storage_candidates' => array(),
'api_status' => 'pending',
'api_username' => '',
'api_error' => '',
'step'=>1,'scan_ok'=>false,'base_url'=>'','host_input'=>'','username'=>'','display_name'=>'','version'=>'','product'=>'Nextcloud',
'users'=>array(),'can_list_users'=>false,'showcase_user'=>'','connection_name'=>'','scan_message'=>'',
'technical_stage'=>'reader_credentials','technical_source'=>'Noch nicht geprüft','technical_error'=>'','technical_complete'=>false,
'db_host'=>'','db_port'=>'5432','db_database'=>'nextcloud','db_user'=>'','db_password_set'=>false,
'source_view'=>'piwigo_showcase_sources','activity_view'=>'piwigo_showcase_activity','gallery_root'=>'','storages'=>array(),'storage_candidates'=>array(),
'api_status'=>'pending','api_username'=>'','api_error'=>'','_password'=>'','_db_password'=>'','_api_key_id'=>'','_api_key_secret'=>'',
'_fallback_user'=>'','_fallback_password'=>'',
), $state);
}
function bratonien_tools_nc_wizard_store(array $state)
{
$_SESSION['bratonien_nc_wizard'] = $state;
}
function bratonien_tools_nc_wizard_reset()
{
unset($_SESSION['bratonien_nc_wizard']);
return array('message'=>'Verbindungsassistent wurde zurückgesetzt.');
}
function bratonien_tools_nc_wizard_store(array $state) { $_SESSION['bratonien_nc_wizard'] = $state; }
function bratonien_tools_nc_wizard_reset() { unset($_SESSION['bratonien_nc_wizard']); return array('message'=>'Verbindungsassistent wurde zurückgesetzt.'); }
function bratonien_tools_nc_wizard_normalize_url($host)
{
$host = trim((string)$host);
if ($host === '')
{
throw new RuntimeException('Nextcloud-Host fehlt.');
}
if (!preg_match('#^https?://#i', $host))
{
$host = 'https://'.$host;
}
$parts = parse_url($host);
if (!is_array($parts) || empty($parts['host']))
{
throw new RuntimeException('Nextcloud-Host ist ungültig.');
}
$scheme = strtolower((string)($parts['scheme'] ?? 'https'));
if (!in_array($scheme, array('http','https'), true))
{
throw new RuntimeException('Nextcloud muss per HTTP oder HTTPS erreichbar sein.');
}
$url = $scheme.'://'.$parts['host'];
if (!empty($parts['port']))
{
$url .= ':'.(int)$parts['port'];
}
if (!empty($parts['path']) && $parts['path'] !== '/')
{
$url .= '/'.trim($parts['path'], '/');
}
return rtrim($url, '/');
$host=trim((string)$host);
if($host==='') throw new RuntimeException('Nextcloud-Adresse fehlt.');
if(!preg_match('#^https?://#i',$host)) $host='https://'.$host;
$parts=parse_url($host);
if(!is_array($parts)||empty($parts['host'])) throw new RuntimeException('Die Nextcloud-Adresse ist ungültig.');
$scheme=strtolower((string)($parts['scheme']??'https'));
if(!in_array($scheme,array('http','https'),true)) throw new RuntimeException('Nextcloud muss per HTTP oder HTTPS erreichbar sein.');
$url=$scheme.'://'.$parts['host'];
if(!empty($parts['port'])) $url.=':'.(int)$parts['port'];
if(!empty($parts['path'])&&$parts['path']!=='/') $url.='/'.trim($parts['path'],'/');
return rtrim($url,'/');
}
function bratonien_tools_nc_wizard_http($url, $username = '', $password = '', array $headers = array())
function bratonien_tools_nc_wizard_candidate_urls($host)
{
if (!function_exists('curl_init'))
{
throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der Nextcloud-Scan kann nicht ausgeführt werden.');
}
$host=trim((string)$host);
if($host==='') throw new RuntimeException('Bitte die Adresse der Nextcloud angeben.');
if(preg_match('#^https?://#i',$host)) return array(bratonien_tools_nc_wizard_normalize_url($host));
return array(bratonien_tools_nc_wizard_normalize_url('https://'.$host),bratonien_tools_nc_wizard_normalize_url('http://'.$host));
}
$ch = curl_init($url);
$http_headers = array_merge(array('Accept: application/json'), $headers);
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => $http_headers,
CURLOPT_USERAGENT => 'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
function bratonien_tools_nc_wizard_http($url,$username='',$password='',array $headers=array())
{
if(!function_exists('curl_init')) throw new RuntimeException('Der Server kann Nextcloud derzeit nicht per HTTP prüfen.');
$ch=curl_init($url);
$options=array(
CURLOPT_RETURNTRANSFER=>true,CURLOPT_FOLLOWLOCATION=>true,CURLOPT_MAXREDIRS=>3,CURLOPT_CONNECTTIMEOUT=>8,CURLOPT_TIMEOUT=>15,
CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'),$headers),
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version')?bratonien_tools_current_version():'dev'),
);
if ($username !== '')
{
$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
$options[CURLOPT_USERPWD] = $username.':'.$password;
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $errno !== 0)
{
throw new RuntimeException('Nextcloud konnte nicht erreicht werden: '.$error);
}
return array('status'=>$status, 'body'=>(string)$body);
if($username!==''){$options[CURLOPT_HTTPAUTH]=CURLAUTH_BASIC;$options[CURLOPT_USERPWD]=$username.':'.$password;}
curl_setopt_array($ch,$options);
$body=curl_exec($ch);$errno=curl_errno($ch);$status=(int)curl_getinfo($ch,CURLINFO_HTTP_CODE);curl_close($ch);
if($body===false||$errno!==0) throw new RuntimeException('Verbindung fehlgeschlagen.');
return array('status'=>$status,'body'=>(string)$body);
}
function bratonien_tools_nc_wizard_ocs_data($body)
{
$decoded = json_decode((string)$body, true);
if (!is_array($decoded))
$decoded=json_decode((string)$body,true);
if(!is_array($decoded)||!isset($decoded['ocs'])||!is_array($decoded['ocs'])) throw new RuntimeException('Nextcloud hat keine gültige Antwort geliefert.');
$meta=isset($decoded['ocs']['meta'])&&is_array($decoded['ocs']['meta'])?$decoded['ocs']['meta']:array();
$status=strtolower(trim((string)($meta['status']??'')));$status_code=isset($meta['statuscode'])?(int)$meta['statuscode']:0;
if($status!=='ok'&&!in_array($status_code,array(100,200),true))
{
throw new RuntimeException('Nextcloud hat keine gültige JSON-Antwort geliefert.');
}
if (isset($decoded['ocs']['meta']['statuscode']) && (int)$decoded['ocs']['meta']['statuscode'] !== 100)
{
$message = (string)($decoded['ocs']['meta']['message'] ?? 'Nextcloud hat die Anfrage abgelehnt.');
$message=trim((string)($meta['message']??''));
if($message===''||strtolower($message)==='ok') $message='Nextcloud hat die Anfrage abgelehnt.';
throw new RuntimeException($message);
}
return isset($decoded['ocs']['data']) && is_array($decoded['ocs']['data']) ? $decoded['ocs']['data'] : array();
return isset($decoded['ocs']['data'])&&is_array($decoded['ocs']['data'])?$decoded['ocs']['data']:array();
}
function bratonien_tools_nc_wizard_fill_profile(array &$state)
{
$url_host = strtolower((string)parse_url($state['base_url'], PHP_URL_HOST));
$state['db_host'] = $url_host;
$state['db_port'] = '5432';
$state['db_database'] = 'nextcloud';
$state['source_view'] = 'piwigo_showcase_sources';
$state['activity_view'] = 'piwigo_showcase_activity';
$state['gallery_root'] = rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud';
$state['technical_source'] = 'Standardwerte vorbereitet';
$url_host=strtolower((string)parse_url($state['base_url'],PHP_URL_HOST));
$state['db_host']='';
$state['db_port']='5432';
$state['db_database']='nextcloud';
$state['source_view']='piwigo_showcase_sources';
$state['activity_view']='piwigo_showcase_activity';
$state['gallery_root']=rtrim(PHPWG_ROOT_PATH,'/').'/galleries/nextcloud';
$state['technical_stage']='reader_credentials';
$state['technical_source']='Noch nicht geprüft';
$state['technical_error']='';
$state['technical_complete']=false;
foreach (bratonien_tools_nc_connector_connections() as $candidate)
foreach(bratonien_tools_nc_connector_connections() as $candidate)
{
$config = isset($candidate['config']) && is_array($candidate['config']) ? $candidate['config'] : array();
$candidate_url = trim((string)($config['nextcloud_url'] ?? ''));
$candidate_host = strtolower(trim((string)($config['host'] ?? '')));
$match = false;
$config=isset($candidate['config'])&&is_array($candidate['config'])?$candidate['config']:array();
$candidate_url=trim((string)($config['nextcloud_url']??''));
$match=false;
if($candidate_url!=='')
{
try{$match=bratonien_tools_nc_wizard_normalize_url($candidate_url)===$state['base_url'];}catch(Throwable $ignored){$match=false;}
}
if(!$match) continue;
if ($candidate_url !== '')
{
try
{
$match = bratonien_tools_nc_wizard_normalize_url($candidate_url) === $state['base_url'];
}
catch (Throwable $ignored)
{
$match = false;
}
}
if (!$match && $candidate_host !== '' && $candidate_host === $url_host)
{
$match = true;
}
if (!$match)
{
continue;
}
$full = bratonien_tools_nc_connector_connection((int)$candidate['id'], true);
if (!$full)
{
continue;
}
$credentials = bratonien_tools_nc_connector_credentials_from_blob($full['secret_blob'] ?? '');
$state['db_host'] = (string)($config['host'] ?? $state['db_host']);
$state['db_port'] = (string)($config['port'] ?? $state['db_port']);
$state['db_database'] = (string)($config['database'] ?? $state['db_database']);
$state['db_user'] = (string)($config['user'] ?? '');
$state['_db_password'] = (string)($credentials['db_password'] ?? '');
$state['db_password_set'] = $state['_db_password'] !== '';
$state['source_view'] = (string)($config['source_view'] ?? $state['source_view']);
$state['activity_view'] = (string)($config['activity_view'] ?? $state['activity_view']);
$state['gallery_root'] = (string)($config['gallery_root'] ?? $state['gallery_root']);
$state['storages'] = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array();
$state['technical_source'] = 'Passende vorhandene Connector-Konfiguration erkannt';
$full=bratonien_tools_nc_connector_connection((int)$candidate['id'],true);
if(!$full) continue;
$credentials=bratonien_tools_nc_connector_credentials_from_blob($full['secret_blob']??'');
$state['db_host']=(string)($config['host']??'');
$state['db_port']=(string)($config['port']??'5432');
$state['db_database']=(string)($config['database']??'nextcloud');
$state['db_user']=(string)($config['user']??'');
$state['_db_password']=(string)($credentials['db_password']??'');
$state['db_password_set']=$state['_db_password']!=='';
$state['source_view']=(string)($config['source_view']??$state['source_view']);
$state['activity_view']=(string)($config['activity_view']??$state['activity_view']);
$state['gallery_root']=(string)($config['gallery_root']??$state['gallery_root']);
$state['storages']=isset($config['storages'])&&is_array($config['storages'])?$config['storages']:array();
$state['storage_candidates']=$state['storages'];
$state['technical_complete']=$state['db_host']!==''&&$state['db_user']!==''&&$state['db_password_set']&&!empty($state['storages']);
$state['technical_stage']=$state['technical_complete']?'ready':'reader_credentials';
$state['technical_source']='Passende vorhandene Verbindung übernommen';
break;
}
$state['technical_complete'] = $state['db_host'] !== ''
&& $state['db_port'] !== ''
&& $state['db_database'] !== ''
&& $state['db_user'] !== ''
&& !empty($state['db_password_set'])
&& $state['source_view'] !== ''
&& $state['activity_view'] !== ''
&& $state['gallery_root'] !== ''
&& !empty($state['storages']);
if($state['db_host']==='') $state['db_host']=$url_host;
}
function bratonien_tools_nc_wizard_scan()
{
$host_input = trim((string)($_POST['nc_wizard_host'] ?? ''));
$username = trim((string)($_POST['nc_wizard_user'] ?? ''));
$password = (string)($_POST['nc_wizard_password'] ?? '');
if ($username === '' || $password === '')
{
throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
}
$state=bratonien_tools_nc_wizard_state();
$host_input=trim((string)($_POST['nc_wizard_host']??$state['host_input']));
$username=trim((string)($_POST['nc_wizard_user']??$state['username']));
$password=array_key_exists('nc_wizard_password',$_POST)?(string)$_POST['nc_wizard_password']:(string)$state['_password'];
$state['step']=1;$state['host_input']=$host_input;$state['username']=$username;$state['_password']=$password;bratonien_tools_nc_wizard_store($state);
if($username===''||$password==='') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
$base_url = bratonien_tools_nc_wizard_normalize_url($host_input);
$status_response = bratonien_tools_nc_wizard_http($base_url.'/status.php');
if ($status_response['status'] < 200 || $status_response['status'] >= 300)
$base_url='';$status_data=null;
foreach(bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
{
throw new RuntimeException('Unter diesem Host wurde keine erreichbare Nextcloud-Instanz erkannt (HTTP '.$status_response['status'].').');
}
$status_data = json_decode($status_response['body'], true);
if (!is_array($status_data) || empty($status_data['installed']))
{
throw new RuntimeException('Der Host antwortet, wurde aber nicht als installierte Nextcloud-Instanz erkannt.');
try
{
$response=bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if($response['status']<200||$response['status']>=300) continue;
$candidate_status=json_decode($response['body'],true);
if(!is_array($candidate_status)||empty($candidate_status['installed'])) continue;
$base_url=$candidate_url;$status_data=$candidate_status;break;
}
catch(Throwable $ignored){}
}
if($base_url===''||!is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
$user_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/user?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($user_response['status'] === 401 || $user_response['status'] === 403)
{
throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
}
if ($user_response['status'] < 200 || $user_response['status'] >= 300)
{
throw new RuntimeException('Nextcloud-Benutzerprüfung ist fehlgeschlagen (HTTP '.$user_response['status'].').');
}
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$user_response=bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/user?format=json',$username,$password,array('OCS-APIRequest: true'));
if($user_response['status']===401||$user_response['status']===403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
if($user_response['status']<200||$user_response['status']>=300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
$user_data=bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$users = array();
$can_list_users = false;
$users=array();$can_list_users=false;
try
{
$users_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/users?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($users_response['status'] >= 200 && $users_response['status'] < 300)
$users_response=bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/users?format=json',$username,$password,array('OCS-APIRequest: true'));
if($users_response['status']>=200&&$users_response['status']<300)
{
$users_data = bratonien_tools_nc_wizard_ocs_data($users_response['body']);
if (isset($users_data['users']) && is_array($users_data['users']))
$users_data=bratonien_tools_nc_wizard_ocs_data($users_response['body']);
if(isset($users_data['users'])&&is_array($users_data['users']))
{
$users = array_values(array_filter(array_map('strval', $users_data['users'])));
sort($users, SORT_NATURAL | SORT_FLAG_CASE);
$can_list_users = true;
$users=array_values(array_filter(array_map('strval',$users_data['users'])));sort($users,SORT_NATURAL|SORT_FLAG_CASE);$can_list_users=true;
}
}
}
catch (Throwable $ignored)
catch(Throwable $ignored){}
$url_host=(string)parse_url($base_url,PHP_URL_HOST);
$state=array_merge($state,array(
'step'=>2,'scan_ok'=>true,'base_url'=>$base_url,'host_input'=>$host_input,
'username'=>(string)($user_data['id']??$username),'display_name'=>(string)($user_data['display-name']??$user_data['displayname']??''),
'version'=>(string)($status_data['versionstring']??$status_data['version']??''),'product'=>(string)($status_data['productname']??'Nextcloud'),
'users'=>$users,'can_list_users'=>$can_list_users,'showcase_user'=>'','connection_name'=>$url_host!==''?$url_host:'Nextcloud',
'scan_message'=>'Nextcloud wurde erkannt und der Benutzerzugriff wurde bestätigt.','_password'=>$password,'api_status'=>'pending','api_username'=>'','api_error'=>''
));
bratonien_tools_nc_wizard_fill_profile($state);bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud wurde gefunden und der Zugang bestätigt.');
}
function bratonien_tools_nc_wizard_finish_data_access(array &$state)
{
$config=array('host'=>$state['db_host'],'port'=>$state['db_port'],'database'=>$state['db_database'],'user'=>$state['db_user']);
$password=(string)$state['_db_password'];
bratonien_tools_nc_connector_psql($config,$password,'SELECT 1');
$source_view=bratonien_tools_nc_connector_view_name($state['source_view']);
$activity_view=bratonien_tools_nc_connector_view_name($state['activity_view']);
bratonien_tools_nc_connector_psql($config,$password,'SELECT COUNT(*) FROM '.$source_view);
bratonien_tools_nc_connector_psql($config,$password,'SELECT 1 FROM '.$activity_view.' LIMIT 1');
$rows=bratonien_tools_nc_connector_psql($config,$password,"SELECT DISTINCT storage_id::text || E'\\t' || COALESCE(source_path, '') FROM ".$source_view." ORDER BY 1");
$candidates=array();
foreach(preg_split('/\r\n|\r|\n/',trim($rows)) as $line)
{
$users = array();
$can_list_users = false;
if($line==='') continue;
$parts=explode("\t",$line,2);
if(count($parts)!==2) continue;
$storage_id=trim($parts[0]);$source_path=trim($parts[1],'/');
if($storage_id==='') continue;
$prefix=$source_path;
if(strpos($source_path,'/')!==false) $prefix=substr($source_path,0,strpos($source_path,'/'));
if(!isset($candidates[$storage_id])) $candidates[$storage_id]=array('storage_id'=>$storage_id,'source_prefix'=>$prefix,'local_mount'=>'');
}
if(!$candidates) throw new RuntimeException('Die Verbindung zur Datenquelle funktioniert, aber es wurden noch keine freigegebenen Speicherorte gefunden.');
$url_host = (string)parse_url($base_url, PHP_URL_HOST);
$state = array(
'step' => 2,
'scan_ok' => true,
'base_url' => $base_url,
'host_input' => $host_input,
'username' => (string)($user_data['id'] ?? $username),
'display_name' => (string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''),
'version' => (string)($status_data['versionstring'] ?? $status_data['version'] ?? ''),
'product' => (string)($status_data['productname'] ?? 'Nextcloud'),
'users' => $users,
'can_list_users' => $can_list_users,
'showcase_user' => '',
'connection_name' => $url_host !== '' ? $url_host : 'Nextcloud',
'scan_message' => 'Nextcloud wurde erkannt und der Benutzerzugriff wurde bestätigt.',
'_password' => $password,
'api_status' => 'pending',
'api_username' => '',
'api_error' => '',
);
bratonien_tools_nc_wizard_fill_profile($state);
bratonien_tools_nc_wizard_store($state);
$known=array();
foreach(bratonien_tools_nc_connector_connections() as $connection)
{
foreach(($connection['config']['storages']??array()) as $storage)
{
$key=(string)($storage['storage_id']??'').'|'.trim((string)($storage['source_prefix']??''),'/');
if($key!=='|') $known[$key]=(string)($storage['local_mount']??'');
}
}
foreach($candidates as &$candidate)
{
$key=$candidate['storage_id'].'|'.trim($candidate['source_prefix'],'/');
if(!empty($known[$key])&&is_dir($known[$key])&&is_readable($known[$key])) $candidate['local_mount']=$known[$key];
}
unset($candidate);
return array('message'=>'Nextcloud-Scan erfolgreich. Erkannte Werte wurden übernommen; fehlende Angaben werden jetzt gezielt abgefragt.');
$state['storage_candidates']=array_values($candidates);
$state['storages']=array_values(array_filter($state['storage_candidates'],function($storage){return trim((string)($storage['local_mount']??''))!=='';}));
$state['technical_error']='';
$state['technical_source']='Datenzugriff erfolgreich geprüft';
$state['technical_complete']=count($state['storages'])===count($state['storage_candidates']);
$state['technical_stage']=$state['technical_complete']?'ready':'mounts';
}
function bratonien_tools_nc_wizard_save_technical()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok']))
$state=bratonien_tools_nc_wizard_state();
if(empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
$state['connection_name']=trim((string)($_POST['nc_wizard_connection_name']??$state['connection_name']));
$state['db_user']=trim((string)($_POST['nc_wizard_db_user']??$state['db_user']));
$db_password=array_key_exists('nc_wizard_db_password',$_POST)?(string)$_POST['nc_wizard_db_password']:(string)$state['_db_password'];
if($db_password!==''){$state['_db_password']=$db_password;$state['db_password_set']=true;}
$advanced=isset($_POST['nc_wizard_db_host'])||$state['technical_stage']==='database_details';
if($advanced)
{
throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
$state['db_host']=trim((string)($_POST['nc_wizard_db_host']??$state['db_host']));
$state['db_port']=(string)max(1,min(65535,(int)($_POST['nc_wizard_db_port']??$state['db_port'])));
$state['db_database']=trim((string)($_POST['nc_wizard_db_database']??$state['db_database']));
}
else
{
$state['db_host']=(string)parse_url($state['base_url'],PHP_URL_HOST);
$state['db_port']='5432';
$state['db_database']='nextcloud';
}
$state['connection_name'] = trim((string)($_POST['nc_wizard_connection_name'] ?? $state['connection_name']));
$state['db_host'] = trim((string)($_POST['nc_wizard_db_host'] ?? $state['db_host']));
$state['db_port'] = (string)max(1, min(65535, (int)($_POST['nc_wizard_db_port'] ?? $state['db_port'])));
$state['db_database'] = trim((string)($_POST['nc_wizard_db_database'] ?? $state['db_database']));
$state['db_user'] = trim((string)($_POST['nc_wizard_db_user'] ?? $state['db_user']));
$db_password = (string)($_POST['nc_wizard_db_password'] ?? '');
if ($db_password !== '')
{
$state['_db_password'] = $db_password;
$state['db_password_set'] = true;
}
if ($state['connection_name'] === '' || $state['db_host'] === '' || $state['db_database'] === '' || $state['db_user'] === '' || empty($state['db_password_set']))
{
bratonien_tools_nc_wizard_store($state);
throw new RuntimeException('Die noch fehlenden Datenbankangaben müssen vollständig angegeben werden.');
}
$config = array(
'host'=>$state['db_host'],
'port'=>$state['db_port'],
'database'=>$state['db_database'],
'user'=>$state['db_user'],
);
$password = (string)($state['_db_password'] ?? '');
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1');
$source_view = bratonien_tools_nc_connector_view_name($state['source_view']);
$activity_view = bratonien_tools_nc_connector_view_name($state['activity_view']);
bratonien_tools_nc_connector_psql($config, $password, 'SELECT COUNT(*) FROM '.$source_view);
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1 FROM '.$activity_view.' LIMIT 1');
$rows = bratonien_tools_nc_connector_psql(
$config,
$password,
"SELECT DISTINCT storage_id::text || E'\\t' || source_path FROM ".$source_view." ORDER BY 1"
);
$candidates = array();
foreach (preg_split('/\r\n|\r|\n/', trim($rows)) as $line)
{
if ($line === '') continue;
$parts = explode("\t", $line, 2);
if (count($parts) !== 2) continue;
$storage_id = trim($parts[0]);
$source_path = trim($parts[1], '/');
$prefix = $source_path;
if (strpos($source_path, '/') !== false)
{
$prefix = substr($source_path, 0, strpos($source_path, '/'));
}
if ($storage_id === '') continue;
if (!isset($candidates[$storage_id]))
{
$candidates[$storage_id] = array('storage_id'=>$storage_id, 'source_prefix'=>$prefix, 'local_mount'=>'');
}
}
if (!$candidates)
{
throw new RuntimeException('Die Source-View ist erreichbar, liefert aber keine Storage-Zuordnung.');
}
$known = array();
foreach (bratonien_tools_nc_connector_connections() as $connection)
{
foreach (($connection['config']['storages'] ?? array()) as $storage)
{
$key = (string)($storage['storage_id'] ?? '').'|'.(string)($storage['source_prefix'] ?? '');
if ($key !== '|') $known[$key] = (string)($storage['local_mount'] ?? '');
}
}
foreach ($candidates as &$candidate)
{
$key = $candidate['storage_id'].'|'.$candidate['source_prefix'];
if (!empty($known[$key]))
{
$candidate['local_mount'] = $known[$key];
}
}
unset($candidate);
$state['storage_candidates'] = array_values($candidates);
$state['storages'] = array_values(array_filter($state['storage_candidates'], function($storage)
{
return trim((string)($storage['local_mount'] ?? '')) !== '';
}));
$state['technical_source'] = 'Datenbank und Views erfolgreich geprüft';
$state['technical_complete'] = count($state['storages']) === count($state['storage_candidates']);
bratonien_tools_nc_wizard_store($state);
return array('message'=>$state['technical_complete']
? 'Technische Verbindung wurde automatisch vervollständigt.'
: 'Datenbank und Views wurden erkannt. Für nicht erkannte Storages wird nur noch der lokale Mount abgefragt.');
}
function bratonien_tools_nc_wizard_save_mounts()
{
$state = bratonien_tools_nc_wizard_state();
$candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array();
$mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array();
if (!$candidates)
{
throw new RuntimeException('Es wurden noch keine Storages erkannt.');
}
$storages = array();
foreach ($candidates as $index => $candidate)
{
$mount = trim((string)($candidate['local_mount'] ?? ''));
if ($mount === '')
{
$mount = trim((string)($mounts[$index] ?? ''));
}
if ($mount === '' || $mount[0] !== '/')
{
throw new RuntimeException('Für Storage '.(string)$candidate['storage_id'].' fehlt ein absoluter lokaler Mount-Pfad.');
}
$mount = rtrim($mount, '/');
if (!is_dir($mount) || !is_readable($mount))
{
throw new RuntimeException('Storage-Mount '.$mount.' ist nicht vorhanden oder nicht lesbar.');
}
$candidate['local_mount'] = $mount;
$storages[] = $candidate;
}
$state['storages'] = $storages;
$state['storage_candidates'] = $storages;
$state['technical_complete'] = true;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Storage-Zuordnung ist vollständig.');
}
function bratonien_tools_nc_wizard_select_user()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok']))
{
throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
}
if (empty($state['technical_complete']))
{
throw new RuntimeException('Die automatische technische Erkennung ist noch nicht vollständig. Bitte nur die noch fehlenden Angaben ergänzen.');
}
$showcase_user = trim((string)($_POST['nc_wizard_showcase_user'] ?? ''));
if ($showcase_user === '')
{
throw new RuntimeException('Bitte einen Nextcloud-Benutzer für die Showcase-Freigaben auswählen.');
}
$connection_name = trim((string)($_POST['nc_wizard_connection_name'] ?? $state['connection_name']));
if ($connection_name === '')
{
throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.');
}
$state['connection_name'] = $connection_name;
$state['showcase_user'] = $showcase_user;
$state['step'] = 3;
$state['api_error'] = '';
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Showcase-Benutzer übernommen. Jetzt folgt der Piwigo-API-Zugang.');
}
function bratonien_tools_nc_wizard_api_test()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 3)
{
throw new RuntimeException('Der API-Test ist in diesem Assistentenschritt nicht verfügbar.');
}
$key_id = trim((string)($_POST['nc_wizard_api_key_id'] ?? ''));
$secret = trim((string)($_POST['nc_wizard_api_key_secret'] ?? ''));
if ($key_id === '' && $secret === '')
{
$stored = bratonien_tools_nc_api_credentials();
$key_id = trim((string)($stored['key_id'] ?? ''));
$secret = trim((string)($stored['key_secret'] ?? ''));
}
if ($key_id === '' || $secret === '')
{
throw new RuntimeException('API-Schlüssel-ID und API-Geheimnis fehlen.');
}
if($state['db_user']===''||empty($state['db_password_set'])) throw new RuntimeException('Für den Datenzugriff werden Lese-Benutzer und Passwort benötigt.');
if($state['db_host']===''||$state['db_database']==='') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.');
try
{
$status = bratonien_tools_nc_connector_piwigo_api_request($key_id, $secret, 'pwg.session.getStatus');
$user_status = strtolower((string)($status['status'] ?? ''));
if (!in_array($user_status, array('admin','webmaster'), true))
{
throw new RuntimeException('Der API-Key gehört keinem Piwigo-Administrator/Webmaster.');
}
$method_result = bratonien_tools_nc_connector_piwigo_api_request($key_id, $secret, 'reflection.getMethodList');
$method_map = array();
bratonien_tools_nc_connector_collect_method_names($method_result, $method_map);
$missing = array_values(array_diff(array('bratonien.nc.syncProductive','bratonien.nc.syncOrphans'), array_keys($method_map)));
if ($missing)
{
throw new RuntimeException('Benötigte Bratonien-Sync-Methoden fehlen: '.implode(', ', $missing).'.');
}
bratonien_tools_nc_api_credentials_store($key_id, $secret);
$state['api_status'] = 'ok';
$state['api_username'] = (string)($status['username'] ?? $status['user'] ?? '');
$state['api_error'] = '';
$state['step'] = 4;
bratonien_tools_nc_wizard_finish_data_access($state);
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Piwigo-API erfolgreich geprüft. Zum Abschluss kann jetzt der Fallback festgelegt werden.');
return array('message'=>$state['technical_complete']?'Datenzugriff wurde erfolgreich geprüft.':'Datenzugriff funktioniert. Ein Speicherort muss noch bestätigt werden.');
}
catch (Throwable $e)
catch(Throwable $e)
{
$state['api_status'] = 'error';
$state['api_error'] = $e->getMessage();
$state['technical_error']=$e->getMessage();
if(!$advanced)
{
$state['technical_stage']='database_details';
bratonien_tools_nc_wizard_store($state);
throw new RuntimeException('Der übliche Datenbankzugang konnte nicht automatisch bestätigt werden. Bitte die Datenbank-Adresse prüfen.');
}
bratonien_tools_nc_wizard_store($state);
throw $e;
}
}
function bratonien_tools_nc_wizard_save_mounts()
{
$state=bratonien_tools_nc_wizard_state();
$candidates=is_array($state['storage_candidates'])?$state['storage_candidates']:array();
$mounts=isset($_POST['nc_wizard_storage_mount'])&&is_array($_POST['nc_wizard_storage_mount'])?$_POST['nc_wizard_storage_mount']:array();
if(!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte erkannt.');
$storages=array();
foreach($candidates as $index=>$candidate)
{
$mount=trim((string)($candidate['local_mount']??''));
if($mount==='') $mount=trim((string)($mounts[$index]??''));
if($mount===''||$mount[0]!=='/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
$mount=rtrim($mount,'/');
if(!is_dir($mount)||!is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount);
$candidate['local_mount']=$mount;$storages[]=$candidate;
}
$state['storages']=$storages;$state['storage_candidates']=$storages;$state['technical_complete']=true;$state['technical_stage']='ready';$state['technical_error']='';bratonien_tools_nc_wizard_store($state);
return array('message'=>'Speicherzuordnung wurde erfolgreich geprüft.');
}
function bratonien_tools_nc_wizard_select_user()
{
$state=bratonien_tools_nc_wizard_state();
if(empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
if(empty($state['technical_complete'])) throw new RuntimeException('Die Verbindung ist noch nicht vollständig geprüft.');
$showcase_user=trim((string)($_POST['nc_wizard_showcase_user']??''));
$connection_name=trim((string)($_POST['nc_wizard_connection_name']??$state['connection_name']));
if($showcase_user==='') throw new RuntimeException('Bitte den Benutzer auswählen, der die Showcase-Freigaben bereitstellt.');
if($connection_name==='') throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.');
$state['connection_name']=$connection_name;$state['showcase_user']=$showcase_user;$state['step']=3;$state['api_error']='';bratonien_tools_nc_wizard_store($state);
return array('message'=>'Showcase-Benutzer übernommen. Jetzt folgt der Piwigo-API-Zugang.');
}
function bratonien_tools_nc_wizard_api_test()
{
$state=bratonien_tools_nc_wizard_state();
if((int)$state['step']!==3) throw new RuntimeException('Der API-Test ist in diesem Assistentenschritt nicht verfügbar.');
$posted_id=trim((string)($_POST['nc_wizard_api_key_id']??''));$posted_secret=trim((string)($_POST['nc_wizard_api_key_secret']??''));
if($posted_id!=='')$state['_api_key_id']=$posted_id;if($posted_secret!=='')$state['_api_key_secret']=$posted_secret;bratonien_tools_nc_wizard_store($state);
$key_id=trim((string)$state['_api_key_id']);$secret=trim((string)$state['_api_key_secret']);
if($key_id===''&&$secret===''){$stored=bratonien_tools_nc_api_credentials();$key_id=trim((string)($stored['key_id']??''));$secret=trim((string)($stored['key_secret']??''));}
if($key_id===''||$secret==='') throw new RuntimeException('API-Schlüssel-ID und API-Geheimnis fehlen.');
try
{
$status=bratonien_tools_nc_connector_piwigo_api_request($key_id,$secret,'pwg.session.getStatus');
$user_status=strtolower((string)($status['status']??''));
if(!in_array($user_status,array('admin','webmaster'),true)) throw new RuntimeException('Der API-Key gehört keinem Piwigo-Administrator/Webmaster.');
$method_result=bratonien_tools_nc_connector_piwigo_api_request($key_id,$secret,'reflection.getMethodList');$method_map=array();bratonien_tools_nc_connector_collect_method_names($method_result,$method_map);
$missing=array_values(array_diff(array('bratonien.nc.syncProductive','bratonien.nc.syncOrphans'),array_keys($method_map)));
if($missing) throw new RuntimeException('Benötigte Bratonien-Sync-Methoden fehlen: '.implode(', ',$missing).'.');
$state['_api_key_id']=$key_id;$state['_api_key_secret']=$secret;$state['api_status']='ok';$state['api_username']=(string)($status['username']??$status['user']??'');$state['api_error']='';$state['step']=4;bratonien_tools_nc_wizard_store($state);
return array('message'=>'Piwigo-API erfolgreich geprüft. Gespeichert wird sie erst beim Abschluss des Assistenten.');
}
catch(Throwable $e){$state['api_status']='error';$state['api_error']=$e->getMessage();bratonien_tools_nc_wizard_store($state);throw $e;}
}
function bratonien_tools_nc_wizard_api_skip()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 3)
{
throw new RuntimeException('Die API kann in diesem Assistentenschritt nicht übersprungen werden.');
}
$state['api_status'] = 'skipped';
$state['api_error'] = '';
$state['step'] = 4;
bratonien_tools_nc_wizard_store($state);
$state=bratonien_tools_nc_wizard_state();if((int)$state['step']!==3)throw new RuntimeException('Die API kann in diesem Assistentenschritt nicht übersprungen werden.');
$state['api_status']='skipped';$state['api_error']='';$state['step']=4;bratonien_tools_nc_wizard_store($state);
return array('message'=>'Piwigo-API wurde übersprungen. Für diese Verbindung ist deshalb ein Fallback-Zugang erforderlich.');
}
function bratonien_tools_nc_wizard_finish()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '')
{
throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
}
$state=bratonien_tools_nc_wizard_state();
if((int)$state['step']!==4||empty($state['technical_complete'])||trim((string)$state['showcase_user'])==='') throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
if(array_key_exists('nc_wizard_fallback_user',$_POST)) $state['_fallback_user']=trim((string)$_POST['nc_wizard_fallback_user']);
if(array_key_exists('nc_wizard_fallback_password',$_POST)) $state['_fallback_password']=(string)$_POST['nc_wizard_fallback_password'];
bratonien_tools_nc_wizard_store($state);
$fallback_user=trim((string)$state['_fallback_user']);$fallback_password=(string)$state['_fallback_password'];
if(($fallback_user==='')!==($fallback_password==='')) throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.');
if($state['api_status']!=='ok'&&$fallback_user==='') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist ein Fallback-Zugang erforderlich.');
$fallback_user = trim((string)($_POST['nc_wizard_fallback_user'] ?? ''));
$fallback_password = (string)($_POST['nc_wizard_fallback_password'] ?? '');
if (($fallback_user === '') !== ($fallback_password === ''))
$storage_lines=array();
foreach($state['storages'] as $storage)
{
throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.');
$storage_lines[]=(string)$storage['storage_id'].' | '.trim((string)$storage['source_prefix'],'/').' | '.(string)$storage['local_mount'];
}
if ($state['api_status'] !== 'ok' && $fallback_user === '')
{
throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist ein Fallback-Zugang erforderlich.');
}
$storage_lines = array();
foreach ($state['storages'] as $storage)
{
$storage_lines[] = (string)$storage['storage_id'].' | '.(string)$storage['source_prefix'].' | '.(string)$storage['local_mount'];
}
$_POST['nc_name'] = (string)$state['connection_name'];
$_POST['nc_host'] = (string)$state['db_host'];
$_POST['nc_port'] = (string)$state['db_port'];
$_POST['nc_database'] = (string)$state['db_database'];
$_POST['nc_user'] = (string)$state['db_user'];
$_POST['nc_db_password'] = (string)($state['_db_password'] ?? '');
$_POST['nc_source_view'] = (string)$state['source_view'];
$_POST['nc_activity_view'] = (string)$state['activity_view'];
$_POST['nc_gallery_root'] = (string)$state['gallery_root'];
$_POST['nc_storages'] = implode("\n", $storage_lines);
$_POST['nc_quiet_seconds'] = '120';
$_POST['nc_max_wait_seconds'] = '900';
$_POST['nc_full_sync_seconds'] = '86400';
$_POST['nc_piwigo_user'] = $fallback_user;
$_POST['nc_piwigo_password'] = $fallback_password;
$_POST['nc_nextcloud_url'] = (string)$state['base_url'];
$_POST['nc_showcase_user'] = (string)$state['showcase_user'];
$_POST['nc_access_user'] = (string)$state['username'];
$_POST['nc_product'] = (string)$state['product'];
$_POST['nc_version'] = (string)$state['version'];
$result = bratonien_tools_nc_connector_create_local_api_first();
unset($_SESSION['bratonien_nc_wizard']);
$result['message'] = 'Verbindung wurde vollständig durch den Assistenten angelegt. '.$result['message'];
return $result;
$_POST['nc_name']=(string)$state['connection_name'];$_POST['nc_host']=(string)$state['db_host'];$_POST['nc_port']=(string)$state['db_port'];$_POST['nc_database']=(string)$state['db_database'];$_POST['nc_user']=(string)$state['db_user'];$_POST['nc_db_password']=(string)$state['_db_password'];$_POST['nc_source_view']=(string)$state['source_view'];$_POST['nc_activity_view']=(string)$state['activity_view'];$_POST['nc_gallery_root']=(string)$state['gallery_root'];$_POST['nc_storages']=implode("\n",$storage_lines);$_POST['nc_quiet_seconds']='120';$_POST['nc_max_wait_seconds']='900';$_POST['nc_full_sync_seconds']='86400';$_POST['nc_piwigo_user']=$fallback_user;$_POST['nc_piwigo_password']=$fallback_password;$_POST['nc_nextcloud_url']=(string)$state['base_url'];$_POST['nc_showcase_user']=(string)$state['showcase_user'];$_POST['nc_access_user']=(string)$state['username'];$_POST['nc_product']=(string)$state['product'];$_POST['nc_version']=(string)$state['version'];$_POST['nc_api_validated']=$state['api_status']==='ok'?'1':'0';
$result=bratonien_tools_nc_connector_create_local_api_first();$connection_id=(int)($result['connection_id']??0);
try{if($state['api_status']==='ok')bratonien_tools_nc_api_credentials_store((string)$state['_api_key_id'],(string)$state['_api_key_secret']);}
catch(Throwable $e){if($connection_id>0){$table=bratonien_tools_nc_connector_table();pwg_query("DELETE FROM `$table` WHERE id=".$connection_id." LIMIT 1");}throw new RuntimeException('Die Verbindung wurde nicht übernommen, weil der geprüfte API-Zugang nicht gespeichert werden konnte: '.$e->getMessage());}
unset($_SESSION['bratonien_nc_wizard']);unset($result['connection_id']);$result['message']='Verbindung wurde vollständig durch den Assistenten angelegt. '.$result['message'];return $result;
}
function bratonien_tools_nc_connector_update_name()
{
$id = (int)($_POST['connection_id'] ?? 0);
$name = trim((string)($_POST['connection_name'] ?? ''));
if ($id < 1 || $name === '')
{
throw new RuntimeException('Verbindung oder Name fehlt.');
}
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Verbindungsname wurde aktualisiert.');
$id=(int)($_POST['connection_id']??0);$name=trim((string)($_POST['connection_name']??''));if($id<1||$name==='')throw new RuntimeException('Verbindung oder Name fehlt.');$connection=bratonien_tools_nc_connector_connection($id,false);if(!$connection)throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');$table=bratonien_tools_nc_connector_table();$now=date('Y-m-d H:i:s');pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");return array('message'=>'Verbindungsname wurde aktualisiert.');
}
function bratonien_tools_nc_connector_update_technical()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
if (!empty($connection['enabled']) || (string)$connection['takeover_state'] === 'active')
{
throw new RuntimeException('Technische Einstellungen einer aktiven Verbindung können nicht geändert werden. Bitte zuerst deaktivieren.');
}
$config = $connection['config'];
$port = (int)($_POST['nc_port'] ?? ($config['port'] ?? 5432));
if ($port < 1 || $port > 65535)
{
throw new RuntimeException('Ungültiger PostgreSQL-Port.');
}
$gallery_root = rtrim(trim((string)($_POST['nc_gallery_root'] ?? ($config['gallery_root'] ?? ''))), '/');
if ($gallery_root === '' || $gallery_root[0] !== '/')
{
throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
}
$config['host'] = trim((string)($_POST['nc_host'] ?? ($config['host'] ?? '')));
$config['port'] = (string)$port;
$config['database'] = trim((string)($_POST['nc_database'] ?? ($config['database'] ?? '')));
$config['user'] = trim((string)($_POST['nc_user'] ?? ($config['user'] ?? '')));
$config['source_view'] = trim((string)($_POST['nc_source_view'] ?? ($config['source_view'] ?? '')));
$config['activity_view'] = trim((string)($_POST['nc_activity_view'] ?? ($config['activity_view'] ?? '')));
$config['gallery_root'] = $gallery_root;
$config['quiet_seconds'] = max(0, (int)($_POST['nc_quiet_seconds'] ?? ($config['quiet_seconds'] ?? 120)));
$config['max_wait_seconds'] = max(60, (int)($_POST['nc_max_wait_seconds'] ?? ($config['max_wait_seconds'] ?? 900)));
$config['full_sync_seconds'] = max(300, (int)($_POST['nc_full_sync_seconds'] ?? ($config['full_sync_seconds'] ?? 86400)));
$config['storages'] = bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? '');
unset($config['verification']);
bratonien_tools_nc_connector_view_name($config['source_view']);
bratonien_tools_nc_connector_view_name($config['activity_view']);
$credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? '');
$db_password = (string)($_POST['nc_db_password'] ?? '');
if ($db_password === '')
{
$db_password = $credentials['db_password'];
}
$secret_blob = bratonien_tools_nc_connector_encrypt_credentials(
$db_password,
$credentials['piwigo_user'],
$credentials['piwigo_password']
);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
}
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET takeover_state='disabled', enabled=0, config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Technische Verbindungseinstellungen wurden gespeichert. Die Verbindung muss erneut geprüft werden.');
$id=(int)($_POST['connection_id']??0);$connection=bratonien_tools_nc_connector_connection($id,true);if(!$connection)throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');if(!empty($connection['enabled'])||(string)$connection['takeover_state']==='active')throw new RuntimeException('Technische Einstellungen einer aktiven Verbindung können nicht geändert werden. Bitte zuerst deaktivieren.');
$config=$connection['config'];$port=(int)($_POST['nc_port']??($config['port']??5432));if($port<1||$port>65535)throw new RuntimeException('Ungültiger PostgreSQL-Port.');$gallery_root=rtrim(trim((string)($_POST['nc_gallery_root']??($config['gallery_root']??''))),'/');if($gallery_root===''||$gallery_root[0]!=='/')throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
$config['host']=trim((string)($_POST['nc_host']??($config['host']??'')));$config['port']=(string)$port;$config['database']=trim((string)($_POST['nc_database']??($config['database']??'')));$config['user']=trim((string)($_POST['nc_user']??($config['user']??'')));$config['source_view']=trim((string)($_POST['nc_source_view']??($config['source_view']??'')));$config['activity_view']=trim((string)($_POST['nc_activity_view']??($config['activity_view']??'')));$config['gallery_root']=$gallery_root;$config['quiet_seconds']=max(0,(int)($_POST['nc_quiet_seconds']??($config['quiet_seconds']??120)));$config['max_wait_seconds']=max(60,(int)($_POST['nc_max_wait_seconds']??($config['max_wait_seconds']??900)));$config['full_sync_seconds']=max(300,(int)($_POST['nc_full_sync_seconds']??($config['full_sync_seconds']??86400)));$config['storages']=bratonien_tools_nc_connector_parse_storages($_POST['nc_storages']??'');unset($config['verification']);bratonien_tools_nc_connector_view_name($config['source_view']);bratonien_tools_nc_connector_view_name($config['activity_view']);
$credentials=bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob']??'');$db_password=(string)($_POST['nc_db_password']??'');if($db_password==='')$db_password=$credentials['db_password'];if($db_password==='')throw new RuntimeException('Datenbankpasswort fehlt.');$secret_blob=bratonien_tools_nc_connector_encrypt_credentials($db_password,$credentials['piwigo_user'],$credentials['piwigo_password']);$config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);if(!is_string($config_json))throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');$table=bratonien_tools_nc_connector_table();$now=date('Y-m-d H:i:s');pwg_query("UPDATE `$table` SET takeover_state='disabled', enabled=0, config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");return array('message'=>'Technische Verbindungseinstellungen wurden gespeichert. Die Verbindung muss erneut geprüft werden.');
}

View File

@@ -1,7 +1,7 @@
<?php
/*
Plugin Name: Bratonien Tools
Version: 0.9.3.36
Version: 0.9.4.2
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien

View File

@@ -34,31 +34,18 @@ function fail($message)
function readKeyValueFile($path)
{
if (!is_readable($path))
{
fail('Konfiguration nicht lesbar: '.$path);
}
if (!is_readable($path)) fail('Konfiguration nicht lesbar: '.$path);
$result = array();
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line)
{
$line = trim($line);
if ($line === '' || $line[0] === '#')
{
continue;
}
if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches))
{
continue;
}
if ($line === '' || $line[0] === '#') continue;
if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches)) continue;
$value = trim($matches[2]);
if (strlen($value) >= 2)
{
$first = $value[0];
$last = $value[strlen($value)-1];
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'"))
{
$value = substr($value, 1, -1);
}
$first = $value[0]; $last = $value[strlen($value)-1];
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) $value = substr($value, 1, -1);
}
$result[$matches[1]] = $value;
}
@@ -67,49 +54,42 @@ function readKeyValueFile($path)
function runCommand(array $command, $allowFailure = false)
{
$spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$spec = array(0=>array('file','/dev/null','r'),1=>array('pipe','w'),2=>array('pipe','w'));
$process = proc_open($command, $spec, $pipes);
if (!is_resource($process))
{
fail('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
if (!is_resource($process)) fail('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
$stdout = stream_get_contents($pipes[1]); $stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]); fclose($pipes[2]);
$exit = proc_close($process);
if ($exit !== 0 && !$allowFailure)
{
$detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
fail('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : ''));
}
return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr);
return array('exit'=>$exit,'stdout'=>(string)$stdout,'stderr'=>(string)$stderr);
}
function decryptConnectorSecret($blob, $hexKey)
{
if (!preg_match('/^[a-f0-9]{64}$/', $hexKey))
{
fail('Connector-Schluessel ist ungueltig.');
}
if (!preg_match('/^[a-f0-9]{64}$/', $hexKey)) fail('Connector-Schluessel ist ungueltig.');
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1)
{
fail('Connector-Zugangsdaten haben ein unbekanntes Format.');
}
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) fail('Connector-Zugangsdaten haben ein unbekanntes Format.');
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false || $plain === '')
if ($plain === false || $plain === '') fail('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
$decoded = json_decode($plain, true);
if (is_array($decoded) && array_key_exists('db_password', $decoded))
{
fail('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
$password = (string)$decoded['db_password'];
if ($password === '') fail('Datenbankpasswort fehlt in den Connector-Zugangsdaten.');
return $password;
}
// Backward compatibility for older imported connections that stored only
// the database password as plaintext inside the encrypted envelope.
return (string)$plain;
}
@@ -121,16 +101,10 @@ function sqlEscape(mysqli $db, $value)
function saveTakeoverResult(mysqli $db, $table, $connectionId, array $config, $state, $enabled)
{
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json))
{
fail('Connector-Status konnte nicht serialisiert werden.');
}
if (!is_string($json)) fail('Connector-Status konnte nicht serialisiert werden.');
$now = date('Y-m-d H:i:s');
$sql = "UPDATE `".$table."` SET takeover_state='".sqlEscape($db, $state)."', enabled=".($enabled ? 1 : 0).", config_json='".sqlEscape($db, $json)."', updated='".sqlEscape($db, $now)."' WHERE id=".(int)$connectionId;
if (!$db->query($sql))
{
fail('Connector-Status konnte nicht gespeichert werden: '.$db->error);
}
if (!$db->query($sql)) fail('Connector-Status konnte nicht gespeichert werden: '.$db->error);
}
$legacyTimerWasEnabled = false;
@@ -141,84 +115,43 @@ $config = array();
try
{
if (!is_readable($dbConfig))
{
fail('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig);
}
if (!is_executable($legacyRuntime))
{
fail('Bestehende Sync-Runtime fehlt: '.$legacyRuntime);
}
if (!is_readable($dbConfig)) fail('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig);
if (!is_executable($legacyRuntime)) fail('Bestehende Sync-Runtime fehlt: '.$legacyRuntime);
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key]))
{
fail('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.');
}
}
$conf = array(); $prefixeTable = 'piwigo_'; require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.');
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno)
{
fail('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
}
if ($db->connect_errno) fail('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$result = $db->query("SELECT id, takeover_state, enabled, config_json, secret_blob FROM `".$table."` WHERE id=".$connectionId." LIMIT 1");
if (!$result || !$result->num_rows)
{
fail('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.');
}
if (!$result || !$result->num_rows) fail('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.');
$row = $result->fetch_assoc();
if ((string)$row['takeover_state'] !== 'ready' || (int)$row['enabled'] !== 0)
{
fail('Connector-Verbindung muss im Zustand ready und deaktiviert sein.');
}
if ((string)$row['takeover_state'] !== 'ready' || (int)$row['enabled'] !== 0) fail('Connector-Verbindung muss im Zustand ready und deaktiviert sein.');
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config) || empty($config['verification']['ok']))
{
fail('Connector-Verbindung besitzt keine erfolgreiche Verifikation.');
}
if (!is_array($config) || empty($config['verification']['ok'])) fail('Connector-Verbindung besitzt keine erfolgreiche Verifikation.');
$keyResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$keyResult || !$keyResult->num_rows)
{
fail('Connector-Schluessel wurde in Piwigo nicht gefunden.');
}
if (!$keyResult || !$keyResult->num_rows) fail('Connector-Schluessel wurde in Piwigo nicht gefunden.');
$keyRow = $keyResult->fetch_assoc();
$dbPassword = decryptConnectorSecret($row['secret_blob'], (string)$keyRow['value']);
$legacy = readKeyValueFile($legacyConfig);
$piwigoUser = trim((string)($legacy['PIWIGO_SYNC_USER'] ?? ''));
$piwigoPasswordFile = trim((string)($legacy['PIWIGO_SYNC_PASSWORD_FILE'] ?? '/etc/piwigo-sync/piwigo-password'));
if ($piwigoUser === '' || !is_readable($piwigoPasswordFile))
{
fail('Legacy-Piwigo-Sync-Zugangsdaten konnten fuer den einmaligen Cutover nicht gelesen werden.');
}
if ($piwigoUser === '' || !is_readable($piwigoPasswordFile)) fail('Legacy-Piwigo-Sync-Zugangsdaten konnten fuer den einmaligen Cutover nicht gelesen werden.');
$piwigoPassword = trim((string)file_get_contents($piwigoPasswordFile));
if ($piwigoPassword === '')
{
fail('Legacy-Piwigo-Sync-Passwort ist leer.');
}
if ($piwigoPassword === '') fail('Legacy-Piwigo-Sync-Passwort ist leer.');
foreach (array('host','port','database','user','source_view','gallery_root','state_dir') as $key)
{
if (!isset($config[$key]) || trim((string)$config[$key]) === '')
{
fail('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
}
if (!isset($config[$key]) || trim((string)$config[$key]) === '') fail('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
}
$runtimeDir = '/etc/bratonien-tools/nc-connector';
if (!is_dir($runtimeDir) && !mkdir($runtimeDir, 0700, true))
{
fail('Connector-Laufzeitverzeichnis konnte nicht angelegt werden.');
}
if (!is_dir($runtimeDir) && !mkdir($runtimeDir, 0700, true)) fail('Connector-Laufzeitverzeichnis konnte nicht angelegt werden.');
chmod($runtimeDir, 0700);
$base = $runtimeDir.'/connection-'.$connectionId;
@@ -230,16 +163,14 @@ try
file_put_contents($dbPasswordPath, $dbPassword."\n", LOCK_EX);
file_put_contents($piwigoPasswordPath, $piwigoPassword."\n", LOCK_EX);
chmod($dbPasswordPath, 0600);
chmod($piwigoPasswordPath, 0600);
chmod($dbPasswordPath, 0600); chmod($piwigoPasswordPath, 0600);
$storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount');
foreach ((array)($config['storages'] ?? array()) as $storage)
{
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t".(string)($storage['source_prefix'] ?? '')."\t".(string)($storage['local_mount'] ?? '');
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t".trim((string)($storage['source_prefix'] ?? ''), '/')."\t".(string)($storage['local_mount'] ?? '');
}
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX);
chmod($storagePath, 0600);
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); chmod($storagePath, 0600);
$piwigoRootConfigured = isset($legacy['PIWIGO_ROOT']) && trim((string)$legacy['PIWIGO_ROOT']) !== '' ? trim((string)$legacy['PIWIGO_ROOT']) : $piwigoRoot;
$lines = array(
@@ -261,8 +192,7 @@ try
'PIWIGO_SYNC_USER='.$piwigoUser,
'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath,
);
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
chmod($configPath, 0600);
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); chmod($configPath, 0600);
$enabledCheck = runCommand(array('systemctl', 'is-enabled', $legacyTimer), true);
$legacyTimerWasEnabled = $enabledCheck['exit'] === 0;
@@ -274,14 +204,8 @@ try
do
{
$active = runCommand(array('systemctl', 'is-active', '--quiet', 'piwigo-sync.service'), true);
if ($active['exit'] !== 0)
{
break;
}
if (time() >= $deadline)
{
fail('Ein laufender Legacy-Sync wurde nach 120 Sekunden nicht beendet.');
}
if ($active['exit'] !== 0) break;
if (time() >= $deadline) fail('Ein laufender Legacy-Sync wurde nach 120 Sekunden nicht beendet.');
sleep(2);
}
while (true);
@@ -295,9 +219,7 @@ try
fail('Erster Connector-Lauf ist technisch fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.'));
}
$runResult = 'no_changes';
$statusState = '';
$statusMessage = '';
$runResult = 'no_changes'; $statusState = ''; $statusMessage = '';
if (is_readable($statusPath))
{
$status = json_decode((string)file_get_contents($statusPath), true);
@@ -306,16 +228,9 @@ try
$statusState = trim((string)($status['state'] ?? ''));
$statusMessage = trim((string)($status['message'] ?? ''));
}
$legacyNoChangeMessage = 'Synchronisierung fehlgeschlagen; bestehende Galerie blieb unverändert';
if ($statusState === 'error' && $statusMessage !== $legacyNoChangeMessage)
{
fail('Erster Connector-Lauf meldete einen technischen Fehler'.($statusMessage !== '' ? ': '.$statusMessage : '.'));
}
if ($statusState === 'ok')
{
$runResult = 'changed';
}
if ($statusState === 'error' && $statusMessage !== $legacyNoChangeMessage) fail('Erster Connector-Lauf meldete einen technischen Fehler'.($statusMessage !== '' ? ': '.$statusMessage : '.'));
if ($statusState === 'ok') $runResult = 'changed';
elseif ($statusState === 'error' && $statusMessage === $legacyNoChangeMessage)
{
$runResult = 'no_changes';
@@ -331,28 +246,18 @@ try
$timerPath = '/etc/systemd/system/'.$newTimer;
$service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=PIWIGO_CONFIG=".$configPath."\nExecStart=".$legacyRuntime."\n\n";
$timer = "[Unit]\nDescription=Bratonien NC Connector regelmaessig pruefen\n\n[Timer]\nOnBootSec=3min\nOnUnitActiveSec=1min\nRandomizedDelaySec=15s\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n";
file_put_contents($servicePath, $service, LOCK_EX);
file_put_contents($timerPath, $timer, LOCK_EX);
chmod($servicePath, 0644);
chmod($timerPath, 0644);
$newTimerInstalled = true;
file_put_contents($servicePath, $service, LOCK_EX); file_put_contents($timerPath, $timer, LOCK_EX); chmod($servicePath,0644); chmod($timerPath,0644); $newTimerInstalled=true;
runCommand(array('systemctl', 'daemon-reload'));
runCommand(array('systemctl', 'disable', $legacyTimer), true);
runCommand(array('systemctl', 'enable', '--now', $newTimer));
runCommand(array('systemctl','daemon-reload'));
runCommand(array('systemctl','disable',$legacyTimer),true);
runCommand(array('systemctl','enable','--now',$newTimer));
$config['takeover']['cutover_at'] = date('Y-m-d H:i:s');
$config['takeover']['legacy_timer_disabled'] = true;
$config['takeover']['connector_timer'] = $newTimer;
$config['takeover']['runtime'] = 'legacy-runtime-transition';
$config['takeover']['first_run'] = array(
'state' => 'success',
'result' => $runResult,
'status_state' => $statusState,
'status_message' => $statusMessage,
'checked_at' => date('Y-m-d H:i:s'),
);
saveTakeoverResult($db, $table, $connectionId, $config, 'active', true);
$config['takeover']['cutover_at']=date('Y-m-d H:i:s');
$config['takeover']['legacy_timer_disabled']=true;
$config['takeover']['connector_timer']=$newTimer;
$config['takeover']['runtime']='legacy-runtime-transition';
$config['takeover']['first_run']=array('state'=>'success','result'=>$runResult,'status_state'=>$statusState,'status_message'=>$statusMessage,'checked_at'=>date('Y-m-d H:i:s'));
saveTakeoverResult($db,$table,$connectionId,$config,'active',true);
echo "Cutover erfolgreich.\n";
echo "Erster Connector-Lauf: ".$runResult."\n";
@@ -365,32 +270,14 @@ catch (Throwable $e)
{
try
{
$config['takeover']['first_run'] = array(
'state' => 'error',
'result' => 'error',
'checked_at' => date('Y-m-d H:i:s'),
'message' => substr($e->getMessage(), 0, 500),
);
saveTakeoverResult($db, $table, $connectionId, $config, 'ready', false);
}
catch (Throwable $ignored)
{
$config['takeover']['first_run']=array('state'=>'error','result'=>'error','checked_at'=>date('Y-m-d H:i:s'),'message'=>substr($e->getMessage(),0,500));
saveTakeoverResult($db,$table,$connectionId,$config,'ready',false);
}
catch (Throwable $ignored){}
}
if ($newTimerInstalled)
{
runCommand(array('systemctl', 'disable', '--now', $newTimer), true);
}
if ($legacyTimerWasEnabled)
{
runCommand(array('systemctl', 'enable', '--now', $legacyTimer), true);
}
else
{
runCommand(array('systemctl', 'start', $legacyTimer), true);
}
if ($newTimerInstalled) runCommand(array('systemctl','disable','--now',$newTimer),true);
if ($legacyTimerWasEnabled) runCommand(array('systemctl','enable','--now',$legacyTimer),true);
else runCommand(array('systemctl','start',$legacyTimer),true);
fwrite(STDERR, "Cutover fehlgeschlagen: ".$e->getMessage()."\n");
fwrite(STDERR, "Legacy-Timer wurde wieder aktiviert/gestartet.\n");
exit(1);

View File

@@ -25,82 +25,57 @@ $stateRoot = '/var/lib/bratonien-tools/nc-connector';
$servicePath = '/etc/systemd/system/bratonien-nc-connector.service';
$timerPath = '/etc/systemd/system/bratonien-nc-connector.timer';
function fail_install($message)
{
throw new RuntimeException($message);
}
function fail_install($message) { throw new RuntimeException($message); }
function run_install(array $command, $allowFailure = false)
{
$spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$spec = array(0=>array('file','/dev/null','r'),1=>array('pipe','w'),2=>array('pipe','w'));
$process = proc_open($command, $spec, $pipes);
if (!is_resource($process))
{
fail_install('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
}
if (!is_resource($process)) fail_install('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
fclose($pipes[1]); fclose($pipes[2]);
$exit = proc_close($process);
if ($exit !== 0 && !$allowFailure)
{
$detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
fail_install('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : ''));
}
return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr);
return array('exit'=>$exit,'stdout'=>(string)$stdout,'stderr'=>(string)$stderr);
}
function decrypt_install_credentials($blob, $hexKey)
{
if (!preg_match('/^[a-f0-9]{64}$/', $hexKey))
{
fail_install('Connector-Schluessel ist ungueltig.');
}
if (!preg_match('/^[a-f0-9]{64}$/', $hexKey)) fail_install('Connector-Schluessel ist ungueltig.');
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1)
{
fail_install('Connector-Zugangsdaten haben ein unbekanntes Format.');
}
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) fail_install('Connector-Zugangsdaten haben ein unbekanntes Format.');
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false || $plain === '')
{
fail_install('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
}
if ($plain === false || $plain === '') fail_install('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
$decoded = json_decode($plain, true);
if (!is_array($decoded) || empty($decoded['db_password']) || empty($decoded['piwigo_user']) || empty($decoded['piwigo_password']))
if (!is_array($decoded))
{
fail_install('Fuer eine native Aktivierung muessen Datenbank- und Piwigo-Zugangsdaten vollstaendig gespeichert sein.');
// Backward compatibility: old connections stored only the DB password.
return array('db_password'=>$plain,'piwigo_user'=>'','piwigo_password'=>'');
}
return $decoded;
if (empty($decoded['db_password'])) fail_install('Datenbankpasswort fehlt in den Connector-Zugangsdaten.');
return array(
'db_password'=>(string)$decoded['db_password'],
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
);
}
function sql_install(mysqli $db, $value)
{
return $db->real_escape_string((string)$value);
}
function sql_install(mysqli $db, $value) { return $db->real_escape_string((string)$value); }
try
{
if (!is_readable($dbConfig))
{
fail_install('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig);
}
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key])) fail_install('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.');
}
if (!is_readable($dbConfig)) fail_install('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig);
$conf = array(); $prefixeTable = 'piwigo_'; require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail_install('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.');
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) fail_install('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
@@ -111,29 +86,27 @@ try
if (!$result || !$result->num_rows) fail_install('Connector-Verbindung #'.$id.' wurde nicht gefunden.');
$row = $result->fetch_assoc();
if ((string)$row['adapter'] !== 'local') fail_install('Native Aktivierung ist derzeit nur fuer lokale Verbindungen verfuegbar.');
if ((string)$row['takeover_state'] !== 'verified' || (int)$row['enabled'] !== 0)
{
fail_install('Die Verbindung muss zuerst erfolgreich verifiziert und deaktiviert sein.');
}
if ((string)$row['takeover_state'] !== 'verified' || (int)$row['enabled'] !== 0) fail_install('Die Verbindung muss zuerst erfolgreich verifiziert und deaktiviert sein.');
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config) || empty($config['verification']['ok'])) fail_install('Erfolgreiche Verifikation fehlt.');
foreach (array('host','port','database','user','source_view','activity_view','gallery_root') as $key)
{
if (trim((string)($config[$key] ?? '')) === '') fail_install('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
}
foreach (array('host','port','database','user','source_view','activity_view','gallery_root') as $key) if (trim((string)($config[$key] ?? '')) === '') fail_install('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
$keyResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$keyResult || !$keyResult->num_rows) fail_install('Connector-Schluessel wurde in Piwigo nicht gefunden.');
$keyRow = $keyResult->fetch_assoc();
$credentials = decrypt_install_credentials((string)$row['secret_blob'], (string)$keyRow['value']);
$credentials = decrypt_install_credentials((string)$row['secret_blob'], (string)$keyResult->fetch_assoc()['value']);
$apiAvailable = false;
$apiResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1");
if ($apiResult && $apiResult->num_rows) $apiAvailable = trim((string)$apiResult->fetch_assoc()['value']) !== '';
$fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== '';
if (!$apiAvailable && !$fallbackAvailable) fail_install('Weder Piwigo-API noch Fallback-Zugang sind gespeichert.');
$stateDir = $stateRoot.'/connection-'.$id;
$statusFile = $stateDir.'/connector-status.json';
if (!is_dir($configDir) && !mkdir($configDir, 0700, true)) fail_install('Konfigurationsverzeichnis konnte nicht angelegt werden.');
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_install('State-Verzeichnis konnte nicht angelegt werden.');
chmod($configDir, 0700);
chmod($stateDir, 0750);
chmod($configDir, 0700); chmod($stateDir, 0750);
$base = $configDir.'/connection-'.$id;
$dbPasswordPath = $base.'.db-password';
@@ -141,19 +114,23 @@ try
$storagePath = $base.'.storages.tsv';
$configPath = $base.'.conf';
file_put_contents($dbPasswordPath, (string)$credentials['db_password']."\n", LOCK_EX);
file_put_contents($piwigoPasswordPath, (string)$credentials['piwigo_password']."\n", LOCK_EX);
chmod($dbPasswordPath, 0600);
chmod($piwigoPasswordPath, 0600);
file_put_contents($dbPasswordPath, (string)$credentials['db_password']."\n", LOCK_EX); chmod($dbPasswordPath, 0600);
if ($fallbackAvailable)
{
file_put_contents($piwigoPasswordPath, (string)$credentials['piwigo_password']."\n", LOCK_EX); chmod($piwigoPasswordPath, 0600);
}
else
{
@unlink($piwigoPasswordPath);
}
$storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount');
foreach ((array)($config['storages'] ?? array()) as $storage)
{
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t".(string)($storage['source_prefix'] ?? '')."\t".(string)($storage['local_mount'] ?? '');
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t".trim((string)($storage['source_prefix'] ?? ''), '/')."\t".(string)($storage['local_mount'] ?? '');
}
if (count($storageLines) === 1) fail_install('Keine Storage-Zuordnungen gespeichert.');
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX);
chmod($storagePath, 0600);
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); chmod($storagePath, 0600);
$lines = array(
'PIWIGO_ROOT='.$piwigoRoot,
@@ -172,44 +149,32 @@ try
'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900),
'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400),
'PIWIGO_SYNC_ENABLED=1',
'PIWIGO_SYNC_USER='.(string)$credentials['piwigo_user'],
'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath,
);
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
chmod($configPath, 0600);
if ($fallbackAvailable)
{
$lines[] = 'PIWIGO_SYNC_USER='.(string)$credentials['piwigo_user'];
$lines[] = 'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath;
}
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); chmod($configPath, 0600);
echo "Teste Verbindung mit Plugin-Runtime...\n";
$test = run_install(array('env', 'PIWIGO_CONFIG='.$configPath, 'bash', $pluginRoot.'/runtime/sync.sh'), true);
if ($test['exit'] !== 0)
{
@unlink($configPath);
@unlink($storagePath);
@unlink($dbPasswordPath);
@unlink($piwigoPasswordPath);
@unlink($configPath); @unlink($storagePath); @unlink($dbPasswordPath); @unlink($piwigoPasswordPath);
$detail = trim($test['stderr']) !== '' ? trim($test['stderr']) : trim($test['stdout']);
fail_install('Runtime-Test fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.'));
}
$service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nExecStart=/usr/bin/env bash ".$pluginRoot."/runtime/run-all.sh\n\n";
$timer = "[Unit]\nDescription=Bratonien NC Connector regelmaessig pruefen\n\n[Timer]\nOnBootSec=3min\nOnUnitActiveSec=1min\nRandomizedDelaySec=15s\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n";
file_put_contents($servicePath, $service, LOCK_EX);
file_put_contents($timerPath, $timer, LOCK_EX);
chmod($servicePath, 0644);
chmod($timerPath, 0644);
file_put_contents($servicePath, $service, LOCK_EX); file_put_contents($timerPath, $timer, LOCK_EX); chmod($servicePath,0644); chmod($timerPath,0644);
$config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile;
$config['runtime'] = array('mode'=>'plugin-runtime', 'config'=>$configPath);
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$now = date('Y-m-d H:i:s');
if (!$db->query("UPDATE `".$table."` SET enabled=1, takeover_state='active', config_json='".sql_install($db, $json)."', updated='".sql_install($db, $now)."' WHERE id=".$id))
{
fail_install('Connector-Status konnte nicht gespeichert werden: '.$db->error);
}
run_install(array('systemctl', 'daemon-reload'));
run_install(array('systemctl', 'enable', '--now', 'bratonien-nc-connector.timer'));
$config['state_dir']=$stateDir;$config['status_file']=$statusFile;$config['runtime']=array('mode'=>'plugin-runtime','config'=>$configPath);
$json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);$now=date('Y-m-d H:i:s');
if (!$db->query("UPDATE `".$table."` SET enabled=1, takeover_state='active', config_json='".sql_install($db,$json)."', updated='".sql_install($db,$now)."' WHERE id=".$id)) fail_install('Connector-Status konnte nicht gespeichert werden: '.$db->error);
run_install(array('systemctl','daemon-reload')); run_install(array('systemctl','enable','--now','bratonien-nc-connector.timer'));
echo "Aktivierung erfolgreich.\n";
echo "Verbindung #".$id." verwendet jetzt den nativen Bratonien-Tools-State unter ".$stateDir.".\n";
echo "Der gemeinsame Connector-Timer verarbeitet alle installierten connection-*.conf Dateien.\n";

View File

@@ -29,6 +29,9 @@ $latest = array(
'state'=>'idle',
'message'=>'Noch kein Connector-Status verfügbar.',
'timestamp'=>0,
'last_run_label'=>'Nicht verfügbar',
'next_run_timestamp'=>0,
'next_run_label'=>'Nicht verfügbar',
'auth_mode'=>'',
'api'=>array('state'=>'not_run','message'=>''),
'fallback'=>array('state'=>'not_run','message'=>''),
@@ -56,4 +59,21 @@ if (is_dir($dir))
}
}
if ((int)$latest['timestamp'] > 0)
{
$latest['last_run_label'] = date('d.m.Y H:i:s', (int)$latest['timestamp']);
}
$system_file = BRATONIEN_TOOLS_PATH.'include/nc_connector_system.inc.php';
if (is_readable($system_file))
{
include_once($system_file);
if (function_exists('bratonien_tools_nc_connector_system_status'))
{
$system = bratonien_tools_nc_connector_system_status(array());
$latest['next_run_timestamp'] = (int)($system['next_run_timestamp'] ?? 0);
$latest['next_run_label'] = (string)($system['next_run_label'] ?? 'Nicht verfügbar');
}
}
echo json_encode($latest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

View File

@@ -7,109 +7,71 @@ import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
import time
from pathlib import Path
IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$")
def validate_view(name: str) -> str:
value = str(name).strip()
if not IDENTIFIER.fullmatch(value):
raise ValueError(f"invalid SQL view name: {value!r}")
return value
def load(path: Path) -> dict[str, object]:
defaults: dict[str, object] = {
"processed": 0,
"observed": 0,
"pending_since": 0,
"last_change": 0,
"last_full": 0,
"source_signature": "",
}
defaults: dict[str, object] = {"processed":0,"observed":0,"pending_since":0,"last_change":0,"last_full":0,"source_signature":""}
if not path.exists():
return defaults
data = json.loads(path.read_text(encoding="utf-8"))
return {
"processed": int(data.get("processed", 0)),
"observed": int(data.get("observed", 0)),
"pending_since": int(data.get("pending_since", 0)),
"last_change": int(data.get("last_change", 0)),
"last_full": int(data.get("last_full", 0)),
"source_signature": str(data.get("source_signature", "")),
}
data=json.loads(path.read_text(encoding="utf-8"))
return {"processed":int(data.get("processed",0)),"observed":int(data.get("observed",0)),"pending_since":int(data.get("pending_since",0)),"last_change":int(data.get("last_change",0)),"last_full":int(data.get("last_full",0)),"source_signature":str(data.get("source_signature",""))}
def save(path: Path, state: dict[str, object]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(dir=path.parent)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(state, handle, sort_keys=True)
handle.write("\n")
path.parent.mkdir(parents=True,exist_ok=True)
fd,name=tempfile.mkstemp(dir=path.parent)
with os.fdopen(fd,"w",encoding="utf-8") as handle:
json.dump(state,handle,sort_keys=True);handle.write("\n")
Path(name).replace(path)
def query(args: argparse.Namespace, sql: str) -> str:
env = os.environ.copy()
env["PGPASSWORD"] = args.password_file.read_text(encoding="utf-8").strip()
command = ["psql", "-XAt", "-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.database, "-c", sql]
return subprocess.run(command, env=env, check=True, text=True, capture_output=True).stdout
env=os.environ.copy();env["PGPASSWORD"]=args.password_file.read_text(encoding="utf-8").strip()
command=["psql","-XAt","-h",args.host,"-p",str(args.port),"-U",args.user,"-d",args.database,"-v","ON_ERROR_STOP=1","-c",sql]
return subprocess.run(command,env=env,check=True,text=True,capture_output=True).stdout
def latest(args: argparse.Namespace) -> int:
return int(query(args, f"SELECT COALESCE(MAX(activity_id), 0) FROM {args.view}").strip())
return int(query(args,f"SELECT COALESCE(MAX(activity_id), 0) FROM {validate_view(args.view)}").strip())
def source_signature(args: argparse.Namespace) -> str:
if not args.source_view:
return ""
payload = query(args, f"SELECT share_id, display_name, storage_id, source_path FROM {args.source_view} ORDER BY share_id")
if not args.source_view:return ""
payload=query(args,f"SELECT share_id, display_name, storage_id, source_path FROM {validate_view(args.source_view)} ORDER BY share_id")
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=("check", "commit"))
parser.add_argument("--state", required=True, type=Path)
parser.add_argument("--host", required=True)
parser.add_argument("--port", type=int, default=5432)
parser.add_argument("--database", required=True)
parser.add_argument("--user", required=True)
parser.add_argument("--password-file", required=True, type=Path)
parser.add_argument("--view", default="piwigo_showcase_activity")
parser.add_argument("--source-view", default="")
parser.add_argument("--quiet", type=int, default=120)
parser.add_argument("--max-wait", type=int, default=900)
parser.add_argument("--full-after", type=int, default=86400)
args = parser.parse_args()
parser=argparse.ArgumentParser();parser.add_argument("action",choices=("check","commit"));parser.add_argument("--state",required=True,type=Path);parser.add_argument("--host",required=True);parser.add_argument("--port",type=int,default=5432);parser.add_argument("--database",required=True);parser.add_argument("--user",required=True);parser.add_argument("--password-file",required=True,type=Path);parser.add_argument("--view",default="piwigo_showcase_activity");parser.add_argument("--source-view",default="");parser.add_argument("--quiet",type=int,default=120);parser.add_argument("--max-wait",type=int,default=900);parser.add_argument("--full-after",type=int,default=86400);args=parser.parse_args()
try:
state = load(args.state)
now = int(time.time())
current = latest(args)
current_source = source_signature(args)
if args.action == "commit":
state.update(processed=current, observed=current, pending_since=0, last_change=0, last_full=now, source_signature=current_source)
save(args.state, state)
return 0
if not int(state["last_full"]):
return 0
if current_source and current_source != str(state["source_signature"]):
return 0
if current > int(state["observed"]):
state["observed"] = current
state["last_change"] = now
state["pending_since"] = int(state["pending_since"]) or now
save(args.state, state)
if now - int(state["last_full"]) >= args.full_after:
return 0
if int(state["observed"]) <= int(state["processed"]):
return 3
if now - int(state["last_change"]) >= args.quiet or now - int(state["pending_since"]) >= args.max_wait:
return 0
validate_view(args.view)
if args.source_view:validate_view(args.source_view)
state=load(args.state);now=int(time.time());current=latest(args);current_source=source_signature(args)
if args.action=="commit":state.update(processed=current,observed=current,pending_since=0,last_change=0,last_full=now,source_signature=current_source);save(args.state,state);return 0
if not int(state["last_full"]):return 0
if current_source and current_source!=str(state["source_signature"]):return 0
if current>int(state["observed"]):state["observed"]=current;state["last_change"]=now;state["pending_since"]=int(state["pending_since"]) or now;save(args.state,state)
if now-int(state["last_full"])>=args.full_after:return 0
if int(state["observed"])<=int(state["processed"]):return 3
if now-int(state["last_change"])>=args.quiet or now-int(state["pending_since"])>=args.max_wait:return 0
return 3
except Exception as error:
print(f"activity-gate: {error}", file=sys.stderr)
return 1
print(f"activity-gate: {error}",file=sys.stderr);return 1
if __name__ == "__main__":
raise SystemExit(main())
if __name__=="__main__":raise SystemExit(main())

View File

@@ -7,174 +7,109 @@ import argparse
import csv
import json
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path, PurePosixPath
IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$")
def validate_view(name: str) -> str:
value = str(name).strip()
if not IDENTIFIER.fullmatch(value):
raise ValueError(f"invalid SQL view name: {value!r}")
return value
def read_config(path: Path) -> dict[str, tuple[str, Path]]:
result: dict[str, tuple[str, Path]] = {}
with path.open(encoding="utf-8") as handle:
for number, line in enumerate(handle, 1):
line = line.rstrip("\n")
if not line or line.startswith("#"):
continue
fields = line.split("\t")
if len(fields) != 3:
raise ValueError(f"{path}:{number}: expected storage_id, source_prefix and local_mount")
storage_id, prefix, mount = fields
result[storage_id] = (prefix.strip("/"), Path(mount))
line=line.rstrip("\n")
if not line or line.startswith("#"):continue
fields=line.split("\t")
if len(fields)!=3:raise ValueError(f"{path}:{number}: expected storage_id, source_prefix and local_mount")
storage_id,prefix,mount=fields
if not storage_id.strip():raise ValueError(f"{path}:{number}: storage_id is empty")
result[storage_id]=(prefix.strip("/"),Path(mount))
return result
def run_query(args: argparse.Namespace, env: dict[str, str], sql: str) -> subprocess.CompletedProcess[str]:
command = [
"psql", "-X", "-A", "-F", "\t", "-t",
"-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.database,
"-c", sql,
]
return subprocess.run(command, env=env, check=False, text=True, capture_output=True)
def run_query(args: argparse.Namespace, env: dict[str,str], sql: str) -> subprocess.CompletedProcess[str]:
command=["psql","-X","-A","-F","\t","-t","-v","ON_ERROR_STOP=1","-h",args.host,"-p",str(args.port),"-U",args.user,"-d",args.database,"-c",sql]
return subprocess.run(command,env=env,check=False,text=True,capture_output=True)
def query_rows(args: argparse.Namespace) -> list[list[str]]:
password = args.password_file.read_text(encoding="utf-8").strip()
env = os.environ.copy()
env["PGPASSWORD"] = password
modern_sql = f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {args.view} ORDER BY share_id"
completed = run_query(args, env, modern_sql)
if completed.returncode == 0:
return list(csv.reader(completed.stdout.splitlines(), delimiter="\t"))
# Existing installations may still expose the original four-column view.
# Keep them usable and derive folder/file from the resolved source path.
if "item_type" not in completed.stderr or "does not exist" not in completed.stderr:
raise RuntimeError(completed.stderr.strip() or "Nextcloud source view query failed")
legacy_sql = f"SELECT share_id, display_name, storage_id, source_path FROM {args.view} ORDER BY share_id"
completed = run_query(args, env, legacy_sql)
if completed.returncode != 0:
raise RuntimeError(completed.stderr.strip() or "legacy Nextcloud source view query failed")
rows: list[list[str]] = []
for row in csv.reader(completed.stdout.splitlines(), delimiter="\t"):
if len(row) == 4:
share_id, display_name, storage_id, source_path = row
rows.append([share_id, "", display_name, storage_id, source_path])
else:
rows.append(row)
password=args.password_file.read_text(encoding="utf-8").strip();env=os.environ.copy();env["PGPASSWORD"]=password;view=validate_view(args.view)
modern_sql=f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {view} ORDER BY share_id"
completed=run_query(args,env,modern_sql)
if completed.returncode==0:return list(csv.reader(completed.stdout.splitlines(),delimiter="\t"))
if "item_type" not in completed.stderr or "does not exist" not in completed.stderr:raise RuntimeError(completed.stderr.strip() or "Nextcloud source view query failed")
legacy_sql=f"SELECT share_id, display_name, storage_id, source_path FROM {view} ORDER BY share_id"
completed=run_query(args,env,legacy_sql)
if completed.returncode!=0:raise RuntimeError(completed.stderr.strip() or "legacy Nextcloud source view query failed")
rows=[]
for row in csv.reader(completed.stdout.splitlines(),delimiter="\t"):
if len(row)==4:
share_id,display_name,storage_id,source_path=row;rows.append([share_id,"",display_name,storage_id,source_path])
else:rows.append(row)
return rows
def contained_join(root: Path, relative: str) -> Path:
parts = PurePosixPath(relative).parts
if relative.startswith("/") or ".." in parts:
raise ValueError(f"unsafe source path: {relative}")
parts=PurePosixPath(relative).parts
if relative.startswith("/") or ".." in parts:raise ValueError(f"unsafe source path: {relative}")
return root.joinpath(*parts)
def build(args: argparse.Namespace) -> dict[str, object]:
adapters = read_config(args.storage_config)
rows = query_rows(args)
if not rows and not args.allow_empty:
raise RuntimeError("Nextcloud returned no Showcase shares; refusing an empty manifest")
manifest: list[str] = []
errors: list[str] = []
folder_count = 0
file_count = 0
def build(args: argparse.Namespace) -> dict[str,object]:
validate_view(args.view);adapters=read_config(args.storage_config);rows=query_rows(args)
if not rows and not args.allow_empty:raise RuntimeError("Nextcloud returned no Showcase shares; refusing an empty manifest")
manifest=[];errors=[];folder_count=0;file_count=0
for row in rows:
if len(row) != 5:
errors.append(f"invalid database row with {len(row)} columns")
continue
share_id, item_type, display_name, storage_id, source_path = row
item_type = item_type.strip().lower()
if item_type and item_type not in {"folder", "file"}:
errors.append(f"share {share_id}: unsupported item_type {item_type!r}")
continue
adapter = adapters.get(storage_id)
if not adapter:
errors.append(f"share {share_id}: unknown storage {storage_id}")
continue
prefix, mount = adapter
relative = source_path.strip("/")
if len(row)!=5:errors.append(f"invalid database row with {len(row)} columns");continue
share_id,item_type,display_name,storage_id,source_path=row;item_type=item_type.strip().lower()
if item_type and item_type not in {"folder","file"}:errors.append(f"share {share_id}: unsupported item_type {item_type!r}");continue
adapter=adapters.get(storage_id)
if not adapter:errors.append(f"share {share_id}: unknown storage {storage_id}");continue
prefix,mount=adapter;relative=source_path.strip("/")
if prefix:
expected = prefix + "/"
if relative != prefix and not relative.startswith(expected):
errors.append(f"share {share_id}: path does not match configured prefix")
continue
relative = relative[len(prefix):].lstrip("/")
source = contained_join(mount, relative)
if not mount.is_mount():
errors.append(f"share {share_id}: storage mount unavailable: {mount}")
continue
expected=prefix+"/"
if relative!=prefix and not relative.startswith(expected):errors.append(f"share {share_id}: path does not match configured prefix");continue
relative=relative[len(prefix):].lstrip("/")
source=contained_join(mount,relative)
if not mount.is_mount():errors.append(f"share {share_id}: storage mount unavailable: {mount}");continue
if not item_type:
if source.is_dir():
item_type = "folder"
elif source.is_file():
item_type = "file"
else:
errors.append(f"share {share_id}: source unavailable: {source}")
continue
if item_type == "folder":
if not source.is_dir():
errors.append(f"share {share_id}: source directory unavailable: {source}")
continue
folder_count += 1
if source.is_dir():item_type="folder"
elif source.is_file():item_type="file"
else:errors.append(f"share {share_id}: source unavailable: {source}");continue
if item_type=="folder":
if not source.is_dir():errors.append(f"share {share_id}: source directory unavailable: {source}");continue
folder_count+=1
else:
if not source.is_file():
errors.append(f"share {share_id}: source file unavailable: {source}")
continue
file_count += 1
if not source.is_file():errors.append(f"share {share_id}: source file unavailable: {source}");continue
file_count+=1
if "\t" in display_name or "\n" in display_name or "\r" in display_name:errors.append(f"share {share_id}: display name contains unsupported control characters");continue
source_text=str(source)
if "\t" in source_text or "\n" in source_text or "\r" in source_text:errors.append(f"share {share_id}: source path contains unsupported control characters");continue
manifest.append(f"{share_id}\t{item_type}\t{display_name.lstrip('/')}\t{source}")
if errors:
raise RuntimeError("; ".join(errors))
if len(manifest) != len(rows):
raise RuntimeError("not all Showcase shares could be resolved")
args.output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=args.output.parent, delete=False) as handle:
handle.write("\n".join(manifest) + ("\n" if manifest else ""))
temporary = Path(handle.name)
if errors:raise RuntimeError("; ".join(errors))
if len(manifest)!=len(rows):raise RuntimeError("not all Showcase shares could be resolved")
args.output.parent.mkdir(parents=True,exist_ok=True)
with tempfile.NamedTemporaryFile("w",encoding="utf-8",dir=args.output.parent,delete=False) as handle:
handle.write("\n".join(manifest)+("\n" if manifest else ""));temporary=Path(handle.name)
temporary.replace(args.output)
return {
"shares": len(manifest),
"folders": folder_count,
"files": file_count,
"manifest": str(args.output),
}
return {"shares":len(manifest),"folders":folder_count,"files":file_count,"manifest":str(args.output)}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--host", required=True)
parser.add_argument("--port", type=int, default=5432)
parser.add_argument("--database", required=True)
parser.add_argument("--user", required=True)
parser.add_argument("--password-file", required=True, type=Path)
parser.add_argument("--view", default="piwigo_showcase_sources")
parser.add_argument("--storage-config", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--allow-empty", action="store_true")
args = parser.parse_args()
try:
print(json.dumps(build(args), ensure_ascii=False))
except Exception as error:
print(f"manifest: {error}", file=sys.stderr)
return 1
parser=argparse.ArgumentParser();parser.add_argument("--host",required=True);parser.add_argument("--port",type=int,default=5432);parser.add_argument("--database",required=True);parser.add_argument("--user",required=True);parser.add_argument("--password-file",required=True,type=Path);parser.add_argument("--view",default="piwigo_showcase_sources");parser.add_argument("--storage-config",required=True,type=Path);parser.add_argument("--output",required=True,type=Path);parser.add_argument("--allow-empty",action="store_true");args=parser.parse_args()
try:print(json.dumps(build(args),ensure_ascii=False))
except Exception as error:print(f"manifest: {error}",file=sys.stderr);return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
if __name__=="__main__":raise SystemExit(main())

View File

@@ -63,7 +63,7 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 900,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-NC-Connector/0.9.3.26',
CURLOPT_USERAGENT => 'Bratonien-NC-Connector',
);
if ($headers)
{

View File

@@ -56,12 +56,7 @@ def load_manifest(path: Path) -> list[dict[str, str]]:
share_id, item_type, display_name, source_path = fields
if item_type not in {"folder", "file"}:
raise ValueError(f"{path}:{line_number}: unsupported item_type {item_type!r}")
entries.append({
"share_id": share_id,
"item_type": item_type,
"display_name": display_name,
"source_path": source_path,
})
entries.append({"share_id": share_id, "item_type": item_type, "display_name": display_name, "source_path": source_path})
return entries
@@ -87,8 +82,7 @@ def preferred_target(source_key: str, raw_name: str, parent_target: Path, old_ma
def mirror_directory(source: Path, target: Path, source_key: str, target_key: Path, old_map: dict[str, str], new_map: dict[str, str]) -> None:
target.mkdir(parents=True, exist_ok=True)
used: set[str] = set()
children = sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name))
for child in children:
for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)):
if child.is_symlink():
continue
child_source_key = f"{source_key}/{child.name}"
@@ -103,51 +97,68 @@ def mirror_directory(source: Path, target: Path, source_key: str, target_key: Pa
child_target.symlink_to(child.resolve())
def remove_tree(path: Path) -> None:
if path.is_symlink() or path.is_file():
path.unlink(missing_ok=True)
elif path.exists():
shutil.rmtree(path)
def build(manifest: Path, destination: Path, state_file: Path) -> None:
entries = load_manifest(manifest)
old_map = load_map(state_file)
new_map: dict[str, str] = {}
staging = destination.with_name(f".{destination.name}.next")
previous = destination.with_name(f".{destination.name}.previous")
state_staging = state_file.with_suffix(state_file.suffix + ".next")
if staging.exists():
shutil.rmtree(staging)
remove_tree(staging)
remove_tree(previous)
if state_staging.exists():
state_staging.unlink()
staging.mkdir(parents=True)
used_roots: set[str] = set()
for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])):
source = Path(entry["source_path"])
source_key = f"share:{entry['share_id']}"
preferred = preferred_target(source_key, entry["display_name"], Path("."), old_map)
is_file_share = entry["item_type"] == "file"
root_name = unique_name(preferred, used_roots, is_file_share)
root_key = Path(root_name)
new_map[source_key] = root_key.as_posix()
try:
used_roots: set[str] = set()
for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])):
source = Path(entry["source_path"])
source_key = f"share:{entry['share_id']}"
preferred = preferred_target(source_key, entry["display_name"], Path("."), old_map)
is_file_share = entry["item_type"] == "file"
root_name = unique_name(preferred, used_roots, is_file_share)
root_key = Path(root_name)
new_map[source_key] = root_key.as_posix()
if is_file_share:
if not source.is_file():
raise FileNotFoundError(f"source is not a readable file: {source}")
(staging / root_name).symlink_to(source.resolve())
else:
if not source.is_dir():
raise FileNotFoundError(f"source is not a readable directory: {source}")
mirror_directory(source, staging / root_name, source_key, root_key, old_map, new_map)
if is_file_share:
if not source.is_file():
raise FileNotFoundError(f"source is not a readable file: {source}")
(staging / root_name).symlink_to(source.resolve())
continue
state_staging.parent.mkdir(parents=True, exist_ok=True)
with state_staging.open("w", encoding="utf-8") as handle:
json.dump(new_map, handle, ensure_ascii=False, indent=2, sort_keys=True)
handle.write("\n")
if not source.is_dir():
raise FileNotFoundError(f"source is not a readable directory: {source}")
mirror_directory(source, staging / root_name, source_key, root_key, old_map, new_map)
state_staging = state_file.with_suffix(state_file.suffix + ".next")
state_staging.parent.mkdir(parents=True, exist_ok=True)
with state_staging.open("w", encoding="utf-8") as handle:
json.dump(new_map, handle, ensure_ascii=False, indent=2, sort_keys=True)
handle.write("\n")
if previous.exists():
shutil.rmtree(previous)
if destination.exists():
destination.rename(previous)
staging.rename(destination)
state_staging.replace(state_file)
if previous.exists():
shutil.rmtree(previous)
had_destination = destination.exists()
if had_destination:
destination.rename(previous)
try:
staging.rename(destination)
state_staging.replace(state_file)
except Exception:
remove_tree(destination)
if had_destination and previous.exists():
previous.rename(destination)
raise
remove_tree(previous)
except Exception:
remove_tree(staging)
if state_staging.exists():
state_staging.unlink()
raise
def main() -> int:

View File

@@ -11,9 +11,7 @@
.bratonien-card { margin-left:0; margin-right:0; }
@media (max-width:760px) { .bratonien-tabs{gap:5px}.bratonien-tab{border-bottom:1px solid rgba(255,255,255,.16);border-radius:4px;margin-bottom:0;padding:9px 11px} }
</style>
<div id="bratonien-tabs-anchor"></div>
<script>
(function () {
'use strict';
@@ -27,114 +25,56 @@
{ id:'wartung', label:'Wartung / Cache' },
{ id:'system', label:'System & Updates' }
];
var panels = [];
definitions.forEach(function (d) {
var ids=d.sections||[d.id], elements=[];
var panels=[];
definitions.forEach(function(d){
var ids=d.sections||[d.id],elements=[];
ids.forEach(function(id){var el=document.getElementById(id);if(el){el.classList.add('bratonien-tab-panel');elements.push(el);}});
if (!elements.length) return;
panels.push({definition:d,elements:elements});
});
if (!panels.length) return;
var anchor = document.getElementById('bratonien-tabs-anchor');
if (!anchor) return;
var tabs = document.createElement('div');
tabs.className = 'bratonien-tabs';
tabs.setAttribute('role','tablist');
tabs.setAttribute('aria-label','Bratonien Tools Bereiche');
panels.forEach(function (item) {
var b = document.createElement('button');
b.type='button';
b.className='bratonien-tab';
b.id='bratonien-tab-'+item.definition.id;
b.dataset.tab=item.definition.id;
b.setAttribute('role','tab');
b.textContent=item.definition.label;
tabs.appendChild(b);
item.elements.forEach(function(el){el.setAttribute('role','tabpanel');el.setAttribute('aria-labelledby',b.id);});
if(elements.length)panels.push({definition:d,elements:elements});
});
if(!panels.length)return;
var anchor=document.getElementById('bratonien-tabs-anchor');if(!anchor)return;
var tabs=document.createElement('div');tabs.className='bratonien-tabs';tabs.setAttribute('role','tablist');tabs.setAttribute('aria-label','Bratonien Tools Bereiche');
panels.forEach(function(item){var b=document.createElement('button');b.type='button';b.className='bratonien-tab';b.id='bratonien-tab-'+item.definition.id;b.dataset.tab=item.definition.id;b.setAttribute('role','tab');b.textContent=item.definition.label;tabs.appendChild(b);item.elements.forEach(function(el){el.setAttribute('role','tabpanel');el.setAttribute('aria-labelledby',b.id);});});
anchor.replaceWith(tabs);
function activate(id, remember) {
var valid=false;
panels.forEach(function (item) {
var active=item.definition.id===id;
item.elements.forEach(function(el){el.hidden=!active;});
var b=tabs.querySelector('[data-tab="'+item.definition.id+'"]');
if (b) {
b.classList.toggle('is-active',active);
b.setAttribute('aria-selected',active?'true':'false');
b.tabIndex=active?0:-1;
}
if (active) valid=true;
});
if (!valid) return activate(panels[0].definition.id,remember);
if (remember!==false) { try { localStorage.setItem('bratonien-tools-active-tab',id); } catch(e) {} }
}
tabs.addEventListener('click',function(e){ var b=e.target.closest('.bratonien-tab'); if(b) activate(b.dataset.tab); });
tabs.addEventListener('keydown',function(e){
if(e.key!=='ArrowLeft'&&e.key!=='ArrowRight') return;
var buttons=[].slice.call(tabs.querySelectorAll('.bratonien-tab'));
var i=buttons.indexOf(document.activeElement); if(i<0) return;
e.preventDefault(); i += e.key==='ArrowRight'?1:-1; if(i>=buttons.length)i=0; if(i<0)i=buttons.length-1;
buttons[i].focus(); activate(buttons[i].dataset.tab);
});
var initial='';
var hash=location.hash?location.hash.slice(1):'';
panels.forEach(function(item){if(item.definition.id===hash||(item.definition.sections||[]).indexOf(hash)!==-1)initial=item.definition.id;});
if(!initial){ try { initial=localStorage.getItem('bratonien-tools-active-tab')||''; } catch(e){} }
if(!panels.some(function(i){return i.definition.id===initial})) initial=panels[0].definition.id;
activate(initial,false);
function activate(id,remember){var valid=false;panels.forEach(function(item){var active=item.definition.id===id;item.elements.forEach(function(el){el.hidden=!active;});var b=tabs.querySelector('[data-tab="'+item.definition.id+'"]');if(b){b.classList.toggle('is-active',active);b.setAttribute('aria-selected',active?'true':'false');b.tabIndex=active?0:-1;}if(active)valid=true;});if(!valid)return activate(panels[0].definition.id,remember);if(remember!==false){try{localStorage.setItem('bratonien-tools-active-tab',id);}catch(e){}}}
tabs.addEventListener('click',function(e){var b=e.target.closest('.bratonien-tab');if(b)activate(b.dataset.tab);});
tabs.addEventListener('keydown',function(e){if(e.key!=='ArrowLeft'&&e.key!=='ArrowRight')return;var buttons=[].slice.call(tabs.querySelectorAll('.bratonien-tab'));var i=buttons.indexOf(document.activeElement);if(i<0)return;e.preventDefault();i+=e.key==='ArrowRight'?1:-1;if(i>=buttons.length)i=0;if(i<0)i=buttons.length-1;buttons[i].focus();activate(buttons[i].dataset.tab);});
var initial='',hash=location.hash?location.hash.slice(1):'';panels.forEach(function(item){if(item.definition.id===hash||(item.definition.sections||[]).indexOf(hash)!==-1)initial=item.definition.id;});if(!initial){try{initial=localStorage.getItem('bratonien-tools-active-tab')||'';}catch(e){}}if(!panels.some(function(i){return i.definition.id===initial;}))initial=panels[0].definition.id;activate(initial,false);
}
function initNCConnectorPolling() {
var section=document.getElementById('nc-connector');
if(!section) return;
var lastSeen=null;
function initNCConnectorPolling(){
var section=document.getElementById('nc-connector');if(!section)return;
var endpoint='plugins/bratonien_tools/nc-connector-status.php';
function poll() {
function valueNodeForLabel(labelText){
var labels=[].slice.call(section.querySelectorAll('.bratonien-label'));
for(var i=0;i<labels.length;i++){
if((labels[i].textContent||'').trim()===labelText){
var node=labels[i].nextElementSibling;
return node&&node.tagName==='STRONG'?node:null;
}
}
return null;
}
var lastRunNode=valueNodeForLabel('Letzter Lauf');
var nextRunNode=valueNodeForLabel('Nächster Lauf');
function poll(){
fetch(endpoint+'?_='+Date.now(),{credentials:'same-origin',cache:'no-store'})
.then(function(response){if(!response.ok) throw new Error('HTTP '+response.status); return response.json();})
.then(function(response){if(!response.ok)throw new Error('HTTP '+response.status);return response.json();})
.then(function(data){
var timestamp=parseInt(data.timestamp||0,10);
if(!timestamp) return;
if(lastSeen===null) {
lastSeen=timestamp;
var resultVisible=section.textContent.indexOf('Letztes Ergebnis:')!==-1;
if(!resultVisible && data.message) {
var reloadKey='bratonien-nc-status-reload-'+timestamp;
try {
if(sessionStorage.getItem(reloadKey)!=='1') {
sessionStorage.setItem(reloadKey,'1');
window.location.reload();
}
} catch(e) {}
}
return;
}
if(timestamp>lastSeen) {
lastSeen=timestamp;
window.location.reload();
}
if(lastRunNode&&data.last_run_label)lastRunNode.textContent=data.last_run_label;
if(nextRunNode&&data.next_run_label)nextRunNode.textContent=data.next_run_label;
})
.catch(function(){});
}
poll();
window.setInterval(poll,5000);
poll();window.setInterval(poll,5000);
}
function initAll() {
initBratonienTabs();
initNCConnectorPolling();
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',initAll); else initAll();
function initAll(){initBratonienTabs();initNCConnectorPolling();}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initAll);else initAll();
})();
</script>
{/literal}

View File

@@ -1,428 +1,272 @@
<section class="bratonien-section" id="nc-connector">
<h3>NC Connector</h3>
<p class="bratonien-section__intro">Bratonien Tools verwaltet die Nextcloud-Anbindung und den regelmäßigen Piwigo-Abgleich.</p>
<p class="bratonien-section__intro">Verbindet Nextcloud mit Piwigo und hält freigegebene Bilder automatisch aktuell.</p>
{assign var=nc_system_available value=isset($NC_CONNECTOR.system)}
<div class="bratonien-grid">
<div class="bratonien-card">
<h4>Connector-Status</h4>
<h4>Status</h4>
<div class="bratonien-form-grid">
<span class="bratonien-label">Phase</span><strong>{$NC_CONNECTOR.phase|escape:html}</strong>
<span class="bratonien-label">Connector-Verbindungen</span><strong>{$NC_CONNECTOR.connection_count|escape:html}</strong>
<span class="bratonien-label">Timer aktiv</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_active}Ja{else}Nein{/if}</strong>
<span class="bratonien-label">Timer aktiviert</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_enabled}Ja{else}Nein{/if}</strong>
<span class="bratonien-label">Letzter Lauf</span><strong>{if $nc_system_available}{$NC_CONNECTOR.system.last_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
<span class="bratonien-label">Nächster Lauf</span><strong>{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
<span class="bratonien-label">Verbindungen</span><strong>{$NC_CONNECTOR.connection_count|escape:html}</strong>
<span class="bratonien-label">Automatischer Abgleich</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_active && $NC_CONNECTOR.system.timer_enabled}Aktiv{else}Nicht aktiv{/if}</strong>
<span class="bratonien-label">Letzter Lauf</span><strong data-nc-last-run>{if $nc_system_available}{$NC_CONNECTOR.system.last_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
<span class="bratonien-label">Nächster Lauf</span><strong data-nc-next-run>{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
</div>
{if $nc_system_available && $NC_CONNECTOR.system.last_run_message}
<p class="bratonien-base-note">Letztes Ergebnis: <strong>{$NC_CONNECTOR.system.last_run_message|escape:html}</strong></p>
{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_api_state == 'error'}
<p class="bratonien-main-cache__warning"><strong>API:</strong> {$NC_CONNECTOR.system.last_run_api_message|escape:html}</p>
{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_fallback_state == 'ok'}
<p class="bratonien-base-note"><strong>Fallback:</strong> erfolgreich übernommen.</p>
{elseif $nc_system_available && $NC_CONNECTOR.system.last_run_fallback_state == 'error'}
<p class="bratonien-main-cache__warning"><strong>Fallback:</strong> {$NC_CONNECTOR.system.last_run_fallback_message|escape:html}</p>
{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_error_detail}
<p class="bratonien-main-cache__warning"><strong>Fehler:</strong> {$NC_CONNECTOR.system.last_run_error_detail|escape:html}</p>
{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_message}<p class="bratonien-base-note">Letztes Ergebnis: <strong>{$NC_CONNECTOR.system.last_run_message|escape:html}</strong></p>{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_api_state == 'error'}<p class="bratonien-main-cache__warning"><strong>API:</strong> {$NC_CONNECTOR.system.last_run_api_message|escape:html}</p>{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_fallback_state == 'error'}<p class="bratonien-main-cache__warning"><strong>Fallback:</strong> {$NC_CONNECTOR.system.last_run_fallback_message|escape:html}</p>{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_error_detail}<details><summary>Technische Fehlerdetails</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.system.last_run_error_detail|escape:html}</p></details>{/if}
</div>
<div class="bratonien-card">
<h4>Runtime</h4>
<p class="bratonien-base-note">Neue Verbindungen verwenden direkt die Plugin-Runtime und einen eigenen State unter <code>/var/lib/bratonien-tools/nc-connector/connection-ID</code>.</p>
{if $nc_system_available && !$NC_CONNECTOR.system.legacy_runtime_exists && !$NC_CONNECTOR.system.legacy_config_exists && !$NC_CONNECTOR.system.legacy_service_exists && !$NC_CONNECTOR.system.legacy_timer_exists}
<p class="bratonien-base-note">Legacy-Bestand: <strong>vollständig entfernt</strong>.</p>
{/if}
</div>
<div class="bratonien-card" style="grid-column:1/-1">
<h4>Neue Verbindung</h4>
<p class="bratonien-base-note">Wähle den geführten Assistenten oder öffne die technische Einrichtung bewusst manuell.</p>
<p class="bratonien-base-note">Für die normale Einrichtung empfehlen wir den Assistenten.</p>
<div class="bratonien-actions">
<button class="buttonLike" type="button" id="bratonien-nc-wizard-open">Mit Assistent anlegen</button>
<button class="buttonLike" type="button" id="bratonien-nc-technical-open">Ohne Assistent anlegen</button>
</div>
<dialog id="bratonien-nc-wizard-dialog" style="width:min(1100px,calc(100vw - 3rem));max-height:88vh;overflow:auto;background:#444;color:inherit;border:1px solid #777;border-radius:4px;padding:0;box-shadow:0 18px 60px rgba(0,0,0,.55)">
<div style="padding:1.25rem 1.5rem">
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem">
<div>
<h4 style="margin:0">Neue Verbindung</h4>
<p class="bratonien-base-note" style="margin:.35rem 0 0"><strong>Assistent · Schritt {$NC_CONNECTOR.wizard.step|escape:html} von 4</strong></p>
</div>
<button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button>
</div>
{if $NC_CONNECTOR.wizard.step == 1}
<p class="bratonien-base-note">Wir beginnen nur mit der Adresse der Nextcloud und einem Benutzer, mit dem die Instanz auf den Scan antworten kann.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_host">Nextcloud-Host</label>
<input id="nc_wizard_host" name="nc_wizard_host" type="text" placeholder="cloud.example.de" required>
<label class="bratonien-label" for="nc_wizard_user">Nextcloud-Benutzer</label>
<input id="nc_wizard_user" name="nc_wizard_user" type="text" autocomplete="username" required>
<label class="bratonien-label" for="nc_wizard_password">Passwort</label>
<input id="nc_wizard_password" name="nc_wizard_password" type="password" autocomplete="current-password" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_scan">Verbinden und scannen</button></p>
</form>
{elseif $NC_CONNECTOR.wizard.step == 2}
<p class="bratonien-base-note"><strong>Nextcloud erkannt.</strong> Alles, was zuverlässig ermittelt werden konnte, wurde bereits übernommen.</p>
<div class="bratonien-form-grid">
<span class="bratonien-label">Nextcloud</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
<span class="bratonien-label">Produkt</span><strong>{$NC_CONNECTOR.wizard.product|escape:html}</strong>
<span class="bratonien-label">Version</span><strong>{if $NC_CONNECTOR.wizard.version}{$NC_CONNECTOR.wizard.version|escape:html}{else}nicht gemeldet{/if}</strong>
<span class="bratonien-label">Zugriff als</span><strong>{$NC_CONNECTOR.wizard.username|escape:html}{if $NC_CONNECTOR.wizard.display_name} · {$NC_CONNECTOR.wizard.display_name|escape:html}{/if}</strong>
<span class="bratonien-label">Technische Erkennung</span><strong>{$NC_CONNECTOR.wizard.technical_source|escape:html}</strong>
<span class="bratonien-label">Piwigo-Ziel</span><strong>{$NC_CONNECTOR.wizard.gallery_root|escape:html}</strong>
</div>
{if !$NC_CONNECTOR.wizard.db_password_set || !$NC_CONNECTOR.wizard.db_user}
<hr>
<h5>Für den Datenabgleich fehlt noch der Datenbankzugang</h5>
<p class="bratonien-base-note">Der Assistent konnte diese Angaben nicht sicher aus Nextcloud ableiten. Deshalb werden nur die fehlenden Werte abgefragt.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Verbindungsname</label><input name="nc_wizard_connection_name" type="text" value="{$NC_CONNECTOR.wizard.connection_name|escape:html}" required>
<label class="bratonien-label">Datenbank-Host</label><input name="nc_wizard_db_host" type="text" value="{$NC_CONNECTOR.wizard.db_host|escape:html}" required>
<label class="bratonien-label">Port</label><input name="nc_wizard_db_port" type="number" min="1" max="65535" value="{$NC_CONNECTOR.wizard.db_port|escape:html}" required>
<label class="bratonien-label">Datenbank</label><input name="nc_wizard_db_database" type="text" value="{$NC_CONNECTOR.wizard.db_database|escape:html}" required>
<label class="bratonien-label">Reader-Benutzer</label><input name="nc_wizard_db_user" type="text" value="{$NC_CONNECTOR.wizard.db_user|escape:html}" required>
<label class="bratonien-label">Reader-Passwort</label><input name="nc_wizard_db_password" type="password" autocomplete="new-password" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Prüfen und automatisch ergänzen</button></p>
</form>
{elseif !$NC_CONNECTOR.wizard.technical_complete && $NC_CONNECTOR.wizard.storage_candidates|@count > 0}
<hr>
<h5>Einige Storages konnten nicht automatisch zugeordnet werden</h5>
<p class="bratonien-main-cache__warning">Mount-Pfade werden nur hier abgefragt, weil sie nicht sicher erkannt werden konnten. Bereits erkannte Zuordnungen sind gesperrt.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index}
<span class="bratonien-label">Storage {$storage.storage_id|escape:html} · {$storage.source_prefix|escape:html}</span>
{if $storage.local_mount}
<strong>{$storage.local_mount|escape:html}</strong>
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}">
{else}
<input name="nc_wizard_storage_mount[{$storage_index|escape:html}]" type="text" placeholder="/mnt/nextcloud/..." required>
{/if}
{/foreach}
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Zuordnung prüfen</button></p>
</form>
{elseif $NC_CONNECTOR.wizard.technical_complete}
<hr>
<h5>Erkannte Verbindung</h5>
<div class="bratonien-form-grid">
<span class="bratonien-label">Datenbank</span><strong>{$NC_CONNECTOR.wizard.db_host|escape:html}:{$NC_CONNECTOR.wizard.db_port|escape:html} / {$NC_CONNECTOR.wizard.db_database|escape:html}</strong>
<span class="bratonien-label">Reader</span><strong>{$NC_CONNECTOR.wizard.db_user|escape:html}</strong>
<span class="bratonien-label">Source-View</span><strong>{$NC_CONNECTOR.wizard.source_view|escape:html}</strong>
<span class="bratonien-label">Activity-View</span><strong>{$NC_CONNECTOR.wizard.activity_view|escape:html}</strong>
<span class="bratonien-label">Storages</span><strong>{$NC_CONNECTOR.wizard.storages|@count}</strong>
</div>
<hr>
<h5>Welcher Benutzer stellt die Bilder bereit?</h5>
<p class="bratonien-base-note"><strong>Empfehlung:</strong> ein eigener Showcase-Benutzer. So bleibt die Connector-Quelle unabhängig von persönlichen Konten.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_connection_name">Verbindungsname</label>
<input id="nc_wizard_connection_name" name="nc_wizard_connection_name" type="text" value="{$NC_CONNECTOR.wizard.connection_name|escape:html}" required>
<label class="bratonien-label" for="nc_wizard_showcase_user">Showcase-Benutzer</label>
{if $NC_CONNECTOR.wizard.can_list_users && $NC_CONNECTOR.wizard.users|@count > 0}
<select id="nc_wizard_showcase_user" name="nc_wizard_showcase_user" required>
{foreach from=$NC_CONNECTOR.wizard.users item=nc_user}
<option value="{$nc_user|escape:html}"{if $nc_user == 'showcase'} selected{/if}>{$nc_user|escape:html}{if $nc_user == 'showcase'} · empfohlen{/if}</option>
{/foreach}
</select>
{else}
<input id="nc_wizard_showcase_user" name="nc_wizard_showcase_user" type="text" value="showcase" required>
{/if}
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_select_user">Weiter zur Piwigo-API</button></p>
</form>
{/if}
<form method="post" style="margin-top:1rem" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_reset" data-bratonien-wizard-end>Neu beginnen</button>
</form>
{elseif $NC_CONNECTOR.wizard.step == 3}
<p class="bratonien-base-note"><strong>Nextcloud verbunden.</strong> Showcase-Benutzer: <strong>{$NC_CONNECTOR.wizard.showcase_user|escape:html}</strong></p>
<h5>Piwigo-API</h5>
<p class="bratonien-base-note">Die API ist der bevorzugte Weg. Du kannst neue Zugangsdaten eingeben, einen bereits gespeicherten API-Zugang testen oder diesen Schritt überspringen.</p>
{if $NC_CONNECTOR.wizard.api_error}
<p class="bratonien-main-cache__warning"><strong>Letzter API-Test:</strong> {$NC_CONNECTOR.wizard.api_error|escape:html}</p>
{/if}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_api_key_id">API-Schlüssel-ID</label>
<input id="nc_wizard_api_key_id" name="nc_wizard_api_key_id" type="text" autocomplete="off" placeholder="leer = gespeicherten Zugang verwenden">
<label class="bratonien-label" for="nc_wizard_api_key_secret">API-Geheimnis</label>
<input id="nc_wizard_api_key_secret" name="nc_wizard_api_key_secret" type="password" autocomplete="off" placeholder="leer = gespeicherten Zugang verwenden">
</div>
<div class="bratonien-actions">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_test">API testen</button>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_skip" formnovalidate>API überspringen</button>
</div>
</form>
{elseif $NC_CONNECTOR.wizard.step == 4}
<p class="bratonien-base-note"><strong>Abschluss.</strong> Nextcloud und technische Verbindung sind vorbereitet.</p>
<div class="bratonien-form-grid">
<span class="bratonien-label">Verbindung</span><strong>{$NC_CONNECTOR.wizard.connection_name|escape:html}</strong>
<span class="bratonien-label">Nextcloud</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
<span class="bratonien-label">Showcase-Benutzer</span><strong>{$NC_CONNECTOR.wizard.showcase_user|escape:html}</strong>
<span class="bratonien-label">Piwigo-API</span><strong>{if $NC_CONNECTOR.wizard.api_status == 'ok'}Erfolgreich getestet{if $NC_CONNECTOR.wizard.api_username} · {$NC_CONNECTOR.wizard.api_username|escape:html}{/if}{else}Übersprungen{/if}</strong>
</div>
<hr>
<h5>Fallback</h5>
{if $NC_CONNECTOR.wizard.api_status == 'ok'}
<p class="bratonien-base-note">Optional. Der Fallback wird nur benötigt, wenn die API später nicht nutzbar ist.</p>
{else}
<p class="bratonien-main-cache__warning">Die API wurde übersprungen. Deshalb ist ein Fallback-Zugang erforderlich.</p>
{/if}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_fallback_user">Piwigo-Benutzer</label>
<input id="nc_wizard_fallback_user" name="nc_wizard_fallback_user" type="text" autocomplete="username"{if $NC_CONNECTOR.wizard.api_status != 'ok'} required{/if}>
<label class="bratonien-label" for="nc_wizard_fallback_password">Piwigo-Passwort</label>
<input id="nc_wizard_fallback_password" name="nc_wizard_fallback_password" type="password" autocomplete="current-password"{if $NC_CONNECTOR.wizard.api_status != 'ok'} required{/if}>
</div>
<div class="bratonien-actions">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_finish" data-bratonien-wizard-end>Verbindung anlegen</button>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_reset" formnovalidate data-bratonien-wizard-end>Abbrechen</button>
</div>
</form>
{/if}
</div>
</dialog>
<details id="bratonien-nc-technical-create" style="margin-top:1.5rem">
<summary style="display:none">Ohne Assistent anlegen</summary>
<p class="bratonien-main-cache__warning">Technische Einrichtung. Falsche Storage- oder Galeriepfade können die Synchronisierung auf den falschen Datenbestand richten.</p>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_name">Name</label><input id="nc_name" name="nc_name" type="text" required>
<label class="bratonien-label" for="nc_host">PostgreSQL-Host</label><input id="nc_host" name="nc_host" type="text" required>
<label class="bratonien-label" for="nc_port">PostgreSQL-Port</label><input id="nc_port" name="nc_port" type="number" min="1" max="65535" value="5432" required>
<label class="bratonien-label" for="nc_database">Datenbank</label><input id="nc_database" name="nc_database" type="text" value="nextcloud" required>
<label class="bratonien-label" for="nc_user">Reader-Benutzer</label><input id="nc_user" name="nc_user" type="text" required>
<label class="bratonien-label" for="nc_db_password">Reader-Passwort</label><input id="nc_db_password" name="nc_db_password" type="password" autocomplete="new-password" required>
<label class="bratonien-label" for="nc_source_view">Source-View</label><input id="nc_source_view" name="nc_source_view" type="text" value="piwigo_showcase_sources" required>
<label class="bratonien-label" for="nc_activity_view">Activity-View</label><input id="nc_activity_view" name="nc_activity_view" type="text" value="piwigo_showcase_activity" required>
<label class="bratonien-label" for="nc_gallery_root">Piwigo-Galeriepfad</label><input id="nc_gallery_root" name="nc_gallery_root" type="text" placeholder="/var/www/piwigo/galleries/nextcloud" required>
<label class="bratonien-label" for="nc_piwigo_user">Piwigo-Fallback-Benutzer</label><input id="nc_piwigo_user" name="nc_piwigo_user" type="text">
<label class="bratonien-label" for="nc_piwigo_password">Piwigo-Fallback-Passwort</label><input id="nc_piwigo_password" name="nc_piwigo_password" type="password" autocomplete="new-password">
<label class="bratonien-label" for="nc_quiet_seconds">Ruhezeit</label><input id="nc_quiet_seconds" name="nc_quiet_seconds" type="number" min="0" value="120">
<label class="bratonien-label" for="nc_max_wait_seconds">Maximale Wartezeit</label><input id="nc_max_wait_seconds" name="nc_max_wait_seconds" type="number" min="60" value="900">
<label class="bratonien-label" for="nc_full_sync_seconds">Vollprüfung nach</label><input id="nc_full_sync_seconds" name="nc_full_sync_seconds" type="number" min="300" value="86400">
</div>
<p><label for="nc_storages"><strong>Storage-Zuordnungen</strong></label></p>
<p class="bratonien-base-note">Eine Zeile pro Storage: <code>storage_id | source_prefix | /lokaler/mount</code></p>
<textarea id="nc_storages" name="nc_storages" rows="4" style="width:100%" required></textarea>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_create_local">Verbindung anlegen</button></p>
</form>
</details>
</div>
<div class="bratonien-card" style="grid-column:1/-1">
<h4>Connector-Verbindungen</h4>
{if $NC_CONNECTOR.connection_count > 0}
<table class="table2">
<thead>
<tr><th>Name</th><th>Adapter</th><th>Host</th><th>Quelle</th><th>Storages</th><th>Status</th><th>Aktion</th></tr>
</thead>
<tbody>
{foreach from=$NC_CONNECTOR.connections item=connection}
<tr>
<td>{$connection.display_name|escape:html}</td>
<td>{$connection.adapter|escape:html}</td>
<td>{if $connection.host}{$connection.host|escape:html}{else}{/if}</td>
<td>{if $connection.source_view}{$connection.source_view|escape:html}{else}{/if}</td>
<td>{$connection.storage_count|escape:html}</td>
<td>{$connection.takeover_state|escape:html}{if $connection.enabled} · aktiv{/if}</td>
<td>
<details>
<summary>Verwalten</summary>
<form method="post" class="bratonien-actions" style="margin-top:.75rem">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="connection_id" value="{$connection.id|escape:html}">
<input name="connection_name" type="text" value="{$connection.name|escape:html}" required>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_name">Name speichern</button>
</form>
<div class="bratonien-actions">
{if $connection.adapter == 'local' && !$connection.enabled}
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="connection_id" value="{$connection.id|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_verify">Verbindung prüfen</button>
</form>
{/if}
{if !$connection.enabled && $connection.takeover_state != 'active'}
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="connection_id" value="{$connection.id|escape:html}">
<button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_delete" onclick="return confirm('Verbindung wirklich löschen? Es werden nur die Connector-Einstellungen entfernt, keine Nextcloud- oder Piwigo-Bilder.');">Löschen</button>
</form>
{/if}
</div>
<details style="margin-top:.75rem">
<summary>Technische Einstellungen</summary>
{if $connection.enabled || $connection.takeover_state == 'active'}
<p class="bratonien-main-cache__warning">Eine aktive Verbindung muss vor technischen Änderungen deaktiviert werden.</p>
{else}
<p class="bratonien-main-cache__warning">Nur ändern, wenn die technische Zuordnung bekannt ist. Storage- und Galeriepfade sind sicherheitskritisch.</p>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="connection_id" value="{$connection.id|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">PostgreSQL-Host</label><input name="nc_host" type="text" value="{$connection.config.host|escape:html}" required>
<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="{$connection.config.port|escape:html}" required>
<label class="bratonien-label">Datenbank</label><input name="nc_database" type="text" value="{$connection.config.database|escape:html}" required>
<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" type="text" value="{$connection.config.user|escape:html}" required>
<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" placeholder="leer = unverändert">
<label class="bratonien-label">Source-View</label><input name="nc_source_view" type="text" value="{$connection.config.source_view|escape:html}" required>
<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" type="text" value="{$connection.config.activity_view|escape:html}" required>
<label class="bratonien-label">Piwigo-Galeriepfad</label><input name="nc_gallery_root" type="text" value="{$connection.config.gallery_root|escape:html}" required>
<label class="bratonien-label">Ruhezeit</label><input name="nc_quiet_seconds" type="number" min="0" value="{$connection.config.quiet_seconds|escape:html}">
<label class="bratonien-label">Maximale Wartezeit</label><input name="nc_max_wait_seconds" type="number" min="60" value="{$connection.config.max_wait_seconds|escape:html}">
<label class="bratonien-label">Vollprüfung nach</label><input name="nc_full_sync_seconds" type="number" min="300" value="{$connection.config.full_sync_seconds|escape:html}">
</div>
<p><strong>Storage-Zuordnungen</strong></p>
<textarea name="nc_storages" rows="4" style="width:100%" required>{$connection.storage_text|escape:html}</textarea>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_technical">Technische Einstellungen speichern</button></p>
</form>
{/if}
</details>
</details>
</td>
</tr>
{if $connection.verification_checks|@count > 0}
<tr><td colspan="7"><strong>Letzte Verifikation{if $connection.verified_at} · {$connection.verified_at|escape:html}{/if}</strong><ul>{foreach from=$connection.verification_checks item=check}<li>{if $check.ok}{else}{/if} <strong>{$check.name|escape:html}:</strong> {$check.detail|escape:html}</li>{/foreach}</ul></td></tr>
{/if}
{if isset($connection.last_sync) && $connection.last_sync.timestamp > 0}
<tr><td colspan="7"><strong>Letzter Sync · {$connection.last_sync.label|escape:html}</strong><p class="bratonien-base-note"><strong>Ergebnis:</strong> {$connection.last_sync.message|escape:html}</p>{if $connection.last_sync.api_state == 'error'}<p class="bratonien-main-cache__warning"><strong>API fehlgeschlagen:</strong> {$connection.last_sync.api_message|escape:html}</p>{/if}{if $connection.last_sync.fallback_state == 'error'}<p class="bratonien-main-cache__warning"><strong>Fallback fehlgeschlagen:</strong> {$connection.last_sync.fallback_message|escape:html}</p>{/if}{if $connection.last_sync.error_detail}<p class="bratonien-main-cache__warning"><strong>Technischer Fehler:</strong> {$connection.last_sync.error_detail|escape:html}</p>{/if}</td></tr>
{/if}
{/foreach}
</tbody>
</table>
{else}
<p>Noch keine Verbindung in der Connector-Verwaltung.</p>
{/if}
</div>
<div class="bratonien-card" style="grid-column:1/-1">
<h4>Piwigo API bevorzugter Sync-Zugang</h4>
<p class="bratonien-base-note">Hier kann der globale API-Zugang unabhängig vom Assistenten geprüft, ersetzt oder gelöscht werden.</p>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_piwigo_api_key_id">API-Schlüssel-ID</label><input id="nc_piwigo_api_key_id" name="nc_piwigo_api_key_id" type="text" autocomplete="off" placeholder="pkid-..." required>
<label class="bratonien-label" for="nc_piwigo_api_key_secret">API-Geheimnis</label><input id="nc_piwigo_api_key_secret" name="nc_piwigo_api_key_secret" type="password" autocomplete="off" required>
</div>
<div class="bratonien-actions">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_test">API prüfen und speichern</button>
<button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_delete" formnovalidate onclick="return confirm('Gespeicherte Piwigo-API-Zugangsdaten wirklich löschen?');">Gespeicherte API löschen</button>
</div>
</form>
{if isset($NC_CONNECTOR.piwigo_api_test) && $NC_CONNECTOR.piwigo_api_test}
<p class="bratonien-base-note"><strong>Bewertung:</strong> {$NC_CONNECTOR.piwigo_api_test.conclusion|escape:html}</p>
{/if}
</div>
<div class="bratonien-card" style="grid-column:1/-1">
<h4>Benutzername/Passwort-Fallback</h4>
{if $NC_CONNECTOR.connection_count > 0}
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_fallback_connection">Verbindung</label><select id="nc_fallback_connection" name="connection_id" required>{foreach from=$NC_CONNECTOR.connections item=connection}<option value="{$connection.id|escape:html}">{$connection.name|escape:html}</option>{/foreach}</select>
<label class="bratonien-label" for="nc_fallback_user">Piwigo-Benutzer</label><input id="nc_fallback_user" name="nc_fallback_user" type="text" autocomplete="username">
<label class="bratonien-label" for="nc_fallback_password">Piwigo-Passwort</label><input id="nc_fallback_password" name="nc_fallback_password" type="password" autocomplete="current-password">
</div>
<div class="bratonien-actions">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_once">Einmalig verwenden</button>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_save">Fest speichern</button>
<button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_fallback_delete" formnovalidate onclick="return confirm('Gespeicherten Benutzername/Passwort-Fallback wirklich löschen?');">Gespeicherten Fallback löschen</button>
</div>
</form>
{else}<p class="bratonien-base-note">Noch keine Connector-Verbindung vorhanden.</p>{/if}
</div>
</div>
<script>
(function () {
var dialog = document.getElementById('bratonien-nc-wizard-dialog');
var openButton = document.getElementById('bratonien-nc-wizard-open');
var closeButton = document.getElementById('bratonien-nc-wizard-close');
var technicalButton = document.getElementById('bratonien-nc-technical-open');
var technical = document.getElementById('bratonien-nc-technical-create');
var storageKey = 'bratonienNcWizardOpen';
<dialog id="bratonien-nc-wizard-dialog" style="width:min(980px,calc(100vw - 3rem));max-height:88vh;overflow:auto;background:#444;color:inherit;border:1px solid #777;border-radius:4px;padding:0;box-shadow:0 18px 60px rgba(0,0,0,.55)">
<div style="padding:1.25rem 1.5rem">
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem">
<div><h4 style="margin:0">Neue Verbindung</h4><p class="bratonien-base-note" style="margin:.35rem 0 0"><strong>Schritt {$NC_CONNECTOR.wizard.step|escape:html} von 4</strong></p></div>
<button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button>
</div>
if (openButton && dialog) {
openButton.addEventListener('click', function () {
sessionStorage.setItem(storageKey, '1');
if (typeof dialog.showModal === 'function') dialog.showModal();
else dialog.setAttribute('open', 'open');
});
}
{if $NC_CONNECTOR.wizard.step == 1}
<p class="bratonien-base-note">Adresse und Zugang reichen für den ersten Scan. Der Assistent prüft den erreichbaren Web-Zugang automatisch.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_host">Nextcloud-Adresse</label>
<input id="nc_wizard_host" name="nc_wizard_host" type="text" placeholder="cloud.example.de" value="{$NC_CONNECTOR.wizard.host_input|escape:html}" required>
<label class="bratonien-label" for="nc_wizard_user">Benutzer</label>
<input id="nc_wizard_user" name="nc_wizard_user" type="text" autocomplete="username" value="{$NC_CONNECTOR.wizard.username|escape:html}" required>
<label class="bratonien-label" for="nc_wizard_password">Passwort</label>
<input id="nc_wizard_password" name="nc_wizard_password" type="password" autocomplete="current-password" value="{$NC_CONNECTOR.wizard._password|escape:html}" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_scan">Verbinden und scannen</button></p>
</form>
if (closeButton && dialog) {
closeButton.addEventListener('click', function () {
sessionStorage.removeItem(storageKey);
if (typeof dialog.close === 'function') dialog.close();
else dialog.removeAttribute('open');
});
}
{elseif $NC_CONNECTOR.wizard.step == 2}
<p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Der Assistent prüft jetzt, wie die freigegebenen Dateien gelesen werden können.</p>
<div class="bratonien-form-grid">
<span class="bratonien-label">Adresse</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
<span class="bratonien-label">Version</span><strong>{if $NC_CONNECTOR.wizard.version}{$NC_CONNECTOR.wizard.version|escape:html}{else}Nicht gemeldet{/if}</strong>
<span class="bratonien-label">Angemeldet als</span><strong>{$NC_CONNECTOR.wizard.username|escape:html}{if $NC_CONNECTOR.wizard.display_name} · {$NC_CONNECTOR.wizard.display_name|escape:html}{/if}</strong>
</div>
if (dialog) {
dialog.addEventListener('cancel', function () {
sessionStorage.removeItem(storageKey);
});
dialog.addEventListener('click', function (event) {
if (event.target === dialog) {
sessionStorage.removeItem(storageKey);
if (typeof dialog.close === 'function') dialog.close();
}
});
dialog.querySelectorAll('form[data-bratonien-wizard-form]').forEach(function (form) {
form.addEventListener('submit', function (event) {
var submitter = event.submitter;
if (submitter && submitter.hasAttribute('data-bratonien-wizard-end')) sessionStorage.removeItem(storageKey);
else sessionStorage.setItem(storageKey, '1');
});
});
if (sessionStorage.getItem(storageKey) === '1') {
if (typeof dialog.showModal === 'function' && !dialog.open) dialog.showModal();
else dialog.setAttribute('open', 'open');
}
}
{if $NC_CONNECTOR.wizard.technical_stage == 'reader_credentials'}
<hr>
<h5>Datenzugriff</h5>
<p class="bratonien-base-note">Nextcloud gibt den internen Lesezugang aus Sicherheitsgründen nicht über die normale API aus. Bitte nur den dafür eingerichteten Lese-Benutzer angeben. Die üblichen Verbindungswerte prüft der Assistent selbst.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Lese-Benutzer</label><input name="nc_wizard_db_user" type="text" value="{$NC_CONNECTOR.wizard.db_user|escape:html}" required>
<label class="bratonien-label">Passwort</label><input name="nc_wizard_db_password" type="password" autocomplete="new-password" value="{$NC_CONNECTOR.wizard._db_password|escape:html}" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Automatisch prüfen</button></p>
</form>
if (technicalButton && technical) {
technicalButton.addEventListener('click', function () {
technical.open = !technical.open;
if (technical.open) technical.scrollIntoView({behavior:'smooth', block:'start'});
});
}
}());
</script>
{elseif $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
<hr>
<h5>Datenbank-Adresse prüfen</h5>
<p class="bratonien-base-note">Der übliche Weg hat nicht funktioniert. Deshalb werden jetzt nur die Angaben abgefragt, die davon abweichen können.</p>
{if $NC_CONNECTOR.wizard.technical_error}<details><summary>Technische Details</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.wizard.technical_error|escape:html}</p></details>{/if}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Datenbank-Adresse</label><input name="nc_wizard_db_host" type="text" value="{$NC_CONNECTOR.wizard.db_host|escape:html}" required>
<label class="bratonien-label">Port</label><input name="nc_wizard_db_port" type="number" min="1" max="65535" value="{$NC_CONNECTOR.wizard.db_port|escape:html}" required>
<label class="bratonien-label">Datenbank</label><input name="nc_wizard_db_database" type="text" value="{$NC_CONNECTOR.wizard.db_database|escape:html}" required>
<label class="bratonien-label">Lese-Benutzer</label><input name="nc_wizard_db_user" type="text" value="{$NC_CONNECTOR.wizard.db_user|escape:html}" required>
<label class="bratonien-label">Passwort</label><input name="nc_wizard_db_password" type="password" autocomplete="new-password" value="{$NC_CONNECTOR.wizard._db_password|escape:html}" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Erneut prüfen</button></p>
</form>
{elseif $NC_CONNECTOR.wizard.technical_stage == 'mounts'}
<hr>
<h5>Speicherort bestätigen</h5>
<p class="bratonien-base-note">Die Datenquelle wurde gefunden. Nur Speicherorte, die nicht sicher erkannt werden konnten, müssen jetzt einmalig zugeordnet werden.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index}
<span class="bratonien-label">Speicher {$storage_index+1}</span>
{if $storage.local_mount}
<strong>Automatisch erkannt</strong>
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}">
{else}
<input name="nc_wizard_storage_mount[{$storage_index|escape:html}]" type="text" placeholder="Eingebundener Speicherpfad" required>
{/if}
{/foreach}
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Speicher prüfen</button></p>
</form>
{elseif $NC_CONNECTOR.wizard.technical_complete}
<hr>
<h5>Welche Freigaben sollen verwendet werden?</h5>
<p class="bratonien-base-note"><strong>Empfehlung:</strong> ein eigener Showcase-Benutzer. So bleibt die Verbindung unabhängig von persönlichen Konten.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_connection_name">Name der Verbindung</label><input id="nc_wizard_connection_name" name="nc_wizard_connection_name" type="text" value="{$NC_CONNECTOR.wizard.connection_name|escape:html}" required>
<label class="bratonien-label" for="nc_wizard_showcase_user">Nextcloud-Benutzer</label>
{if $NC_CONNECTOR.wizard.can_list_users && $NC_CONNECTOR.wizard.users|@count > 0}
<select id="nc_wizard_showcase_user" name="nc_wizard_showcase_user" required>
{foreach from=$NC_CONNECTOR.wizard.users item=nc_user}<option value="{$nc_user|escape:html}"{if $nc_user == 'showcase'} selected{/if}>{$nc_user|escape:html}{if $nc_user == 'showcase'} · empfohlen{/if}</option>{/foreach}
</select>
{else}
<input id="nc_wizard_showcase_user" name="nc_wizard_showcase_user" type="text" value="{if $NC_CONNECTOR.wizard.showcase_user}{$NC_CONNECTOR.wizard.showcase_user|escape:html}{else}showcase{/if}" required>
{/if}
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_select_user">Weiter</button></p>
</form>
{/if}
<form method="post" style="margin-top:1rem" data-bratonien-wizard-form><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_reset" data-bratonien-wizard-end>Neu beginnen</button></form>
{elseif $NC_CONNECTOR.wizard.step == 3}
<p class="bratonien-base-note"><strong>Nextcloud ist vorbereitet.</strong> Jetzt wird der bevorzugte Piwigo-Zugang geprüft.</p>
{if $NC_CONNECTOR.wizard.api_error}<p class="bratonien-main-cache__warning"><strong>API-Test fehlgeschlagen:</strong> {$NC_CONNECTOR.wizard.api_error|escape:html}</p>{/if}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_api_key_id">API-Schlüssel-ID</label><input id="nc_wizard_api_key_id" name="nc_wizard_api_key_id" type="text" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_id|escape:html}" placeholder="leer = bereits gespeicherten Zugang testen">
<label class="bratonien-label" for="nc_wizard_api_key_secret">API-Geheimnis</label><input id="nc_wizard_api_key_secret" name="nc_wizard_api_key_secret" type="password" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_secret|escape:html}" placeholder="leer = bereits gespeicherten Zugang testen">
</div>
<div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_test">API testen</button><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_skip" formnovalidate>Überspringen</button></div>
</form>
{elseif $NC_CONNECTOR.wizard.step == 4}
<p class="bratonien-base-note"><strong>Fast fertig.</strong> Die Verbindung wurde noch nicht angelegt. Erst der letzte Button übernimmt die Einstellungen.</p>
<div class="bratonien-form-grid">
<span class="bratonien-label">Verbindung</span><strong>{$NC_CONNECTOR.wizard.connection_name|escape:html}</strong>
<span class="bratonien-label">Nextcloud</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
<span class="bratonien-label">Freigaben von</span><strong>{$NC_CONNECTOR.wizard.showcase_user|escape:html}</strong>
<span class="bratonien-label">Piwigo-API</span><strong>{if $NC_CONNECTOR.wizard.api_status == 'ok'}Erfolgreich geprüft{else}Übersprungen{/if}</strong>
</div>
<hr>
<h5>Fallback</h5>
{if $NC_CONNECTOR.wizard.api_status == 'ok'}<p class="bratonien-base-note">Optional. Wird nur verwendet, falls die API später nicht verfügbar ist.</p>{else}<p class="bratonien-main-cache__warning">Ohne API ist ein Fallback-Zugang erforderlich.</p>{/if}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_fallback_user">Piwigo-Benutzer</label><input id="nc_wizard_fallback_user" name="nc_wizard_fallback_user" type="text" autocomplete="username" value="{$NC_CONNECTOR.wizard._fallback_user|escape:html}"{if $NC_CONNECTOR.wizard.api_status != 'ok'} required{/if}>
<label class="bratonien-label" for="nc_wizard_fallback_password">Piwigo-Passwort</label><input id="nc_wizard_fallback_password" name="nc_wizard_fallback_password" type="password" autocomplete="current-password" value="{$NC_CONNECTOR.wizard._fallback_password|escape:html}"{if $NC_CONNECTOR.wizard.api_status != 'ok'} required{/if}>
</div>
<div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_finish" data-bratonien-wizard-end>Verbindung anlegen</button><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_reset" formnovalidate data-bratonien-wizard-end>Abbrechen</button></div>
</form>
{/if}
</div>
</dialog>
<details id="bratonien-nc-technical-create" class="bratonien-card" style="margin-top:1.5rem">
<summary style="display:none">Ohne Assistent anlegen</summary>
<h4>Technische Einrichtung</h4>
<p class="bratonien-main-cache__warning">Nur verwenden, wenn die technische Zuordnung bekannt ist. Falsche Pfade können die Synchronisierung auf den falschen Datenbestand richten.</p>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Name</label><input name="nc_name" type="text" required>
<label class="bratonien-label">PostgreSQL-Host</label><input name="nc_host" type="text" required>
<label class="bratonien-label">PostgreSQL-Port</label><input name="nc_port" type="number" min="1" max="65535" value="5432" required>
<label class="bratonien-label">Datenbank</label><input name="nc_database" type="text" value="nextcloud" required>
<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" type="text" required>
<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" required>
<label class="bratonien-label">Source-View</label><input name="nc_source_view" type="text" value="piwigo_showcase_sources" required>
<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" type="text" value="piwigo_showcase_activity" required>
<label class="bratonien-label">Piwigo-Galeriepfad</label><input name="nc_gallery_root" type="text" placeholder="/var/www/piwigo/galleries/nextcloud" required>
<label class="bratonien-label">Fallback-Benutzer</label><input name="nc_piwigo_user" type="text">
<label class="bratonien-label">Fallback-Passwort</label><input name="nc_piwigo_password" type="password" autocomplete="new-password">
<label class="bratonien-label">Ruhezeit</label><input name="nc_quiet_seconds" type="number" min="0" value="120">
<label class="bratonien-label">Maximale Wartezeit</label><input name="nc_max_wait_seconds" type="number" min="60" value="900">
<label class="bratonien-label">Vollprüfung nach</label><input name="nc_full_sync_seconds" type="number" min="300" value="86400">
</div>
<p><strong>Storage-Zuordnungen</strong></p><p class="bratonien-base-note">Format: Storage-ID | optionales Quellpräfix | lokaler Speicherpfad</p><textarea name="nc_storages" rows="4" style="width:100%" required></textarea>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_create_local">Verbindung anlegen</button></p>
</form>
</details>
<div class="bratonien-card" style="margin-top:1.5rem">
<h4>Bestehende Verbindungen</h4>
{if $NC_CONNECTOR.connection_count > 0}
{foreach from=$NC_CONNECTOR.connections item=connection}
<details style="margin:.6rem 0">
<summary><strong>{$connection.display_name|escape:html}</strong> · {if $connection.enabled}aktiv{else}{$connection.takeover_state|escape:html}{/if}</summary>
<div style="padding:.75rem 0">
<form method="post" class="bratonien-actions"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><input name="connection_name" type="text" value="{$connection.name|escape:html}" required><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_name">Name speichern</button></form>
<div class="bratonien-actions" style="margin-top:.6rem">
{if $connection.adapter == 'local' && !$connection.enabled}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_verify">Verbindung prüfen</button></form>{/if}
{if !$connection.enabled && $connection.takeover_state != 'active'}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_delete" onclick="return confirm('Verbindung wirklich löschen? Bilder in Nextcloud oder Piwigo werden nicht gelöscht.');">Löschen</button></form>{/if}
</div>
{if isset($connection.last_sync) && $connection.last_sync.timestamp > 0}<p class="bratonien-base-note"><strong>Letzter Abgleich:</strong> {$connection.last_sync.label|escape:html} · {$connection.last_sync.message|escape:html}</p>{/if}
{if $connection.verification_checks|@count > 0}<details><summary>Letzte Prüfung</summary><ul>{foreach from=$connection.verification_checks item=check}<li>{if $check.ok}{else}{/if} {$check.name|escape:html}: {$check.detail|escape:html}</li>{/foreach}</ul></details>{/if}
<details style="margin-top:.75rem"><summary>Technische Einstellungen</summary>
{if $connection.enabled || $connection.takeover_state == 'active'}<p class="bratonien-main-cache__warning">Aktive Verbindungen können technisch nicht geändert werden.</p>{else}
<p class="bratonien-main-cache__warning">Nur ändern, wenn die technische Zuordnung bekannt ist.</p>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">PostgreSQL-Host</label><input name="nc_host" type="text" value="{$connection.config.host|escape:html}" required>
<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="{$connection.config.port|escape:html}" required>
<label class="bratonien-label">Datenbank</label><input name="nc_database" type="text" value="{$connection.config.database|escape:html}" required>
<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" type="text" value="{$connection.config.user|escape:html}" required>
<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" placeholder="leer = unverändert">
<label class="bratonien-label">Source-View</label><input name="nc_source_view" type="text" value="{$connection.config.source_view|escape:html}" required>
<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" type="text" value="{$connection.config.activity_view|escape:html}" required>
<label class="bratonien-label">Piwigo-Galeriepfad</label><input name="nc_gallery_root" type="text" value="{$connection.config.gallery_root|escape:html}" required>
<label class="bratonien-label">Ruhezeit</label><input name="nc_quiet_seconds" type="number" min="0" value="{$connection.config.quiet_seconds|escape:html}">
<label class="bratonien-label">Maximale Wartezeit</label><input name="nc_max_wait_seconds" type="number" min="60" value="{$connection.config.max_wait_seconds|escape:html}">
<label class="bratonien-label">Vollprüfung nach</label><input name="nc_full_sync_seconds" type="number" min="300" value="{$connection.config.full_sync_seconds|escape:html}">
</div>
<p><strong>Storage-Zuordnungen</strong></p><p class="bratonien-base-note">Format: Storage-ID | optionales Quellpräfix | lokaler Speicherpfad</p><textarea name="nc_storages" rows="4" style="width:100%" required>{$connection.storage_text|escape:html}</textarea>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_technical">Technische Einstellungen speichern</button></p>
</form>
{/if}
</details>
</div>
</details>
{/foreach}
{else}<p class="bratonien-base-note">Noch keine Verbindung vorhanden.</p>{/if}
</div>
<details class="bratonien-card" style="margin-top:1.5rem"><summary>Erweiterte Piwigo-Zugänge</summary>
<h4>Piwigo API</h4>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">API-Schlüssel-ID</label><input name="nc_piwigo_api_key_id" type="text" autocomplete="off" required><label class="bratonien-label">API-Geheimnis</label><input name="nc_piwigo_api_key_secret" type="password" autocomplete="off" required></div><div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_test">API prüfen und speichern</button><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_delete" formnovalidate>Gespeicherte API löschen</button></div></form>
<h4 style="margin-top:1rem">Fallback speichern</h4>
{if $NC_CONNECTOR.connection_count > 0}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">Verbindung</label><select name="connection_id" required>{foreach from=$NC_CONNECTOR.connections item=connection}<option value="{$connection.id|escape:html}">{$connection.name|escape:html}</option>{/foreach}</select><label class="bratonien-label">Piwigo-Benutzer</label><input name="nc_fallback_user" type="text" autocomplete="username"><label class="bratonien-label">Piwigo-Passwort</label><input name="nc_fallback_password" type="password" autocomplete="current-password"></div><div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_save">Fallback speichern</button><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_fallback_delete" formnovalidate>Fallback löschen</button></div></form>{/if}
<h4 style="margin-top:1rem">Einmaliger Fallback-Test</h4>
<p class="bratonien-base-note">Verwendet die eingegebenen Piwigo-Zugangsdaten einmalig und speichert sie nicht.</p>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">Piwigo-Benutzer</label><input name="nc_fallback_user" type="text" required><label class="bratonien-label">Piwigo-Passwort</label><input name="nc_fallback_password" type="password" required></div><p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_once">Einmalig testen</button></p></form>
</details>
{literal}
<script>
(function(){
var dialog=document.getElementById('bratonien-nc-wizard-dialog');
var openButton=document.getElementById('bratonien-nc-wizard-open');
var closeButton=document.getElementById('bratonien-nc-wizard-close');
var technicalButton=document.getElementById('bratonien-nc-technical-open');
var technical=document.getElementById('bratonien-nc-technical-create');
var key='bratonienNcWizardOpen';
function openWizard(){try{sessionStorage.setItem(key,'1');}catch(e){} if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();else dialog.setAttribute('open','open');}
function closeWizard(){try{sessionStorage.removeItem(key);}catch(e){} if(typeof dialog.close==='function')dialog.close();else dialog.removeAttribute('open');}
if(openButton&&dialog)openButton.addEventListener('click',openWizard);
if(closeButton&&dialog)closeButton.addEventListener('click',closeWizard);
if(dialog){
dialog.addEventListener('cancel',function(){try{sessionStorage.removeItem(key);}catch(e){}});
dialog.addEventListener('click',function(e){if(e.target===dialog)closeWizard();});
dialog.querySelectorAll('form[data-bratonien-wizard-form]').forEach(function(form){form.addEventListener('submit',function(e){var s=e.submitter;try{if(s&&s.hasAttribute('data-bratonien-wizard-end'))sessionStorage.removeItem(key);else sessionStorage.setItem(key,'1');}catch(x){}});});
try{if(sessionStorage.getItem(key)==='1')openWizard();}catch(e){}
}
if(technicalButton&&technical)technicalButton.addEventListener('click',function(){technical.open=!technical.open;if(technical.open)technical.scrollIntoView(true);});
})();
</script>
{/literal}
</section>