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 # Aktueller Entwicklungsstand
Stand: 17.08.2026 Stand: 18.08.2026
## Plugin ## Plugin
- Aktuelle Plugin-Version: **0.9.3.17** - Aktuelle Plugin-Version: **0.9.4.2**
- Aktueller Entwicklungsblock: **NC Connector Verbindungsverwaltung / laufende Optimierung** - Aktueller Entwicklungsblock: **NC Connector Endnutzer-Assistent und Konsolidierung**
- NC Connector ist Feature 10 und noch nicht vollständig abgeschlossen. - `0.9.3.43` markiert den abgeschlossenen vorherigen Meilenstein.
- Solange dieser Optimierungsblock läuft, bleibt die Version im Bereich `0.9.3.x`. - `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. ## 0.9.4.1
- 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.
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; ## 0.9.4.2
- 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; ### Verbindungsassistent
- 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 Assistent wurde erneut aus Endnutzersicht konsolidiert:
- 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; 1. Nextcloud-Adresse + Benutzer + Passwort.
- 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. 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 ## Architektur NC Connector
- Nextcloud bleibt die einzige dauerhafte Quelle der Originalbilder. - Nextcloud ist Quelle der Originalbilder.
- Piwigo erhält nur die benötigte Verzeichnis-/Symlink-Struktur und erzeugt daraus seine Derivate und Cache-Dateien. - Piwigo erhält einen Shadow Tree aus Verzeichnissen und Symlinks.
- Der Connector arbeitet mit einem PostgreSQL-Leser und den Views `piwigo_showcase_sources` und `piwigo_showcase_activity`. - PostgreSQL-Reader und die Views `piwigo_showcase_sources` / `piwigo_showcase_activity` liefern Quellen und Aktivität.
- Laufzeitdaten liegen unter `/var/lib/bratonien-tools/nc-connector/...`. - Runtime-Konfigurationen: `/etc/bratonien-tools/nc-connector/connection-*.conf`
- Verbindungs-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/connection-*.conf`. - State: `/var/lib/bratonien-tools/nc-connector/connection-ID`
- Der Shadow Tree enthält Verzeichnisse und Symlinks, keine Kopien der Originaldateien. - Öffentlicher Admin-Status: Piwigo `_data/bratonien-tools/nc-connector-status/`
- Alle aktivierten Verbindungen werden über `runtime/run-all.sh` gemeinsam verarbeitet. - Runner: `runtime/run-all.sh`
- Einzelverbindung: `runtime/sync.sh`
## Nextcloud-Freigabemodell ## Piwigo-Synchronisierung
Der Connector unterstützt komplette Ordner und einzelne Bilder. Priorität:
- `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:
1. gespeicherter Piwigo-API-Key; 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; 2. bei nicht nutzbarer API gespeicherter Benutzername/Passwort-Fallback;
3. ohne nutzbaren API- und Fallback-Zugang wird der Piwigo-Sync mit einer klaren Fehlermeldung abgebrochen. 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; ## Sicherheit
- 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.
## 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`; - vollständiger Remote-Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff;
2. API-Key im NC Connector prüfen und speichern; - weitere Bereinigung alter Legacy-Migrationshelfer, sobald sie nicht mehr benötigt werden;
3. produktiver Lauf verwendet `bratonien.nc.syncProductive` erfolgreich; - weitere Endnutzer-Optimierung des Assistenten auf Basis realer Fehler- und Installationspfade.
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.
## Repository-Fallback ## 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 # 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 ### 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
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 Ablauf:
- `file` - einzelne Dateifreigabe; die Datei wird direkt als Symlink im Galerie-Root angelegt und separat als Piwigo-Orphan ohne Albumzuordnung registriert
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 Der derzeit produktive Adapter verwendet:
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 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 Der regelmäßige Lauf berücksichtigt:
- 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
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 Der produktive Lauf ist API-first:
- vorhandene Bildderivate neu erzeugen
- Cache-Aufbau als Worker-Prozess starten und abbrechen
- Worker-Einstellungen verwalten
- Originalbilder bleiben unangetastet
### 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 ### Runtime
- Wasserzeichenprofile
- Standardprofil
- albumbezogene Regeln
- getrennte Einstellungen fuer Positionierung und Verarbeitung
- eigener Runtime-Filter fuer Piwigo-Bildderivate
- Aktivierung und Deaktivierung der erweiterten Wasserzeichen-Engine
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/` Der Shadow-Tree-Austausch besitzt einen Rollback: Scheitert der Wechsel auf den neuen Baum, wird der vorherige Galeriebaum wiederhergestellt.
- 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`
### 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 ### Bestehende Verbindungen
- 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
**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 Für ältere Installationen mit dem früheren separaten `piwigo-sync` existieren weiterhin Migrations-/Cutover-Helfer. Neue Installationen sollen den nativen Connector-Weg verwenden.
- 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
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 ## Bilddateien und Pfade
- 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
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 **Abhängigkeit:** Batch Downloader muss installiert und aktiv sein.
- 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
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 ## Geschützte Albumfreigaben
- rechts: naechstes Bild
- oben mittig: zurueck zur Vorschau beziehungsweise Albumansicht
- Mitte: PhotoSwipe / Vollbildansicht
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 ## Selbstaktualisierung
- Update-Pruefung wird kurzzeitig zwischengespeichert
- Updates duerfen nur vom Piwigo-Webmaster ausgefuehrt werden Der integrierte Updater:
- Download des aktuellen `main`-Branches als ZIP
- Pruefung auf `ZipArchive` und Schreibrechte des Plugin-Verzeichnisses - liest den Zielstand aus GitHub;
- detailliertere Downloadfehler - bindet ein Update an einen konkreten Commit statt an einen während des Updates beweglichen Branch-Stand;
- Status wird nach Update-Pruefungen und Aktionen neu eingelesen - 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 ## 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 und Runtime-Hooks
- `admin.php` zentraler Admin-Controller mit Post/Redirect/Get
- `main.inc.php` - Plugin-Einstieg, Runtime-Module und Admin-Menue - `include/tool_registry.inc.php` Registry administrativer Aktionen
- `admin.php` - zentraler Admin-Controller - `include/nc_connector.inc.php` Datenmodell und gemeinsame Connector-Funktionen
- `include/tool_registry.inc.php` - Registry der administrativen Aktionen - `include/nc_connector_wizard.inc.php` Endnutzer-Assistent und Connection-Bearbeitung
- `include/nc_connector.inc.php` - Connector-Datenmodell, Legacy-Import und gemeinsame Low-Level-Funktionen - `include/nc_connector_manage.inc.php` Credential-Format, Storage-Mappings, Verifikation und Löschen
- `include/nc_connector_manage.inc.php` - native Connection-Verwaltung und Verifikation - `include/nc_connector_create_api.inc.php` API-first-Verbindungserstellung
- `include/nc_connector_takeover.inc.php` - kontrollierte Legacy-Uebergabe - `include/nc_connector_piwigo_api.inc.php` API-Zugang und Fallback-Verwaltung
- `include/nc_connector_system.inc.php` - Timer- und Laufzeitstatus ueber alle aktiven Verbindungen - `include/nc_connector_system.inc.php` Timer- und Laufzeitstatus
- `include/nc_connector_ws.inc.php` - read-only Paritaets-/Synchronisationspruefung ueber den Piwigo-Webservice - `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync
- `include/nc_orphan_ws.inc.php` - produktive Synchronisation einzelner Root-Dateien als Piwigo-Orphans - `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung
- `runtime/sync.sh` - Plugin-eigene Sync-Runtime einer Verbindung - `runtime/lib/activity_gate.py` Activity Gate
- `runtime/run-all.sh` - gemeinsamer Runner fuer alle installierten Verbindungen - `runtime/lib/build_manifest.py` Auflösung der Nextcloud-Freigaben auf Storage-Mounts
- `runtime/lib/activity_gate.py` - Activity-Gate, Share-Fingerprint und zeitgesteuerte Reconciliation - `runtime/lib/shadow_tree.py` atomarer Shadow Tree mit Rollback
- `runtime/lib/build_manifest.py` - Aufloesung von Ordner- und Dateifreigaben auf konfigurierte Storage-Mounts - `runtime/lib/piwigo-sync.php` API-first-Piwigo-Sync mit Fallback
- `runtime/lib/shadow_tree.py` - Aufbau des Piwigo-kompatiblen Shadow Trees ohne Kopieren der Originale - `runtime/run-all.sh` Multi-Connection-Runner
- `runtime/lib/piwigo-db-check.php` - Konsistenzpruefung vorhandener Piwigo-Alben - `runtime/sync.sh` Ablauf einer Verbindung
- `runtime/lib/piwigo-db-sync.pl` - Ausloesen der Piwigo-Dateisynchronisierung und anschliessender Orphan-Abgleich - `nc-connector-install.php` native Root-Aktivierung
- `nc-connector-install.php` - native Aktivierung einer verifizierten Verbindung - `nc-connector-disable.php` Deaktivierung
- `nc-connector-disable.php` - kontrollierte Deaktivierung einer aktiven Verbindung - `nc-connector-normalize.php` Normalisierung älterer aktiver Verbindungen
- `nc-connector-normalize.php` - Ueberfuehrung einer migrierten aktiven Verbindung in den nativen State- und Multi-Connection-Aufbau - `nc-connector-*-cleanup/switch/cutover` Legacy-Migrationshelfer
- `nc-connector-migrate.php` - einmaliger Migrationshelfer fuer bestehende Legacy-Installationen - `include/self_update.inc.php` Self-Updater
- `nc-connector-cutover-v2.php` - kontrollierter Legacy-Cutover - `include/album_shares.inc.php` geschützte Albumfreigaben
- `nc-connector-runtime-switch.php` - Umstellung einer aktiven Legacy-Verbindung auf die Plugin-eigene Runtime - `include/public_selection.inc.php` Fotoauswahl
- `nc-connector-legacy-cleanup.php` - kontrollierte Entfernung alter Sync-Scripts und systemd-Units - `include/batch_titles.inc.php` fortlaufende Titel
- `include/public_selection.inc.php` - oeffentliche Fotoauswahl und Batch-Downloader-Anbindung - `include/watermark_*.inc.php` Wasserzeichen-Engine
- `include/picture_navigation.inc.php` - Einbindung der erweiterten Bildnavigation - `tools/` administrative Einzelwerkzeuge
- `include/batch_titles.inc.php` - fortlaufende Titelvergabe in Piwigos Stapelverarbeitung - `template/`, `js/`, `css/` Oberfläche
- `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.
## Sicherheit ## Sicherheit
Administrative Aktionen pruefen Piwigo-Berechtigungen und verwenden fuer schreibende Aktionen Piwigos CSRF-Schutz. - administrative Schreibaktionen verwenden Piwigos CSRF-Schutz;
- Connector-Zugangsdaten werden verschlüsselt gespeichert;
Weitere Schutzmechanismen sind funktionsabhaengig, unter anderem: - Wizard-Geheimnisse werden nicht in Browser-Web-Storage persistiert;
- Storage-Mounts werden explizit zugeordnet und validiert;
- verschluesselte Speicherung der Connector-Zugangsdaten mit einem lokalen Connector-Secret - SQL-View-Namen werden vor Verwendung validiert;
- PostgreSQL-Reader mit minimalen Leserechten fuer lokale Nextcloud-Verbindungen - gefährliche technische Pfade sind nicht Teil des normalen Wizard-Happy-Paths;
- explizite Storage-Zuordnungen statt automatischer Freigabe beliebiger Dateipfade - produktive Piwigo-API ist versionsgebunden;
- Verifikation von Datenbank, Views und Mounts vor einer Aktivierung - Originalbilder werden vom Connector nicht gelöscht;
- Runtime-Test vor der nativen Aktivierung - Update-Pakete werden an Commit und Hash gebunden;
- Root-only-Hilfsprogramme fuer systemd-, Secret- und State-Aenderungen - Root-Helfer prüfen CLI-/Root-Ausführung, bevor sie Systemdienste oder `/etc`-/`/var/lib`-Daten verändern.
- 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.
## Entwicklungsstand ## 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. `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.
Als naechster groesserer Connector-Ausbauschritt bleibt der Remote-Nextcloud-Adapter offen.

View File

@@ -15,6 +15,32 @@ $errors = array();
$self_update_override = null; $self_update_override = null;
$nc_piwigo_api_test = 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 ( if (
$_SERVER['REQUEST_METHOD'] === 'POST' $_SERVER['REQUEST_METHOD'] === 'POST'
&& empty($_POST) && empty($_POST)
@@ -59,6 +85,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bratonien_tool']))
$errors[] = $e->getMessage(); $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(); $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_activity_view' => 'Activity-View',
'nc_gallery_root' => 'Galerie-Pfad', 'nc_gallery_root' => 'Galerie-Pfad',
); );
foreach ($required as $field => $label) foreach ($required as $field => $label)
{ {
if (trim((string)($_POST[$field] ?? '')) === '') if (trim((string)($_POST[$field] ?? '')) === '') throw new RuntimeException($label.' fehlt.');
{
throw new RuntimeException($label.' fehlt.');
}
} }
$fallback_user = trim((string)($_POST['nc_piwigo_user'] ?? '')); $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(); $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); $port = (int)($_POST['nc_port'] ?? 5432);
if ($port < 1 || $port > 65535) if ($port < 1 || $port > 65535) throw new RuntimeException('Ungueltiger PostgreSQL-Port.');
{
throw new RuntimeException('Ungueltiger PostgreSQL-Port.');
}
$gallery_root = rtrim(trim((string)$_POST['nc_gallery_root']), '/'); $gallery_root = rtrim(trim((string)$_POST['nc_gallery_root']), '/');
if (strpos($gallery_root, './') === 0) if (strpos($gallery_root, './') === 0)
{ {
$piwigo_root = realpath(PHPWG_ROOT_PATH); $piwigo_root = realpath(PHPWG_ROOT_PATH);
if ($piwigo_root === false) if ($piwigo_root === false) throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgelöst werden.');
{
throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgelöst werden.');
}
$gallery_root = rtrim($piwigo_root, '/').'/'.ltrim(substr($gallery_root, 2), '/'); $gallery_root = rtrim($piwigo_root, '/').'/'.ltrim(substr($gallery_root, 2), '/');
} }
if ($gallery_root === '' || $gallery_root[0] !== '/') if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
{
throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
}
$config = array( $config = array(
'host' => trim((string)$_POST['nc_host']), 'host'=>trim((string)$_POST['nc_host']),
'port' => (string)$port, 'port'=>(string)$port,
'database' => trim((string)$_POST['nc_database']), 'database'=>trim((string)$_POST['nc_database']),
'user' => trim((string)$_POST['nc_user']), 'user'=>trim((string)$_POST['nc_user']),
'source_view' => trim((string)$_POST['nc_source_view']), 'source_view'=>trim((string)$_POST['nc_source_view']),
'activity_view' => trim((string)$_POST['nc_activity_view']), 'activity_view'=>trim((string)$_POST['nc_activity_view']),
'gallery_root' => $gallery_root, 'gallery_root'=>$gallery_root,
'state_dir' => '', 'state_dir'=>'','status_file'=>'',
'status_file' => '', 'quiet_seconds'=>max(0,(int)($_POST['nc_quiet_seconds'] ?? 120)),
'quiet_seconds' => max(0, (int)($_POST['nc_quiet_seconds'] ?? 120)), 'max_wait_seconds'=>max(60,(int)($_POST['nc_max_wait_seconds'] ?? 900)),
'max_wait_seconds' => max(60, (int)($_POST['nc_max_wait_seconds'] ?? 900)), 'full_sync_seconds'=>max(300,(int)($_POST['nc_full_sync_seconds'] ?? 86400)),
'full_sync_seconds' => max(300, (int)($_POST['nc_full_sync_seconds'] ?? 86400)), 'storages'=>bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''),
'storages' => bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''), 'origin'=>'native','piwigo_auth'=>'api-first',
'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] ?? '')); $value=trim((string)($_POST[$post_key] ?? '')); if($value!=='') $config[$config_key]=$value;
if ($value !== '')
{
$config[$config_key] = $value;
}
} }
bratonien_tools_nc_connector_view_name($config['source_view']); bratonien_tools_nc_connector_view_name($config['source_view']);
bratonien_tools_nc_connector_view_name($config['activity_view']); bratonien_tools_nc_connector_view_name($config['activity_view']);
$table = bratonien_tools_nc_connector_table(); $table=bratonien_tools_nc_connector_table(); bratonien_tools_nc_connector_ensure_table(); $now=date('Y-m-d H:i:s');
bratonien_tools_nc_connector_ensure_table(); $connection_key='local-'.bin2hex(random_bytes(12));
$now = date('Y-m-d H:i:s'); $secret_blob=bratonien_tools_nc_connector_encrypt_credentials((string)$_POST['nc_db_password'],$fallback_user,$fallback_password);
$connection_key = 'local-'.bin2hex(random_bytes(12)); $config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
$secret_blob = bratonien_tools_nc_connector_encrypt_credentials( if(!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
(string)$_POST['nc_db_password'],
$fallback_user, 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)."')");
$fallback_password $id=(int)pwg_db_insert_id();
); if($id<1) throw new RuntimeException('Die Connector-Verbindung konnte nicht eindeutig angelegt werden.');
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $config['state_dir']='/var/lib/bratonien-tools/nc-connector/connection-'.$id;
if (!is_string($config_json)) $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); pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id);
return array( return array(
'message' => $fallback_user === '' 'connection_id'=>$id,
? 'Nextcloud-Verbindung wurde API-first ohne dauerhaft gespeicherten Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.' '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.'
: '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) function bratonien_tools_nc_connector_credentials_from_blob($blob)
{ {
$plain = bratonien_tools_nc_connector_decrypt_secret($blob); $plain = bratonien_tools_nc_connector_decrypt_secret($blob);
if ($plain === '') if ($plain === '') return array('db_password'=>'', 'piwigo_user'=>'', 'piwigo_password'=>'');
{
return array('db_password'=>'', 'piwigo_user'=>'', 'piwigo_password'=>'');
}
$decoded = json_decode($plain, true); $decoded = json_decode($plain, true);
if (is_array($decoded) && isset($decoded['db_password'])) if (is_array($decoded) && isset($decoded['db_password']))
{ {
return array( return array(
'db_password' => (string)($decoded['db_password'] ?? ''), 'db_password'=>(string)($decoded['db_password'] ?? ''),
'piwigo_user' => (string)($decoded['piwigo_user'] ?? ''), 'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
'piwigo_password' => (string)($decoded['piwigo_password'] ?? ''), 'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
); );
} }
// Backwards compatibility for migrated 0.14.x connections.
return array('db_password'=>$plain, 'piwigo_user'=>'', 'piwigo_password'=>''); return array('db_password'=>$plain, 'piwigo_user'=>'', 'piwigo_password'=>'');
} }
function bratonien_tools_nc_connector_encrypt_credentials($db_password, $piwigo_user, $piwigo_password) function bratonien_tools_nc_connector_encrypt_credentials($db_password, $piwigo_user, $piwigo_password)
{ {
$payload = json_encode(array( $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);
'v' => 1, if(!is_string($payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
'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); return bratonien_tools_nc_connector_encrypt_secret($payload);
} }
function bratonien_tools_nc_connector_parse_storages($raw) function bratonien_tools_nc_connector_parse_storages($raw)
{ {
$storages = array(); $storages=array();
$lines = preg_split('/\r\n|\r|\n/', trim((string)$raw)); $lines=preg_split('/\r\n|\r|\n/',trim((string)$raw));
foreach ($lines as $line) foreach($lines as $line)
{ {
$line = trim($line); if(trim($line)==='') continue;
if ($line === '') $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($parts[2][0]!=='/') throw new RuntimeException('Der lokale Speicherpfad muss ein absoluter Pfad sein.');
if (count($parts) !== 3 || $parts[0] === '' || $parts[1] === '' || $parts[2] === '') $storages[]=array(
{ 'storage_id'=>$parts[0],
throw new RuntimeException('Storage-Zeilen muessen das Format storage_id | source_prefix | local_mount verwenden.'); 'source_prefix'=>trim($parts[1],'/'),
} 'local_mount'=>rtrim($parts[2],'/'),
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 (!$storages) if(!$storages) throw new RuntimeException('Mindestens ein Speicherort muss zugeordnet werden.');
{
throw new RuntimeException('Mindestens ein Storage muss konfiguriert werden.');
}
return $storages; return $storages;
} }
function bratonien_tools_nc_connector_create_local() function bratonien_tools_nc_connector_create_local()
{ {
$required = array( $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');
'nc_name' => 'Name', foreach($required as $field=>$label)if(trim((string)($_POST[$field]??''))==='')throw new RuntimeException($label.' fehlt.');
'nc_host' => 'PostgreSQL-Host', $port=(int)($_POST['nc_port']??5432);if($port<1||$port>65535)throw new RuntimeException('Ungueltiger PostgreSQL-Port.');
'nc_database' => 'Datenbank', $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.');
'nc_user' => 'Reader-Benutzer', $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');
'nc_db_password' => 'Reader-Passwort', bratonien_tools_nc_connector_view_name($config['source_view']);bratonien_tools_nc_connector_view_name($config['activity_view']);
'nc_source_view' => 'Source-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.');
'nc_activity_view' => 'Activity-View', 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)."')");
'nc_gallery_root' => 'Galerie-Pfad', $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);
'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.'); return array('message'=>'Nextcloud-Verbindung wurde angelegt. Bitte zuerst technisch pruefen und danach im LXC aktivieren.');
} }
function bratonien_tools_nc_connector_verify_managed() function bratonien_tools_nc_connector_verify_managed()
{ {
$id = isset($_POST['connection_id']) ? (int)$_POST['connection_id'] : 0; $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.');
$connection = bratonien_tools_nc_connector_connection($id, true); $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.');
if (!$connection) $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.'); 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;}
if ((string)$connection['adapter'] !== 'local')
{
throw new RuntimeException('Diese Verifikation ist derzeit nur fuer lokale Connector-Verbindungen verfuegbar.');
} }
$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']; function bratonien_tools_nc_connector_remove_tree($path)
foreach (array('host','port','database','user','source_view','activity_view') as $key) {
{ $path=rtrim((string)$path,'/');if($path===''||$path==='/'||!file_exists($path))return true;
if (trim((string)($config[$key] ?? '')) === '') if(is_link($path)||is_file($path))return @unlink($path);
{ $items=@scandir($path);if(!is_array($items))return false;
throw new RuntimeException('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.'); foreach($items as $item){if($item==='.'||$item==='..')continue;if(!bratonien_tools_nc_connector_remove_tree($path.'/'.$item))return false;}
} return @rmdir($path);
}
$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_delete() function bratonien_tools_nc_connector_delete()
{ {
$id = (int)($_POST['connection_id'] ?? 0); $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.');
$connection = bratonien_tools_nc_connector_connection($id, false); $config=is_array($connection['config'])?$connection['config']:array();
if (!$connection) $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') $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.');
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.');
} }

View File

@@ -6,689 +6,372 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_nc_wizard_state() function bratonien_tools_nc_wizard_state()
{ {
$state = isset($_SESSION['bratonien_nc_wizard']) && is_array($_SESSION['bratonien_nc_wizard']) $state = isset($_SESSION['bratonien_nc_wizard']) && is_array($_SESSION['bratonien_nc_wizard']) ? $_SESSION['bratonien_nc_wizard'] : array();
? $_SESSION['bratonien_nc_wizard']
: array();
return array_merge(array( return array_merge(array(
'step' => 1, 'step'=>1,'scan_ok'=>false,'base_url'=>'','host_input'=>'','username'=>'','display_name'=>'','version'=>'','product'=>'Nextcloud',
'scan_ok' => false, 'users'=>array(),'can_list_users'=>false,'showcase_user'=>'','connection_name'=>'','scan_message'=>'',
'base_url' => '', 'technical_stage'=>'reader_credentials','technical_source'=>'Noch nicht geprüft','technical_error'=>'','technical_complete'=>false,
'host_input' => '', 'db_host'=>'','db_port'=>'5432','db_database'=>'nextcloud','db_user'=>'','db_password_set'=>false,
'username' => '', 'source_view'=>'piwigo_showcase_sources','activity_view'=>'piwigo_showcase_activity','gallery_root'=>'','storages'=>array(),'storage_candidates'=>array(),
'display_name' => '', 'api_status'=>'pending','api_username'=>'','api_error'=>'','_password'=>'','_db_password'=>'','_api_key_id'=>'','_api_key_secret'=>'',
'version' => '', '_fallback_user'=>'','_fallback_password'=>'',
'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' => '',
), $state); ), $state);
} }
function bratonien_tools_nc_wizard_store(array $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.'); }
$_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) function bratonien_tools_nc_wizard_normalize_url($host)
{ {
$host = trim((string)$host); $host=trim((string)$host);
if ($host === '') if($host==='') throw new RuntimeException('Nextcloud-Adresse fehlt.');
{ if(!preg_match('#^https?://#i',$host)) $host='https://'.$host;
throw new RuntimeException('Nextcloud-Host fehlt.'); $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 (!preg_match('#^https?://#i', $host)) if(!in_array($scheme,array('http','https'),true)) throw new RuntimeException('Nextcloud muss per HTTP oder HTTPS erreichbar sein.');
{ $url=$scheme.'://'.$parts['host'];
$host = 'https://'.$host; if(!empty($parts['port'])) $url.=':'.(int)$parts['port'];
} if(!empty($parts['path'])&&$parts['path']!=='/') $url.='/'.trim($parts['path'],'/');
return rtrim($url,'/');
$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, '/');
} }
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')) $host=trim((string)$host);
{ if($host==='') throw new RuntimeException('Bitte die Adresse der Nextcloud angeben.');
throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der Nextcloud-Scan kann nicht ausgeführt werden.'); 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); function bratonien_tools_nc_wizard_http($url,$username='',$password='',array $headers=array())
$http_headers = array_merge(array('Accept: application/json'), $headers); {
$options = array( if(!function_exists('curl_init')) throw new RuntimeException('Der Server kann Nextcloud derzeit nicht per HTTP prüfen.');
CURLOPT_RETURNTRANSFER => true, $ch=curl_init($url);
CURLOPT_FOLLOWLOCATION => true, $options=array(
CURLOPT_MAXREDIRS => 3, CURLOPT_RETURNTRANSFER=>true,CURLOPT_FOLLOWLOCATION=>true,CURLOPT_MAXREDIRS=>3,CURLOPT_CONNECTTIMEOUT=>8,CURLOPT_TIMEOUT=>15,
CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'),$headers),
CURLOPT_TIMEOUT => 15, CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version')?bratonien_tools_current_version():'dev'),
CURLOPT_HTTPHEADER => $http_headers,
CURLOPT_USERAGENT => 'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
); );
if ($username !== '') if($username!==''){$options[CURLOPT_HTTPAUTH]=CURLAUTH_BASIC;$options[CURLOPT_USERPWD]=$username.':'.$password;}
{ curl_setopt_array($ch,$options);
$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; $body=curl_exec($ch);$errno=curl_errno($ch);$status=(int)curl_getinfo($ch,CURLINFO_HTTP_CODE);curl_close($ch);
$options[CURLOPT_USERPWD] = $username.':'.$password; if($body===false||$errno!==0) throw new RuntimeException('Verbindung fehlgeschlagen.');
} return array('status'=>$status,'body'=>(string)$body);
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);
} }
function bratonien_tools_nc_wizard_ocs_data($body) function bratonien_tools_nc_wizard_ocs_data($body)
{ {
$decoded = json_decode((string)$body, true); $decoded=json_decode((string)$body,true);
if (!is_array($decoded)) 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.'); $message=trim((string)($meta['message']??''));
} if($message===''||strtolower($message)==='ok') $message='Nextcloud hat die Anfrage abgelehnt.';
if (isset($decoded['ocs']['meta']['statuscode']) && (int)$decoded['ocs']['meta']['statuscode'] !== 100)
{
$message = (string)($decoded['ocs']['meta']['message'] ?? 'Nextcloud hat die Anfrage abgelehnt.');
throw new RuntimeException($message); 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) function bratonien_tools_nc_wizard_fill_profile(array &$state)
{ {
$url_host = strtolower((string)parse_url($state['base_url'], PHP_URL_HOST)); $url_host=strtolower((string)parse_url($state['base_url'],PHP_URL_HOST));
$state['db_host'] = $url_host; $state['db_host']='';
$state['db_port'] = '5432'; $state['db_port']='5432';
$state['db_database'] = 'nextcloud'; $state['db_database']='nextcloud';
$state['source_view'] = 'piwigo_showcase_sources'; $state['source_view']='piwigo_showcase_sources';
$state['activity_view'] = 'piwigo_showcase_activity'; $state['activity_view']='piwigo_showcase_activity';
$state['gallery_root'] = rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud'; $state['gallery_root']=rtrim(PHPWG_ROOT_PATH,'/').'/galleries/nextcloud';
$state['technical_source'] = 'Standardwerte vorbereitet'; $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(); $config=isset($candidate['config'])&&is_array($candidate['config'])?$candidate['config']:array();
$candidate_url = trim((string)($config['nextcloud_url'] ?? '')); $candidate_url=trim((string)($config['nextcloud_url']??''));
$candidate_host = strtolower(trim((string)($config['host'] ?? ''))); $match=false;
$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 !== '') $full=bratonien_tools_nc_connector_connection((int)$candidate['id'],true);
{ if(!$full) continue;
try $credentials=bratonien_tools_nc_connector_credentials_from_blob($full['secret_blob']??'');
{ $state['db_host']=(string)($config['host']??'');
$match = bratonien_tools_nc_wizard_normalize_url($candidate_url) === $state['base_url']; $state['db_port']=(string)($config['port']??'5432');
} $state['db_database']=(string)($config['database']??'nextcloud');
catch (Throwable $ignored) $state['db_user']=(string)($config['user']??'');
{ $state['_db_password']=(string)($credentials['db_password']??'');
$match = false; $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']);
if (!$match && $candidate_host !== '' && $candidate_host === $url_host) $state['gallery_root']=(string)($config['gallery_root']??$state['gallery_root']);
{ $state['storages']=isset($config['storages'])&&is_array($config['storages'])?$config['storages']:array();
$match = true; $state['storage_candidates']=$state['storages'];
} $state['technical_complete']=$state['db_host']!==''&&$state['db_user']!==''&&$state['db_password_set']&&!empty($state['storages']);
if (!$match) $state['technical_stage']=$state['technical_complete']?'ready':'reader_credentials';
{ $state['technical_source']='Passende vorhandene Verbindung übernommen';
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';
break; break;
} }
$state['technical_complete'] = $state['db_host'] !== '' if($state['db_host']==='') $state['db_host']=$url_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']);
} }
function bratonien_tools_nc_wizard_scan() function bratonien_tools_nc_wizard_scan()
{ {
$host_input = trim((string)($_POST['nc_wizard_host'] ?? '')); $state=bratonien_tools_nc_wizard_state();
$username = trim((string)($_POST['nc_wizard_user'] ?? '')); $host_input=trim((string)($_POST['nc_wizard_host']??$state['host_input']));
$password = (string)($_POST['nc_wizard_password'] ?? ''); $username=trim((string)($_POST['nc_wizard_user']??$state['username']));
if ($username === '' || $password === '') $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);
throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.'); 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); $base_url='';$status_data=null;
$status_response = bratonien_tools_nc_wizard_http($base_url.'/status.php'); foreach(bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
if ($status_response['status'] < 200 || $status_response['status'] >= 300)
{ {
throw new RuntimeException('Unter diesem Host wurde keine erreichbare Nextcloud-Instanz erkannt (HTTP '.$status_response['status'].').'); try
} {
$status_data = json_decode($status_response['body'], true); $response=bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if (!is_array($status_data) || empty($status_data['installed'])) if($response['status']<200||$response['status']>=300) continue;
{ $candidate_status=json_decode($response['body'],true);
throw new RuntimeException('Der Host antwortet, wurde aber nicht als installierte Nextcloud-Instanz erkannt.'); 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( $user_response=bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/user?format=json',$username,$password,array('OCS-APIRequest: true'));
$base_url.'/ocs/v2.php/cloud/user?format=json', if($user_response['status']===401||$user_response['status']===403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
$username, if($user_response['status']<200||$user_response['status']>=300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
$password, $user_data=bratonien_tools_nc_wizard_ocs_data($user_response['body']);
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']);
$users = array(); $users=array();$can_list_users=false;
$can_list_users = false;
try try
{ {
$users_response = bratonien_tools_nc_wizard_http( $users_response=bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/users?format=json',$username,$password,array('OCS-APIRequest: true'));
$base_url.'/ocs/v2.php/cloud/users?format=json', if($users_response['status']>=200&&$users_response['status']<300)
$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']); $users_data=bratonien_tools_nc_wizard_ocs_data($users_response['body']);
if (isset($users_data['users']) && is_array($users_data['users'])) if(isset($users_data['users'])&&is_array($users_data['users']))
{ {
$users = array_values(array_filter(array_map('strval', $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;
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(); if($line==='') continue;
$can_list_users = false; $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); $known=array();
$state = array( foreach(bratonien_tools_nc_connector_connections() as $connection)
'step' => 2, {
'scan_ok' => true, foreach(($connection['config']['storages']??array()) as $storage)
'base_url' => $base_url, {
'host_input' => $host_input, $key=(string)($storage['storage_id']??'').'|'.trim((string)($storage['source_prefix']??''),'/');
'username' => (string)($user_data['id'] ?? $username), if($key!=='|') $known[$key]=(string)($storage['local_mount']??'');
'display_name' => (string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''), }
'version' => (string)($status_data['versionstring'] ?? $status_data['version'] ?? ''), }
'product' => (string)($status_data['productname'] ?? 'Nextcloud'), foreach($candidates as &$candidate)
'users' => $users, {
'can_list_users' => $can_list_users, $key=$candidate['storage_id'].'|'.trim($candidate['source_prefix'],'/');
'showcase_user' => '', if(!empty($known[$key])&&is_dir($known[$key])&&is_readable($known[$key])) $candidate['local_mount']=$known[$key];
'connection_name' => $url_host !== '' ? $url_host : 'Nextcloud', }
'scan_message' => 'Nextcloud wurde erkannt und der Benutzerzugriff wurde bestätigt.', unset($candidate);
'_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-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() function bratonien_tools_nc_wizard_save_technical()
{ {
$state = bratonien_tools_nc_wizard_state(); $state=bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok'])) 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); bratonien_tools_nc_wizard_store($state);
if($state['db_user']===''||empty($state['db_password_set'])) throw new RuntimeException('Für den Datenzugriff werden Lese-Benutzer und Passwort benötigt.');
return array('message'=>$state['technical_complete'] if($state['db_host']===''||$state['db_database']==='') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.');
? '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.');
}
try try
{ {
$status = bratonien_tools_nc_connector_piwigo_api_request($key_id, $secret, 'pwg.session.getStatus'); bratonien_tools_nc_wizard_finish_data_access($state);
$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_store($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['technical_error']=$e->getMessage();
$state['api_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); bratonien_tools_nc_wizard_store($state);
throw $e; 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() function bratonien_tools_nc_wizard_api_skip()
{ {
$state = bratonien_tools_nc_wizard_state(); $state=bratonien_tools_nc_wizard_state();if((int)$state['step']!==3)throw new RuntimeException('Die API kann in diesem Assistentenschritt nicht übersprungen werden.');
if ((int)$state['step'] !== 3) $state['api_status']='skipped';$state['api_error']='';$state['step']=4;bratonien_tools_nc_wizard_store($state);
{
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.'); return array('message'=>'Piwigo-API wurde übersprungen. Für diese Verbindung ist deshalb ein Fallback-Zugang erforderlich.');
} }
function bratonien_tools_nc_wizard_finish() function bratonien_tools_nc_wizard_finish()
{ {
$state = bratonien_tools_nc_wizard_state(); $state=bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '') 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']);
throw new RuntimeException('Der Assistent ist noch nicht vollständig.'); 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'] ?? '')); $storage_lines=array();
$fallback_password = (string)($_POST['nc_wizard_fallback_password'] ?? ''); foreach($state['storages'] as $storage)
if (($fallback_user === '') !== ($fallback_password === ''))
{ {
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 === '') $_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);
throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist ein Fallback-Zugang erforderlich.'); 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;
$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;
} }
function bratonien_tools_nc_connector_update_name() function bratonien_tools_nc_connector_update_name()
{ {
$id = (int)($_POST['connection_id'] ?? 0); $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.');
$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() function bratonien_tools_nc_connector_update_technical()
{ {
$id = (int)($_POST['connection_id'] ?? 0); $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.');
$connection = bratonien_tools_nc_connector_connection($id, true); $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.');
if (!$connection) $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.');
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.');
} }

View File

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

View File

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

View File

@@ -25,82 +25,57 @@ $stateRoot = '/var/lib/bratonien-tools/nc-connector';
$servicePath = '/etc/systemd/system/bratonien-nc-connector.service'; $servicePath = '/etc/systemd/system/bratonien-nc-connector.service';
$timerPath = '/etc/systemd/system/bratonien-nc-connector.timer'; $timerPath = '/etc/systemd/system/bratonien-nc-connector.timer';
function fail_install($message) function fail_install($message) { throw new RuntimeException($message); }
{
throw new RuntimeException($message);
}
function run_install(array $command, $allowFailure = false) function run_install(array $command, $allowFailure = false)
{ {
$spec = array( $spec = array(0=>array('file','/dev/null','r'),1=>array('pipe','w'),2=>array('pipe','w'));
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$process = proc_open($command, $spec, $pipes); $process = proc_open($command, $spec, $pipes);
if (!is_resource($process)) if (!is_resource($process)) fail_install('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
{
fail_install('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
}
$stdout = stream_get_contents($pipes[1]); $stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]); $stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]); fclose($pipes[1]); fclose($pipes[2]);
fclose($pipes[2]);
$exit = proc_close($process); $exit = proc_close($process);
if ($exit !== 0 && !$allowFailure) if ($exit !== 0 && !$allowFailure)
{ {
$detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout); $detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
fail_install('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : '')); 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) function decrypt_install_credentials($blob, $hexKey)
{ {
if (!preg_match('/^[a-f0-9]{64}$/', $hexKey)) if (!preg_match('/^[a-f0-9]{64}$/', $hexKey)) fail_install('Connector-Schluessel ist ungueltig.');
{
fail_install('Connector-Schluessel ist ungueltig.');
}
$outer = base64_decode(trim((string)$blob), true); $outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null; $payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) fail_install('Connector-Zugangsdaten haben ein unbekanntes Format.');
{
fail_install('Connector-Zugangsdaten haben ein unbekanntes Format.');
}
$iv = base64_decode((string)($payload['iv'] ?? ''), true); $iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true); $tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true); $cipher = base64_decode((string)($payload['data'] ?? ''), true);
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag); $plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false || $plain === '') if ($plain === false || $plain === '') fail_install('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
{
fail_install('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
}
$decoded = json_decode($plain, true); $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) function sql_install(mysqli $db, $value) { return $db->real_escape_string((string)$value); }
{
return $db->real_escape_string((string)$value);
}
try try
{ {
if (!is_readable($dbConfig)) if (!is_readable($dbConfig)) fail_install('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig);
{ $conf = array(); $prefixeTable = 'piwigo_'; require $dbConfig;
fail_install('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig); foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail_install('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_install('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.');
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); $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); 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.'); if (!$result || !$result->num_rows) fail_install('Connector-Verbindung #'.$id.' wurde nicht gefunden.');
$row = $result->fetch_assoc(); $row = $result->fetch_assoc();
if ((string)$row['adapter'] !== 'local') fail_install('Native Aktivierung ist derzeit nur fuer lokale Verbindungen verfuegbar.'); 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) if ((string)$row['takeover_state'] !== 'verified' || (int)$row['enabled'] !== 0) fail_install('Die Verbindung muss zuerst erfolgreich verifiziert und deaktiviert sein.');
{
fail_install('Die Verbindung muss zuerst erfolgreich verifiziert und deaktiviert sein.');
}
$config = json_decode((string)$row['config_json'], true); $config = json_decode((string)$row['config_json'], true);
if (!is_array($config) || empty($config['verification']['ok'])) fail_install('Erfolgreiche Verifikation fehlt.'); 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) 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.');
{
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"); $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.'); 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)$keyResult->fetch_assoc()['value']);
$credentials = decrypt_install_credentials((string)$row['secret_blob'], (string)$keyRow['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; $stateDir = $stateRoot.'/connection-'.$id;
$statusFile = $stateDir.'/connector-status.json'; $statusFile = $stateDir.'/connector-status.json';
if (!is_dir($configDir) && !mkdir($configDir, 0700, true)) fail_install('Konfigurationsverzeichnis konnte nicht angelegt werden.'); 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.'); if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_install('State-Verzeichnis konnte nicht angelegt werden.');
chmod($configDir, 0700); chmod($configDir, 0700); chmod($stateDir, 0750);
chmod($stateDir, 0750);
$base = $configDir.'/connection-'.$id; $base = $configDir.'/connection-'.$id;
$dbPasswordPath = $base.'.db-password'; $dbPasswordPath = $base.'.db-password';
@@ -141,19 +114,23 @@ try
$storagePath = $base.'.storages.tsv'; $storagePath = $base.'.storages.tsv';
$configPath = $base.'.conf'; $configPath = $base.'.conf';
file_put_contents($dbPasswordPath, (string)$credentials['db_password']."\n", LOCK_EX); file_put_contents($dbPasswordPath, (string)$credentials['db_password']."\n", LOCK_EX); chmod($dbPasswordPath, 0600);
file_put_contents($piwigoPasswordPath, (string)$credentials['piwigo_password']."\n", LOCK_EX); if ($fallbackAvailable)
chmod($dbPasswordPath, 0600); {
chmod($piwigoPasswordPath, 0600); 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'); $storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount');
foreach ((array)($config['storages'] ?? array()) as $storage) 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.'); if (count($storageLines) === 1) fail_install('Keine Storage-Zuordnungen gespeichert.');
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); chmod($storagePath, 0600);
chmod($storagePath, 0600);
$lines = array( $lines = array(
'PIWIGO_ROOT='.$piwigoRoot, 'PIWIGO_ROOT='.$piwigoRoot,
@@ -172,44 +149,32 @@ try
'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900), 'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900),
'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400), 'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400),
'PIWIGO_SYNC_ENABLED=1', '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); if ($fallbackAvailable)
chmod($configPath, 0600); {
$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"; echo "Teste Verbindung mit Plugin-Runtime...\n";
$test = run_install(array('env', 'PIWIGO_CONFIG='.$configPath, 'bash', $pluginRoot.'/runtime/sync.sh'), true); $test = run_install(array('env', 'PIWIGO_CONFIG='.$configPath, 'bash', $pluginRoot.'/runtime/sync.sh'), true);
if ($test['exit'] !== 0) if ($test['exit'] !== 0)
{ {
@unlink($configPath); @unlink($configPath); @unlink($storagePath); @unlink($dbPasswordPath); @unlink($piwigoPasswordPath);
@unlink($storagePath);
@unlink($dbPasswordPath);
@unlink($piwigoPasswordPath);
$detail = trim($test['stderr']) !== '' ? trim($test['stderr']) : trim($test['stdout']); $detail = trim($test['stderr']) !== '' ? trim($test['stderr']) : trim($test['stdout']);
fail_install('Runtime-Test fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.')); 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"; $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"; $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($servicePath, $service, LOCK_EX); file_put_contents($timerPath, $timer, LOCK_EX); chmod($servicePath,0644); chmod($timerPath,0644);
file_put_contents($timerPath, $timer, LOCK_EX);
chmod($servicePath, 0644);
chmod($timerPath, 0644);
$config['state_dir'] = $stateDir; $config['state_dir']=$stateDir;$config['status_file']=$statusFile;$config['runtime']=array('mode'=>'plugin-runtime','config'=>$configPath);
$config['status_file'] = $statusFile; $json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);$now=date('Y-m-d H:i:s');
$config['runtime'] = array('mode'=>'plugin-runtime', 'config'=>$configPath); 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);
$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'));
run_install(array('systemctl','daemon-reload')); run_install(array('systemctl','enable','--now','bratonien-nc-connector.timer'));
echo "Aktivierung erfolgreich.\n"; echo "Aktivierung erfolgreich.\n";
echo "Verbindung #".$id." verwendet jetzt den nativen Bratonien-Tools-State unter ".$stateDir.".\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"; echo "Der gemeinsame Connector-Timer verarbeitet alle installierten connection-*.conf Dateien.\n";

View File

@@ -29,6 +29,9 @@ $latest = array(
'state'=>'idle', 'state'=>'idle',
'message'=>'Noch kein Connector-Status verfügbar.', 'message'=>'Noch kein Connector-Status verfügbar.',
'timestamp'=>0, 'timestamp'=>0,
'last_run_label'=>'Nicht verfügbar',
'next_run_timestamp'=>0,
'next_run_label'=>'Nicht verfügbar',
'auth_mode'=>'', 'auth_mode'=>'',
'api'=>array('state'=>'not_run','message'=>''), 'api'=>array('state'=>'not_run','message'=>''),
'fallback'=>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); echo json_encode($latest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

View File

@@ -7,109 +7,71 @@ import argparse
import hashlib import hashlib
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time import time
from pathlib import Path 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]: def load(path: Path) -> dict[str, object]:
defaults: dict[str, object] = { defaults: dict[str, object] = {"processed":0,"observed":0,"pending_since":0,"last_change":0,"last_full":0,"source_signature":""}
"processed": 0,
"observed": 0,
"pending_since": 0,
"last_change": 0,
"last_full": 0,
"source_signature": "",
}
if not path.exists(): if not path.exists():
return defaults return defaults
data = json.loads(path.read_text(encoding="utf-8")) data=json.loads(path.read_text(encoding="utf-8"))
return { 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",""))}
"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: def save(path: Path, state: dict[str, object]) -> None:
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True,exist_ok=True)
fd, name = tempfile.mkstemp(dir=path.parent) fd,name=tempfile.mkstemp(dir=path.parent)
with os.fdopen(fd, "w", encoding="utf-8") as handle: with os.fdopen(fd,"w",encoding="utf-8") as handle:
json.dump(state, handle, sort_keys=True) json.dump(state,handle,sort_keys=True);handle.write("\n")
handle.write("\n")
Path(name).replace(path) Path(name).replace(path)
def query(args: argparse.Namespace, sql: str) -> str: def query(args: argparse.Namespace, sql: str) -> str:
env = os.environ.copy() env=os.environ.copy();env["PGPASSWORD"]=args.password_file.read_text(encoding="utf-8").strip()
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]
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
return subprocess.run(command, env=env, check=True, text=True, capture_output=True).stdout
def latest(args: argparse.Namespace) -> int: 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: def source_signature(args: argparse.Namespace) -> str:
if not args.source_view: if not args.source_view:return ""
return "" payload=query(args,f"SELECT share_id, display_name, storage_id, source_path FROM {validate_view(args.source_view)} ORDER BY share_id")
payload = query(args, f"SELECT share_id, display_name, storage_id, source_path FROM {args.source_view} ORDER BY share_id")
return hashlib.sha256(payload.encode("utf-8")).hexdigest() return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser() 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.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: try:
state = load(args.state) validate_view(args.view)
now = int(time.time()) if args.source_view:validate_view(args.source_view)
current = latest(args) state=load(args.state);now=int(time.time());current=latest(args);current_source=source_signature(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 args.action == "commit": if current_source and current_source!=str(state["source_signature"]):return 0
state.update(processed=current, observed=current, pending_since=0, last_change=0, last_full=now, source_signature=current_source) 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)
save(args.state, state) if now-int(state["last_full"])>=args.full_after:return 0
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
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 return 3
except Exception as error: except Exception as error:
print(f"activity-gate: {error}", file=sys.stderr) print(f"activity-gate: {error}",file=sys.stderr);return 1
return 1
if __name__ == "__main__": if __name__=="__main__":raise SystemExit(main())
raise SystemExit(main())

View File

@@ -7,174 +7,109 @@ import argparse
import csv import csv
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from pathlib import Path, PurePosixPath 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]]: def read_config(path: Path) -> dict[str, tuple[str, Path]]:
result: dict[str, tuple[str, Path]] = {} result: dict[str, tuple[str, Path]] = {}
with path.open(encoding="utf-8") as handle: with path.open(encoding="utf-8") as handle:
for number, line in enumerate(handle, 1): for number, line in enumerate(handle, 1):
line = line.rstrip("\n") line=line.rstrip("\n")
if not line or line.startswith("#"): if not line or line.startswith("#"):continue
continue fields=line.split("\t")
fields = line.split("\t") if len(fields)!=3:raise ValueError(f"{path}:{number}: expected storage_id, source_prefix and local_mount")
if len(fields) != 3: storage_id,prefix,mount=fields
raise ValueError(f"{path}:{number}: expected storage_id, source_prefix and local_mount") if not storage_id.strip():raise ValueError(f"{path}:{number}: storage_id is empty")
storage_id, prefix, mount = fields result[storage_id]=(prefix.strip("/"),Path(mount))
result[storage_id] = (prefix.strip("/"), Path(mount))
return result return result
def run_query(args: argparse.Namespace, env: dict[str, str], sql: str) -> subprocess.CompletedProcess[str]: def run_query(args: argparse.Namespace, env: dict[str,str], sql: str) -> subprocess.CompletedProcess[str]:
command = [ 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]
"psql", "-X", "-A", "-F", "\t", "-t", return subprocess.run(command,env=env,check=False,text=True,capture_output=True)
"-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]]: def query_rows(args: argparse.Namespace) -> list[list[str]]:
password = args.password_file.read_text(encoding="utf-8").strip() password=args.password_file.read_text(encoding="utf-8").strip();env=os.environ.copy();env["PGPASSWORD"]=password;view=validate_view(args.view)
env = os.environ.copy() modern_sql=f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {view} ORDER BY share_id"
env["PGPASSWORD"] = password completed=run_query(args,env,modern_sql)
if completed.returncode==0:return list(csv.reader(completed.stdout.splitlines(),delimiter="\t"))
modern_sql = f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {args.view} ORDER BY share_id" 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")
completed = run_query(args, env, modern_sql) legacy_sql=f"SELECT share_id, display_name, storage_id, source_path FROM {view} ORDER BY share_id"
if completed.returncode == 0: completed=run_query(args,env,legacy_sql)
return list(csv.reader(completed.stdout.splitlines(), delimiter="\t")) if completed.returncode!=0:raise RuntimeError(completed.stderr.strip() or "legacy Nextcloud source view query failed")
rows=[]
# Existing installations may still expose the original four-column view. for row in csv.reader(completed.stdout.splitlines(),delimiter="\t"):
# Keep them usable and derive folder/file from the resolved source path. if len(row)==4:
if "item_type" not in completed.stderr or "does not exist" not in completed.stderr: share_id,display_name,storage_id,source_path=row;rows.append([share_id,"",display_name,storage_id,source_path])
raise RuntimeError(completed.stderr.strip() or "Nextcloud source view query failed") else:rows.append(row)
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)
return rows return rows
def contained_join(root: Path, relative: str) -> Path: def contained_join(root: Path, relative: str) -> Path:
parts = PurePosixPath(relative).parts parts=PurePosixPath(relative).parts
if relative.startswith("/") or ".." in parts: if relative.startswith("/") or ".." in parts:raise ValueError(f"unsafe source path: {relative}")
raise ValueError(f"unsafe source path: {relative}")
return root.joinpath(*parts) return root.joinpath(*parts)
def build(args: argparse.Namespace) -> dict[str, object]: def build(args: argparse.Namespace) -> dict[str,object]:
adapters = read_config(args.storage_config) validate_view(args.view);adapters=read_config(args.storage_config);rows=query_rows(args)
rows = query_rows(args) if not rows and not args.allow_empty:raise RuntimeError("Nextcloud returned no Showcase shares; refusing an empty manifest")
if not rows and not args.allow_empty: manifest=[];errors=[];folder_count=0;file_count=0
raise RuntimeError("Nextcloud returned no Showcase shares; refusing an empty manifest")
manifest: list[str] = []
errors: list[str] = []
folder_count = 0
file_count = 0
for row in rows: for row in rows:
if len(row) != 5: if len(row)!=5:errors.append(f"invalid database row with {len(row)} columns");continue
errors.append(f"invalid database row with {len(row)} columns") share_id,item_type,display_name,storage_id,source_path=row;item_type=item_type.strip().lower()
continue 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)
share_id, item_type, display_name, storage_id, source_path = row if not adapter:errors.append(f"share {share_id}: unknown storage {storage_id}");continue
item_type = item_type.strip().lower() prefix,mount=adapter;relative=source_path.strip("/")
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: if prefix:
expected = prefix + "/" expected=prefix+"/"
if relative != prefix and not relative.startswith(expected): if relative!=prefix and not relative.startswith(expected):errors.append(f"share {share_id}: path does not match configured prefix");continue
errors.append(f"share {share_id}: path does not match configured prefix") relative=relative[len(prefix):].lstrip("/")
continue source=contained_join(mount,relative)
relative = relative[len(prefix):].lstrip("/") if not mount.is_mount():errors.append(f"share {share_id}: storage mount unavailable: {mount}");continue
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 not item_type:
if source.is_dir(): if source.is_dir():item_type="folder"
item_type = "folder" elif source.is_file():item_type="file"
elif source.is_file(): else:errors.append(f"share {share_id}: source unavailable: {source}");continue
item_type = "file" if item_type=="folder":
else: if not source.is_dir():errors.append(f"share {share_id}: source directory unavailable: {source}");continue
errors.append(f"share {share_id}: source unavailable: {source}") folder_count+=1
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: else:
if not source.is_file(): if not source.is_file():errors.append(f"share {share_id}: source file unavailable: {source}");continue
errors.append(f"share {share_id}: source file unavailable: {source}") file_count+=1
continue 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
file_count += 1 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}") manifest.append(f"{share_id}\t{item_type}\t{display_name.lstrip('/')}\t{source}")
if errors:raise RuntimeError("; ".join(errors))
if errors: if len(manifest)!=len(rows):raise RuntimeError("not all Showcase shares could be resolved")
raise RuntimeError("; ".join(errors)) args.output.parent.mkdir(parents=True,exist_ok=True)
if len(manifest) != len(rows): with tempfile.NamedTemporaryFile("w",encoding="utf-8",dir=args.output.parent,delete=False) as handle:
raise RuntimeError("not all Showcase shares could be resolved") handle.write("\n".join(manifest)+("\n" if manifest else ""));temporary=Path(handle.name)
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) 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: def main() -> int:
parser = argparse.ArgumentParser() 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()
parser.add_argument("--host", required=True) try:print(json.dumps(build(args),ensure_ascii=False))
parser.add_argument("--port", type=int, default=5432) except Exception as error:print(f"manifest: {error}",file=sys.stderr);return 1
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 return 0
if __name__ == "__main__": if __name__=="__main__":raise SystemExit(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_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 900, CURLOPT_TIMEOUT => 900,
CURLOPT_FOLLOWLOCATION => false, CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-NC-Connector/0.9.3.26', CURLOPT_USERAGENT => 'Bratonien-NC-Connector',
); );
if ($headers) 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 share_id, item_type, display_name, source_path = fields
if item_type not in {"folder", "file"}: if item_type not in {"folder", "file"}:
raise ValueError(f"{path}:{line_number}: unsupported item_type {item_type!r}") raise ValueError(f"{path}:{line_number}: unsupported item_type {item_type!r}")
entries.append({ entries.append({"share_id": share_id, "item_type": item_type, "display_name": display_name, "source_path": source_path})
"share_id": share_id,
"item_type": item_type,
"display_name": display_name,
"source_path": source_path,
})
return entries 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: 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) target.mkdir(parents=True, exist_ok=True)
used: set[str] = set() used: set[str] = set()
children = sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)) for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)):
for child in children:
if child.is_symlink(): if child.is_symlink():
continue continue
child_source_key = f"{source_key}/{child.name}" 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()) 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: def build(manifest: Path, destination: Path, state_file: Path) -> None:
entries = load_manifest(manifest) entries = load_manifest(manifest)
old_map = load_map(state_file) old_map = load_map(state_file)
new_map: dict[str, str] = {} new_map: dict[str, str] = {}
staging = destination.with_name(f".{destination.name}.next") staging = destination.with_name(f".{destination.name}.next")
previous = destination.with_name(f".{destination.name}.previous") previous = destination.with_name(f".{destination.name}.previous")
state_staging = state_file.with_suffix(state_file.suffix + ".next")
if staging.exists(): remove_tree(staging)
shutil.rmtree(staging) remove_tree(previous)
if state_staging.exists():
state_staging.unlink()
staging.mkdir(parents=True) staging.mkdir(parents=True)
used_roots: set[str] = set() try:
for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])): used_roots: set[str] = set()
source = Path(entry["source_path"]) for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])):
source_key = f"share:{entry['share_id']}" source = Path(entry["source_path"])
preferred = preferred_target(source_key, entry["display_name"], Path("."), old_map) source_key = f"share:{entry['share_id']}"
is_file_share = entry["item_type"] == "file" preferred = preferred_target(source_key, entry["display_name"], Path("."), old_map)
root_name = unique_name(preferred, used_roots, is_file_share) is_file_share = entry["item_type"] == "file"
root_key = Path(root_name) root_name = unique_name(preferred, used_roots, is_file_share)
new_map[source_key] = root_key.as_posix() 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: state_staging.parent.mkdir(parents=True, exist_ok=True)
if not source.is_file(): with state_staging.open("w", encoding="utf-8") as handle:
raise FileNotFoundError(f"source is not a readable file: {source}") json.dump(new_map, handle, ensure_ascii=False, indent=2, sort_keys=True)
(staging / root_name).symlink_to(source.resolve()) handle.write("\n")
continue
if not source.is_dir(): had_destination = destination.exists()
raise FileNotFoundError(f"source is not a readable directory: {source}") if had_destination:
mirror_directory(source, staging / root_name, source_key, root_key, old_map, new_map) destination.rename(previous)
try:
state_staging = state_file.with_suffix(state_file.suffix + ".next") staging.rename(destination)
state_staging.parent.mkdir(parents=True, exist_ok=True) state_staging.replace(state_file)
with state_staging.open("w", encoding="utf-8") as handle: except Exception:
json.dump(new_map, handle, ensure_ascii=False, indent=2, sort_keys=True) remove_tree(destination)
handle.write("\n") if had_destination and previous.exists():
previous.rename(destination)
if previous.exists(): raise
shutil.rmtree(previous) remove_tree(previous)
if destination.exists(): except Exception:
destination.rename(previous) remove_tree(staging)
staging.rename(destination) if state_staging.exists():
state_staging.replace(state_file) state_staging.unlink()
if previous.exists(): raise
shutil.rmtree(previous)
def main() -> int: def main() -> int:

View File

@@ -11,9 +11,7 @@
.bratonien-card { margin-left:0; margin-right:0; } .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} } @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> </style>
<div id="bratonien-tabs-anchor"></div> <div id="bratonien-tabs-anchor"></div>
<script> <script>
(function () { (function () {
'use strict'; 'use strict';
@@ -27,114 +25,56 @@
{ id:'wartung', label:'Wartung / Cache' }, { id:'wartung', label:'Wartung / Cache' },
{ id:'system', label:'System & Updates' } { id:'system', label:'System & Updates' }
]; ];
var panels = []; var panels=[];
definitions.forEach(function (d) { definitions.forEach(function(d){
var ids=d.sections||[d.id], elements=[]; 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);}}); ids.forEach(function(id){var el=document.getElementById(id);if(el){el.classList.add('bratonien-tab-panel');elements.push(el);}});
if (!elements.length) return; if(elements.length)panels.push({definition:d,elements:elements});
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(!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); 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){}}}
function activate(id, remember) { tabs.addEventListener('click',function(e){var b=e.target.closest('.bratonien-tab');if(b)activate(b.dataset.tab);});
var valid=false; 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);});
panels.forEach(function (item) { 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);
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 initNCConnectorPolling() { function initNCConnectorPolling(){
var section=document.getElementById('nc-connector'); var section=document.getElementById('nc-connector');if(!section)return;
if(!section) return;
var lastSeen=null;
var endpoint='plugins/bratonien_tools/nc-connector-status.php'; 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'}) 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){ .then(function(data){
var timestamp=parseInt(data.timestamp||0,10); if(lastRunNode&&data.last_run_label)lastRunNode.textContent=data.last_run_label;
if(!timestamp) return; if(nextRunNode&&data.next_run_label)nextRunNode.textContent=data.next_run_label;
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();
}
}) })
.catch(function(){}); .catch(function(){});
} }
poll(); poll();window.setInterval(poll,5000);
window.setInterval(poll,5000);
} }
function initAll() { function initAll(){initBratonienTabs();initNCConnectorPolling();}
initBratonienTabs(); if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initAll);else initAll();
initNCConnectorPolling();
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',initAll); else initAll();
})(); })();
</script> </script>
{/literal} {/literal}

View File

@@ -1,428 +1,272 @@
<section class="bratonien-section" id="nc-connector"> <section class="bratonien-section" id="nc-connector">
<h3>NC Connector</h3> <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)} {assign var=nc_system_available value=isset($NC_CONNECTOR.system)}
<div class="bratonien-grid"> <div class="bratonien-grid">
<div class="bratonien-card"> <div class="bratonien-card">
<h4>Connector-Status</h4> <h4>Status</h4>
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<span class="bratonien-label">Phase</span><strong>{$NC_CONNECTOR.phase|escape:html}</strong> <span class="bratonien-label">Verbindungen</span><strong>{$NC_CONNECTOR.connection_count|escape:html}</strong>
<span class="bratonien-label">Connector-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">Timer aktiv</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_active}Ja{else}Nein{/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">Timer aktiviert</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_enabled}Ja{else}Nein{/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>
<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>
</div> </div>
{if $nc_system_available && $NC_CONNECTOR.system.last_run_message} {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}
<p class="bratonien-base-note">Letztes Ergebnis: <strong>{$NC_CONNECTOR.system.last_run_message|escape:html}</strong></p> {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} {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_api_state == 'error'} {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}
<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}
</div> </div>
<div class="bratonien-card"> <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> <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"> <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-wizard-open">Mit Assistent anlegen</button>
<button class="buttonLike" type="button" id="bratonien-nc-technical-open">Ohne Assistent anlegen</button> <button class="buttonLike" type="button" id="bratonien-nc-technical-open">Ohne Assistent anlegen</button>
</div> </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>
</div> </div>
<script> <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)">
(function () { <div style="padding:1.25rem 1.5rem">
var dialog = document.getElementById('bratonien-nc-wizard-dialog'); <div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem">
var openButton = document.getElementById('bratonien-nc-wizard-open'); <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>
var closeButton = document.getElementById('bratonien-nc-wizard-close'); <button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button>
var technicalButton = document.getElementById('bratonien-nc-technical-open'); </div>
var technical = document.getElementById('bratonien-nc-technical-create');
var storageKey = 'bratonienNcWizardOpen';
if (openButton && dialog) { {if $NC_CONNECTOR.wizard.step == 1}
openButton.addEventListener('click', function () { <p class="bratonien-base-note">Adresse und Zugang reichen für den ersten Scan. Der Assistent prüft den erreichbaren Web-Zugang automatisch.</p>
sessionStorage.setItem(storageKey, '1'); <form method="post" data-bratonien-wizard-form>
if (typeof dialog.showModal === 'function') dialog.showModal(); <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
else dialog.setAttribute('open', 'open'); <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) { {elseif $NC_CONNECTOR.wizard.step == 2}
closeButton.addEventListener('click', function () { <p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Der Assistent prüft jetzt, wie die freigegebenen Dateien gelesen werden können.</p>
sessionStorage.removeItem(storageKey); <div class="bratonien-form-grid">
if (typeof dialog.close === 'function') dialog.close(); <span class="bratonien-label">Adresse</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
else dialog.removeAttribute('open'); <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) { {if $NC_CONNECTOR.wizard.technical_stage == 'reader_credentials'}
dialog.addEventListener('cancel', function () { <hr>
sessionStorage.removeItem(storageKey); <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>
dialog.addEventListener('click', function (event) { <form method="post" data-bratonien-wizard-form>
if (event.target === dialog) { <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
sessionStorage.removeItem(storageKey); <div class="bratonien-form-grid">
if (typeof dialog.close === 'function') dialog.close(); <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>
dialog.querySelectorAll('form[data-bratonien-wizard-form]').forEach(function (form) { <p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Automatisch prüfen</button></p>
form.addEventListener('submit', function (event) { </form>
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 (technicalButton && technical) { {elseif $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
technicalButton.addEventListener('click', function () { <hr>
technical.open = !technical.open; <h5>Datenbank-Adresse prüfen</h5>
if (technical.open) technical.scrollIntoView({behavior:'smooth', block:'start'}); <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}">
</script> <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> </section>