Compare commits

...

212 Commits

Author SHA1 Message Date
Terranom674
370ead2b90 ignore stale branch marker 2026-08-19 14:59:22 +02:00
Terranom674
5d9e017131 noop 2026-08-19 14:59:03 +02:00
Terranom674
76de2b037c 0.9.6.15 bump version 2026-08-19 14:43:23 +02:00
Terranom674
e2e82162c6 0.9.6.15 load source selection clarification UI 2026-08-19 14:42:54 +02:00
Terranom674
7a9eb1baa6 0.9.6.15 separate legacy storage from WebDAV source selection 2026-08-19 14:42:28 +02:00
Terranom674
e800f11f64 Merge pull request #7 from Terranom674/fix/adapter-ui-detection-09614
0.9.6.14: Verbindungstyp nicht mehr aus UI-Text erraten
2026-08-19 14:34:37 +02:00
Terranom674
4808f08590 0.9.6.14 bump version for adapter-safe edit actions 2026-08-19 14:33:33 +02:00
Terranom674
6fd89df5d0 0.9.6.14 determine connection type from backend, not display text 2026-08-19 14:33:03 +02:00
Terranom674
99126088e7 Remove accidental noop file 2026-08-19 14:31:02 +02:00
Terranom674
e53a3848bb noop 2026-08-19 14:30:49 +02:00
Terranom674
0f0c71acaf Merge pull request #6 from Terranom674/fix/webdav-scheme-fallback-09613
0.9.6.13: Nextcloud-Protokoll automatisch erkennen
2026-08-19 14:21:56 +02:00
Terranom674
9847690f50 0.9.6.13 bump version for WebDAV scheme detection 2026-08-19 14:21:02 +02:00
Terranom674
a2f5dc3919 0.9.6.13 try HTTP when scheme is omitted 2026-08-19 14:20:37 +02:00
Terranom674
cd3bf7c8ee Merge pull request #5 from Terranom674/fix/dialog-diagnostics-09612
0.9.6.12: konkrete Fehlerursachen im Verbindungsdialog
2026-08-19 14:14:55 +02:00
Terranom674
79ea403555 0.9.6.12 bump version for actionable diagnostics 2026-08-19 14:14:03 +02:00
Terranom674
33783dd30f 0.9.6.12 include stage and technical cause in visible error 2026-08-19 14:13:35 +02:00
Terranom674
ca6a5cf96f 0.9.6.12 return actionable connection diagnostics 2026-08-19 14:12:58 +02:00
Terranom674
62ada617fb Merge pull request #4 from Terranom674/fix/dialog-validation-09611
0.9.6.11: Dialog bei Fehler offen halten und Felder markieren
2026-08-19 13:59:01 +02:00
Terranom674
898f9b1bc9 0.9.6.11 bump version for dialog validation 2026-08-19 13:58:08 +02:00
Terranom674
5b48e1f6f2 0.9.6.11 keep edit dialog open and mark invalid fields 2026-08-19 13:57:39 +02:00
Terranom674
fdf4584028 0.9.6.11 keep connection editor open on validation errors 2026-08-19 13:56:59 +02:00
Terranom674
880744a8b7 Merge pull request #3 from Terranom674/fix/connection-editing-09610
0.9.6.10: vollständige Verbindungsdaten und migrationsfähiger WebDAV-Start
2026-08-19 13:49:19 +02:00
Terranom674
2ed84ad089 0.9.6.10 lint standalone connector editor JavaScript 2026-08-19 13:48:26 +02:00
Terranom674
60c0a51f01 0.9.6.10 bump plugin version 2026-08-19 13:48:07 +02:00
Terranom674
84d263b16b 0.9.6.10 load complete connector editor on admin page 2026-08-19 13:47:48 +02:00
Terranom674
94c81789ed 0.9.6.10 rebuild connection editor around complete connection data 2026-08-19 13:46:26 +02:00
Terranom674
e3b1f06be3 0.9.6.10 expose complete safe edit metadata and migration readiness 2026-08-19 13:44:46 +02:00
Terranom674
15a3ceb01c 0.9.6.10 complete legacy edit credentials and migration readiness 2026-08-19 13:44:26 +02:00
Terranom674
0f5812a179 0.9.6.9: Verbindung bearbeiten und Migration trennen
* 0.9.6.9 separate edit and migration flows

* 0.9.6.9 register separate edit and migration actions

* 0.9.6.9 add safe connection edit data endpoint

* 0.9.6.9 make connection editing explicit and in-place

* Bump version to 0.9.6.9

* 0.9.6.9 catch duplicate functions and admin JavaScript syntax
2026-08-19 13:35:12 +02:00
Terranom674
377b29a127 Bump Bratonien Tools to 0.9.6.8 2026-08-19 13:21:11 +02:00
Terranom674
d9696172c5 Fix duplicate connector edit handlers 2026-08-19 13:20:55 +02:00
Terranom674
3f2a85fb0b Bump version to 0.9.6.7 2026-08-19 13:17:34 +02:00
Terranom674
c882ba994b Expose editor for every connector connection 2026-08-19 13:17:13 +02:00
Terranom674
cd2c9e350b Make WebDAV connections editable in place 2026-08-19 13:16:29 +02:00
Terranom674
394f375e91 Register connection editor 2026-08-19 13:16:00 +02:00
Terranom674
3b1ccd99a5 Add end-user connection editor 2026-08-19 13:15:30 +02:00
Terranom674
04d88cd40f Bump version to 0.9.6.6 2026-08-19 13:05:02 +02:00
Terranom674
a4f953551c Report real WebDAV failure cause and missing WebDAV configuration 2026-08-19 13:04:36 +02:00
Terranom674
b4161471ca Bump version to 0.9.6.5 2026-08-19 13:00:28 +02:00
Terranom674
71fdadb7f8 Show WebDAV failure reason in connector status 2026-08-19 13:00:10 +02:00
Terranom674
98134b5ab8 Expose WebDAV failure reason before legacy fallback 2026-08-19 12:59:55 +02:00
Terranom674
5f82f2ad71 Bump version to 0.9.6.4 2026-08-19 12:56:17 +02:00
Terranom674
494f8bf192 Show active connector route in status polling 2026-08-19 12:55:41 +02:00
Terranom674
c46f9bb5bd Bump Bratonien Tools to 0.9.6.3 2026-08-19 12:12:57 +02:00
Terranom674
992cb55714 0.9.6.2 show WebDAV vs legacy fallback 2026-08-19 11:43:12 +02:00
Terranom674
46782ea9a3 0.9.6.2 expose active connector route 2026-08-19 11:42:48 +02:00
Terranom674
d03410c9ef 0.9.6.2 make WebDAV primary and legacy local true fallback 2026-08-19 11:24:59 +02:00
Terranom674
aa2a6e4c02 0.9.6.2 pair WebDAV migration with legacy fallback 2026-08-19 11:11:39 +02:00
Terranom674
ebdd77fb0a Release 0.9.6.2 migration phase one 2026-08-19 11:07:01 +02:00
Terranom674
6c36aaeb8a Begin 0.9.6.2 legacy connector migration 2026-08-19 11:06:07 +02:00
Terranom674
2c7cd9e780 Align WebDAV image stream version with 0.9.6.1 2026-08-19 00:13:11 +02:00
Terranom674
6155955eee Align WebDAV preview runtime version with 0.9.6.1 2026-08-19 00:12:39 +02:00
Terranom674
5179c1cc33 Align derivative builder version with 0.9.6.1 2026-08-19 00:12:08 +02:00
Terranom674
facb270b13 Refresh README for 0.9.6.1 2026-08-19 00:11:53 +02:00
Terranom674
b4f56ac04b Update current status for 0.9.6.1 2026-08-19 00:11:27 +02:00
Terranom674
16571f9322 Bump version to 0.9.6.1 2026-08-18 23:46:33 +02:00
Terranom674
d7fd99eb7c Clean stale WebDAV Piwigo content in runner 2026-08-18 23:46:16 +02:00
Terranom674
207c2ae502 Clean stale WebDAV Piwigo sites after connection deletion 2026-08-18 23:46:03 +02:00
Terranom674
4d04f115e1 Remove WebDAV Piwigo content when deleting connection 2026-08-18 23:45:42 +02:00
Terranom674
ed9088a8e2 Keep WebDAV precache on 0.9.5.x 2026-08-18 23:38:22 +02:00
Terranom674
8c7476a5d2 Keep derivative builder on 0.9.5.x 2026-08-18 23:37:54 +02:00
Terranom674
bc9d378bfd Restore 0.9.5.x version line 2026-08-18 23:37:35 +02:00
Terranom674
3da61079f2 Release 0.9.6.0 WebDAV derivative rebuild 2026-08-18 23:33:16 +02:00
Terranom674
f380ee4aec Serve WebDAV previews with their real MIME type 2026-08-18 23:32:01 +02:00
Terranom674
96dda18fc9 Build WebDAV derivatives with validated Piwigo image sources 2026-08-18 23:31:26 +02:00
Terranom674
3767620867 Make WebDAV derivatives safe and Piwigo-compatible 2026-08-18 23:30:56 +02:00
Terranom674
d18e7108dd Rebuild WebDAV previews in Piwigo-compatible formats 2026-08-18 23:30:22 +02:00
Terranom674
450f9a0ac5 Bump version to 0.9.5.22 2026-08-18 23:17:53 +02:00
Terranom674
47182a86a2 Hotfix: never build WebDAV derivatives during page rendering 2026-08-18 23:16:43 +02:00
Terranom674
907fba8c8f Expose WebDAV derivative builder runtime version 2026-08-18 23:15:24 +02:00
Terranom674
bf03fea647 Bump version to 0.9.5.21 2026-08-18 23:10:12 +02:00
Terranom674
b3aef2744f Handle WebDAV derivative identity correctly 2026-08-18 23:09:53 +02:00
Terranom674
bcf98c63ef Fix WebDAV derivative metadata and identity handling 2026-08-18 23:09:23 +02:00
Terranom674
fa3ce09589 Bump version to 0.9.5.20 2026-08-18 22:59:19 +02:00
Terranom674
fde03e66f9 Build WebDAV Piwigo derivatives during sync 2026-08-18 22:59:04 +02:00
Terranom674
b780636791 Prebuild Piwigo derivatives for WebDAV images 2026-08-18 22:58:32 +02:00
Terranom674
8785a1ccc5 Generate WebDAV derivatives from prepared previews 2026-08-18 22:58:14 +02:00
Terranom674
554778601f Bump version to 0.9.5.19 2026-08-18 22:48:46 +02:00
Terranom674
c1665e53ad Preload WebDAV previews during sync 2026-08-18 22:48:27 +02:00
Terranom674
047045ad23 Serve precached WebDAV previews 2026-08-18 22:48:03 +02:00
Terranom674
0355f4a555 Use precached WebDAV previews for derivatives 2026-08-18 22:47:44 +02:00
Terranom674
a71c07cbce Precache WebDAV previews during sync 2026-08-18 22:47:29 +02:00
Terranom674
69213adabf Bump version to 0.9.5.18 2026-08-18 22:42:38 +02:00
Terranom674
272930ead5 Fix WebDAV image lookup without private runtime map dependency 2026-08-18 22:42:23 +02:00
Terranom674
5a115e3b4a Route WebDAV photos to live Nextcloud image stream 2026-08-18 22:30:35 +02:00
Terranom674
6791b65e50 Stream Nextcloud image bytes for WebDAV-backed photos 2026-08-18 22:29:53 +02:00
Terranom674
2238845579 Serve WebDAV originals through Piwigo image URLs 2026-08-18 22:29:32 +02:00
Terranom674
4c862c77b3 Bump version to 0.9.5.16 2026-08-18 22:23:48 +02:00
Terranom674
370572c77d Register WebDAV roots as dedicated Piwigo sites 2026-08-18 22:23:30 +02:00
Terranom674
74911e473c Use dedicated WebDAV gallery roots and suppress duplicate sources 2026-08-18 22:22:51 +02:00
Terranom674
65cd8898d3 Bump version for wizard validation lifecycle hardening 2026-08-18 22:18:54 +02:00
Terranom674
e46196c143 Keep NC wizard open across validation errors and form posts 2026-08-18 22:18:34 +02:00
Terranom674
6a71532839 Bump plugin version to 0.9.5.14 2026-08-18 22:14:57 +02:00
Terranom674
3cd647ea6b Clarify WebDAV Piwigo fallback authentication failures 2026-08-18 22:14:41 +02:00
Terranom674
639e50afb5 Bump Bratonien Tools to 0.9.5.13 2026-08-18 22:12:33 +02:00
Terranom674
17f7fa8d52 Validate WebDAV wizard Piwigo fallback before saving 2026-08-18 22:12:11 +02:00
Terranom674
af527e496b Validate connection-scoped Piwigo fallback credentials 2026-08-18 22:11:36 +02:00
Terranom674
577d1ebf81 Bump plugin version to 0.9.5.12 2026-08-18 21:56:49 +02:00
Terranom674
3bd3158817 Improve WebDAV sync failure diagnostics 2026-08-18 21:56:30 +02:00
Terranom674
c916d742f5 Bump version to 0.9.5.11 2026-08-18 21:53:40 +02:00
Terranom674
6f0891f6a8 Enable Piwigo registration for parallel WebDAV connections 2026-08-18 21:53:13 +02:00
Terranom674
b8ea6b1fe2 Bump plugin version to 0.9.5.10 2026-08-18 21:45:51 +02:00
Terranom674
aeba24c6fa Clarify WebDAV registration through existing productive sync 2026-08-18 21:45:22 +02:00
Terranom674
0d81d83d12 Build WebDAV shadow trees before productive Piwigo sync 2026-08-18 21:45:04 +02:00
Terranom674
dbedb83c14 Bump plugin version for WebDAV-first wizard 2026-08-18 21:39:03 +02:00
Terranom674
04620f20a2 Persist wizard auth for WebDAV connections 2026-08-18 21:38:38 +02:00
Terranom674
2127649209 Route wizard through WebDAV-first flow 2026-08-18 21:38:18 +02:00
Terranom674
fd5b55ac53 Add WebDAV-first wizard flow 2026-08-18 21:37:55 +02:00
Terranom674
933db21d88 Release 0.9.5.8 2026-08-18 21:34:48 +02:00
Terranom674
a854bda115 Keep connector status polling from reloading admin page 2026-08-18 21:34:32 +02:00
Terranom674
08040edf8f Document WebDAV parallel runtime in 0.9.5.7 2026-08-18 21:27:24 +02:00
Terranom674
bb024000f2 Bump plugin version to 0.9.5.7 2026-08-18 21:26:27 +02:00
Terranom674
539605b19a Run WebDAV path beside existing connector runtime 2026-08-18 21:25:57 +02:00
Terranom674
a550bbd01a Add isolated WebDAV shadow tree runner 2026-08-18 21:25:44 +02:00
Terranom674
e344455e0c Add isolated WebDAV runtime reconciliation 2026-08-18 21:25:29 +02:00
Terranom674
88edd5aee9 Update README for 0.9.5.6 WebDAV parallel path 2026-08-18 21:23:05 +02:00
Terranom674
aa7ddefa0b Record parallel WebDAV connector foundation 2026-08-18 21:22:32 +02:00
Terranom674
ef37e3b612 Bump plugin version to 0.9.5.6 2026-08-18 21:21:44 +02:00
Terranom674
eadee3cc24 Preserve WebDAV credentials in scoped secret updates 2026-08-18 21:21:26 +02:00
Terranom674
d05a6f4f46 Register parallel WebDAV connector action 2026-08-18 21:20:50 +02:00
Terranom674
04b32309df Add parallel WebDAV connector mode 2026-08-18 21:20:08 +02:00
Terranom674
db28692cfb Pass WebDAV credentials from wizard into connection secret 2026-08-18 21:19:45 +02:00
Terranom674
5866824398 Store Nextcloud WebDAV credentials per connection 2026-08-18 21:19:26 +02:00
Terranom674
ac419ac56f Bring README to current 0.9.5.5 connector architecture 2026-08-18 21:17:27 +02:00
Terranom674
65910375d2 Document current connector status and WebDAV migration plan 2026-08-18 21:16:56 +02:00
Terranom674
c547c9b089 Bump plugin version to 0.9.5.5 2026-08-18 21:13:48 +02:00
Terranom674
21caddcec5 Clean deleted connector runtime before sync 2026-08-18 21:13:30 +02:00
Terranom674
237500a8fa Use safe connector deletion handler 2026-08-18 21:13:21 +02:00
Terranom674
f5dae306db Clean stale connector runtime files before sync 2026-08-18 21:13:02 +02:00
Terranom674
20bd5105f3 Fix connector deletion across runtime ownership boundaries 2026-08-18 21:12:50 +02:00
Terranom674
82019fb14d Bump plugin version for WebDAV placeholder work 2026-08-18 21:10:51 +02:00
Terranom674
17e4f66126 Add WebDAV placeholder source builder 2026-08-18 21:08:50 +02:00
Terranom674
1bb2088d06 Release 0.9.5.3 2026-08-18 19:12:15 +02:00
Terranom674
d97ac4b5db NC runtime: reconcile legacy and selected sources generically 2026-08-18 19:10:46 +02:00
Terranom674
e733b6f6e8 NC connector: register generic source scope 2026-08-18 19:09:59 +02:00
Terranom674
c7966db8f2 NC connector: persist generic fileid source scope 2026-08-18 19:09:40 +02:00
Terranom674
e1d01d02f5 NC wizard: route technical check through generic connector views 2026-08-18 19:09:17 +02:00
Terranom674
28f8bb7d96 NC wizard: resolve WebDAV selections by fileid and storage adapter 2026-08-18 19:09:03 +02:00
Terranom674
cd7d8aa2f1 NC runtime: use generic user and fileid source modes 2026-08-18 19:07:56 +02:00
Terranom674
69815b1cf4 NC runtime: scope activity checks per connection user 2026-08-18 19:07:20 +02:00
Terranom674
b301853ea2 NC runtime: make share manifest user-filterable 2026-08-18 19:07:01 +02:00
Terranom674
0e1c4faedd NC runtime: build manifest from selected Nextcloud file IDs 2026-08-18 19:06:33 +02:00
Terranom674
cfa2322d75 NC runtime: remove abandoned user filesystem manifest 2026-08-18 18:37:14 +02:00
Terranom674
1513500e2e NC runtime: remove abandoned user-filesystem mapper 2026-08-18 18:37:05 +02:00
Terranom674
2b16604e6e NC runtime: remove experimental user-filesystem reconcile 2026-08-18 18:36:54 +02:00
Terranom674
c9e8cee89e Bratonien Tools 0.9.5.2 2026-08-18 17:29:08 +02:00
Terranom674
77981a4e05 NC Status: Ergebnistext im Polling tatsächlich aktualisieren 2026-08-18 17:28:37 +02:00
Terranom674
875832b73e NC 9.5: Nextcloud Benutzerstamm unter uid/files sicher erkennen 2026-08-18 17:27:58 +02:00
Terranom674
774c82ab68 Release 0.9.5.1 2026-08-18 17:07:36 +02:00
Terranom674
7024d3cd3d 0.9.5.1: Connector-Ergebnis beim Polling aktualisieren 2026-08-18 17:07:06 +02:00
Terranom674
042a090db4 0.9.5.1: gespeicherten Nextcloud-Zugriffsbenutzer verwenden 2026-08-18 17:06:39 +02:00
Terranom674
2edacf5e0b 0.9.5.1: Benutzerisolation vor jedem Lauf anwenden 2026-08-18 17:06:07 +02:00
Terranom674
e53041f1c1 0.9.5.1: native Verbindungen auf Benutzer-Stamm isolieren 2026-08-18 17:05:53 +02:00
Terranom674
3512f60136 0.9.5.1: Runtime nach Verbindungsquelle trennen 2026-08-18 17:05:04 +02:00
Terranom674
73e4b08dbd 0.9.5.1: benutzerbezogenes Manifest ohne Showcase-View 2026-08-18 17:04:20 +02:00
Terranom674
e06539c125 Bratonien Tools 0.9.4.13 2026-08-18 16:50:13 +02:00
Terranom674
e0cd91a0b7 NC-Connector echte Runtime-Fehlerausgabe erhalten 2026-08-18 16:49:54 +02:00
Terranom674
c28a8c6f80 Version 0.9.4.12 2026-08-18 16:46:58 +02:00
Terranom674
032d9a151c Shell-Syntax im CI mitpruefen 2026-08-18 16:46:34 +02:00
Terranom674
86fad7aa1f NC Connector Laufzeitfehler aussagekraeftig erfassen 2026-08-18 16:46:08 +02:00
Terranom674
62b17fcd09 Bump version for automatic multi-connection runtime reconciliation 2026-08-18 16:22:58 +02:00
Terranom674
1e77988831 Reconcile stored connections before shared runtime 2026-08-18 16:22:31 +02:00
Terranom674
eb3bc48ceb Reconcile wizard connections into shared runtime 2026-08-18 16:22:19 +02:00
Terranom674
b8d31e37ca Version 0.9.4.10 2026-08-18 16:13:59 +02:00
Terranom674
ad90cd678b Technische NC-Anlage uebernimmt vorhandene API einmalig 2026-08-18 16:13:05 +02:00
Terranom674
1065651bee NC-Verbindungen immer loeschbar und Laufzeitfehler sichtbar machen 2026-08-18 16:12:19 +02:00
Terranom674
4e0c9007c3 Tombstone-Pfad pro NC-Verbindung aus Konfiguration bestimmen 2026-08-18 16:10:17 +02:00
Terranom674
1485acbd45 Fallback-Handler auf verbindungsspezifische Secrets umstellen 2026-08-18 16:10:03 +02:00
Terranom674
81a28a9282 Fallback-Aenderungen erhalten verbindungseigene API 2026-08-18 16:09:31 +02:00
Terranom674
201a44a890 Piwigo Sync verwendet API der jeweiligen Verbindung 2026-08-18 16:06:41 +02:00
Terranom674
a64d5b92c5 Verbindungspruefung migriert Authentifizierung auf Verbindungsumfang 2026-08-18 16:05:22 +02:00
Terranom674
14e4943102 Bestehende native Verbindungen auf eigenen Auth-Modus migrieren 2026-08-18 16:04:54 +02:00
Terranom674
b562309c6e Geloeschte NC-Verbindungen vor Laufzeitabruf entfernen 2026-08-18 16:03:40 +02:00
Terranom674
8fc0ae96d4 NC Connector Handler auf verbindungsspezifische Laufzeit umstellen 2026-08-18 16:03:19 +02:00
Terranom674
cbcaf2d575 NC Connector Laufzeit und Authentifizierung pro Verbindung kapseln 2026-08-18 16:02:53 +02:00
Terranom674
0fa5430ff6 NC Connector Authentifizierung pro Verbindung speichern 2026-08-18 16:02:23 +02:00
Terranom674
7d0d97d844 NC-Wizard sichtbare Fenster eindeutig benennen 2026-08-18 15:45:03 +02:00
Terranom674
3325efce31 Version 0.9.4.9 2026-08-18 15:42:00 +02:00
Terranom674
8b4f3193d3 NC-Wizard Flow-Handler registrieren 2026-08-18 15:41:37 +02:00
Terranom674
47ba20e247 NC-Wizard technischen Schrittfluss konsolidieren 2026-08-18 15:41:13 +02:00
Terranom674
e44cceaacc NC-Wizard sichtbare Fenster einzeln navigierbar machen 2026-08-18 15:40:34 +02:00
Terranom674
48822bc9e9 Alten NC-Wizard AJAX-Sonderweg entfernen 2026-08-18 15:30:45 +02:00
Terranom674
d638dcc590 Alten NC-Wizard JS-Sonderweg entfernen 2026-08-18 15:30:39 +02:00
Terranom674
d520209956 Bratonien Tools 0.9.4.8 serverseitiger NC-Wizard 2026-08-18 15:30:22 +02:00
Terranom674
08b33997f5 NC-Wizard Ordnerwahl und Navigation serverseitig rendern 2026-08-18 15:29:55 +02:00
Terranom674
56b18e650c NC-Wizard serverseitige Ordneraktionen registrieren 2026-08-18 15:28:29 +02:00
Terranom674
c3d40faa1e NC-Wizard Ordnerbrowser serverseitig konsolidieren 2026-08-18 15:28:05 +02:00
Terranom674
867d8e9804 Bratonien Tools 0.9.4.7 2026-08-18 15:05:22 +02:00
Terranom674
b891e06fe5 NC-Wizard sichere WebDAV-Ordnerauswahl und Navigation 2026-08-18 15:04:33 +02:00
Terranom674
5694659004 NC-Wizard Ordner per User-WebDAV laden 2026-08-18 15:03:34 +02:00
Terranom674
eeaac9caaa NC-Wizard User-Scope und Zurück-Navigation registrieren 2026-08-18 15:03:19 +02:00
Terranom674
0a7548414b NC-Wizard strikt auf angemeldeten Nextcloud-Nutzer begrenzen 2026-08-18 15:02:56 +02:00
Terranom674
f1a117818f NC-Wizard Verzeichnisdialog und Hinweise verfeinern 2026-08-18 14:56:44 +02:00
Terranom674
c2c8652557 Automatische Syntaxpruefung fuer PHP und Python einfuehren 2026-08-18 14:55:35 +02:00
Terranom674
ea3a65664d NC-Connector Verzeichnisfilter in Runtime schreiben 2026-08-18 14:54:47 +02:00
Terranom674
c85aa2e833 Bratonien Tools 0.9.4.6 Verzeichnisauswahl laden 2026-08-18 14:54:02 +02:00
Terranom674
f671e76170 NC-Wizard dynamische Verzeichnisauswahl hinzufuegen 2026-08-18 14:53:29 +02:00
Terranom674
755e7d39fa NC-Wizard Verzeichnisoptionen bereitstellen 2026-08-18 14:53:09 +02:00
Terranom674
6ed7c483e3 NC-Wizard Verzeichnisaktionen registrieren 2026-08-18 14:52:57 +02:00
Terranom674
3eef7180d0 NC-Wizard Verzeichnisauswahl und Stammverzeichnis ergaenzen 2026-08-18 14:52:38 +02:00
Terranom674
779dc3037f NC-Connector Verzeichnisfilter pro Storage ergaenzen 2026-08-18 14:51:06 +02:00
Terranom674
eeb86722ae NC-Wizard Reader vor erstem DB-Test laden 2026-08-18 14:34:49 +02:00
Terranom674
5d15f0d6ba Version auf 0.9.4.5 anheben 2026-08-18 14:33:09 +02:00
Terranom674
917a36d21e NC-Wizard Reader-Bruecke registrieren 2026-08-18 14:32:55 +02:00
Terranom674
80a25d49e7 NC-Wizard bekannte Reader-Zugangsdaten wiederverwenden 2026-08-18 14:32:10 +02:00
Terranom674
65de8020e9 Update NC wizard status for 0.9.4.4 2026-08-18 11:16:49 +02:00
Terranom674
b264f6cf8c Document unified Nextcloud and database wizard credentials 2026-08-18 11:16:27 +02:00
Terranom674
efeebf9d56 Bump Bratonien Tools to 0.9.4.4 2026-08-18 11:15:48 +02:00
Terranom674
77988184a3 Remove duplicate database credentials from NC wizard 2026-08-18 11:15:27 +02:00
Terranom674
a1373aa6a1 Reuse Nextcloud credentials for database access in wizard 2026-08-18 11:14:20 +02:00
Terranom674
18007c6853 Release 0.9.4.3 2026-08-18 09:13:19 +02:00
Terranom674
82e6757f71 Clear stale NC wizard sessions on open and close 2026-08-18 09:12:58 +02:00
44 changed files with 7895 additions and 861 deletions

59
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Syntax Check
on:
push:
pull_request:
jobs:
syntax:
runs-on: ubuntu-latest
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: PHP installieren
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
coverage: none
- name: PHP Syntax pruefen
shell: bash
run: |
set -euo pipefail
while IFS= read -r -d '' file; do
php -l "$file"
done < <(find . -type f -name '*.php' -print0)
- name: Doppelte Bratonien-Funktionen pruefen
shell: bash
run: |
set -euo pipefail
duplicates="$(grep -RhoE 'function[[:space:]]+bratonien_tools_[A-Za-z0-9_]+' --include='*.php' . | awk '{print $2}' | sort | uniq -d || true)"
if [[ -n "$duplicates" ]]; then
echo "Doppelte globale Bratonien-Funktionen gefunden:" >&2
printf '%s\n' "$duplicates" >&2
exit 1
fi
- name: Admin JavaScript Syntax pruefen
shell: bash
run: |
set -euo pipefail
sed -n '/<script>/,/<\/script>/p' template/admin_tabs.tpl | sed '1d;$d' > /tmp/bratonien-admin-tabs.js
node --check /tmp/bratonien-admin-tabs.js
node --check js/nc_connector_edit_v2.js
- name: Python Syntax pruefen
shell: bash
run: |
set -euo pipefail
python3 -m compileall -q runtime
- name: Shell Syntax pruefen
shell: bash
run: |
set -euo pipefail
while IFS= read -r -d '' file; do
bash -n "$file"
done < <(find . -type f -name '*.sh' -print0)

View File

@@ -1,121 +1,139 @@
# Aktueller Entwicklungsstand # Aktueller Entwicklungsstand
Stand: 18.08.2026 Stand: 19.08.2026
## Plugin ## Plugin
- Aktuelle Plugin-Version: **0.9.4.2** - Aktuelle Plugin-Version: **0.9.6.1**
- Aktueller Entwicklungsblock: **NC Connector Endnutzer-Assistent und Konsolidierung** - Aktueller Entwicklungsblock: **NC Connector WebDAV-End-to-End-Pfad mit Piwigo-Registrierung, Preview-/Derivatstrecke und sauberem Verbindungs-Lebenszyklus**
- `0.9.3.43` markiert den abgeschlossenen vorherigen Meilenstein. - GitHub ist das führende Repository.
- `0.9.4.x` ist der neue Entwicklungsblock. - Der bestehende lokale Connector bleibt vollständig erhalten und wird nicht automatisch migriert.
## Produktiver Stand ## Nicht verhandelbare Architekturregeln
Der lokale NC Connector ist im aktuellen Bratonien-Einsatz End-to-End funktionsfähig: - Nextcloud bleibt die Quelle der Originalbilder.
- Originalbilder werden **nicht dauerhaft nach Piwigo kopiert**.
- Der bestehende lokale Produktionsweg bleibt funktionsfähig.
- WebDAV benötigt nur Nextcloud-Adresse, Benutzer/App-Passwort und normale WebDAV-Berechtigungen des Benutzers.
- Kein Nextcloud-PostgreSQL-Zugriff, kein `occ`, keine Storage-IDs, keine Host-Mounts, kein FUSE, davfs oder rclone als Voraussetzung.
- WebDAV-Verbindungen werden voneinander isoliert verarbeitet.
- Fehler der WebDAV-Bildverarbeitung dürfen keinen normalen Piwigo-Seitenaufruf zum Absturz bringen.
- Nextcloud-Freigaben `folder` und `file` werden verarbeitet. ## Bestehende Quellenmodi
- 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.
## 0.9.4.1 Weiterhin vorhanden:
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. - `legacy-view`
- `user-shares`
- `selected-fileids`
## 0.9.4.2 Neuer WebDAV-Modus:
### Verbindungsassistent - `webdav-placeholder`
Der Assistent wurde erneut aus Endnutzersicht konsolidiert: Es gibt keine automatische Migration bestehender Verbindungen auf WebDAV.
1. Nextcloud-Adresse + Benutzer + Passwort. ## WebDAV-Ablauf in 0.9.6.1
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. 1. Der Assistent prüft Nextcloud über WebDAV und lässt sichtbare Verzeichnisse auswählen.
2. `runtime/reconcile-webdav.php` erzeugt die verbindungseigene Runtime-Konfiguration.
### Storage-Mappings 3. `runtime/lib/build_webdav_placeholder_source.py` liest die ausgewählten Verzeichnisse rekursiv per PROPFIND.
4. Ordner, Dateinamen, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
Das Storage-Format erlaubt jetzt einen leeren `source_prefix`. Damit kann ein kompletter Storage direkt auf einen lokalen Mount zeigen. Die frühere Fehlermeldung 5. Eine lokale Platzhalterquelle wird erzeugt, ohne Originalbilder dauerhaft zu speichern.
6. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree.
`Storage-Zeilen muessen das Format storage_id | source_prefix | local_mount verwenden.` 7. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID` und wird als eigene physische Piwigo-Site registriert.
8. Dadurch erscheint die ausgewählte Nextcloud-Wurzel als normales Piwigo-Album; technische `bratonien-webdav-ID`-Wrapper werden nicht angezeigt.
tritt bei einem legitimen Root-Mapping nicht mehr auf. 9. Vorbereitete Arbeitsbilder liegen unter `_data/bratonien-tools/nc-webdav-preview/connection-ID`.
10. Arbeitsbilder werden als JPEG beziehungsweise PNG erzeugt und maximal auf 4096 px Kantenlänge begrenzt, damit Piwigos Bildbibliothek sie zuverlässig verarbeiten kann.
### Native Aktivierung 11. Der CLI-Derivat-Builder korrigiert die Piwigo-Bildabmessungen anhand des Arbeitsbilds und erzeugt die aktuell konfigurierten Standard- und Custom-Derivate.
12. Der normale Frontend-Aufruf erzeugt **keine** Derivate. Fehlt ein Derivat, wird das vorbereitete Bild als sicherer Fallback ausgeliefert.
`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. 13. Originalbilder werden bei echtem Originalabruf serverseitig über `webdav-image.php` aus Nextcloud gestreamt; Nextcloud-Zugangsdaten erscheinen nicht im Browser.
### Dokumentation
README und CURRENT_STATUS wurden auf den aktuellen `0.9.4.x`-Stand gebracht und beschreiben jetzt den realen API-first-Sync, den Wizard, die Statusaktualisierung ohne Reload und die aktuelle Runtime-Architektur.
## Architektur NC Connector
- Nextcloud ist Quelle der Originalbilder.
- Piwigo erhält einen Shadow Tree aus Verzeichnissen und Symlinks.
- PostgreSQL-Reader und die Views `piwigo_showcase_sources` / `piwigo_showcase_activity` liefern Quellen und Aktivität.
- Runtime-Konfigurationen: `/etc/bratonien-tools/nc-connector/connection-*.conf`
- State: `/var/lib/bratonien-tools/nc-connector/connection-ID`
- Öffentlicher Admin-Status: Piwigo `_data/bratonien-tools/nc-connector-status/`
- Runner: `runtime/run-all.sh`
- Einzelverbindung: `runtime/sync.sh`
## Piwigo-Synchronisierung ## Piwigo-Synchronisierung
Priorität: WebDAV verwendet denselben Piwigo-Synchronisationspfad wie der bestehende Connector, aber mit einer verbindungseigenen physischen Site.
1. gespeicherter Piwigo-API-Key; Nach der Dateisynchronisierung laufen die vorhandenen Piwigo-Nacharbeiten, darunter Metadaten-, Integritäts-, Kategorie-, Pfad-, Rang- und Cache-Aktualisierungen sowie der Orphan-Abgleich.
2. bei nicht nutzbarer API gespeicherter Benutzername/Passwort-Fallback;
3. ohne beide Wege Abbruch mit Fehlerstatus.
Produktive API-Methoden: Die direkte produktive Synchronisierung ist derzeit ausdrücklich auf **Piwigo 16.4.0** abgestimmt.
- `bratonien.nc.syncProductive` ## WebDAV-Bildausgabe und Derivate
- `bratonien.nc.syncOrphans`
`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. Wichtige Dateien:
## Activity Gate und Shadow Tree - `include/webdav_image_runtime.inc.php` Zuordnung Piwigo-Bild ↔ WebDAV-Quelle, Preview-/Derivat-Helfer und URL-Filter.
- `webdav-image.php` berechtigungsgeprüfter serverseitiger Original-/Preview-Abruf.
- `runtime/lib/precache-webdav-previews.php` vorbereitete JPEG-/PNG-Arbeitsbilder ohne dauerhafte Originalkopie.
- `runtime/lib/build-webdav-derivatives.php` CLI-Aufbau der konfigurierten Piwigo-Derivate.
Das Activity Gate berücksichtigt: Der Frontend-Hook führt keine Bildgenerierung mehr aus. Damit bleibt ein Fehler in GD/Imagick/External ImageMagick auf den Hintergrundlauf begrenzt.
- Aktivitätsstand; ## Löschen einer WebDAV-Verbindung seit 0.9.6.1
- Share-Fingerprint;
- Quiet-Time;
- Max-Wartezeit;
- periodischen Full-Sync;
- Reparaturbedarf bei beschädigter lokaler Struktur.
Der Shadow-Tree-Wechsel besitzt einen Rollback auf den vorherigen Baum, falls das Umschalten fehlschlägt. Eine gelöschte WebDAV-Verbindung darf keine weiterhin sichtbaren oder über alte Piwigo-Datensätze erreichbaren Bilder hinterlassen.
Beim Löschen über die Oberfläche:
- werden die zur Verbindung gehörende Piwigo-Site, ihre Alben und Bilddatensätze entfernt;
- werden zugehörige Piwigo-Derivate entfernt;
- bleiben die Nextcloud-Originaldateien unangetastet;
- werden Laufzeit- und Preview-Daten anschließend bereinigt.
Zusätzlich läuft `runtime/cleanup-webdav-piwigo.php` im gemeinsamen Runner. Er erkennt verwaiste WebDAV-Piwigo-Sites, für die keine Connector-Verbindung mehr existiert, und entfernt deren Piwigo-Inhalte sowie Gallery-, Source-, Preview-, State-, Status- und Runtime-Reste. Damit werden auch bereits vor 0.9.6.1 gelöschte Verbindungen nachträglich bereinigt.
## Gemeinsamer Runner
`runtime/run-all.sh` verarbeitet die Schritte in kontrollierter Reihenfolge:
1. lokale Verbindungen reconciliieren;
2. WebDAV-Verbindungen reconciliieren;
3. verwaiste WebDAV-Piwigo-Inhalte bereinigen;
4. verwaiste allgemeine Runtime-Dateien bereinigen;
5. aktive WebDAV-Verbindungen synchronisieren;
6. aktive lokale Verbindungen synchronisieren.
Der vorhandene systemd-Timer ruft direkt die Runtime aus dem installierten Plugin auf.
## Status- und UI-Regeln
- Hintergrund-Polling lädt die Admin-Seite nicht neu.
- Ein geöffneter Verbindungsassistent bleibt bei Statusaktualisierungen geöffnet.
- Fehlgeschlagene Fallback-/Credential-Prüfungen schließen den Assistenten nicht.
- Laufzeitfehler unterscheiden WebDAV-Lesen, Piwigo-Synchronisierung, Preview- und Derivatfehler.
## Sicherheit ## Sicherheit
- Nextcloud-Originale werden nicht gelöscht.
- Connector-Zugangsdaten werden verschlüsselt gespeichert. - Connector-Zugangsdaten werden verschlüsselt gespeichert.
- Wizard-Secrets bleiben serverseitig. - WebDAV-Credentials werden nur serverseitig verwendet.
- Storage-Pfade werden validiert. - Bildzugriffe über `webdav-image.php` werden gegen Piwigos Zugriffsrechte geprüft.
- SQL-View-Namen werden validiert. - Nextcloud-Originale werden weder beim Sync noch beim Löschen einer Verbindung verändert.
- Piwigo-API ist versionsgebunden. - Derivate und Arbeitsbilder sind lokale Cache-/Arbeitsdaten und dürfen entfernt beziehungsweise neu aufgebaut werden.
- 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.
## Noch offen ## Aktueller Prüfstand
- vollständiger Remote-Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff; Bereits umgesetzt:
- weitere Bereinigung alter Legacy-Migrationshelfer, sobald sie nicht mehr benötigt werden;
- weitere Endnutzer-Optimierung des Assistenten auf Basis realer Fehler- und Installationspfade.
## Repository-Fallback - WebDAV-Verbindungsassistent;
- Verzeichnisauswahl;
- rekursiver WebDAV-Scan;
- Shadow Tree;
- separate Piwigo-Site je WebDAV-Verbindung;
- sichtbare Albumstruktur ohne technische Wrapper;
- echte Bildausgabe aus Nextcloud;
- lokale vorbereitete Arbeitsbilder;
- CLI-Derivatstrecke;
- Piwigo-Nacharbeiten und Orphan-Abgleich;
- ETag-basierte Wiederverwendung vorbereiteter Bilder;
- Löschen einer Verbindung einschließlich Piwigo-Inhalten;
- nachträgliche Bereinigung bereits verwaister WebDAV-Piwigo-Sites.
GitHub bleibt das führende Repository. Das private Gitea-System dient weiterhin als Mirror/Fallback. Weiter zu prüfen beziehungsweise unter realer Last zu härten:
- vollständiger Derivataufbau für alle in der Installation vorkommenden Standard- und Custom-Größen;
- Verhalten bei großen Bildmengen und vielen gleichzeitig noch nicht vorbereiteten Bildern;
- Fehlerfälle verschiedener Bildformate und Bild-Backends;
- langfristige Änderungserkennung und Performance im produktiven Einsatz.
## Versionsregel
Die aktuelle Versionslinie ist **0.9.6.x**. Weitere Sprünge auf eine neue Minor-/Major-Linie erfolgen nur bewusst und ausdrücklich.

214
README.md
View File

@@ -2,7 +2,7 @@
Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo. Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo.
Aktuelle Plugin-Version: **0.9.4.2** Aktuelle Plugin-Version: **0.9.6.1**
## Grundprinzip ## Grundprinzip
@@ -12,113 +12,131 @@ Für die Administration gilt: Der normale Nutzer sieht Aufgaben, Entscheidungen
## NC Connector ## NC Connector
Der NC Connector synchronisiert ausdrücklich freigegebene Nextcloud-Inhalte mit Piwigo. Der NC Connector synchronisiert ausdrücklich ausgewählte Nextcloud-Inhalte mit Piwigo.
### Datenmodell ### Datenmodell
- Nextcloud bleibt die Quelle der Originaldateien. - Nextcloud bleibt die Quelle der Originaldateien.
- Originalbilder werden nicht in eine zweite dauerhafte Bibliothek kopiert. - Originalbilder werden nicht dauerhaft in eine zweite Bibliothek unter Piwigo kopiert.
- Ein Shadow Tree erzeugt die für Piwigo benötigte Verzeichnisstruktur mit Symlinks. - Ein Shadow Tree erzeugt die für Piwigo benötigte Ordnerstruktur.
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert. - Ordner werden als physische Piwigo-Alben synchronisiert.
- Einzeldateifreigaben werden im Galerie-Root verlinkt und als echte Piwigo-Orphans ohne Albumzuordnung registriert. - Root-Dateien können als Piwigo-Orphans registriert werden.
- Entfernte Freigaben werden aus Shadow Tree und Piwigo entfernt, das Nextcloud-Original bleibt erhalten.
- Neu importierte physische Connector-Alben werden privat angelegt. - Neu importierte physische Connector-Alben werden privat angelegt.
- Entfernte Quellen werden aus Shadow Tree und Piwigo entfernt; die Nextcloud-Originale bleiben erhalten.
### Verbindungsassistent ### Bestehende lokale Quellenmodi
Neue Verbindungen werden bevorzugt mit einem Endnutzer-Assistenten angelegt. Der bestehende Connector unterstützt weiterhin:
Ablauf: - `legacy-view`
- `user-shares`
- `selected-fileids`
1. Nextcloud-Adresse, Benutzer und Passwort eingeben. Diese Modi bleiben erhalten. Es gibt keine automatische Migration auf WebDAV.
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.
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. ### WebDAV-Modus
Die vollständige technische Maske bleibt weiterhin über **Ohne Assistent anlegen** sowie bei bestehenden Verbindungen unter **Technische Einstellungen** erreichbar. Der neue Endnutzerpfad verwendet `source_mode=webdav-placeholder` und `adapter=remote`.
### Lokaler Adapter Benötigt werden nur:
Der derzeit produktive Adapter verwendet: - Nextcloud-Adresse;
- Nextcloud-Benutzer bzw. App-Passwort;
- normaler WebDAV-Zugriff auf die Inhalte dieses Benutzers;
- eine normale Piwigo-Installation mit ihrer vorhandenen PHP-/Bildverarbeitungsumgebung.
- PostgreSQL-Reader mit minimalen Leserechten; Nicht vorausgesetzt werden:
- 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.
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. - PostgreSQL-Zugriff auf Nextcloud;
- `occ`-Adminzugriff;
- Storage-IDs oder Backend-Pfade;
- zusätzliche Host-Mounts;
- FUSE;
- davfs;
- rclone.
### Activity Gate ### WebDAV-Ablauf
Der regelmäßige Lauf berücksichtigt: 1. Der Assistent prüft Nextcloud und liest die sichtbaren Verzeichnisse per WebDAV.
2. Ausgewählte Verzeichnisse werden rekursiv per PROPFIND eingelesen.
- Nextcloud-Aktivität; 3. Ordner, Dateiname, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
- Signatur der sichtbaren Freigaben; 4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie.
- Quiet-Time; 5. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree.
- maximale Wartezeit; 6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`.
- periodischen Full-Sync; 7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert. Die ausgewählte Nextcloud-Wurzel erscheint dadurch direkt als Album; technische `bratonien-webdav-ID`-Wrapper bleiben unsichtbar.
- notwendigen Reparaturlauf bei beschädigtem Shadow Tree oder fehlenden Piwigo-Alben. 8. `runtime/lib/precache-webdav-previews.php` erzeugt vorbereitete Arbeitsbilder als JPEG beziehungsweise PNG mit maximal 4096 px Kantenlänge.
9. `runtime/lib/build-webdav-derivatives.php` korrigiert die Piwigo-Bildabmessungen und erzeugt die konfigurierten Standard- und Custom-Derivate im Hintergrundlauf.
10. Der Frontend-Aufruf erzeugt keine Derivate. Fehlt eines, wird das vorbereitete Arbeitsbild als sicherer Fallback ausgeliefert.
11. Wenn das Original benötigt wird, streamt `webdav-image.php` es serverseitig direkt aus Nextcloud. Zugangsdaten werden dem Browser nicht offengelegt.
### Piwigo-Synchronisierung ### Piwigo-Synchronisierung
Der produktive Lauf ist API-first: Der WebDAV-Pfad nutzt die vorhandene Piwigo-Synchronisationslogik mit einer verbindungseigenen Site.
1. `bratonien.nc.syncProductive` für physische Alben und Bilder; Nach dem Dateiabgleich laufen die vorhandenen Nacharbeiten, darunter:
2. `bratonien.nc.syncOrphans` für Root-Dateien;
3. Benutzername/Passwort nur als optionaler Fallback.
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. - Metadaten-Synchronisierung;
- Bild- und Kategorieintegrität;
- Uppercats/Kategoriestruktur;
- globale Ränge;
- Pfadpflege;
- Rating-Score;
- Benutzer-Cache-Invalidierung;
- Orphan-Abgleich.
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. Die direkte produktive Synchronisierung ist aktuell ausdrücklich auf **Piwigo 16.4.0** abgestimmt.
### Fallback-Zugang
Piwigo wird API-first angesprochen. Ist für eine Verbindung keine API konfiguriert, kann Benutzername/Passwort als Fallback verwendet werden.
Der Assistent prüft Fallback-Zugangsdaten vor dem Speichern. Eine fehlgeschlagene Prüfung schließt oder leert den Assistenten nicht.
### Statusanzeige
Die Administration zeigt den Laufzeitstatus regelmäßig im Hintergrund an. Dafür wird die Admin-Seite nicht neu geladen; offene Dialoge, Assistenten und aufgeklappte Bereiche bleiben erhalten.
Fehler werden nach Prozessschritt getrennt dargestellt, unter anderem für WebDAV-Einlesen, Piwigo-Synchronisierung, Preview-Erzeugung und Derivat-Erzeugung.
### Löschen einer Verbindung
Seit **0.9.6.1** besitzt der WebDAV-Pfad einen vollständigen Lösch-Lebenszyklus:
- Beim Löschen einer WebDAV-Verbindung werden ihre Piwigo-Site, Alben und Bilddatensätze entfernt.
- Zugehörige Piwigo-Derivate werden entfernt.
- Nextcloud-Originale bleiben unverändert.
- Runtime-, Shadowtree-, Source-, Preview- und Statusdaten werden bereinigt.
- `runtime/cleanup-webdav-piwigo.php` erkennt zusätzlich bereits verwaiste WebDAV-Piwigo-Sites, für die keine Connector-Verbindung mehr existiert, und bereinigt sie im gemeinsamen Runner nachträglich.
### Runtime ### Runtime
Aktive Verbindungen werden über einen gemeinsamen systemd-Timer verarbeitet: Aktive Verbindungen werden über den gemeinsamen Runner verarbeitet:
- `bratonien-nc-connector.timer` - `bratonien-nc-connector.timer`
- `bratonien-nc-connector.service` - `bratonien-nc-connector.service`
- `runtime/run-all.sh` - `runtime/run-all.sh`
- `runtime/sync.sh`
Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`, State-Daten unter `/var/lib/bratonien-tools/nc-connector/connection-ID`. Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`, State-Daten unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
Der Shadow-Tree-Austausch besitzt einen Rollback: Scheitert der Wechsel auf den neuen Baum, wird der vorherige Galeriebaum wiederhergestellt. Die Reihenfolge des gemeinsamen Laufs ist:
### Statusanzeige 1. lokale Verbindungen reconciliieren;
2. WebDAV-Verbindungen reconciliieren;
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. 3. verwaiste WebDAV-Piwigo-Inhalte bereinigen;
4. allgemeine verwaiste Runtime-Dateien bereinigen;
### Bestehende Verbindungen 5. WebDAV-Verbindungen synchronisieren;
6. lokale Verbindungen synchronisieren.
Bestehende Verbindungen können:
- umbenannt;
- geprüft;
- technisch bearbeitet, solange sie nicht aktiv sind;
- deaktiviert und anschließend gelöscht werden.
Das Löschen einer Connector-Verbindung entfernt keine Nextcloud-Originale und keine Piwigo-Bilder. Erreichbare Connector-eigene Status-/State-Daten der Verbindung werden bereinigt.
### Legacy-Migration
Für ältere Installationen mit dem früheren separaten `piwigo-sync` existieren weiterhin Migrations-/Cutover-Helfer. Neue Installationen sollen den nativen Connector-Weg verwenden.
## Bildcache ## Bildcache
Bratonien Tools kann:
- Piwigo-Bildderivate gezielt leeren; - Piwigo-Bildderivate gezielt leeren;
- vorhandene Bildgrößen neu erzeugen; - vorhandene Bildgrößen neu erzeugen;
- Cache-Aufbau als Worker-Prozess starten und abbrechen; - den Cache-Aufbau als Worker-Prozess starten und abbrechen;
- Worker-Zahl automatisch oder manuell konfigurieren; - die Worker-Zahl automatisch oder manuell konfigurieren.
- Originalbilder bleiben unangetastet.
Originalbilder bleiben unangetastet.
## Wasserzeichenverwaltung ## Wasserzeichenverwaltung
@@ -128,7 +146,7 @@ Für ältere Installationen mit dem früheren separaten `piwigo-sync` existieren
- Album-Ausnahmen und Vererbung; - Album-Ausnahmen und Vererbung;
- Position, Transparenz und Skalierung; - Position, Transparenz und Skalierung;
- eigener Runtime-Filter für Piwigo-Derivate; - eigener Runtime-Filter für Piwigo-Derivate;
- bisherige Piwigo-Wasserzeichenkonfiguration wird beim Aktivieren gesichert. - Sicherung der bisherigen Piwigo-Wasserzeichenkonfiguration beim Aktivieren.
## Bilddateien und Pfade ## Bilddateien und Pfade
@@ -171,37 +189,34 @@ Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges B
Der integrierte Updater: Der integrierte Updater:
- liest den Zielstand aus GitHub; - liest den Zielstand aus GitHub;
- bindet ein Update an einen konkreten Commit statt an einen während des Updates beweglichen Branch-Stand; - bindet ein Update an einen konkreten Commit;
- prüft Version und SHA-256 des Zielstands; - prüft Version und SHA-256 des Zielstands;
- erstellt vor dem Austausch ein Backup; - erstellt vor dem Austausch ein Backup;
- nutzt Post/Redirect/Get, damit ein Browser-Reload keine schreibende Aktion erneut ausführt; - nutzt Post/Redirect/Get;
- verlangt Webmaster-Rechte, `ZipArchive` und ausreichende Schreibrechte. - verlangt Webmaster-Rechte, `ZipArchive` und ausreichende Schreibrechte.
## Architektur ## Wichtige Dateien und Verzeichnisse
Wichtige Dateien und Verzeichnisse:
- `main.inc.php` Plugin-Einstieg und Runtime-Hooks - `main.inc.php` Plugin-Einstieg und Runtime-Hooks
- `admin.php` zentraler Admin-Controller mit Post/Redirect/Get - `admin.php` zentraler Admin-Controller
- `include/tool_registry.inc.php` Registry administrativer Aktionen
- `include/nc_connector.inc.php` Datenmodell und gemeinsame Connector-Funktionen - `include/nc_connector.inc.php` Datenmodell und gemeinsame Connector-Funktionen
- `include/nc_connector_wizard.inc.php` Endnutzer-Assistent und Connection-Bearbeitung - `include/nc_connector_wizard.inc.php` Verbindungsassistent
- `include/nc_connector_manage.inc.php` Credential-Format, Storage-Mappings, Verifikation und Löschen - `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-first-Wizardablauf
- `include/nc_connector_create_api.inc.php` API-first-Verbindungserstellung - `include/nc_connector_delete_safe.inc.php` sicheres Löschen einschließlich WebDAV-Piwigo-Inhalten
- `include/nc_connector_piwigo_api.inc.php` API-Zugang und Fallback-Verwaltung
- `include/nc_connector_system.inc.php` Timer- und Laufzeitstatus
- `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync - `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync
- `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung - `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung
- `runtime/lib/activity_gate.py` Activity Gate - `include/webdav_image_runtime.inc.php` WebDAV-Bildzuordnung, Preview-/Derivathilfen und URL-Filter
- `runtime/lib/build_manifest.py` Auflösung der Nextcloud-Freigaben auf Storage-Mounts - `webdav-image.php` berechtigungsgeprüfter WebDAV-Bildstream
- `runtime/lib/shadow_tree.py` atomarer Shadow Tree mit Rollback - `runtime/reconcile-webdav.php` WebDAV-Runtime-Reconcile
- `runtime/lib/piwigo-sync.php` API-first-Piwigo-Sync mit Fallback - `runtime/cleanup-webdav-piwigo.php` Bereinigung gelöschter/verwaister WebDAV-Verbindungen in Piwigo
- `runtime/run-all.sh` Multi-Connection-Runner - `runtime/sync-webdav.sh` Ablauf einer WebDAV-Verbindung
- `runtime/sync.sh` Ablauf einer Verbindung - `runtime/run-all.sh` gemeinsamer Multi-Connection-Runner
- `nc-connector-install.php` native Root-Aktivierung - `runtime/lib/build_webdav_placeholder_source.py` rekursiver WebDAV-Scan und Platzhalterquelle
- `nc-connector-disable.php` Deaktivierung - `runtime/lib/precache-webdav-previews.php` vorbereitete JPEG-/PNG-Arbeitsbilder
- `nc-connector-normalize.php` Normalisierung älterer aktiver Verbindungen - `runtime/lib/build-webdav-derivatives.php` CLI-Derivat-Builder
- `nc-connector-*-cleanup/switch/cutover` Legacy-Migrationshelfer - `runtime/lib/shadow_tree.py` atomarer Shadow Tree
- `runtime/lib/piwigo-sync.php` Piwigo-Sync mit API/Fallback
- `main-cache-build.php` allgemeiner Piwigo-Derivat-/Cache-Builder
- `include/self_update.inc.php` Self-Updater - `include/self_update.inc.php` Self-Updater
- `include/album_shares.inc.php` geschützte Albumfreigaben - `include/album_shares.inc.php` geschützte Albumfreigaben
- `include/public_selection.inc.php` Fotoauswahl - `include/public_selection.inc.php` Fotoauswahl
@@ -214,15 +229,18 @@ Wichtige Dateien und Verzeichnisse:
- administrative Schreibaktionen verwenden Piwigos CSRF-Schutz; - administrative Schreibaktionen verwenden Piwigos CSRF-Schutz;
- Connector-Zugangsdaten werden verschlüsselt gespeichert; - Connector-Zugangsdaten werden verschlüsselt gespeichert;
- Wizard-Geheimnisse werden nicht in Browser-Web-Storage persistiert; - Nextcloud-Zugangsdaten werden verbindungseigen gespeichert und nur serverseitig verwendet;
- Storage-Mounts werden explizit zugeordnet und validiert; - Wizard-Geheimnisse werden nicht im Browser-Web-Storage persistiert;
- SQL-View-Namen werden vor Verwendung validiert; - SQL-View-Namen werden vor Verwendung validiert;
- gefährliche technische Pfade sind nicht Teil des normalen Wizard-Happy-Paths;
- produktive Piwigo-API ist versionsgebunden; - produktive Piwigo-API ist versionsgebunden;
- Originalbilder werden vom Connector nicht gelöscht; - Originalbilder werden vom Connector nicht gelöscht;
- Update-Pakete werden an Commit und Hash gebunden; - WebDAV-Originalbilder werden nicht dauerhaft lokal gespeichert;
- Root-Helfer prüfen CLI-/Root-Ausführung, bevor sie Systemdienste oder `/etc`-/`/var/lib`-Daten verändern. - Update-Pakete werden an Commit und Hash gebunden.
## Entwicklungsstand ## Entwicklungsstand
`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. `0.9.6.x` ist die aktuelle Versionslinie.
Mit **0.9.6.1** ist der WebDAV-Pfad einschließlich Piwigo-Registrierung, vorbereiteter Bildquelle, CLI-Derivatstrecke und Lösch-/Cleanup-Lebenszyklus im Repository umgesetzt. Der bestehende lokale Connector bleibt parallel erhalten.
Der jeweils detaillierte Prüf- und Entwicklungsstand steht in [`CURRENT_STATUS.md`](CURRENT_STATUS.md).

View File

@@ -0,0 +1,231 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_finish_connection_scoped()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '') throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']);
if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password'];
bratonien_tools_nc_wizard_store($state);
$fallback_user = trim((string)$state['_fallback_user']);
$fallback_password = (string)$state['_fallback_password'];
if (($fallback_user === '') !== ($fallback_password === '')) throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.');
if ($state['api_status'] !== 'ok' && $fallback_user === '') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist für diese Verbindung ein Fallback-Zugang erforderlich.');
$mapping_lines = array();
foreach ((array)$state['storages'] as $storage)
{
$key = (string)$storage['storage_id'].'|'.trim((string)$storage['source_prefix'], '/').'|'.(string)$storage['local_mount'];
$mapping_lines[$key] = (string)$storage['storage_id'].' | '.trim((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",array_values($mapping_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_nextcloud_user']=(string)$state['username'];
$_POST['nc_nextcloud_password']=(string)$state['_password'];
$_POST['nc_product']=(string)$state['product'];
$_POST['nc_version']=(string)$state['version'];
$_POST['nc_api_validated']=$state['api_status']==='ok'?'1':'0';
$_POST['nc_connection_api_key_id']=$state['api_status']==='ok'?(string)$state['_api_key_id']:'';
$_POST['nc_connection_api_key_secret']=$state['api_status']==='ok'?(string)$state['_api_key_secret']:'';
$result = bratonien_tools_nc_connector_create_local_api_first();
$connection_id = (int)($result['connection_id'] ?? 0);
if ($connection_id < 1) throw new RuntimeException('Die neue Verbindung konnte nicht gespeichert werden.');
$connection = bratonien_tools_nc_connector_connection($connection_id, false);
if (!$connection) throw new RuntimeException('Die neue Verbindung konnte nach dem Anlegen nicht gelesen werden.');
$config = $connection['config'];
$config['storages'] = array_values($state['storages']);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('Die Verzeichnisauswahl konnte nicht serialisiert werden.');
$table = bratonien_tools_nc_connector_table();
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$connection_id." LIMIT 1");
unset($_SESSION['bratonien_nc_wizard']);
unset($result['connection_id']);
$result['message'] = 'Verbindung wurde vollständig mit eigener Authentifizierung angelegt. '.$result['message'];
return $result;
}
function bratonien_tools_nc_connector_delete_any()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status';
if (!is_dir($status_dir) && !@mkdir($status_dir, 0755, true) && !is_dir($status_dir)) throw new RuntimeException('Der Connector-Statusordner konnte nicht angelegt werden.');
$tombstone = $status_dir.'/deleted-'.$id;
if (@file_put_contents($tombstone, date('c')."\n", LOCK_EX) === false) throw new RuntimeException('Die Verbindung konnte nicht sicher für die Laufzeit deaktiviert werden.');
$public_status = $status_dir.'/connection-'.$id.'.json';
if (is_file($public_status)) @unlink($public_status);
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
$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))
{
bratonien_tools_nc_connector_remove_tree($state_dir);
}
$table = bratonien_tools_nc_connector_table();
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
return array('message'=>'Connector-Verbindung wurde gelöscht und für weitere Laufzeitabrufe gesperrt. Nextcloud- und Piwigo-Bilder blieben unverändert.');
}
function bratonien_tools_nc_connector_verify_connection_scoped()
{
$result = bratonien_tools_nc_connector_verify_managed();
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) return $result;
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
if ((string)($config['origin'] ?? '') !== 'native' || array_key_exists('api_enabled', $config)) return $result;
$credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? '');
$db_password = (string)($credentials['db_password'] ?? '');
$fallback_user = (string)($credentials['piwigo_user'] ?? '');
$fallback_password = (string)($credentials['piwigo_password'] ?? '');
if ($db_password === '') return $result;
$api_key_id = '';
$api_key_secret = '';
if ($fallback_user === '')
{
$legacy_api = bratonien_tools_nc_api_credentials();
$api_key_id = trim((string)($legacy_api['key_id'] ?? ''));
$api_key_secret = trim((string)($legacy_api['key_secret'] ?? ''));
}
$payload = json_encode(array(
'v'=>3,
'db_password'=>$db_password,
'piwigo_user'=>$fallback_user,
'piwigo_password'=>$fallback_password,
'api_key_id'=>$api_key_id,
'api_key_secret'=>$api_key_secret,
'nextcloud_user'=>'',
'nextcloud_password'=>'',
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($payload)) throw new RuntimeException('Verbindungsauthentifizierung konnte nicht migriert werden.');
$config['piwigo_auth'] = 'connection-scoped';
$config['api_enabled'] = $api_key_id !== '';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht migriert werden.');
$blob = bratonien_tools_nc_connector_encrypt_secret($payload);
$table = bratonien_tools_nc_connector_table();
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($blob)."' WHERE id=".$id." LIMIT 1");
return $result;
}
function bratonien_tools_nc_connector_scoped_secret(array $connection)
{
$plain = bratonien_tools_nc_connector_decrypt_secret($connection['secret_blob'] ?? '');
$decoded = json_decode($plain, true);
if (is_array($decoded) && array_key_exists('db_password', $decoded))
{
return array(
'v'=>3,
'db_password'=>(string)($decoded['db_password'] ?? ''),
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
'api_key_id'=>(string)($decoded['api_key_id'] ?? ''),
'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''),
'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''),
'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''),
);
}
return array(
'v'=>3,
'db_password'=>(string)$plain,
'piwigo_user'=>'',
'piwigo_password'=>'',
'api_key_id'=>'',
'api_key_secret'=>'',
'nextcloud_user'=>'',
'nextcloud_password'=>'',
);
}
function bratonien_tools_nc_connector_store_scoped_secret($id, array $connection, array $credentials)
{
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
$is_webdav = (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
if (!$is_webdav && trim((string)($credentials['db_password'] ?? '')) === '') throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.');
if ($is_webdav && (trim((string)($credentials['nextcloud_user'] ?? '')) === '' || (string)($credentials['nextcloud_password'] ?? '') === ''))
{
throw new RuntimeException('Die Nextcloud-Zugangsdaten der WebDAV-Verbindung fehlen.');
}
$credentials['v'] = 3;
$payload = json_encode($credentials, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($payload)) throw new RuntimeException('Verbindungszugangsdaten konnten nicht serialisiert werden.');
$blob = bratonien_tools_nc_connector_encrypt_secret($payload);
if (array_key_exists('api_enabled', $config))
{
$config['api_enabled'] = trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '';
}
$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 secret_blob='".pwg_db_real_escape_string($blob)."', config_json='".pwg_db_real_escape_string($config_json)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".(int)$id." LIMIT 1");
}
function bratonien_tools_nc_connector_fallback_save_scoped()
{
$id = (int)($_POST['connection_id'] ?? 0);
$username = trim((string)($_POST['nc_fallback_user'] ?? ''));
$password = (string)($_POST['nc_fallback_password'] ?? '');
if ($username === '' || $password === '') throw new RuntimeException('Benutzername und Passwort für den Fallback müssen angegeben werden.');
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$credentials['piwigo_user'] = $username;
$credentials['piwigo_password'] = $password;
bratonien_tools_nc_connector_store_scoped_secret($id, $connection, $credentials);
return array('message'=>'Piwigo-Benutzername und Passwort wurden als Fallback dieser Verbindung gespeichert.');
}
function bratonien_tools_nc_connector_fallback_delete_scoped()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$credentials['piwigo_user'] = '';
$credentials['piwigo_password'] = '';
bratonien_tools_nc_connector_store_scoped_secret($id, $connection, $credentials);
return array('message'=>'Fallback dieser Verbindung wurde gelöscht. Ein vorhandener API-Zugang blieb unverändert.');
}

View File

@@ -5,6 +5,11 @@ if (!defined('PHPWG_ROOT_PATH'))
} }
function bratonien_tools_nc_connector_create_local_api_first() function bratonien_tools_nc_connector_create_local_api_first()
{
throw new RuntimeException('Neue Verbindungen über die alte lokale PostgreSQL-/Storage-Struktur sind ab 0.9.6.2 gesperrt. Bestehende lokale Verbindungen bleiben für die schrittweise Migration weiterhin aktiv. Neue Verbindungen müssen über den WebDAV-Assistenten angelegt werden.');
}
function bratonien_tools_nc_connector_create_local_api_first_legacy()
{ {
$required = array( $required = array(
'nc_name' => 'Name', 'nc_name' => 'Name',
@@ -28,11 +33,30 @@ function bratonien_tools_nc_connector_create_local_api_first()
throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort muessen entweder beide angegeben oder beide leer gelassen werden.'); throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort muessen entweder beide angegeben oder beide leer gelassen werden.');
} }
$api = bratonien_tools_nc_api_credentials(); $nextcloud_user = trim((string)($_POST['nc_nextcloud_user'] ?? $_POST['nc_access_user'] ?? ''));
$validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1'; $nextcloud_password = (string)($_POST['nc_nextcloud_password'] ?? '');
if (($api['key_id'] ?? '') === '' && $fallback_user === '' && !$validated_pending_api) if (($nextcloud_user === '') !== ($nextcloud_password === ''))
{ {
throw new RuntimeException('Es ist weder ein geprüfter Piwigo-API-Zugang noch ein Benutzername/Passwort-Fallback vorhanden.'); throw new RuntimeException('Nextcloud-Benutzer und Nextcloud-Passwort muessen entweder beide vorhanden oder beide leer sein.');
}
$wizard_auth_present = array_key_exists('nc_api_validated', $_POST);
$validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1';
$api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ''));
$api_key_secret = trim((string)($_POST['nc_connection_api_key_secret'] ?? ''));
if (($api_key_id === '') !== ($api_key_secret === ''))
{
throw new RuntimeException('API-Schluessel-ID und API-Geheimnis muessen entweder beide vorhanden oder beide leer sein.');
}
if (($validated_pending_api || (!$wizard_auth_present && $fallback_user === '')) && $api_key_id === '')
{
$legacy_api = bratonien_tools_nc_api_credentials();
$api_key_id = trim((string)($legacy_api['key_id'] ?? ''));
$api_key_secret = trim((string)($legacy_api['key_secret'] ?? ''));
}
if ($api_key_id === '' && $fallback_user === '')
{
throw new RuntimeException('Fuer diese Verbindung ist weder ein eigener gepruefter API-Zugang noch ein Benutzername/Passwort-Fallback vorhanden.');
} }
$port = (int)($_POST['nc_port'] ?? 5432); $port = (int)($_POST['nc_port'] ?? 5432);
@@ -42,7 +66,7 @@ function bratonien_tools_nc_connector_create_local_api_first()
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) throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgelöst werden.'); if ($piwigo_root === false) throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgeloest 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] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.'); if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
@@ -60,7 +84,9 @@ function bratonien_tools_nc_connector_create_local_api_first()
'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'=>'connection-scoped',
'api_enabled'=>$api_key_id !== '',
); );
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)
{ {
@@ -69,9 +95,21 @@ function bratonien_tools_nc_connector_create_local_api_first()
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']);
$secret_payload = json_encode(array(
'v'=>3,
'db_password'=>(string)$_POST['nc_db_password'],
'piwigo_user'=>$fallback_user,
'piwigo_password'=>$fallback_password,
'api_key_id'=>$api_key_id,
'api_key_secret'=>$api_key_secret,
'nextcloud_user'=>$nextcloud_user,
'nextcloud_password'=>$nextcloud_password,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($secret_payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
$table=bratonien_tools_nc_connector_table(); bratonien_tools_nc_connector_ensure_table(); $now=date('Y-m-d H:i:s'); $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)); $connection_key='local-'.bin2hex(random_bytes(12));
$secret_blob=bratonien_tools_nc_connector_encrypt_credentials((string)$_POST['nc_db_password'],$fallback_user,$fallback_password);
$config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); $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.'); if(!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
@@ -90,6 +128,6 @@ function bratonien_tools_nc_connector_create_local_api_first()
return array( return array(
'connection_id'=>$id, 'connection_id'=>$id,
'message'=>$fallback_user==='' ? 'Nextcloud-Verbindung wurde API-first ohne dauerhaft gespeicherten Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.' : 'Nextcloud-Verbindung wurde API-first mit verschluesseltem Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.' 'message'=>'Nextcloud-Verbindung wurde mit verbindungseigener Piwigo-Authentifizierung angelegt.'
); );
} }

View File

@@ -0,0 +1,135 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_webdav_site_id(array $connection)
{
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
if ((string)($connection['adapter'] ?? '') !== 'remote') return 0;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') return 0;
$gallery_root = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
$piwigo_root = rtrim(PHPWG_ROOT_PATH, '/');
if ($gallery_root === '' || strpos($gallery_root, $piwigo_root.'/') !== 0) return 0;
$relative = ltrim(substr($gallery_root, strlen($piwigo_root)), '/');
if ($relative === '') return 0;
$site_url = './'.rtrim($relative, '/').'/';
$result = pwg_query(
"SELECT id FROM ".SITES_TABLE.
" WHERE galleries_url='".pwg_db_real_escape_string($site_url)."' LIMIT 1"
);
if (!pwg_db_num_rows($result)) return 0;
$row = pwg_db_fetch_assoc($result);
return (int)$row['id'];
}
function bratonien_tools_nc_connector_remove_webdav_piwigo_content(array $connection)
{
$site_id = bratonien_tools_nc_connector_webdav_site_id($connection);
if ($site_id < 1) return array('site_id'=>0, 'images'=>0);
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$image_rows = array();
$query = '
SELECT DISTINCT i.id, i.path, i.representative_ext
FROM '.IMAGES_TABLE.' AS i
LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id
LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id
LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id
WHERE sc.site_id = '.$site_id.' OR vc.site_id = '.$site_id.'
;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
$row['id'] = (int)$row['id'];
$image_rows[$row['id']] = $row;
}
foreach ($image_rows as $row)
{
try
{
delete_element_derivatives($row);
}
catch (Throwable $e)
{
error_log('Bratonien WebDAV delete derivative cleanup #'.(int)$row['id'].': '.$e->getMessage());
}
}
delete_site($site_id);
if ($image_rows)
{
$ids = array_keys($image_rows);
$remaining = query2array(
'SELECT id FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $ids)).')',
null,
'id'
);
if ($remaining)
{
delete_elements(array_map('intval', $remaining), false);
}
}
invalidate_user_cache(true);
return array('site_id'=>$site_id, 'images'=>count($image_rows));
}
/**
* Delete a connector connection from Piwigo without depending on the web
* server being allowed to write into root-owned runtime directories.
* WebDAV-managed Piwigo records are removed before the connection itself so
* deleted remote content cannot remain visible or addressable in Piwigo.
*/
function bratonien_tools_nc_connector_delete_safe()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
$cleanup = array('site_id'=>0, 'images'=>0);
if (
(string)($connection['adapter'] ?? '') === 'remote'
&& (string)($connection['config']['source_mode'] ?? '') === 'webdav-placeholder'
)
{
$cleanup = bratonien_tools_nc_connector_remove_webdav_piwigo_content($connection);
}
$table = bratonien_tools_nc_connector_table();
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
$status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status';
$public_status = $status_dir.'/connection-'.$id.'.json';
if (is_file($public_status))
{
@unlink($public_status);
}
if (is_dir($status_dir) || @mkdir($status_dir, 0755, true))
{
@file_put_contents($status_dir.'/deleted-'.$id, date('c')."\n", LOCK_EX);
}
if ((int)$cleanup['site_id'] > 0)
{
return array(
'message'=>'Connector-Verbindung wurde gelöscht. Die zugehörigen Piwigo-Alben und '.(int)$cleanup['images'].' Bilder wurden aus Piwigo entfernt. Nextcloud-Dateien blieben unverändert. Laufzeit- und Vorschaudaten werden automatisch bereinigt.',
);
}
return array(
'message'=>'Connector-Verbindung wurde gelöscht. Verbliebene Laufzeitdateien werden vor dem nächsten Connector-Lauf automatisch entfernt. Quelldateien blieben unverändert.',
);
}

View File

@@ -0,0 +1,338 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_migration_state(array $connection)
{
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$missing = array();
if (trim((string)($config['nextcloud_url'] ?? '')) === '') $missing[] = 'Nextcloud-Adresse';
if (trim((string)($credentials['nextcloud_user'] ?? '')) === '') $missing[] = 'Nextcloud-Benutzer';
if ((string)($credentials['nextcloud_password'] ?? '') === '') $missing[] = 'Nextcloud-Passwort';
if (trim((string)($credentials['api_key_id'] ?? '')) === '') $missing[] = 'Piwigo-API-Schluessel-ID';
if (trim((string)($credentials['api_key_secret'] ?? '')) === '') $missing[] = 'Piwigo-API-Geheimnis';
return array(
'ready'=>!$missing,
'missing'=>$missing,
);
}
function bratonien_tools_nc_connector_validate_nextcloud_access($base_url, $username, $password)
{
$base_url = bratonien_tools_nc_wizard_normalize_url($base_url);
$username = trim((string)$username);
$password = (string)$password;
if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden benoetigt.');
$status_response = bratonien_tools_nc_wizard_http($base_url.'/status.php');
if ($status_response['status'] < 200 || $status_response['status'] >= 300)
{
throw new RuntimeException('Die Nextcloud-Adresse konnte nicht bestaetigt werden.');
}
$status = json_decode((string)$status_response['body'], true);
if (!is_array($status) || empty($status['installed']))
{
throw new RuntimeException('Unter dieser Adresse wurde keine installierte Nextcloud erkannt.');
}
$user_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/user?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($user_response['status'] === 401 || $user_response['status'] === 403)
{
throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
}
if ($user_response['status'] < 200 || $user_response['status'] >= 300)
{
throw new RuntimeException('Nextcloud ist erreichbar, aber der Benutzerzugang konnte nicht geprueft werden.');
}
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$resolved = trim((string)($user_data['id'] ?? $username));
return array(
'base_url'=>$base_url,
'username'=>$resolved !== '' ? $resolved : $username,
);
}
function bratonien_tools_nc_connector_validate_scoped_api($api_key_id, $api_key_secret)
{
$api_key_id = trim((string)$api_key_id);
$api_key_secret = trim((string)$api_key_secret);
if ($api_key_id === '' || $api_key_secret === '')
{
throw new RuntimeException('Piwigo-API-Schluessel-ID und API-Geheimnis werden benoetigt.');
}
$status = bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, 'pwg.session.getStatus');
if (!is_array($status)) throw new RuntimeException('Piwigo hat keinen auswertbaren API-Benutzerstatus geliefert.');
$role = strtolower(trim((string)($status['status'] ?? '')));
if (!in_array($role, array('admin','webmaster'), true))
{
throw new RuntimeException('Der API-Key funktioniert, gehoert aber keinem Piwigo-Administrator/Webmaster.');
}
$method_result = bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, 'reflection.getMethodList');
$method_map = array();
bratonien_tools_nc_connector_collect_method_names($method_result, $method_map);
$required = array('bratonien.nc.syncProductive', 'bratonien.nc.syncOrphans');
$missing = array_values(array_diff($required, array_keys($method_map)));
if ($missing)
{
throw new RuntimeException('Der API-Key ist gueltig, aber benoetigte Bratonien-Sync-Methoden fehlen: '.implode(', ', $missing).'.');
}
}
function bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection(array $connection, $mode)
{
$id = (int)$connection['id'];
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$base_url = trim((string)($config['nextcloud_url'] ?? ''));
$username = trim((string)($credentials['nextcloud_user'] ?? ''));
if ($username === '')
{
$username = trim((string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? ''));
}
$password = (string)($credentials['nextcloud_password'] ?? '');
$roots = isset($config['roots']) && is_array($config['roots']) ? array_values($config['roots']) : array();
$selected = array();
$selected_ids = array();
foreach ($roots as $root)
{
$path = trim((string)($root['webdav_path'] ?? ''), '/');
$fileid = (int)($root['fileid'] ?? 0);
if ($path === '' || $fileid < 1) continue;
$selected[] = $path;
$selected_ids[$path] = $fileid;
}
$state = bratonien_tools_nc_wizard_state();
$state = array_merge($state, array(
'editing_connection_id'=>$id,
'editing_adapter'=>(string)$connection['adapter'],
'editing_mode'=>(string)$mode,
'connection_name'=>(string)$connection['name'],
'host_input'=>$base_url,
'base_url'=>$base_url,
'username'=>$username,
'_password'=>$password,
'_fallback_user'=>(string)($credentials['piwigo_user'] ?? ''),
'_fallback_password'=>(string)($credentials['piwigo_password'] ?? ''),
'_api_key_id'=>(string)($credentials['api_key_id'] ?? ''),
'_api_key_secret'=>(string)($credentials['api_key_secret'] ?? ''),
'api_status'=>trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '' ? 'ok' : 'pending',
'roots'=>$roots,
'directory_selected'=>$selected,
'directory_selected_fileids'=>$selected_ids,
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
));
if ($base_url !== '' && $username !== '' && $password !== '')
{
$state['step'] = 2;
$state['scan_ok'] = true;
$state['technical_stage'] = 'mounts';
$state['technical_source'] = 'WebDAV';
$state['technical_error'] = '';
$state['technical_complete'] = false;
$state['directory_selection_ready'] = true;
$state['directory_path'] = '';
$state['directory_parent'] = '';
$state['directory_children'] = array();
$state['directory_current_fileid'] = 0;
try
{
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
}
catch (Throwable $e)
{
$state['step'] = 1;
$state['scan_ok'] = false;
$state['technical_error'] = $e->getMessage();
}
}
else
{
$state['step'] = 1;
$state['scan_ok'] = false;
}
bratonien_tools_nc_wizard_store($state);
}
function bratonien_tools_nc_connector_edit_start()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$is_webdav = (string)$connection['adapter'] === 'remote'
&& (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
if (!$is_webdav)
{
throw new RuntimeException('Diese Legacy-Verbindung wird direkt bearbeitet. Eine WebDAV-Migration ist eine separate Aktion.');
}
bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection($connection, 'update');
return array('message'=>'Verbindung #'.$id.' wurde zum Bearbeiten geoeffnet.');
}
function bratonien_tools_nc_connector_migrate_start()
{
$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('Nur eine Legacy-Verbindung kann auf WebDAV migriert werden.');
}
$migration = bratonien_tools_nc_connector_migration_state($connection);
if (empty($migration['ready']))
{
throw new RuntimeException('Die WebDAV-Migration ist noch nicht bereit. Unter Bearbeiten fehlen: '.implode(', ', $migration['missing']).'.');
}
bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection($connection, 'migrate');
return array('message'=>'Die WebDAV-Migration fuer Verbindung #'.$id.' wurde geoeffnet. Die bestehende Legacy-Verbindung bleibt bis zum erfolgreichen Umstieg unveraendert.');
}
function bratonien_tools_nc_connector_update_local_friendly()
{
$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 Bearbeitung ist nur fuer Legacy-Verbindungen vorgesehen.');
$name = trim((string)($_POST['connection_name'] ?? ''));
if ($name === '') throw new RuntimeException('Bitte einen Namen fuer die Verbindung angeben.');
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$host = trim((string)($_POST['nc_host'] ?? ''));
$port = (int)($_POST['nc_port'] ?? 5432);
$database = trim((string)($_POST['nc_database'] ?? ''));
$user = trim((string)($_POST['nc_user'] ?? ''));
$gallery_root = rtrim(trim((string)($_POST['nc_gallery_root'] ?? '')), '/');
$source_view = trim((string)($_POST['nc_source_view'] ?? ''));
$activity_view = trim((string)($_POST['nc_activity_view'] ?? ''));
if ($host === '') throw new RuntimeException('Datenbank-Server fehlt.');
if ($port < 1 || $port > 65535) throw new RuntimeException('Der Datenbank-Port ist ungueltig.');
if ($database === '') throw new RuntimeException('Datenbankname fehlt.');
if ($user === '') throw new RuntimeException('Reader-Benutzer fehlt.');
if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Piwigo-Galerieordner muss ein absoluter Pfad sein.');
if ($source_view === '' || $activity_view === '') throw new RuntimeException('Die gespeicherten Datenbankansichten duerfen nicht leer sein.');
bratonien_tools_nc_connector_view_name($source_view);
bratonien_tools_nc_connector_view_name($activity_view);
$storage_ids = isset($_POST['nc_storage_id']) && is_array($_POST['nc_storage_id']) ? $_POST['nc_storage_id'] : array();
$storage_prefixes = isset($_POST['nc_source_prefix']) && is_array($_POST['nc_source_prefix']) ? $_POST['nc_source_prefix'] : array();
$storage_mounts = isset($_POST['nc_local_mount']) && is_array($_POST['nc_local_mount']) ? $_POST['nc_local_mount'] : array();
$storages = array();
$count = max(count($storage_ids), count($storage_prefixes), count($storage_mounts));
for ($index = 0; $index < $count; $index++)
{
$storage_id = trim((string)($storage_ids[$index] ?? ''));
$prefix = trim((string)($storage_prefixes[$index] ?? ''), '/');
$mount = rtrim(trim((string)($storage_mounts[$index] ?? '')), '/');
if ($storage_id === '' && $prefix === '' && $mount === '') continue;
if ($storage_id === '') throw new RuntimeException('Bei einem Speicherort fehlt die Storage-ID.');
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Bei einem Speicherort fehlt ein gueltiger lokaler Pfad.');
$storages[] = array('storage_id'=>$storage_id, 'source_prefix'=>$prefix, 'local_mount'=>$mount);
}
if (!$storages) throw new RuntimeException('Mindestens ein Speicherort muss vorhanden sein.');
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$new_db_password = (string)($_POST['nc_db_password'] ?? '');
if ($new_db_password !== '') $credentials['db_password'] = $new_db_password;
if (trim((string)($credentials['db_password'] ?? '')) === '') throw new RuntimeException('Fuer die Legacy-Verbindung ist kein Datenbankpasswort gespeichert.');
$nextcloud_url = trim((string)($_POST['nc_nextcloud_url'] ?? ($config['nextcloud_url'] ?? '')));
$nextcloud_user = trim((string)($_POST['nc_nextcloud_user'] ?? ($credentials['nextcloud_user'] ?? '')));
$new_nextcloud_password = (string)($_POST['nc_nextcloud_password'] ?? '');
if ($new_nextcloud_password !== '') $credentials['nextcloud_password'] = $new_nextcloud_password;
$nextcloud_password = (string)($credentials['nextcloud_password'] ?? '');
$api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ($credentials['api_key_id'] ?? '')));
$new_api_secret = trim((string)($_POST['nc_connection_api_key_secret'] ?? ''));
$old_api_id = trim((string)($credentials['api_key_id'] ?? ''));
if ($api_key_id !== $old_api_id && $new_api_secret === '')
{
throw new RuntimeException('Wenn die API-Schluessel-ID geaendert wird, muss auch das API-Geheimnis neu eingegeben werden.');
}
if ($new_api_secret !== '') $credentials['api_key_secret'] = $new_api_secret;
$api_key_secret = trim((string)($credentials['api_key_secret'] ?? ''));
if (($nextcloud_url === '' || $nextcloud_user === '' || $nextcloud_password === '') && ($nextcloud_url !== '' || $nextcloud_user !== '' || $nextcloud_password !== ''))
{
throw new RuntimeException('Nextcloud-Adresse, Nextcloud-Benutzer und Nextcloud-Passwort muessen fuer WebDAV gemeinsam vollstaendig sein.');
}
if (($api_key_id === '') !== ($api_key_secret === ''))
{
throw new RuntimeException('Piwigo-API-Schluessel-ID und API-Geheimnis muessen gemeinsam vollstaendig sein.');
}
if ($nextcloud_url !== '')
{
$validated_nc = bratonien_tools_nc_connector_validate_nextcloud_access($nextcloud_url, $nextcloud_user, $nextcloud_password);
$nextcloud_url = $validated_nc['base_url'];
$nextcloud_user = $validated_nc['username'];
$credentials['nextcloud_user'] = $nextcloud_user;
}
if ($api_key_id !== '')
{
bratonien_tools_nc_connector_validate_scoped_api($api_key_id, $api_key_secret);
$credentials['api_key_id'] = $api_key_id;
}
$config['host'] = $host;
$config['port'] = (string)$port;
$config['database'] = $database;
$config['user'] = $user;
$config['source_view'] = $source_view;
$config['activity_view'] = $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'] = $storages;
if ($nextcloud_url !== '')
{
$config['nextcloud_url'] = $nextcloud_url;
$config['access_user'] = $nextcloud_user;
$config['nextcloud_access_user'] = $nextcloud_user;
}
$config['piwigo_auth'] = 'connection-scoped';
$config['api_enabled'] = $api_key_id !== '' && $api_key_secret !== '';
unset($config['verification']);
$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.');
$secret_payload = json_encode($credentials, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($secret_payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
$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)."', 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");
$updated = bratonien_tools_nc_connector_connection($id, true);
$migration = $updated ? bratonien_tools_nc_connector_migration_state($updated) : array('ready'=>false);
return array(
'message'=>'Verbindung #'.$id.' wurde gespeichert. '.(!empty($migration['ready']) ? 'Die WebDAV-Migration ist jetzt bereit.' : 'Die WebDAV-Migration ist noch nicht vollstaendig vorbereitet.'),
);
}

View File

@@ -0,0 +1,101 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_finish_generic_scope()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)($state['step'] ?? 0) !== 4 || empty($state['technical_complete']) || trim((string)($state['username'] ?? '')) === '')
{
throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
}
$roots = isset($state['roots']) && is_array($state['roots']) ? $state['roots'] : array();
$storages = isset($state['storages']) && is_array($state['storages']) ? $state['storages'] : array();
if (!$roots) throw new RuntimeException('Es wurden keine Nextcloud-Quellen ausgewählt.');
if (!$storages) throw new RuntimeException('Es wurden keine Storage-Adapter zugeordnet.');
if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']);
if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password'];
bratonien_tools_nc_wizard_store($state);
$fallback_user = trim((string)$state['_fallback_user']);
$fallback_password = (string)$state['_fallback_password'];
if (($fallback_user === '') !== ($fallback_password === '')) throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.');
if (($state['api_status'] ?? '') !== 'ok' && $fallback_user === '') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist für diese Verbindung ein Fallback-Zugang erforderlich.');
$mapping_lines = array();
foreach ($storages as $storage)
{
$storage_id = trim((string)($storage['storage_id'] ?? ''));
$source_prefix = trim((string)($storage['source_prefix'] ?? ''), '/');
$local_mount = rtrim(trim((string)($storage['local_mount'] ?? '')), '/');
if ($storage_id === '' || $local_mount === '') throw new RuntimeException('Eine Storage-Zuordnung ist unvollständig.');
$key = $storage_id.'|'.$source_prefix.'|'.$local_mount;
$mapping_lines[$key] = $storage_id.' | '.$source_prefix.' | '.$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']='piwigo_connector_files';
$_POST['nc_activity_view']='piwigo_connector_activity';
$_POST['nc_gallery_root']=(string)$state['gallery_root'];
$_POST['nc_storages']=implode("\n", array_values($mapping_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_access_user']=(string)$state['username'];
$_POST['nc_nextcloud_user']=(string)$state['username'];
$_POST['nc_nextcloud_password']=(string)$state['_password'];
$_POST['nc_product']=(string)$state['product'];
$_POST['nc_version']=(string)$state['version'];
$_POST['nc_api_validated']=($state['api_status'] ?? '')==='ok'?'1':'0';
$_POST['nc_connection_api_key_id']=($state['api_status'] ?? '')==='ok'?(string)$state['_api_key_id']:'';
$_POST['nc_connection_api_key_secret']=($state['api_status'] ?? '')==='ok'?(string)$state['_api_key_secret']:'';
$result = bratonien_tools_nc_connector_create_local_api_first();
$connection_id = (int)($result['connection_id'] ?? 0);
if ($connection_id < 1) throw new RuntimeException('Die neue Verbindung konnte nicht gespeichert werden.');
$connection = bratonien_tools_nc_connector_connection($connection_id, false);
if (!$connection) throw new RuntimeException('Die neue Verbindung konnte nach dem Anlegen nicht gelesen werden.');
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
$config['source_mode'] = 'selected-fileids';
$config['source_view'] = 'piwigo_connector_files';
$config['activity_view'] = 'piwigo_connector_activity';
$config['access_user'] = (string)$state['username'];
$config['nextcloud_access_user'] = (string)$state['username'];
$config['storages'] = array_values($storages);
$config['roots'] = array_values(array_map(function($root) {
return array(
'fileid'=>(int)($root['fileid'] ?? 0),
'display_name'=>(string)($root['display_name'] ?? ''),
'webdav_path'=>(string)($root['webdav_path'] ?? ''),
);
}, $roots));
unset($config['showcase_user']);
foreach ($config['roots'] as $root)
{
if ((int)$root['fileid'] < 1 || trim((string)$root['display_name']) === '') throw new RuntimeException('Eine ausgewählte Nextcloud-Quelle ist unvollständig.');
}
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('Die Quellenkonfiguration konnte nicht serialisiert werden.');
$table = bratonien_tools_nc_connector_table();
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$connection_id." LIMIT 1");
unset($_SESSION['bratonien_nc_wizard']);
unset($result['connection_id']);
$result['message'] = 'Verbindung wurde mit generischer Nextcloud-Quellenauswahl angelegt. '.$result['message'];
return $result;
}

View File

@@ -179,6 +179,76 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s
return bratonien_tools_nc_connector_api_payload($decoded); return bratonien_tools_nc_connector_api_payload($decoded);
} }
function bratonien_tools_nc_connector_validate_fallback_credentials($username, $password)
{
if (!function_exists('curl_init'))
{
throw new RuntimeException('cURL ist in PHP nicht verfuegbar. Der Piwigo-Fallback kann nicht geprueft werden.');
}
$username = trim((string)$username);
$password = (string)$password;
if ($username === '' || $password === '')
{
throw new RuntimeException('Piwigo-Benutzername und Passwort muessen angegeben werden.');
}
$cookie_file = tempnam(sys_get_temp_dir(), 'br-pwg-auth-');
if ($cookie_file === false)
{
throw new RuntimeException('Temporare Piwigo-Sitzung konnte nicht angelegt werden.');
}
try
{
$url = rtrim(get_absolute_root_url(true), '/').'/ws.php?format=json';
$request = function(array $fields) use ($url, $cookie_file)
{
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_POST=>true,
CURLOPT_POSTFIELDS=>http_build_query($fields),
CURLOPT_COOKIEJAR=>$cookie_file,
CURLOPT_COOKIEFILE=>$cookie_file,
CURLOPT_CONNECTTIMEOUT=>10,
CURLOPT_TIMEOUT=>20,
CURLOPT_FOLLOWLOCATION=>false,
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Connector/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
));
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-Fallback konnte nicht geprueft werden: '.$error);
if ($http < 200 || $http >= 300) throw new RuntimeException('Piwigo-Fallback-Pruefung antwortete mit HTTP '.$http.'.');
$decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $type);
if (($decoded['stat'] ?? '') !== 'ok')
{
$message = trim((string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo hat die Anmeldung abgelehnt.'));
throw new RuntimeException($message !== '' ? $message : 'Piwigo hat die Anmeldung abgelehnt.');
}
return bratonien_tools_nc_connector_api_payload($decoded);
};
$request(array('method'=>'pwg.session.login', 'username'=>$username, 'password'=>$password));
$status = $request(array('method'=>'pwg.session.getStatus'));
if (!is_array($status)) throw new RuntimeException('Piwigo hat keinen auswertbaren Benutzerstatus geliefert.');
$role = strtolower(trim((string)($status['status'] ?? '')));
if (!in_array($role, array('admin','webmaster'), true))
{
throw new RuntimeException('Der Piwigo-Fallback funktioniert, gehoert aber keinem Administrator/Webmaster.');
}
return array('username'=>(string)($status['username'] ?? $username), 'status'=>$role);
}
finally
{
@unlink($cookie_file);
}
}
function bratonien_tools_nc_connector_piwigo_api_test() function bratonien_tools_nc_connector_piwigo_api_test()
{ {
$api_key_id = trim((string)($_POST['nc_piwigo_api_key_id'] ?? '')); $api_key_id = trim((string)($_POST['nc_piwigo_api_key_id'] ?? ''));

View File

@@ -0,0 +1,261 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
/**
* Return the only existing local connector as migration fallback.
*/
function bratonien_tools_nc_connector_single_local_fallback()
{
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
$result = pwg_query("SELECT id, enabled, takeover_state, config_json FROM `$table` WHERE adapter='local' ORDER BY id");
$rows = array();
while ($row = pwg_db_fetch_assoc($result))
{
$rows[] = $row;
if (count($rows) > 1) return null;
}
if (count($rows) !== 1) return null;
$row = $rows[0];
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) $config = array();
return array(
'id'=>(int)$row['id'],
'enabled'=>(bool)$row['enabled'],
'takeover_state'=>(string)$row['takeover_state'],
'config'=>$config,
);
}
/**
* Link a WebDAV connection with the single existing local fallback.
*/
function bratonien_tools_nc_connector_pair_migration_fallback($webdav_id, array &$webdav_config, $now)
{
$legacy = bratonien_tools_nc_connector_single_local_fallback();
if (!$legacy) return null;
$legacy_id = (int)$legacy['id'];
if ($legacy_id < 1 || $legacy_id === (int)$webdav_id) return null;
$webdav_config['migration'] = array(
'role'=>'webdav-primary-candidate',
'legacy_fallback_connection_id'=>$legacy_id,
'fallback_policy'=>'keep-running',
'paired_at'=>(string)$now,
'cutover_state'=>'parallel',
);
$legacy_config = $legacy['config'];
$legacy_config['migration'] = array(
'role'=>'legacy-fallback',
'webdav_successor_connection_id'=>(int)$webdav_id,
'fallback_policy'=>'keep-running',
'paired_at'=>(string)$now,
'cutover_state'=>'parallel',
);
$legacy_json = json_encode($legacy_config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($legacy_json))
{
throw new RuntimeException('Die bestehende Verbindung konnte nicht als Migrations-Fallback markiert werden.');
}
$table = bratonien_tools_nc_connector_table();
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($legacy_json)."', updated='".pwg_db_real_escape_string((string)$now)."' WHERE id=".$legacy_id." LIMIT 1");
return $legacy_id;
}
/**
* Create or update a WebDAV-backed connector from the user-facing wizard.
*
* Remote connections are updated in place. When the editor was opened for a
* legacy connection, a WebDAV successor is created and the legacy connection
* stays available as fallback.
*/
function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok']) || empty($state['technical_complete']))
{
throw new RuntimeException('Die WebDAV-Verbindung wurde im Assistenten noch nicht vollständig vorbereitet.');
}
$base_url = rtrim(trim((string)($state['base_url'] ?? '')), '/');
$username = trim((string)($state['username'] ?? ''));
$password = (string)($state['_password'] ?? '');
if ($base_url === '' || $username === '' || $password === '')
{
throw new RuntimeException('Für den WebDAV-Zugang fehlen Nextcloud-Adresse oder Zugangsdaten.');
}
$selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
? array_values(array_unique(array_map(function($path) { return trim((string)$path, '/'); }, $state['directory_selected'])))
: array();
$selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids'])
? $state['directory_selected_fileids']
: array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
$roots = array();
foreach ($selected as $path)
{
$fileid = isset($selected_ids[$path]) ? (int)$selected_ids[$path] : 0;
if ($fileid < 1) throw new RuntimeException('Für ein ausgewähltes Nextcloud-Verzeichnis fehlt die eindeutige Datei-ID.');
$roots[] = array(
'fileid'=>$fileid,
'display_name'=>basename($path),
'webdav_path'=>$path,
);
}
$name = trim((string)($state['connection_name'] ?? ''));
if ($name === '') $name = 'Nextcloud WebDAV';
$gallery_root = rtrim(trim((string)($state['gallery_root'] ?? '')), '/');
if ($gallery_root === '') $gallery_root = rtrim(PHPWG_ROOT_PATH, '/').'/galleries';
$api_key_id = ($state['api_status'] ?? '') === 'ok' ? trim((string)($state['_api_key_id'] ?? '')) : '';
$api_key_secret = ($state['api_status'] ?? '') === 'ok' ? trim((string)($state['_api_key_secret'] ?? '')) : '';
$fallback_user = trim((string)($state['_fallback_user'] ?? ''));
$fallback_password = (string)($state['_fallback_password'] ?? '');
$api_enabled = $api_key_id !== '' && $api_key_secret !== '';
$editing_id = (int)($state['editing_connection_id'] ?? 0);
$editing_mode = (string)($state['editing_mode'] ?? '');
$editing_connection = $editing_id > 0 ? bratonien_tools_nc_connector_connection($editing_id, false) : null;
$editing_remote = $editing_connection
&& (string)$editing_connection['adapter'] === 'remote'
&& (string)($editing_connection['config']['source_mode'] ?? '') === 'webdav-placeholder'
&& $editing_mode === 'update';
$config = array(
'origin'=>'native',
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
'nextcloud_url'=>$base_url,
'access_user'=>$username,
'nextcloud_access_user'=>$username,
'gallery_root'=>$gallery_root,
'roots'=>$roots,
'state_dir'=>'',
'status_file'=>'',
'quiet_seconds'=>120,
'max_wait_seconds'=>900,
'full_sync_seconds'=>86400,
'parallel_test'=>true,
'piwigo_auth'=>'connection-scoped',
'api_enabled'=>$api_enabled,
);
if ($editing_remote)
{
$old_config = is_array($editing_connection['config'] ?? null) ? $editing_connection['config'] : array();
foreach (array('state_dir','status_file','parallel_gallery_root','source_fingerprint','runtime','migration') as $preserve)
{
if (array_key_exists($preserve, $old_config)) $config[$preserve] = $old_config[$preserve];
}
foreach (array('quiet_seconds','max_wait_seconds','full_sync_seconds') as $preserve)
{
if (isset($old_config[$preserve])) $config[$preserve] = (int)$old_config[$preserve];
}
}
foreach (array('product'=>'nextcloud_product', 'version'=>'nextcloud_version') as $state_key=>$config_key)
{
$value = trim((string)($state[$state_key] ?? ''));
if ($value !== '') $config[$config_key] = $value;
}
$secret_payload = json_encode(array(
'v'=>3,
'db_password'=>'',
'piwigo_user'=>$fallback_user,
'piwigo_password'=>$fallback_password,
'api_key_id'=>$api_key_id,
'api_key_secret'=>$api_key_secret,
'nextcloud_user'=>$username,
'nextcloud_password'=>$password,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($secret_payload)) throw new RuntimeException('WebDAV-Zugangsdaten konnten nicht serialisiert werden.');
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
if ($editing_remote)
{
$config['state_dir'] = trim((string)($config['state_dir'] ?? '')) !== '' ? $config['state_dir'] : '/var/lib/bratonien-tools/nc-connector/connection-'.$editing_id;
$config['status_file'] = trim((string)($config['status_file'] ?? '')) !== '' ? $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('WebDAV-Konfiguration konnte nicht serialisiert werden.');
pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', 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=".$editing_id." LIMIT 1");
unset($_SESSION['bratonien_nc_wizard']);
return array(
'connection_id'=>$editing_id,
'message'=>'WebDAV-Verbindung #'.$editing_id.' wurde gespeichert. Der nächste Connector-Lauf verwendet die neuen Einstellungen.',
);
}
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nicht serialisiert werden.');
$connection_key = 'webdav-'.bin2hex(random_bytes(12));
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($name)."','remote',0,'disabled','"
.pwg_db_real_escape_string($config_json)."','"
.pwg_db_real_escape_string($secret_blob)."','"
.pwg_db_real_escape_string($now)."','"
.pwg_db_real_escape_string($now)."')");
$id = (int)pwg_db_insert_id();
if ($id < 1) throw new RuntimeException('Die WebDAV-Verbindung konnte nicht eindeutig angelegt werden.');
try
{
$config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json';
$legacy_fallback_id = bratonien_tools_nc_connector_pair_migration_fallback($id, $config, $now);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Die WebDAV-Verbindung konnte nach dem Anlegen nicht serialisiert werden.');
}
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." LIMIT 1");
}
catch (Throwable $e)
{
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
throw $e;
}
unset($_SESSION['bratonien_nc_wizard']);
if ($legacy_fallback_id !== null)
{
return array(
'connection_id'=>$id,
'legacy_fallback_connection_id'=>$legacy_fallback_id,
'message'=>'WebDAV-Nachfolger wurde angelegt. Die bestehende lokale Verbindung #'.$legacy_fallback_id.' bleibt als Fallback erhalten.',
);
}
return array(
'connection_id'=>$id,
'message'=>'WebDAV-Verbindung wurde angelegt.',
);
}

View File

@@ -7,32 +7,74 @@ 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']) ? $_SESSION['bratonien_nc_wizard'] : array(); $state = isset($_SESSION['bratonien_nc_wizard']) && is_array($_SESSION['bratonien_nc_wizard']) ? $_SESSION['bratonien_nc_wizard'] : array();
return array_merge(array( return array_merge(array(
'step'=>1,'scan_ok'=>false,'base_url'=>'','host_input'=>'','username'=>'','display_name'=>'','version'=>'','product'=>'Nextcloud', 'step'=>1,
'users'=>array(),'can_list_users'=>false,'showcase_user'=>'','connection_name'=>'','scan_message'=>'', 'scan_ok'=>false,
'technical_stage'=>'reader_credentials','technical_source'=>'Noch nicht geprüft','technical_error'=>'','technical_complete'=>false, 'base_url'=>'',
'db_host'=>'','db_port'=>'5432','db_database'=>'nextcloud','db_user'=>'','db_password_set'=>false, 'host_input'=>'',
'source_view'=>'piwigo_showcase_sources','activity_view'=>'piwigo_showcase_activity','gallery_root'=>'','storages'=>array(),'storage_candidates'=>array(), 'username'=>'',
'api_status'=>'pending','api_username'=>'','api_error'=>'','_password'=>'','_db_password'=>'','_api_key_id'=>'','_api_key_secret'=>'', 'display_name'=>'',
'_fallback_user'=>'','_fallback_password'=>'', 'version'=>'',
'product'=>'Nextcloud',
'users'=>array(),
'can_list_users'=>false,
'showcase_user'=>'',
'connection_name'=>'',
'scan_message'=>'',
'technical_stage'=>'auto_check',
'technical_source'=>'Noch nicht geprüft',
'technical_error'=>'',
'technical_complete'=>false,
'db_host'=>'',
'db_port'=>'5432',
'db_database'=>'nextcloud',
'db_user'=>'',
'db_password_set'=>false,
'source_view'=>'piwigo_showcase_sources',
'activity_view'=>'piwigo_showcase_activity',
'gallery_root'=>'',
'storages'=>array(),
'storage_candidates'=>array(),
'api_status'=>'pending',
'api_username'=>'',
'api_error'=>'',
'_password'=>'',
'_db_password'=>'',
'_api_key_id'=>'',
'_api_key_secret'=>'',
'_fallback_user'=>'',
'_fallback_password'=>'',
), $state); ), $state);
} }
function bratonien_tools_nc_wizard_store(array $state) { $_SESSION['bratonien_nc_wizard'] = $state; } function bratonien_tools_nc_wizard_store(array $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 === '') throw new RuntimeException('Nextcloud-Adresse fehlt.'); if ($host === '') throw new RuntimeException('Nextcloud-Adresse fehlt.');
if (!preg_match('#^https?://#i', $host)) $host = 'https://'.$host; if (!preg_match('#^https?://#i', $host)) $host = 'https://'.$host;
$parts = parse_url($host); $parts = parse_url($host);
if (!is_array($parts) || empty($parts['host'])) throw new RuntimeException('Die Nextcloud-Adresse ist ungültig.'); if (!is_array($parts) || empty($parts['host'])) throw new RuntimeException('Die Nextcloud-Adresse ist ungültig.');
$scheme = strtolower((string)($parts['scheme'] ?? 'https')); $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.'); if (!in_array($scheme, array('http','https'), true)) throw new RuntimeException('Nextcloud muss per HTTP oder HTTPS erreichbar sein.');
$url = $scheme.'://'.$parts['host']; $url = $scheme.'://'.$parts['host'];
if (!empty($parts['port'])) $url .= ':'.(int)$parts['port']; if (!empty($parts['port'])) $url .= ':'.(int)$parts['port'];
if (!empty($parts['path']) && $parts['path'] !== '/') $url .= '/'.trim($parts['path'], '/'); if (!empty($parts['path']) && $parts['path'] !== '/') $url .= '/'.trim($parts['path'], '/');
return rtrim($url, '/'); return rtrim($url, '/');
} }
@@ -41,22 +83,42 @@ function bratonien_tools_nc_wizard_candidate_urls($host)
$host = trim((string)$host); $host = trim((string)$host);
if ($host === '') throw new RuntimeException('Bitte die Adresse der Nextcloud angeben.'); if ($host === '') throw new RuntimeException('Bitte die Adresse der Nextcloud angeben.');
if (preg_match('#^https?://#i', $host)) return array(bratonien_tools_nc_wizard_normalize_url($host)); 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));
return array(
bratonien_tools_nc_wizard_normalize_url('https://'.$host),
bratonien_tools_nc_wizard_normalize_url('http://'.$host),
);
} }
function bratonien_tools_nc_wizard_http($url, $username = '', $password = '', array $headers = array()) function bratonien_tools_nc_wizard_http($url, $username = '', $password = '', array $headers = array())
{ {
if (!function_exists('curl_init')) throw new RuntimeException('Der Server kann Nextcloud derzeit nicht per HTTP prüfen.'); if (!function_exists('curl_init')) throw new RuntimeException('Der Server kann Nextcloud derzeit nicht per HTTP prüfen.');
$ch = curl_init($url); $ch = curl_init($url);
$options = array( $options = array(
CURLOPT_RETURNTRANSFER=>true,CURLOPT_FOLLOWLOCATION=>true,CURLOPT_MAXREDIRS=>3,CURLOPT_CONNECTTIMEOUT=>8,CURLOPT_TIMEOUT=>15, CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>true,
CURLOPT_MAXREDIRS=>3,
CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>15,
CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers), CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers),
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'), CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
); );
if($username!==''){$options[CURLOPT_HTTPAUTH]=CURLAUTH_BASIC;$options[CURLOPT_USERPWD]=$username.':'.$password;}
if ($username !== '')
{
$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
$options[CURLOPT_USERPWD] = $username.':'.$password;
}
curl_setopt_array($ch, $options); curl_setopt_array($ch, $options);
$body=curl_exec($ch);$errno=curl_errno($ch);$status=(int)curl_getinfo($ch,CURLINFO_HTTP_CODE);curl_close($ch); $body = curl_exec($ch);
$errno = curl_errno($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $errno !== 0) throw new RuntimeException('Verbindung fehlgeschlagen.'); if ($body === false || $errno !== 0) throw new RuntimeException('Verbindung fehlgeschlagen.');
return array('status'=>$status, 'body'=>(string)$body); return array('status'=>$status, 'body'=>(string)$body);
} }
@@ -64,28 +126,38 @@ function bratonien_tools_nc_wizard_ocs_data($body)
{ {
$decoded = json_decode((string)$body, true); $decoded = json_decode((string)$body, true);
if (!is_array($decoded) || !isset($decoded['ocs']) || !is_array($decoded['ocs'])) throw new RuntimeException('Nextcloud hat keine gültige Antwort geliefert.'); 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(); $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; $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)) if ($status !== 'ok' && !in_array($status_code, array(100,200), true))
{ {
$message = trim((string)($meta['message'] ?? '')); $message = trim((string)($meta['message'] ?? ''));
if ($message === '' || strtolower($message) === 'ok') $message = 'Nextcloud hat die Anfrage abgelehnt.'; if ($message === '' || strtolower($message) === 'ok') $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']='';
$state['db_host'] = $url_host;
$state['db_port'] = '5432'; $state['db_port'] = '5432';
$state['db_database'] = 'nextcloud'; $state['db_database'] = 'nextcloud';
$state['db_user'] = (string)$state['username'];
$state['_db_password'] = (string)$state['_password'];
$state['db_password_set'] = $state['_db_password'] !== '';
$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_stage']='reader_credentials'; $state['storages'] = array();
$state['technical_source']='Noch nicht geprüft'; $state['storage_candidates'] = array();
$state['technical_stage'] = 'auto_check';
$state['technical_source'] = 'Automatische Prüfung';
$state['technical_error'] = ''; $state['technical_error'] = '';
$state['technical_complete'] = false; $state['technical_complete'] = false;
@@ -94,95 +166,44 @@ function bratonien_tools_nc_wizard_fill_profile(array &$state)
$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'] ?? ''));
$match = false; $match = false;
if ($candidate_url !== '') if ($candidate_url !== '')
{ {
try{$match=bratonien_tools_nc_wizard_normalize_url($candidate_url)===$state['base_url'];}catch(Throwable $ignored){$match=false;} try
{
$match = bratonien_tools_nc_wizard_normalize_url($candidate_url) === $state['base_url'];
} }
catch (Throwable $ignored)
{
$match = false;
}
}
if (!$match) continue; if (!$match) continue;
$full=bratonien_tools_nc_connector_connection((int)$candidate['id'],true); $state['db_host'] = trim((string)($config['host'] ?? $state['db_host']));
if(!$full) continue; $state['db_port'] = trim((string)($config['port'] ?? $state['db_port']));
$credentials=bratonien_tools_nc_connector_credentials_from_blob($full['secret_blob']??''); $state['db_database'] = trim((string)($config['database'] ?? $state['db_database']));
$state['db_host']=(string)($config['host']??''); $state['source_view'] = trim((string)($config['source_view'] ?? $state['source_view']));
$state['db_port']=(string)($config['port']??'5432'); $state['activity_view'] = trim((string)($config['activity_view'] ?? $state['activity_view']));
$state['db_database']=(string)($config['database']??'nextcloud'); $state['gallery_root'] = trim((string)($config['gallery_root'] ?? $state['gallery_root']));
$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['storages'] = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array();
$state['storage_candidates'] = $state['storages']; $state['storage_candidates'] = $state['storages'];
$state['technical_complete']=$state['db_host']!==''&&$state['db_user']!==''&&$state['db_password_set']&&!empty($state['storages']); $state['technical_source'] = 'Bekannte Verbindungswerte werden erneut geprüft';
$state['technical_stage']=$state['technical_complete']?'ready':'reader_credentials';
$state['technical_source']='Passende vorhandene Verbindung übernommen';
break; break;
} }
if($state['db_host']==='') $state['db_host']=$url_host;
}
function bratonien_tools_nc_wizard_scan()
{
$state=bratonien_tools_nc_wizard_state();
$host_input=trim((string)($_POST['nc_wizard_host']??$state['host_input']));
$username=trim((string)($_POST['nc_wizard_user']??$state['username']));
$password=array_key_exists('nc_wizard_password',$_POST)?(string)$_POST['nc_wizard_password']:(string)$state['_password'];
$state['step']=1;$state['host_input']=$host_input;$state['username']=$username;$state['_password']=$password;bratonien_tools_nc_wizard_store($state);
if($username===''||$password==='') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
$base_url='';$status_data=null;
foreach(bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
{
try
{
$response=bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if($response['status']<200||$response['status']>=300) continue;
$candidate_status=json_decode($response['body'],true);
if(!is_array($candidate_status)||empty($candidate_status['installed'])) continue;
$base_url=$candidate_url;$status_data=$candidate_status;break;
}
catch(Throwable $ignored){}
}
if($base_url===''||!is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
$user_response=bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/user?format=json',$username,$password,array('OCS-APIRequest: true'));
if($user_response['status']===401||$user_response['status']===403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
if($user_response['status']<200||$user_response['status']>=300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
$user_data=bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$users=array();$can_list_users=false;
try
{
$users_response=bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/users?format=json',$username,$password,array('OCS-APIRequest: true'));
if($users_response['status']>=200&&$users_response['status']<300)
{
$users_data=bratonien_tools_nc_wizard_ocs_data($users_response['body']);
if(isset($users_data['users'])&&is_array($users_data['users']))
{
$users=array_values(array_filter(array_map('strval',$users_data['users'])));sort($users,SORT_NATURAL|SORT_FLAG_CASE);$can_list_users=true;
}
}
}
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) 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']); $config = array(
'host'=>$state['db_host'],
'port'=>$state['db_port'],
'database'=>$state['db_database'],
'user'=>$state['db_user'],
);
$password = (string)$state['_db_password']; $password = (string)$state['_db_password'];
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1'); bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1');
$source_view = bratonien_tools_nc_connector_view_name($state['source_view']); $source_view = bratonien_tools_nc_connector_view_name($state['source_view']);
@@ -190,19 +211,37 @@ function bratonien_tools_nc_wizard_finish_data_access(array &$state)
bratonien_tools_nc_connector_psql($config, $password, 'SELECT COUNT(*) FROM '.$source_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'); 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"); $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(); $candidates = array();
foreach (preg_split('/\r\n|\r|\n/', trim($rows)) as $line) foreach (preg_split('/\r\n|\r|\n/', trim($rows)) as $line)
{ {
if ($line === '') continue; if ($line === '') continue;
$parts = explode("\t", $line, 2); $parts = explode("\t", $line, 2);
if (count($parts) !== 2) continue; if (count($parts) !== 2) continue;
$storage_id=trim($parts[0]);$source_path=trim($parts[1],'/');
$storage_id = trim($parts[0]);
$source_path = trim($parts[1], '/');
if ($storage_id === '') continue; if ($storage_id === '') continue;
$prefix = $source_path; $prefix = $source_path;
if (strpos($source_path, '/') !== false) $prefix = substr($source_path, 0, strpos($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 (!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.'); if (!$candidates) throw new RuntimeException('Die Verbindung zur Datenquelle funktioniert, aber es wurden noch keine freigegebenen Speicherorte gefunden.');
$known = array(); $known = array();
@@ -214,6 +253,7 @@ function bratonien_tools_nc_wizard_finish_data_access(array &$state)
if ($key !== '|') $known[$key] = (string)($storage['local_mount'] ?? ''); if ($key !== '|') $known[$key] = (string)($storage['local_mount'] ?? '');
} }
} }
foreach ($candidates as &$candidate) foreach ($candidates as &$candidate)
{ {
$key = $candidate['storage_id'].'|'.trim($candidate['source_prefix'], '/'); $key = $candidate['storage_id'].'|'.trim($candidate['source_prefix'], '/');
@@ -222,57 +262,175 @@ function bratonien_tools_nc_wizard_finish_data_access(array &$state)
unset($candidate); unset($candidate);
$state['storage_candidates'] = array_values($candidates); $state['storage_candidates'] = array_values($candidates);
$state['storages']=array_values(array_filter($state['storage_candidates'],function($storage){return trim((string)($storage['local_mount']??''))!=='';})); $state['storages'] = array_values(array_filter($state['storage_candidates'], function($storage) {
return trim((string)($storage['local_mount'] ?? '')) !== '';
}));
$state['technical_error'] = ''; $state['technical_error'] = '';
$state['technical_source'] = 'Datenzugriff erfolgreich geprüft'; $state['technical_source'] = 'Datenzugriff erfolgreich geprüft';
$state['technical_complete'] = count($state['storages']) === count($state['storage_candidates']); $state['technical_complete'] = count($state['storages']) === count($state['storage_candidates']);
$state['technical_stage'] = $state['technical_complete'] ? 'ready' : 'mounts'; $state['technical_stage'] = $state['technical_complete'] ? 'ready' : 'mounts';
} }
function bratonien_tools_nc_wizard_scan()
{
$state = bratonien_tools_nc_wizard_state();
$host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input']));
$username = trim((string)($_POST['nc_wizard_user'] ?? $state['username']));
$password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password'];
$state['step'] = 1;
$state['host_input'] = $host_input;
$state['username'] = $username;
$state['_password'] = $password;
bratonien_tools_nc_wizard_store($state);
if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
$base_url = '';
$status_data = null;
foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
{
try
{
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
$base_url = $candidate_url;
$status_data = $candidate_status;
break;
}
catch (Throwable $ignored)
{
}
}
if ($base_url === '' || !is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
$user_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/user?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($user_response['status'] === 401 || $user_response['status'] === 403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
if ($user_response['status'] < 200 || $user_response['status'] >= 300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$users = array();
$can_list_users = false;
try
{
$users_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/users?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($users_response['status'] >= 200 && $users_response['status'] < 300)
{
$users_data = bratonien_tools_nc_wizard_ocs_data($users_response['body']);
if (isset($users_data['users']) && is_array($users_data['users']))
{
$users = array_values(array_filter(array_map('strval', $users_data['users'])));
sort($users, SORT_NATURAL | SORT_FLAG_CASE);
$can_list_users = true;
}
}
}
catch (Throwable $ignored)
{
}
$resolved_username = trim((string)($user_data['id'] ?? $username));
if ($resolved_username === '') $resolved_username = $username;
$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'=>$resolved_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);
try
{
bratonien_tools_nc_wizard_finish_data_access($state);
}
catch (Throwable $e)
{
$state['technical_complete'] = false;
$state['technical_stage'] = 'database_details';
$state['technical_error'] = $e->getMessage();
}
bratonien_tools_nc_wizard_store($state);
if ($state['technical_stage'] === 'database_details')
{
return array('message'=>'Nextcloud wurde gefunden. Die Datenbank-Adresse muss noch bestätigt werden.');
}
if ($state['technical_stage'] === 'mounts')
{
return array('message'=>'Nextcloud und Datenzugriff wurden bestätigt. Ein Speicherort muss noch zugeordnet werden.');
}
return array('message'=>'Nextcloud und Datenzugriff wurden erfolgreich bestätigt.');
}
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'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.'); if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
if ((string)$state['username'] === '' || (string)$state['_password'] === '') throw new RuntimeException('Die Zugangsdaten aus dem ersten Schritt sind nicht mehr verfügbar. Bitte den Assistenten neu starten.');
$state['connection_name']=trim((string)($_POST['nc_wizard_connection_name']??$state['connection_name'])); $state['db_user'] = (string)$state['username'];
$state['db_user']=trim((string)($_POST['nc_wizard_db_user']??$state['db_user'])); $state['_db_password'] = (string)$state['_password'];
$db_password=array_key_exists('nc_wizard_db_password',$_POST)?(string)$_POST['nc_wizard_db_password']:(string)$state['_db_password']; $state['db_password_set'] = true;
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)
{
$state['db_host'] = trim((string)($_POST['nc_wizard_db_host'] ?? $state['db_host'])); $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_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_database'] = trim((string)($_POST['nc_wizard_db_database'] ?? $state['db_database']));
} $state['technical_stage'] = 'database_details';
else
{
$state['db_host']=(string)parse_url($state['base_url'],PHP_URL_HOST);
$state['db_port']='5432';
$state['db_database']='nextcloud';
}
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.');
if ($state['db_host'] === '' || $state['db_database'] === '') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.'); if ($state['db_host'] === '' || $state['db_database'] === '') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.');
try try
{ {
bratonien_tools_nc_wizard_finish_data_access($state); bratonien_tools_nc_wizard_finish_data_access($state);
bratonien_tools_nc_wizard_store($state); bratonien_tools_nc_wizard_store($state);
return array('message'=>$state['technical_complete']?'Datenzugriff wurde erfolgreich geprüft.':'Datenzugriff funktioniert. Ein Speicherort muss noch bestätigt 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['technical_error'] = $e->getMessage(); $state['technical_error'] = $e->getMessage();
if(!$advanced)
{
$state['technical_stage'] = 'database_details'; $state['technical_stage'] = 'database_details';
bratonien_tools_nc_wizard_store($state); bratonien_tools_nc_wizard_store($state);
throw new RuntimeException('Der übliche Datenbankzugang konnte nicht automatisch bestätigt werden. Bitte die Datenbank-Adresse prüfen.');
}
bratonien_tools_nc_wizard_store($state);
throw $e; throw $e;
} }
} }
@@ -282,18 +440,30 @@ function bratonien_tools_nc_wizard_save_mounts()
$state = bratonien_tools_nc_wizard_state(); $state = bratonien_tools_nc_wizard_state();
$candidates = is_array($state['storage_candidates']) ? $state['storage_candidates'] : array(); $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(); $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.'); if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte erkannt.');
$storages = array(); $storages = array();
foreach ($candidates as $index=>$candidate) foreach ($candidates as $index=>$candidate)
{ {
$mount = trim((string)($candidate['local_mount'] ?? '')); $mount = trim((string)($candidate['local_mount'] ?? ''));
if ($mount === '') $mount = trim((string)($mounts[$index] ?? '')); if ($mount === '') $mount = trim((string)($mounts[$index] ?? ''));
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.'); if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
$mount = rtrim($mount, '/'); $mount = rtrim($mount, '/');
if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$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;
$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);
$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.'); return array('message'=>'Speicherzuordnung wurde erfolgreich geprüft.');
} }
@@ -302,11 +472,18 @@ function bratonien_tools_nc_wizard_select_user()
$state = bratonien_tools_nc_wizard_state(); $state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.'); 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.'); 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'] ?? '')); $showcase_user = trim((string)($_POST['nc_wizard_showcase_user'] ?? ''));
$connection_name = trim((string)($_POST['nc_wizard_connection_name'] ?? $state['connection_name'])); $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 ($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.'); 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);
$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.'); return array('message'=>'Showcase-Benutzer übernommen. Jetzt folgt der Piwigo-API-Zugang.');
} }
@@ -314,29 +491,66 @@ function bratonien_tools_nc_wizard_api_test()
{ {
$state = bratonien_tools_nc_wizard_state(); $state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 3) throw new RuntimeException('Der API-Test ist in diesem Assistentenschritt nicht verfügbar.'); 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); $posted_id = trim((string)($_POST['nc_wizard_api_key_id'] ?? ''));
$key_id=trim((string)$state['_api_key_id']);$secret=trim((string)$state['_api_key_secret']); $posted_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 ($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.'); 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'); $status = bratonien_tools_nc_connector_piwigo_api_request($key_id, $secret, 'pwg.session.getStatus');
$user_status = strtolower((string)($status['status'] ?? '')); $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.'); 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);
$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))); $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).'.'); 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);
$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.'); 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;} 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();if((int)$state['step']!==3)throw new RuntimeException('Die API kann in diesem Assistentenschritt nicht übersprungen werden.'); $state = bratonien_tools_nc_wizard_state();
$state['api_status']='skipped';$state['api_error']='';$state['step']=4;bratonien_tools_nc_wizard_store($state); if ((int)$state['step'] !== 3) throw new RuntimeException('Die API kann in diesem Assistentenschritt nicht übersprungen werden.');
$state['api_status'] = 'skipped';
$state['api_error'] = '';
$state['step'] = 4;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Piwigo-API wurde übersprungen. Für diese Verbindung ist deshalb ein Fallback-Zugang erforderlich.'); return array('message'=>'Piwigo-API wurde übersprungen. Für diese Verbindung ist deshalb ein Fallback-Zugang erforderlich.');
} }
@@ -344,10 +558,13 @@ 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']) === '') throw new RuntimeException('Der Assistent ist noch nicht vollständig.'); if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '') throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']); if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']);
if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password']; if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password'];
bratonien_tools_nc_wizard_store($state); bratonien_tools_nc_wizard_store($state);
$fallback_user=trim((string)$state['_fallback_user']);$fallback_password=(string)$state['_fallback_password'];
$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 (($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.'); if ($state['api_status'] !== 'ok' && $fallback_user === '') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist ein Fallback-Zugang erforderlich.');
@@ -356,22 +573,111 @@ function bratonien_tools_nc_wizard_finish()
{ {
$storage_lines[] = (string)$storage['storage_id'].' | '.trim((string)$storage['source_prefix'], '/').' | '.(string)$storage['local_mount']; $storage_lines[] = (string)$storage['storage_id'].' | '.trim((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'];$_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); $_POST['nc_name'] = (string)$state['connection_name'];
try{if($state['api_status']==='ok')bratonien_tools_nc_api_credentials_store((string)$state['_api_key_id'],(string)$state['_api_key_secret']);} $_POST['nc_host'] = (string)$state['db_host'];
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());} $_POST['nc_port'] = (string)$state['db_port'];
unset($_SESSION['bratonien_nc_wizard']);unset($result['connection_id']);$result['message']='Verbindung wurde vollständig durch den Assistenten angelegt. '.$result['message'];return $result; $_POST['nc_database'] = (string)$state['db_database'];
$_POST['nc_user'] = (string)$state['db_user'];
$_POST['nc_db_password'] = (string)$state['_db_password'];
$_POST['nc_source_view'] = (string)$state['source_view'];
$_POST['nc_activity_view'] = (string)$state['activity_view'];
$_POST['nc_gallery_root'] = (string)$state['gallery_root'];
$_POST['nc_storages'] = implode("\n", $storage_lines);
$_POST['nc_quiet_seconds'] = '120';
$_POST['nc_max_wait_seconds'] = '900';
$_POST['nc_full_sync_seconds'] = '86400';
$_POST['nc_piwigo_user'] = $fallback_user;
$_POST['nc_piwigo_password'] = $fallback_password;
$_POST['nc_nextcloud_url'] = (string)$state['base_url'];
$_POST['nc_showcase_user'] = (string)$state['showcase_user'];
$_POST['nc_access_user'] = (string)$state['username'];
$_POST['nc_product'] = (string)$state['product'];
$_POST['nc_version'] = (string)$state['version'];
$_POST['nc_api_validated'] = $state['api_status'] === 'ok' ? '1' : '0';
$result = bratonien_tools_nc_connector_create_local_api_first();
$connection_id = (int)($result['connection_id'] ?? 0);
try
{
if ($state['api_status'] === 'ok') bratonien_tools_nc_api_credentials_store((string)$state['_api_key_id'], (string)$state['_api_key_secret']);
}
catch (Throwable $e)
{
if ($connection_id > 0)
{
$table = bratonien_tools_nc_connector_table();
pwg_query("DELETE FROM `$table` WHERE id=".$connection_id." LIMIT 1");
}
throw new RuntimeException('Die Verbindung wurde nicht übernommen, weil der geprüfte API-Zugang nicht gespeichert werden konnte: '.$e->getMessage());
}
unset($_SESSION['bratonien_nc_wizard']);
unset($result['connection_id']);
$result['message'] = 'Verbindung wurde vollständig durch den Assistenten angelegt. '.$result['message'];
return $result;
} }
function bratonien_tools_nc_connector_update_name() function bratonien_tools_nc_connector_update_name()
{ {
$id=(int)($_POST['connection_id']??0);$name=trim((string)($_POST['connection_name']??''));if($id<1||$name==='')throw new RuntimeException('Verbindung oder Name fehlt.');$connection=bratonien_tools_nc_connector_connection($id,false);if(!$connection)throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');$table=bratonien_tools_nc_connector_table();$now=date('Y-m-d H:i:s');pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");return array('message'=>'Verbindungsname wurde aktualisiert.'); $id = (int)($_POST['connection_id'] ?? 0);
$name = trim((string)($_POST['connection_name'] ?? ''));
if ($id < 1 || $name === '') throw new RuntimeException('Verbindung oder Name fehlt.');
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Verbindungsname wurde aktualisiert.');
} }
function bratonien_tools_nc_connector_update_technical() function bratonien_tools_nc_connector_update_technical()
{ {
$id=(int)($_POST['connection_id']??0);$connection=bratonien_tools_nc_connector_connection($id,true);if(!$connection)throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');if(!empty($connection['enabled'])||(string)$connection['takeover_state']==='active')throw new RuntimeException('Technische Einstellungen einer aktiven Verbindung können nicht geändert werden. Bitte zuerst deaktivieren.'); $id = (int)($_POST['connection_id'] ?? 0);
$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.'); $connection = bratonien_tools_nc_connector_connection($id, true);
$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']); if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$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.'); if (!empty($connection['enabled']) || (string)$connection['takeover_state'] === 'active') throw new RuntimeException('Technische Einstellungen einer aktiven Verbindung können nicht geändert werden. Bitte zuerst deaktivieren.');
$config = $connection['config'];
$port = (int)($_POST['nc_port'] ?? ($config['port'] ?? 5432));
if ($port < 1 || $port > 65535) throw new RuntimeException('Ungültiger PostgreSQL-Port.');
$gallery_root = rtrim(trim((string)($_POST['nc_gallery_root'] ?? ($config['gallery_root'] ?? ''))), '/');
if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
$config['host'] = trim((string)($_POST['nc_host'] ?? ($config['host'] ?? '')));
$config['port'] = (string)$port;
$config['database'] = trim((string)($_POST['nc_database'] ?? ($config['database'] ?? '')));
$config['user'] = trim((string)($_POST['nc_user'] ?? ($config['user'] ?? '')));
$config['source_view'] = trim((string)($_POST['nc_source_view'] ?? ($config['source_view'] ?? '')));
$config['activity_view'] = trim((string)($_POST['nc_activity_view'] ?? ($config['activity_view'] ?? '')));
$config['gallery_root'] = $gallery_root;
$config['quiet_seconds'] = max(0, (int)($_POST['nc_quiet_seconds'] ?? ($config['quiet_seconds'] ?? 120)));
$config['max_wait_seconds'] = max(60, (int)($_POST['nc_max_wait_seconds'] ?? ($config['max_wait_seconds'] ?? 900)));
$config['full_sync_seconds'] = max(300, (int)($_POST['nc_full_sync_seconds'] ?? ($config['full_sync_seconds'] ?? 86400)));
$config['storages'] = bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? '');
unset($config['verification']);
bratonien_tools_nc_connector_view_name($config['source_view']);
bratonien_tools_nc_connector_view_name($config['activity_view']);
$credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? '');
$db_password = (string)($_POST['nc_db_password'] ?? '');
if ($db_password === '') $db_password = $credentials['db_password'];
if ($db_password === '') throw new RuntimeException('Datenbankpasswort fehlt.');
$secret_blob = bratonien_tools_nc_connector_encrypt_credentials($db_password, $credentials['piwigo_user'], $credentials['piwigo_password']);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET takeover_state='disabled', enabled=0, config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Technische Verbindungseinstellungen wurden gespeichert. Die Verbindung muss erneut geprüft werden.');
} }

View File

@@ -0,0 +1,448 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_apply_known_database_profile(array &$state)
{
if (empty($state['scan_ok']) || trim((string)($state['base_url'] ?? '')) === '') return false;
$base_host = strtolower(trim((string)parse_url((string)$state['base_url'], PHP_URL_HOST)));
foreach (bratonien_tools_nc_connector_connections() as $candidate)
{
$config = isset($candidate['config']) && is_array($candidate['config']) ? $candidate['config'] : array();
$candidate_url = trim((string)($config['nextcloud_url'] ?? ''));
$match = false;
if ($candidate_url !== '')
{
try
{
$match = bratonien_tools_nc_wizard_normalize_url($candidate_url) === (string)$state['base_url'];
}
catch (Throwable $ignored)
{
$match = false;
}
}
elseif ($base_host !== '')
{
$match = strtolower(trim((string)($config['host'] ?? ''))) === $base_host;
}
if (!$match) continue;
$connection = bratonien_tools_nc_connector_connection((int)$candidate['id'], true);
if (!$connection || !is_array($connection['config'] ?? null)) continue;
$known = $connection['config'];
$db_user = trim((string)($known['user'] ?? ''));
if ($db_user === '') continue;
$db_password = '';
try
{
$plain = bratonien_tools_nc_connector_decrypt_secret($connection['secret_blob'] ?? '');
$decoded = json_decode($plain, true);
$db_password = is_array($decoded) && array_key_exists('db_password', $decoded)
? (string)$decoded['db_password']
: (string)$plain;
}
catch (Throwable $ignored)
{
$db_password = '';
}
if ($db_password === '') continue;
$state['db_host'] = trim((string)($known['host'] ?? $state['db_host']));
$state['db_port'] = trim((string)($known['port'] ?? $state['db_port']));
$state['db_database'] = trim((string)($known['database'] ?? $state['db_database']));
$state['db_user'] = $db_user;
$state['_db_password'] = $db_password;
$state['db_password_set'] = true;
$state['source_view'] = trim((string)($known['source_view'] ?? $state['source_view']));
$state['activity_view'] = trim((string)($known['activity_view'] ?? $state['activity_view']));
$state['gallery_root'] = trim((string)($known['gallery_root'] ?? $state['gallery_root']));
$state['storages'] = isset($known['storages']) && is_array($known['storages']) ? $known['storages'] : array();
$state['storage_candidates'] = $state['storages'];
$state['technical_source'] = 'Gespeicherte Reader-Verbindung wird erneut geprüft';
$state['technical_error'] = '';
return true;
}
return false;
}
function bratonien_tools_nc_wizard_prepare_directory_stage(array &$state, array $known_storages)
{
if ((string)($state['technical_stage'] ?? '') === 'database_details') return;
$known_by_storage = array();
foreach ($known_storages as $known)
{
$storage_id = trim((string)($known['storage_id'] ?? ''));
$mount = rtrim(trim((string)($known['local_mount'] ?? '')), '/');
if ($storage_id === '' || $mount === '') continue;
if (!isset($known_by_storage[$storage_id])) $known_by_storage[$storage_id] = $known;
}
$candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array();
$all_mapped = !empty($candidates);
foreach ($candidates as &$candidate)
{
$storage_id = trim((string)($candidate['storage_id'] ?? ''));
$known = isset($known_by_storage[$storage_id]) ? $known_by_storage[$storage_id] : null;
if (is_array($known))
{
$candidate['source_prefix'] = trim((string)($known['source_prefix'] ?? $candidate['source_prefix'] ?? ''), '/');
$candidate['local_mount'] = rtrim(trim((string)($known['local_mount'] ?? '')), '/');
}
$mount = trim((string)($candidate['local_mount'] ?? ''));
if ($mount === '' || !is_dir($mount) || !is_readable($mount)) $all_mapped = false;
$candidate['include_prefix'] = '';
}
unset($candidate);
$state['storage_candidates'] = $candidates;
$state['storages'] = array();
$state['technical_complete'] = false;
$state['technical_stage'] = 'mounts';
$state['technical_source'] = $all_mapped
? 'Datenzugriff und Speicherzuordnung erkannt; Verzeichnisauswahl offen'
: 'Datenzugriff erkannt; Speicherzuordnung muss bestätigt werden';
$state['directory_selection_ready'] = $all_mapped;
}
function bratonien_tools_nc_wizard_finish_data_access_for_selection(array &$state, array $known_storages)
{
bratonien_tools_nc_wizard_finish_data_access($state);
bratonien_tools_nc_wizard_prepare_directory_stage($state, $known_storages);
}
function bratonien_tools_nc_wizard_scan_with_known_database()
{
$state = bratonien_tools_nc_wizard_state();
$host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input']));
$username = trim((string)($_POST['nc_wizard_user'] ?? $state['username']));
$password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password'];
$state['step'] = 1;
$state['host_input'] = $host_input;
$state['username'] = $username;
$state['_password'] = $password;
bratonien_tools_nc_wizard_store($state);
if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
$base_url = '';
$status_data = null;
foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
{
try
{
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
$base_url = $candidate_url;
$status_data = $candidate_status;
break;
}
catch (Throwable $ignored)
{
}
}
if ($base_url === '' || !is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
$user_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/user?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($user_response['status'] === 401 || $user_response['status'] === 403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
if ($user_response['status'] < 200 || $user_response['status'] >= 300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$users = array();
$can_list_users = false;
try
{
$users_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/users?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($users_response['status'] >= 200 && $users_response['status'] < 300)
{
$users_data = bratonien_tools_nc_wizard_ocs_data($users_response['body']);
if (isset($users_data['users']) && is_array($users_data['users']))
{
$users = array_values(array_filter(array_map('strval', $users_data['users'])));
sort($users, SORT_NATURAL | SORT_FLAG_CASE);
$can_list_users = true;
}
}
}
catch (Throwable $ignored)
{
}
$resolved_username = trim((string)($user_data['id'] ?? $username));
if ($resolved_username === '') $resolved_username = $username;
$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'=>$resolved_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'=>'',
'db_host'=>strtolower($url_host),'db_port'=>'5432','db_database'=>'nextcloud','db_user'=>'','db_password_set'=>false,'_db_password'=>'',
'source_view'=>'piwigo_showcase_sources','activity_view'=>'piwigo_showcase_activity',
'gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud',
'storages'=>array(),'storage_candidates'=>array(),
'technical_stage'=>'auto_check','technical_source'=>'Automatische Prüfung','technical_error'=>'','technical_complete'=>false,
'directory_selection_ready'=>false,
));
if (!bratonien_tools_nc_wizard_apply_known_database_profile($state))
{
$state['technical_stage'] = 'database_details';
$state['technical_source'] = 'Keine bekannte Reader-Verbindung gefunden';
$state['technical_error'] = 'Für diese Nextcloud ist noch keine bekannte Datenbank-Reader-Verbindung gespeichert. Die Nextcloud-Anmeldedaten werden nicht als PostgreSQL-Zugang verwendet.';
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud wurde gefunden. Für den Datenzugriff wird eine separate Reader-Verbindung benötigt.');
}
$known_storages = isset($state['storages']) && is_array($state['storages']) ? $state['storages'] : array();
try
{
bratonien_tools_nc_wizard_finish_data_access_for_selection($state, $known_storages);
}
catch (Throwable $e)
{
$state['technical_complete'] = false;
$state['technical_stage'] = 'database_details';
$state['technical_error'] = $e->getMessage();
}
bratonien_tools_nc_wizard_store($state);
if ($state['technical_stage'] === 'database_details') return array('message'=>'Nextcloud wurde gefunden. Die bekannte Reader-Verbindung konnte noch nicht vollständig bestätigt werden.');
return array('message'=>'Nextcloud und Datenzugriff wurden bestätigt. Jetzt können die gewünschten Verzeichnisse gewählt werden.');
}
function bratonien_tools_nc_wizard_save_technical_with_known_database()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
if (trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') bratonien_tools_nc_wizard_apply_known_database_profile($state);
if (trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') throw new RuntimeException('Für diese Nextcloud sind noch keine Datenbank-Reader-Zugangsdaten bekannt. Nextcloud-Benutzer und -Passwort werden dafür bewusst nicht verwendet.');
$known_storages = isset($state['storages']) && is_array($state['storages']) ? $state['storages'] : array();
$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']));
if ($state['db_host'] === '' || $state['db_database'] === '') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.');
try
{
bratonien_tools_nc_wizard_finish_data_access_for_selection($state, $known_storages);
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Datenzugriff wurde erfolgreich geprüft. Jetzt können die gewünschten Verzeichnisse gewählt werden.');
}
catch (Throwable $e)
{
$state['technical_error'] = $e->getMessage();
$state['technical_stage'] = 'database_details';
bratonien_tools_nc_wizard_store($state);
throw $e;
}
}
function bratonien_tools_nc_wizard_directory_options(array $state)
{
if (empty($state['scan_ok']) || trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') return array();
$config = array('host'=>$state['db_host'],'port'=>$state['db_port'],'database'=>$state['db_database'],'user'=>$state['db_user']);
$source_view = bratonien_tools_nc_connector_view_name($state['source_view']);
$rows = bratonien_tools_nc_connector_psql($config, (string)$state['_db_password'], "SELECT DISTINCT storage_id::text || E'\\t' || COALESCE(source_path, '') FROM ".$source_view." ORDER BY 1");
$paths = 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], '/');
if ($storage_id === '') continue;
if (!isset($paths[$storage_id])) $paths[$storage_id] = array();
$paths[$storage_id][] = $source_path;
}
$result = array();
foreach ((array)($state['storage_candidates'] ?? array()) as $index=>$candidate)
{
$storage_id = trim((string)($candidate['storage_id'] ?? ''));
$mapping_prefix = trim((string)($candidate['source_prefix'] ?? ''), '/');
$options = array(''=>'Stammverzeichnis');
foreach (($paths[$storage_id] ?? array()) as $source_path)
{
$relative = $source_path;
if ($mapping_prefix !== '')
{
if ($source_path === $mapping_prefix) $relative = '';
elseif (strpos($source_path, $mapping_prefix.'/') === 0) $relative = substr($source_path, strlen($mapping_prefix) + 1);
else continue;
}
$relative = trim($relative, '/');
if ($relative === '') continue;
$segments = explode('/', $relative);
$current = '';
foreach ($segments as $segment)
{
if ($segment === '') continue;
$current = $current === '' ? $segment : $current.'/'.$segment;
$options[$current] = $current;
}
}
if (count($options) > 1)
{
$root = $options[''];
unset($options['']);
natcasesort($options);
$options = array(''=>$root) + $options;
}
$result[] = array(
'index'=>(int)$index,
'storage_id'=>$storage_id,
'mount'=>(string)($candidate['local_mount'] ?? ''),
'ready'=>trim((string)($candidate['local_mount'] ?? '')) !== '',
'options'=>$options,
);
}
return $result;
}
function bratonien_tools_nc_wizard_save_mounts_with_directories()
{
$state = bratonien_tools_nc_wizard_state();
$candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array();
if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte erkannt.');
$mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array();
$directories = isset($_POST['nc_wizard_directory']) && is_array($_POST['nc_wizard_directory']) ? $_POST['nc_wizard_directory'] : array();
$storages = array();
foreach ($candidates as $index=>$candidate)
{
$mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/');
if ($mount === '') $mount = rtrim(trim((string)($mounts[$index] ?? '')), '/');
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount);
$selected = isset($directories[$index]) && is_array($directories[$index]) ? $directories[$index] : array();
$normalized = array();
foreach ($selected as $directory)
{
$directory = trim((string)$directory, '/');
if ($directory === '') continue;
if ($directory[0] === '/' || preg_match('#(^|/)\.\.(/|$)#', $directory)) throw new RuntimeException('Ungültige Verzeichnisauswahl.');
$normalized[$directory] = true;
}
if (!$normalized) $normalized[''] = true;
foreach (array_keys($normalized) as $include_prefix)
{
$storages[] = array(
'storage_id'=>(string)($candidate['storage_id'] ?? ''),
'source_prefix'=>trim((string)($candidate['source_prefix'] ?? ''), '/'),
'local_mount'=>$mount,
'include_prefix'=>$include_prefix,
);
}
}
$state['storages'] = $storages;
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['technical_error'] = '';
$state['directory_selection_ready'] = false;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Verzeichnisauswahl wurde übernommen.');
}
function bratonien_tools_nc_wizard_finish_with_directories()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '') throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']);
if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password'];
bratonien_tools_nc_wizard_store($state);
$fallback_user = trim((string)$state['_fallback_user']);
$fallback_password = (string)$state['_fallback_password'];
if (($fallback_user === '') !== ($fallback_password === '')) throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.');
if ($state['api_status'] !== 'ok' && $fallback_user === '') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist ein Fallback-Zugang erforderlich.');
$mapping_lines = array();
foreach ((array)$state['storages'] as $storage)
{
$key = (string)$storage['storage_id'].'|'.trim((string)$storage['source_prefix'], '/').'|'.(string)$storage['local_mount'];
$mapping_lines[$key] = (string)$storage['storage_id'].' | '.trim((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",array_values($mapping_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);
if ($connection_id < 1) throw new RuntimeException('Die neue Verbindung konnte nicht gespeichert werden.');
$connection = bratonien_tools_nc_connector_connection($connection_id, false);
if (!$connection) throw new RuntimeException('Die neue Verbindung konnte nach dem Anlegen nicht gelesen werden.');
$config = $connection['config'];
$config['storages'] = array_values($state['storages']);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('Die Verzeichnisauswahl konnte nicht serialisiert werden.');
$table = bratonien_tools_nc_connector_table();
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$connection_id." LIMIT 1");
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)
{
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;
}

View File

@@ -0,0 +1,10 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_save_technical_flow()
{
return bratonien_tools_nc_wizard_save_technical_generic();
}

View File

@@ -0,0 +1,499 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_webdav_list(array $state, $path = '')
{
if (empty($state['scan_ok']) || trim((string)$state['base_url']) === '' || trim((string)$state['username']) === '' || (string)$state['_password'] === '')
{
throw new RuntimeException('Die Nextcloud-Sitzung des Assistenten ist nicht vollständig.');
}
if (!function_exists('curl_init')) throw new RuntimeException('cURL ist für die Verzeichnisauswahl nicht verfügbar.');
$path = trim((string)$path, '/');
if ($path !== '' && preg_match('#(^|/)\.\.(/|$)#', $path)) throw new RuntimeException('Ungültiger Verzeichnispfad.');
$segments = $path === '' ? array() : array_map('rawurlencode', explode('/', $path));
$user = rawurlencode((string)$state['username']);
$url = rtrim((string)$state['base_url'], '/').'/remote.php/dav/files/'.$user.'/'.implode('/', $segments);
if (substr($url, -1) !== '/') $url .= '/';
$body = '<?xml version="1.0"?><d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"><d:prop><d:resourcetype/><d:displayname/><oc:fileid/></d:prop></d:propfind>';
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_CUSTOMREQUEST=>'PROPFIND',
CURLOPT_POSTFIELDS=>$body,
CURLOPT_HTTPHEADER=>array('Depth: 1','Content-Type: application/xml; charset=utf-8'),
CURLOPT_HTTPAUTH=>CURLAUTH_BASIC,
CURLOPT_USERPWD=>(string)$state['username'].':'.(string)$state['_password'],
CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>20,
));
$response = curl_exec($ch);
$errno = curl_errno($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $errno !== 0) throw new RuntimeException('Nextcloud-Verzeichnisse konnten nicht geladen werden.');
if ($status === 401 || $status === 403) throw new RuntimeException('Nextcloud hat den Zugriff auf dieses Verzeichnis abgelehnt.');
if ($status !== 207) throw new RuntimeException('Nextcloud-Verzeichnisabfrage antwortete mit HTTP '.$status.'.');
libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$response);
if ($xml === false) throw new RuntimeException('Nextcloud hat eine ungültige WebDAV-Antwort geliefert.');
$xml->registerXPathNamespace('d', 'DAV:');
$xml->registerXPathNamespace('oc', 'http://owncloud.org/ns');
$children = array();
$fileids = array();
$current_fileid = 0;
$base_path = (string)parse_url($url, PHP_URL_PATH);
foreach ($xml->xpath('//d:response') as $item)
{
$item->registerXPathNamespace('d', 'DAV:');
$item->registerXPathNamespace('oc', 'http://owncloud.org/ns');
$hrefs = $item->xpath('d:href');
$collections = $item->xpath('d:propstat/d:prop/d:resourcetype/d:collection');
$ids = $item->xpath('d:propstat/d:prop/oc:fileid');
if (!$hrefs || !$collections || !$ids) continue;
$fileid = (int)trim((string)$ids[0]);
if ($fileid < 1) continue;
$href = rawurldecode((string)$hrefs[0]);
$href_path = (string)parse_url($href, PHP_URL_PATH);
if (rtrim($href_path, '/') === rtrim($base_path, '/'))
{
$current_fileid = $fileid;
continue;
}
$name = basename(rtrim($href_path, '/'));
if ($name === '') continue;
$child_path = $path === '' ? $name : $path.'/'.$name;
$children[$child_path] = $name;
$fileids[$child_path] = $fileid;
}
natcasesort($children);
if ($current_fileid < 1) throw new RuntimeException('Nextcloud hat für das aktuelle Verzeichnis keine eindeutige Datei-ID geliefert.');
$parent = '';
if ($path !== '')
{
$parts = explode('/', $path);
array_pop($parts);
$parent = implode('/', $parts);
}
return array(
'current'=>$path,
'parent'=>$parent,
'children'=>$children,
'current_fileid'=>$current_fileid,
'fileids'=>$fileids,
);
}
function bratonien_tools_nc_wizard_refresh_directory_state(array &$state, $path = null)
{
if ($path === null) $path = (string)($state['directory_path'] ?? '');
$listing = bratonien_tools_nc_wizard_webdav_list($state, $path);
$state['directory_path'] = (string)$listing['current'];
$state['directory_parent'] = (string)$listing['parent'];
$state['directory_children'] = (array)$listing['children'];
$state['directory_current_fileid'] = (int)$listing['current_fileid'];
$state['directory_fileids'] = (array)$listing['fileids'];
if (!isset($state['directory_selected']) || !is_array($state['directory_selected'])) $state['directory_selected'] = array();
if (!isset($state['directory_selected_fileids']) || !is_array($state['directory_selected_fileids'])) $state['directory_selected_fileids'] = array();
}
function bratonien_tools_nc_wizard_generic_db_config(array $state)
{
return array(
'host'=>(string)$state['db_host'],
'port'=>(string)$state['db_port'],
'database'=>(string)$state['db_database'],
'user'=>(string)$state['db_user'],
);
}
function bratonien_tools_nc_wizard_verify_generic_data_access(array &$state)
{
$config = bratonien_tools_nc_wizard_generic_db_config($state);
$password = (string)$state['_db_password'];
if (trim((string)$config['user']) === '' || $password === '') throw new RuntimeException('Die gespeicherte Reader-Verbindung ist unvollständig.');
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1');
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1 FROM piwigo_connector_files LIMIT 1');
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1 FROM piwigo_connector_activity LIMIT 1');
$state['source_view'] = 'piwigo_connector_files';
$state['activity_view'] = 'piwigo_connector_activity';
$state['source_mode'] = 'selected-fileids';
$state['storages'] = array();
$state['storage_candidates'] = array();
$state['roots'] = array();
$state['technical_complete'] = false;
$state['technical_stage'] = 'mounts';
$state['technical_source'] = 'Generische Reader-Schnittstelle geprüft';
$state['technical_error'] = '';
$state['directory_selection_ready'] = true;
}
function bratonien_tools_nc_wizard_known_adapter($storage_id, $source_path)
{
$storage_id = trim((string)$storage_id);
$source_path = trim((string)$source_path, '/');
$matches = array();
foreach (bratonien_tools_nc_connector_connections() as $connection)
{
foreach ((array)($connection['config']['storages'] ?? array()) as $storage)
{
if ((string)($storage['storage_id'] ?? '') !== $storage_id) continue;
$prefix = trim((string)($storage['source_prefix'] ?? ''), '/');
if ($prefix !== '' && $source_path !== $prefix && strpos($source_path, $prefix.'/') !== 0) continue;
$mount = rtrim(trim((string)($storage['local_mount'] ?? '')), '/');
if ($mount === '' || !is_dir($mount) || !is_readable($mount)) continue;
$key = $prefix.'|'.$mount;
$matches[$key] = array('storage_id'=>$storage_id,'source_prefix'=>$prefix,'local_mount'=>$mount);
}
}
if (!$matches) return null;
$max = -1;
foreach ($matches as $match) $max = max($max, strlen((string)$match['source_prefix']));
$best = array_values(array_filter($matches, function($match) use ($max) { return strlen((string)$match['source_prefix']) === $max; }));
return count($best) === 1 ? $best[0] : null;
}
function bratonien_tools_nc_wizard_resolve_selected_roots(array &$state)
{
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) ? $state['directory_selected'] : array();
$selected_fileids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids']) ? $state['directory_selected_fileids'] : array();
if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
$ids = array();
foreach ($selected as $path)
{
if (!isset($selected_fileids[$path]) || (int)$selected_fileids[$path] < 1) throw new RuntimeException('Für eine Auswahl fehlt die eindeutige Nextcloud-Datei-ID. Bitte das Verzeichnis erneut auswählen.');
$ids[(int)$selected_fileids[$path]] = (string)$path;
}
$config = bratonien_tools_nc_wizard_generic_db_config($state);
$id_list = implode(',', array_map('intval', array_keys($ids)));
$rows = bratonien_tools_nc_connector_psql(
$config,
(string)$state['_db_password'],
"SELECT fileid::text || E'\\t' || storage_id || E'\\t' || COALESCE(source_path, '') FROM piwigo_connector_files WHERE fileid IN (".$id_list.") ORDER BY fileid"
);
$resolved = array();
foreach (preg_split('/\r\n|\r|\n/', trim($rows)) as $line)
{
if ($line === '') continue;
$parts = explode("\t", $line, 3);
if (count($parts) !== 3 || !ctype_digit($parts[0])) continue;
$resolved[(int)$parts[0]] = array('storage_id'=>(string)$parts[1], 'source_path'=>trim((string)$parts[2], '/'));
}
$roots = array();
$candidates = array();
foreach ($ids as $fileid=>$path)
{
if (!isset($resolved[$fileid])) throw new RuntimeException('Das ausgewählte Nextcloud-Verzeichnis mit Datei-ID '.$fileid.' konnte nicht mehr aufgelöst werden.');
$storage_id = trim((string)$resolved[$fileid]['storage_id']);
$source_path = trim((string)$resolved[$fileid]['source_path'], '/');
if ($storage_id === '') throw new RuntimeException('Nextcloud hat für Datei-ID '.$fileid.' keine Storage-ID geliefert.');
$display_name = $path === '' ? ((string)($state['display_name'] ?? '') !== '' ? (string)$state['display_name'] : 'Nextcloud') : basename($path);
$roots[] = array(
'fileid'=>(int)$fileid,
'display_name'=>$display_name,
'webdav_path'=>$path,
'storage_id'=>$storage_id,
'source_path'=>$source_path,
);
$adapter = bratonien_tools_nc_wizard_known_adapter($storage_id, $source_path);
if (!$adapter) $adapter = array('storage_id'=>$storage_id,'source_prefix'=>'','local_mount'=>'');
$key = $adapter['storage_id'].'|'.$adapter['source_prefix'].'|'.$adapter['local_mount'];
$candidates[$key] = $adapter;
}
$state['roots'] = array_values($roots);
$state['storage_candidates'] = array_values($candidates);
$state['storages'] = $state['storage_candidates'];
$all_mapped = !empty($state['storage_candidates']);
foreach ($state['storage_candidates'] as $candidate)
{
$mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/');
if ($mount === '' || !is_dir($mount) || !is_readable($mount)) $all_mapped = false;
}
if ($all_mapped)
{
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['directory_selection_ready'] = false;
return;
}
$state['technical_complete'] = false;
$state['technical_stage'] = 'mounts';
$state['directory_selection_ready'] = false;
$state['mount_prompted'] = true;
}
function bratonien_tools_nc_wizard_scan_user_scoped()
{
$state = bratonien_tools_nc_wizard_state();
$host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input']));
$username = trim((string)($_POST['nc_wizard_user'] ?? $state['username']));
$password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password'];
$state['step'] = 1;
$state['host_input'] = $host_input;
$state['username'] = $username;
$state['_password'] = $password;
bratonien_tools_nc_wizard_store($state);
if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
$base_url = '';
$status_data = null;
foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
{
try
{
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
$base_url = $candidate_url;
$status_data = $candidate_status;
break;
}
catch (Throwable $ignored) {}
}
if ($base_url === '' || !is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
$user_response = bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/user?format=json', $username, $password, array('OCS-APIRequest: true'));
if ($user_response['status'] === 401 || $user_response['status'] === 403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
if ($user_response['status'] < 200 || $user_response['status'] >= 300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$resolved_username = trim((string)($user_data['id'] ?? $username));
if ($resolved_username === '') $resolved_username = $username;
$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'=>$resolved_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'=>array(),'can_list_users'=>false,'showcase_user'=>$resolved_username,'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'=>'','db_host'=>strtolower($url_host),'db_port'=>'5432','db_database'=>'nextcloud','db_user'=>'','db_password_set'=>false,'_db_password'=>'',
'source_view'=>'piwigo_connector_files','activity_view'=>'piwigo_connector_activity','source_mode'=>'selected-fileids','gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud',
'storages'=>array(),'storage_candidates'=>array(),'roots'=>array(),'technical_stage'=>'auto_check','technical_source'=>'Automatische Prüfung','technical_error'=>'','technical_complete'=>false,
'directory_selection_ready'=>false,'directory_path'=>'','directory_parent'=>'','directory_children'=>array(),'directory_current_fileid'=>0,'directory_fileids'=>array(),'directory_selected'=>array(),'directory_selected_fileids'=>array(),
'database_prompted'=>false,'mount_prompted'=>false,
));
if (!bratonien_tools_nc_wizard_apply_known_database_profile($state))
{
$state['technical_stage'] = 'database_details';
$state['database_prompted'] = true;
$state['technical_source'] = 'Keine bekannte Reader-Verbindung gefunden';
$state['technical_error'] = 'Für diese Nextcloud ist noch keine bekannte Datenbank-Reader-Verbindung gespeichert.';
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud wurde gefunden. Für den Datenzugriff wird eine separate Reader-Verbindung benötigt.');
}
try
{
bratonien_tools_nc_wizard_verify_generic_data_access($state);
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
}
catch (Throwable $e)
{
$state['technical_complete'] = false;
$state['technical_stage'] = 'database_details';
$state['database_prompted'] = true;
$state['technical_error'] = $e->getMessage().' Die generischen Connector-Views müssen in Nextcloud eingerichtet sein.';
}
bratonien_tools_nc_wizard_store($state);
if ($state['technical_stage'] === 'database_details') return array('message'=>'Nextcloud wurde gefunden. Die generische Reader-Schnittstelle konnte noch nicht bestätigt werden.');
return array('message'=>'Nextcloud und generischer Datenzugriff wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers gewählt werden.');
}
function bratonien_tools_nc_wizard_save_technical_generic()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
if (trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') bratonien_tools_nc_wizard_apply_known_database_profile($state);
if (trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') throw new RuntimeException('Für diese Nextcloud sind noch keine Datenbank-Reader-Zugangsdaten bekannt.');
$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']));
if ($state['db_host'] === '' || $state['db_database'] === '') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.');
try
{
bratonien_tools_nc_wizard_verify_generic_data_access($state);
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Generischer Datenzugriff wurde erfolgreich geprüft. Jetzt können Verzeichnisse ausgewählt werden.');
}
catch (Throwable $e)
{
$state['technical_error'] = $e->getMessage();
$state['technical_stage'] = 'database_details';
$state['database_prompted'] = true;
bratonien_tools_nc_wizard_store($state);
throw $e;
}
}
function bratonien_tools_nc_wizard_directory_browse()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts' || empty($state['directory_selection_ready'])) throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
$path = trim((string)($_POST['nc_wizard_directory_path'] ?? ''), '/');
bratonien_tools_nc_wizard_refresh_directory_state($state, $path);
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Verzeichnis geöffnet.');
}
function bratonien_tools_nc_wizard_directory_add()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts' || empty($state['directory_selection_ready'])) throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
$path = trim((string)($state['directory_path'] ?? ''), '/');
$fileid = (int)($state['directory_current_fileid'] ?? 0);
if ($fileid < 1) throw new RuntimeException('Für dieses Verzeichnis fehlt die eindeutige Nextcloud-Datei-ID.');
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) ? $state['directory_selected'] : array();
if (!in_array($path, $selected, true)) $selected[] = $path;
$state['directory_selected'] = array_values($selected);
if (!isset($state['directory_selected_fileids']) || !is_array($state['directory_selected_fileids'])) $state['directory_selected_fileids'] = array();
$state['directory_selected_fileids'][$path] = $fileid;
bratonien_tools_nc_wizard_store($state);
return array('message'=>$path === '' ? 'Stammverzeichnis ausgewählt.' : 'Verzeichnis hinzugefügt.');
}
function bratonien_tools_nc_wizard_directory_remove()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || empty($state['directory_selection_ready'])) throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
$path = trim((string)($_POST['nc_wizard_directory_remove'] ?? ''), '/');
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) ? $state['directory_selected'] : array();
$state['directory_selected'] = array_values(array_filter($selected, function($value) use ($path) { return (string)$value !== $path; }));
if (isset($state['directory_selected_fileids'][$path])) unset($state['directory_selected_fileids'][$path]);
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Verzeichnis entfernt.');
}
function bratonien_tools_nc_wizard_save_mounts_server_side()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts') throw new RuntimeException('Die Speicher-/Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
if (!empty($state['directory_selection_ready']))
{
bratonien_tools_nc_wizard_resolve_selected_roots($state);
bratonien_tools_nc_wizard_store($state);
return array('message'=>!empty($state['technical_complete']) ? 'Verzeichnisse und vorhandene Storage-Adapter wurden übernommen.' : 'Verzeichnisse wurden aufgelöst. Ein neuer Storage-Adapter muss noch zugeordnet werden.');
}
$candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array();
if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte aus der Verzeichnisauswahl aufgelöst.');
$mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array();
foreach ($candidates as $index=>&$candidate)
{
$mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/');
if ($mount === '') $mount = rtrim(trim((string)($mounts[$index] ?? '')), '/');
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount);
$candidate['local_mount'] = $mount;
}
unset($candidate);
$state['storage_candidates'] = array_values($candidates);
$state['storages'] = $state['storage_candidates'];
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['technical_error'] = '';
$state['directory_selection_ready'] = false;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Storage-Adapter wurde geprüft. Die Verbindung ist technisch vollständig.');
}
function bratonien_tools_nc_wizard_select_current_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.');
$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'] = (string)$state['username'];
$state['step'] = 3;
$state['api_error'] = '';
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud-Benutzer übernommen. Jetzt folgt der Piwigo-API-Zugang.');
}
function bratonien_tools_nc_wizard_back()
{
$state = bratonien_tools_nc_wizard_state();
$step = (int)($state['step'] ?? 1);
if ($step <= 1) return array('message'=>'Bereits am Anfang des Assistenten.');
if ($step === 4)
{
$state['step'] = 3;
}
elseif ($step === 3)
{
$state['step'] = 2;
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['directory_selection_ready'] = false;
}
elseif (!empty($state['technical_complete']))
{
$state['technical_complete'] = false;
$state['technical_stage'] = 'mounts';
$state['directory_selection_ready'] = true;
bratonien_tools_nc_wizard_refresh_directory_state($state);
}
elseif ((string)($state['technical_stage'] ?? '') === 'mounts' && !empty($state['directory_selection_ready']))
{
if (!empty($state['database_prompted'])) $state['technical_stage'] = 'database_details'; else $state['step'] = 1;
$state['directory_selection_ready'] = false;
}
elseif ((string)($state['technical_stage'] ?? '') === 'mounts')
{
$state['directory_selection_ready'] = true;
bratonien_tools_nc_wizard_refresh_directory_state($state);
}
elseif ((string)($state['technical_stage'] ?? '') === 'database_details')
{
$state['step'] = 1;
}
else
{
$state['step'] = 1;
}
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Ein Fenster zurück.');
}

View File

@@ -0,0 +1,207 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_scan_webdav_first()
{
$state = bratonien_tools_nc_wizard_state();
$host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input']));
$username = trim((string)($_POST['nc_wizard_user'] ?? $state['username']));
$password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password'];
$state['step'] = 1;
$state['host_input'] = $host_input;
$state['username'] = $username;
$state['_password'] = $password;
bratonien_tools_nc_wizard_store($state);
if ($username === '' || $password === '')
{
throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
}
$base_url = '';
$status_data = null;
foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
{
try
{
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
$base_url = $candidate_url;
$status_data = $candidate_status;
break;
}
catch (Throwable $ignored) {}
}
if ($base_url === '' || !is_array($status_data))
{
throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
}
$user_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/user?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($user_response['status'] === 401 || $user_response['status'] === 403)
{
throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
}
if ($user_response['status'] < 200 || $user_response['status'] >= 300)
{
throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
}
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$resolved_username = trim((string)($user_data['id'] ?? $username));
if ($resolved_username === '') $resolved_username = $username;
$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'=>$resolved_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'=>array(),
'can_list_users'=>false,
'showcase_user'=>$resolved_username,
'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud WebDAV',
'scan_message'=>'Nextcloud und WebDAV-Zugriff wurden bestätigt.',
'_password'=>$password,
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
'gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries',
'storages'=>array(),
'storage_candidates'=>array(),
'roots'=>array(),
'technical_stage'=>'mounts',
'technical_source'=>'WebDAV',
'technical_error'=>'',
'technical_complete'=>false,
'directory_selection_ready'=>true,
'directory_path'=>'',
'directory_parent'=>'',
'directory_children'=>array(),
'directory_current_fileid'=>0,
'directory_fileids'=>array(),
'directory_selected'=>array(),
'directory_selected_fileids'=>array(),
'database_prompted'=>false,
'mount_prompted'=>false,
'api_status'=>'pending',
'api_username'=>'',
'api_error'=>'',
));
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud und WebDAV wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers ausgewählt werden.');
}
function bratonien_tools_nc_wizard_save_sources_dispatch()
{
$state = bratonien_tools_nc_wizard_state();
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder')
{
return bratonien_tools_nc_wizard_save_mounts_server_side();
}
if ((int)($state['step'] ?? 0) !== 2 || empty($state['directory_selection_ready']))
{
throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
}
$selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
? array_values(array_unique(array_map(function($path) { return trim((string)$path, '/'); }, $state['directory_selected'])))
: array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected)
{
throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
}
$selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids'])
? $state['directory_selected_fileids']
: array();
$roots = array();
foreach ($selected as $path)
{
$fileid = (int)($selected_ids[$path] ?? 0);
if ($fileid < 1)
{
throw new RuntimeException('Für eine Auswahl fehlt die eindeutige Nextcloud-Datei-ID. Bitte das Verzeichnis erneut auswählen.');
}
$roots[] = array(
'fileid'=>$fileid,
'display_name'=>basename($path),
'webdav_path'=>$path,
);
}
$state['roots'] = $roots;
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['technical_source'] = 'WebDAV-Verzeichnisse ausgewählt';
$state['technical_error'] = '';
$state['directory_selection_ready'] = false;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den parallelen Testweg vorbereitet.');
}
function bratonien_tools_nc_wizard_finish_dispatch()
{
$state = bratonien_tools_nc_wizard_state();
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder')
{
return bratonien_tools_nc_wizard_finish_generic_scope();
}
if ((int)($state['step'] ?? 0) !== 4 || empty($state['technical_complete']))
{
throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
}
if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']);
if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password'];
bratonien_tools_nc_wizard_store($state);
$fallback_user = trim((string)($state['_fallback_user'] ?? ''));
$fallback_password = (string)($state['_fallback_password'] ?? '');
if (($fallback_user === '') !== ($fallback_password === ''))
{
throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.');
}
if (($state['api_status'] ?? '') !== 'ok' && $fallback_user === '')
{
throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist für diese Verbindung ein Fallback-Zugang erforderlich.');
}
if ($fallback_user !== '')
{
try
{
bratonien_tools_nc_connector_validate_fallback_credentials($fallback_user, $fallback_password);
}
catch (Throwable $e)
{
throw new RuntimeException('Der Piwigo-Fallback wurde nicht gespeichert: '.$e->getMessage());
}
}
$result = bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard();
unset($_SESSION['bratonien_nc_wizard']);
return $result;
}

View File

@@ -4,6 +4,23 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
if (isset($GLOBALS['template']) && is_object($GLOBALS['template']) && method_exists($GLOBALS['template'], 'func_combine_script'))
{
$script_version = function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : '0.9.6.15';
$GLOBALS['template']->func_combine_script(array(
'id'=>'bratonien_nc_connector_edit_v2',
'path'=>BRATONIEN_TOOLS_PATH.'js/nc_connector_edit_v2.js',
'load'=>'footer',
'version'=>$script_version,
));
$GLOBALS['template']->func_combine_script(array(
'id'=>'bratonien_nc_connector_source_ui',
'path'=>BRATONIEN_TOOLS_PATH.'js/nc_connector_source_ui.js',
'load'=>'footer',
'version'=>$script_version,
));
}
require_once(BRATONIEN_TOOLS_PATH . 'tools/image_cache.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/image_cache.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php');
@@ -22,6 +39,15 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_auth.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_db_bridge.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_user_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_flow.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_connection_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_generic_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_edit.inc.php');
function bratonien_tools_get_tools() function bratonien_tools_get_tools()
{ {
@@ -47,25 +73,33 @@ function bratonien_tools_get_tools()
'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'), 'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'),
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'), 'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'),
'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'), 'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'),
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete'), 'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'),
'nc_connector_edit_start' => array('handler' => 'bratonien_tools_nc_connector_edit_start'),
'nc_connector_migrate_start' => array('handler' => 'bratonien_tools_nc_connector_migrate_start'),
'nc_connector_update_local' => array('handler' => 'bratonien_tools_nc_connector_update_local_friendly'),
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'),
'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'), 'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'), 'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan'), 'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'),
'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical'), 'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'),
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_mounts'), 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
'nc_connector_wizard_select_user' => array('handler' => 'bratonien_tools_nc_wizard_select_user'), 'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_sources_dispatch'),
'nc_connector_wizard_select_user' => array('handler' => 'bratonien_tools_nc_wizard_select_current_user'),
'nc_connector_wizard_api_test' => array('handler' => 'bratonien_tools_nc_wizard_api_test'), 'nc_connector_wizard_api_test' => array('handler' => 'bratonien_tools_nc_wizard_api_test'),
'nc_connector_wizard_api_skip' => array('handler' => 'bratonien_tools_nc_wizard_api_skip'), 'nc_connector_wizard_api_skip' => array('handler' => 'bratonien_tools_nc_wizard_api_skip'),
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish'), 'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'),
'nc_connector_wizard_back' => array('handler' => 'bratonien_tools_nc_wizard_back'),
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'), 'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'),
'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'), 'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'),
'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_managed'), 'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'),
'nc_connector_prepare_takeover' => array('handler' => 'bratonien_tools_nc_connector_prepare_takeover'), 'nc_connector_prepare_takeover' => array('handler' => 'bratonien_tools_nc_connector_prepare_takeover'),
'nc_connector_cancel_takeover' => array('handler' => 'bratonien_tools_nc_connector_cancel_takeover'), 'nc_connector_cancel_takeover' => array('handler' => 'bratonien_tools_nc_connector_cancel_takeover'),
'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'), 'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'),
'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'), 'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'),
'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save'), 'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'),
'nc_connector_fallback_delete' => array('handler' => 'bratonien_tools_nc_connector_fallback_delete'), 'nc_connector_fallback_delete' => array('handler' => 'bratonien_tools_nc_connector_fallback_delete_scoped'),
'nc_connector_fallback_once' => array('handler' => 'bratonien_tools_nc_connector_fallback_once'), 'nc_connector_fallback_once' => array('handler' => 'bratonien_tools_nc_connector_fallback_once'),
'self_update_check' => array('handler' => 'bratonien_tools_self_update_check'), 'self_update_check' => array('handler' => 'bratonien_tools_self_update_check'),
'self_update_run' => array('handler' => 'bratonien_tools_self_update_run'), 'self_update_run' => array('handler' => 'bratonien_tools_self_update_run'),

View File

@@ -0,0 +1,434 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_webdav_image_source_info($image_id)
{
static $cache = array();
$image_id = (int)$image_id;
if ($image_id < 1) return null;
if (array_key_exists($image_id, $cache)) return $cache[$image_id];
$result = pwg_query('SELECT path, coi FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result)) return $cache[$image_id] = null;
$row = pwg_db_fetch_assoc($result);
$path = (string)($row['path'] ?? '');
if ($path === '') return $cache[$image_id] = null;
$absolute = $path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $absolute), '/');
}
$resolved = realpath($absolute);
if ($resolved === false) return $cache[$image_id] = null;
$normalized = str_replace('\\', '/', $resolved);
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
return $cache[$image_id] = null;
}
$connection_id = (int)$match[1];
$root_fileid = (int)$match[2];
$relative_path = trim((string)$match[3], '/');
if ($relative_path === '') return $cache[$image_id] = null;
$table = defined('BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE')
? BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$connection_result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($connection_result)) return $cache[$image_id] = null;
$connection_row = pwg_db_fetch_assoc($connection_result);
$config = json_decode((string)$connection_row['config_json'], true);
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder')
{
return $cache[$image_id] = null;
}
$root_path = '';
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
foreach ($roots as $root)
{
if ((int)($root['fileid'] ?? 0) === $root_fileid)
{
$root_path = trim((string)($root['webdav_path'] ?? ''), '/');
break;
}
}
if ($root_path === '') return $cache[$image_id] = null;
$webdav_path = $root_path.'/'.$relative_path;
$content_type = '';
$size = 0;
$etag = '';
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
{
$mapping_file = $state_dir.'/webdav-map.json';
if (is_readable($mapping_file))
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
{
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/');
$content_type = (string)($entry['content_type'] ?? '');
$size = (int)($entry['size'] ?? 0);
$etag = (string)($entry['etag'] ?? '');
}
}
}
}
return $cache[$image_id] = array(
'image_id'=>$image_id,
'connection_id'=>$connection_id,
'webdav_path'=>$webdav_path,
'content_type'=>$content_type,
'size'=>$size,
'etag'=>$etag,
'coi'=>$row['coi'] ?? null,
);
}
function bratonien_tools_webdav_image_url($image_id, $preview=false)
{
$info = bratonien_tools_webdav_image_source_info($image_id);
if (!$info) return null;
$url = get_root_url().'plugins/'.BRATONIEN_TOOLS_ID.'/webdav-image.php?id='.(int)$image_id;
if ($preview) $url .= '&preview=1';
if ($info['etag'] !== '') $url .= '&v='.rawurlencode(substr(sha1($info['etag']), 0, 12));
return $url;
}
function bratonien_tools_webdav_preview_path(array $info)
{
$connection_id = (int)($info['connection_id'] ?? 0);
$webdav_path = trim((string)($info['webdav_path'] ?? ''), '/');
if ($connection_id < 1 || $webdav_path === '') return null;
$base = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-preview/connection-'.$connection_id.'/'.sha1($webdav_path);
foreach (array('jpg', 'png', 'webp') as $ext)
{
$path = $base.'.'.$ext;
if (is_file($path) && is_readable($path)) return $path;
}
return null;
}
function bratonien_tools_webdav_preview_content_type($path)
{
$ext = strtolower(pathinfo((string)$path, PATHINFO_EXTENSION));
if ($ext === 'png') return 'image/png';
if ($ext === 'webp') return 'image/webp';
return 'image/jpeg';
}
function bratonien_tools_webdav_custom_derivative_params($key)
{
if (!class_exists('DerivativeParams') || !class_exists('SizingParams')) return null;
$tokens = explode('_', (string)$key);
if (!$tokens) return null;
$token = array_shift($tokens);
$crop = 0;
$min_size = null;
$parse_size = function($value)
{
$parts = explode('x', (string)$value, 2);
if (count($parts) === 1)
{
$size = max(1, (int)$parts[0]);
return array($size, $size);
}
return array(max(1, (int)$parts[0]), max(1, (int)$parts[1]));
};
if (isset($token[0]) && $token[0] === 's')
{
$size = $parse_size(substr($token, 1));
}
elseif (isset($token[0]) && $token[0] === 'e')
{
$crop = 1;
$size = $min_size = $parse_size(substr($token, 1));
}
else
{
if (count($tokens) < 2) return null;
$size = $parse_size($token);
$crop_token = array_shift($tokens);
$min_size = $parse_size(array_shift($tokens));
$crop = function_exists('char_to_fraction') ? char_to_fraction($crop_token) : 0;
}
$params = new DerivativeParams(new SizingParams($size, $crop, $min_size));
if (class_exists('ImageStdParams')) ImageStdParams::apply_global($params);
return $params;
}
function bratonien_tools_webdav_derivative_variants()
{
$variants = array();
if (!class_exists('ImageStdParams')) return $variants;
foreach (ImageStdParams::get_defined_type_map() as $type => $params)
{
$variants['standard:'.$type] = $params;
}
foreach (ImageStdParams::$custom as $custom_key => $last_used)
{
$params = bratonien_tools_webdav_custom_derivative_params($custom_key);
if ($params) $variants['custom:'.$custom_key] = $params;
}
return $variants;
}
function bratonien_tools_webdav_generate_derivative($params, $src_image, &$detail=null)
{
global $conf;
$detail = '';
if (!is_object($src_image) || empty($src_image->id))
{
$detail = 'SrcImage fehlt.';
return false;
}
$info = bratonien_tools_webdav_image_source_info((int)$src_image->id);
if (!$info)
{
$detail = 'WebDAV-Quellzuordnung fehlt.';
return false;
}
$preview = bratonien_tools_webdav_preview_path($info);
if (!$preview)
{
$detail = 'Vorbereitetes WebDAV-Preview fehlt.';
return false;
}
$preview_ext = strtolower(pathinfo($preview, PATHINFO_EXTENSION));
if (!in_array($preview_ext, array('jpg', 'jpeg', 'png', 'gif'), true))
{
$detail = 'Preview-Format '.$preview_ext.' ist für die Piwigo-Bildbibliothek nicht sicher nutzbar.';
return false;
}
if (!class_exists('DerivativeImage')) require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
if (!class_exists('pwg_image')) require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
$derivative = new DerivativeImage($params, $src_image);
if ($derivative->same_as_source())
{
$detail = 'Identisch mit Quelle; kein eigenes Derivat erforderlich.';
return true;
}
$target = $derivative->get_path();
$derivative_root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR;
if ($target === '' || strpos($target, $derivative_root) !== 0)
{
$detail = 'Ungültiger Derivat-Zielpfad: '.$target;
return false;
}
$preview_mtime = @filemtime($preview) ?: 0;
if (is_file($target) && is_readable($target) && (@filemtime($target) ?: 0) >= max($preview_mtime, (int)($params->last_mod_time ?? 0)))
{
$detail = 'Bereits vorhanden.';
return true;
}
$directory = dirname($target);
$dir_ok = function_exists('mkgetdir') ? mkgetdir($directory) : (is_dir($directory) || @mkdir($directory, 0755, true));
if (!$dir_ok || !is_dir($directory))
{
$detail = 'Derivat-Verzeichnis konnte nicht angelegt werden: '.$directory;
return false;
}
$image = null;
try
{
$image = new pwg_image($preview);
$original_size = array((int)$image->get_width(), (int)$image->get_height());
if ($original_size[0] < 1 || $original_size[1] < 1)
{
$detail = 'Preview-Abmessungen sind ungültig.';
return false;
}
$crop_rect = null;
$scaled_size = null;
$params->sizing->compute($original_size, $info['coi'] ?? null, $crop_rect, $scaled_size);
if ($crop_rect)
{
if (!$image->crop($crop_rect->width(), $crop_rect->height(), $crop_rect->l, $crop_rect->t))
{
$detail = 'Crop fehlgeschlagen.';
return false;
}
}
$final_size = $original_size;
if ($crop_rect)
{
$final_size = array($crop_rect->width(), $crop_rect->height());
}
if ($scaled_size)
{
if (!$image->resize($scaled_size[0], $scaled_size[1]))
{
$detail = 'Resize fehlgeschlagen.';
return false;
}
$final_size = array((int)$scaled_size[0], (int)$scaled_size[1]);
}
if (!empty($params->sharpen) && !$image->sharpen($params->sharpen))
{
$detail = 'Sharpen fehlgeschlagen.';
return false;
}
if ($params->will_watermark($final_size))
{
$wm = ImageStdParams::get_watermark();
if (!empty($wm->file))
{
$wm_path = PHPWG_ROOT_PATH.$wm->file;
if (!is_file($wm_path) || !is_readable($wm_path))
{
$detail = 'Wasserzeichen-Datei fehlt: '.$wm_path;
return false;
}
$wm_image = new pwg_image($wm_path);
try
{
$wm_size = array((int)$wm_image->get_width(), (int)$wm_image->get_height());
if ($final_size[0] < $wm_size[0] || $final_size[1] < $wm_size[1])
{
$wm_scaling = SizingParams::classic($final_size[0], $final_size[1]);
$tmp = null;
$wm_scaled = null;
$wm_scaling->compute($wm_size, null, $tmp, $wm_scaled);
if ($wm_scaled)
{
$wm_image->resize($wm_scaled[0], $wm_scaled[1]);
$wm_size = array((int)$wm_scaled[0], (int)$wm_scaled[1]);
}
}
$x = (int)round(($wm->xpos / 100) * ($final_size[0] - $wm_size[0]));
$y = (int)round(($wm->ypos / 100) * ($final_size[1] - $wm_size[1]));
$image->compose($wm_image, $x, $y, $wm->opacity);
if ($wm->xrepeat || $wm->yrepeat)
{
$xpad = $wm_size[0] + max(30, (int)round($wm_size[0] / 4));
$ypad = $wm_size[1] + max(30, (int)round($wm_size[1] / 4));
for ($i = -$wm->xrepeat; $i <= $wm->xrepeat; $i++)
{
for ($j = -$wm->yrepeat; $j <= $wm->yrepeat; $j++)
{
if (!$i && !$j) continue;
$x2 = $x + $i * $xpad;
$y2 = $y + $j * $ypad;
if ($x2 >= 0 && $x2 + $wm_size[0] < $final_size[0] && $y2 >= 0 && $y2 + $wm_size[1] < $final_size[1])
{
$image->compose($wm_image, $x2, $y2, $wm->opacity);
}
}
}
}
}
finally
{
$wm_image->destroy();
}
}
}
if (isset($conf['derivatives_strip_metadata_threshold']) && $final_size[0] * $final_size[1] < (int)$conf['derivatives_strip_metadata_threshold'])
{
$image->strip();
}
$quality = (int)ImageStdParams::$quality;
if (isset($params->type) && in_array($params->type, array(IMG_3XLARGE, IMG_4XLARGE), true))
{
$quality = min($quality, 75);
}
$image->set_compression_quality($quality);
$written = $image->write($target);
if ($written === false)
{
$detail = 'Bildbibliothek konnte das Derivat nicht schreiben.';
return false;
}
}
catch (Throwable $e)
{
$detail = get_class($e).': '.$e->getMessage();
return false;
}
finally
{
if (is_object($image)) $image->destroy();
}
@chmod($target, 0644);
clearstatcache(true, $target);
$size = @getimagesize($target);
if (!is_file($target) || !is_readable($target) || !is_array($size) || empty($size[0]) || empty($size[1]))
{
$detail = 'Derivatdatei wurde nicht gültig erzeugt: '.$target;
return false;
}
$detail = 'Erzeugt: '.$target.' ('.$size[0].'x'.$size[1].').';
return true;
}
function bratonien_tools_filter_webdav_src_url($url, $src_image)
{
if (!is_object($src_image) || empty($src_image->id)) return $url;
$webdav_url = bratonien_tools_webdav_image_url((int)$src_image->id, false);
return $webdav_url ?: $url;
}
function bratonien_tools_filter_webdav_derivative_url($url, $params, $src_image, $rel_url)
{
if (!is_object($src_image) || empty($src_image->id)) return $url;
$info = bratonien_tools_webdav_image_source_info((int)$src_image->id);
if (!$info) return $url;
try
{
$derivative = new DerivativeImage($params, $src_image);
if (!$derivative->same_as_source())
{
$path = $derivative->get_path();
if ($path !== '' && is_file($path) && is_readable($path)) return $url;
}
}
catch (Throwable $e)
{
error_log('Bratonien WebDAV derivative lookup #'.(int)$src_image->id.': '.$e->getMessage());
}
$preview_url = bratonien_tools_webdav_image_url((int)$src_image->id, true);
return $preview_url ?: $url;
}

View File

@@ -1,6 +1,94 @@
(function () { (function () {
'use strict'; 'use strict';
function initNcRouteStatus() {
var section = document.getElementById('nc-connector');
if (!section) return;
var statusCard = section.querySelector('.bratonien-grid .bratonien-card');
if (!statusCard) return;
var grid = statusCard.querySelector('.bratonien-form-grid');
if (!grid) return;
var routeLabel = document.createElement('span');
routeLabel.className = 'bratonien-label';
routeLabel.textContent = 'Aktiver Datenweg';
var routeValue = document.createElement('strong');
routeValue.setAttribute('data-nc-route-status', '');
routeValue.textContent = 'Noch kein Lauf erfasst';
var routeTimeLabel = document.createElement('span');
routeTimeLabel.className = 'bratonien-label';
routeTimeLabel.textContent = 'Datenweg zuletzt';
var routeTimeValue = document.createElement('strong');
routeTimeValue.setAttribute('data-nc-route-time', '');
routeTimeValue.textContent = 'Nicht verfügbar';
grid.appendChild(routeLabel);
grid.appendChild(routeValue);
grid.appendChild(routeTimeLabel);
grid.appendChild(routeTimeValue);
var detail = document.createElement('p');
detail.className = 'bratonien-base-note';
detail.setAttribute('data-nc-route-detail', '');
detail.textContent = 'Der Datenweg wird nach dem nächsten Connector-Lauf angezeigt.';
statusCard.appendChild(detail);
var basePath = window.location.pathname.replace(/admin\.php.*$/, '');
var statusUrl = basePath + '_data/bratonien-tools/nc-connector-status/route-status.json';
function formatTime(timestamp) {
if (!timestamp) return 'Nicht verfügbar';
var date = new Date(Number(timestamp) * 1000);
if (Number.isNaN(date.getTime())) return 'Nicht verfügbar';
return date.toLocaleString('de-DE');
}
function render(data) {
var label = data && data.label ? String(data.label) : 'Unbekannt';
var route = data && data.route ? String(data.route) : '';
routeValue.textContent = label;
routeTimeValue.textContent = formatTime(data && data.timestamp);
if (route === 'webdav') {
routeValue.textContent = 'WEBDAV PRIMÄR';
detail.textContent = 'Portierung läuft über WebDAV. Legacy wurde in diesem Lauf nicht benutzt.';
} else if (route === 'legacy_fallback') {
routeValue.textContent = 'LEGACY-FALLBACK AKTIV';
detail.textContent = 'WebDAV war nicht erfolgreich. Dieser Lauf wurde über die alte Struktur abgefangen.';
} else if (route === 'failed') {
routeValue.textContent = 'FEHLER - KEIN ERFOLGREICHER DATENWEG';
detail.textContent = data && data.detail ? String(data.detail) : 'WebDAV und Fallback sind fehlgeschlagen.';
} else {
detail.textContent = data && data.detail ? String(data.detail) : 'Unbekannter Datenweg.';
}
}
function refresh() {
fetch(statusUrl + '?t=' + Date.now(), {
cache: 'no-store',
credentials: 'same-origin'
})
.then(function (response) {
if (!response.ok) throw new Error('status unavailable');
return response.json();
})
.then(render)
.catch(function () {
routeValue.textContent = 'Noch kein Lauf erfasst';
routeTimeValue.textContent = 'Nicht verfügbar';
detail.textContent = 'Nach dem nächsten Connector-Lauf wird hier WebDAV oder Legacy-Fallback angezeigt.';
});
}
refresh();
window.setInterval(refresh, 10000);
}
function initBratonienTabs() { function initBratonienTabs() {
var admin = document.querySelector('.bratonien-admin'); var admin = document.querySelector('.bratonien-admin');
if (!admin) return; if (!admin) return;
@@ -113,9 +201,14 @@
activate(initial, false); activate(initial, false);
} }
if (document.readyState === 'loading') { function init() {
document.addEventListener('DOMContentLoaded', initBratonienTabs);
} else {
initBratonienTabs(); initBratonienTabs();
initNcRouteStatus();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
} }
})(); })();

324
js/nc_connector_edit_v2.js Normal file
View File

@@ -0,0 +1,324 @@
(function () {
'use strict';
function ready(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { window.setTimeout(callback, 0); });
} else {
window.setTimeout(callback, 0);
}
}
ready(function () {
var section = document.getElementById('nc-connector');
if (!section) return;
var pwgTokenInput = section.querySelector('input[name="pwg_token"]');
var pwgToken = pwgTokenInput ? pwgTokenInput.value : '';
var modeKey = 'bratonienNcWizardMode';
function escapeHtml(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot',"'":'&#039;'}[c];
});
}
function setMode(value) {
try { sessionStorage.setItem(modeKey, value); } catch (e) {}
}
function postForm(label, tool, id, mode) {
var form = document.createElement('form');
form.method = 'post';
form.style.display = 'inline';
form.dataset.ncV2Action = tool;
form.innerHTML = '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<input type="hidden" name="connection_id" value="'+escapeHtml(id)+'">'
+ '<button class="buttonLike" type="submit" name="bratonien_tool" value="'+escapeHtml(tool)+'">'+escapeHtml(label)+'</button>';
if (mode) form.addEventListener('submit', function () { setMode(mode); }, true);
return form;
}
function loadConnection(id) {
return fetch('plugins/bratonien_tools/nc-connector-edit-data.php?connection_id='+encodeURIComponent(id)+'&_='+Date.now(), {
credentials:'same-origin',
cache:'no-store',
headers:{'Accept':'application/json'}
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok) throw new Error(data.error || ('HTTP '+response.status));
return data;
});
});
}
function dialog() {
var node = document.getElementById('bratonien-nc-connection-edit-v2');
if (node) return node;
node = document.createElement('dialog');
node.id = 'bratonien-nc-connection-edit-v2';
node.className = 'bratonien-edit-dialog';
node.innerHTML = '<div class="bratonien-edit-dialog__body">'
+ '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem">'
+ '<div><h4 style="margin:0">Verbindung bearbeiten</h4><p class="bratonien-base-note" style="margin:.35rem 0 0">Alle Daten dieser Verbindung werden hier gepflegt. Eine Migration ist ein eigener Vorgang.</p></div>'
+ '<button type="button" class="buttonLike" data-edit-v2-close>Schließen</button></div>'
+ '<div data-edit-v2-content style="margin-top:1rem"></div></div>';
document.body.appendChild(node);
node.querySelector('[data-edit-v2-close]').addEventListener('click', function () { node.close(); });
node.addEventListener('click', function (event) { if (event.target === node) node.close(); });
return node;
}
function storageRow(storage) {
storage = storage || {};
return '<div class="bratonien-storage-row" data-storage-row>'
+ '<label>Storage-ID<input name="nc_storage_id[]" value="'+escapeHtml(storage.storage_id || '')+'" required></label>'
+ '<label>Quellordner<input name="nc_source_prefix[]" value="'+escapeHtml(storage.source_prefix || '')+'" placeholder="optional"></label>'
+ '<label>Lokaler Speicherpfad<input name="nc_local_mount[]" value="'+escapeHtml(storage.local_mount || '')+'" required></label>'
+ '<button type="button" class="buttonLike" data-remove-storage>Entfernen</button></div>';
}
function clearValidation(form) {
[].slice.call(form.querySelectorAll('[aria-invalid="true"]')).forEach(function (field) {
field.removeAttribute('aria-invalid');
field.style.borderColor = '';
field.style.boxShadow = '';
});
[].slice.call(form.querySelectorAll('[data-edit-v2-field-error]')).forEach(function (node) { node.remove(); });
var box = form.querySelector('[data-edit-v2-error-summary]');
if (box) box.remove();
}
function fieldNodes(form, fieldName) {
return [].slice.call(form.querySelectorAll('[name="'+fieldName.replace(/"/g, '\\"')+'"]'));
}
function showValidation(form, data) {
clearValidation(form);
var message = (data && data.message) ? data.message : 'Die Verbindung konnte nicht gespeichert werden.';
var summary = document.createElement('div');
summary.dataset.editV2ErrorSummary = '1';
summary.className = 'bratonien-main-cache__warning';
summary.style.margin = '0 0 1rem';
summary.style.padding = '.75rem';
summary.style.border = '1px solid currentColor';
summary.innerHTML = '<strong>Speichern fehlgeschlagen:</strong> '+escapeHtml(message);
form.insertBefore(summary, form.firstChild);
var first = null;
var fields = data && Array.isArray(data.fields) ? data.fields : [];
fields.forEach(function (fieldName) {
fieldNodes(form, fieldName).forEach(function (field) {
field.setAttribute('aria-invalid', 'true');
field.style.borderColor = '#d65a5a';
field.style.boxShadow = '0 0 0 1px #d65a5a';
if (!first) first = field;
var note = document.createElement('span');
note.dataset.editV2FieldError = '1';
note.className = 'bratonien-main-cache__warning';
note.style.display = 'block';
note.style.marginTop = '.25rem';
note.textContent = message;
field.insertAdjacentElement('afterend', note);
});
});
if (first) {
first.scrollIntoView({block:'center', behavior:'smooth'});
window.setTimeout(function () { first.focus(); }, 150);
} else {
summary.scrollIntoView({block:'center', behavior:'smooth'});
}
}
function submitEditor(form, editDialog) {
clearValidation(form);
if (!form.reportValidity()) return;
var submit = form.querySelector('button[type="submit"]');
if (submit) submit.disabled = true;
var body = new FormData(form);
fetch('plugins/bratonien_tools/nc-connector-edit-save.php', {
method:'POST',
credentials:'same-origin',
cache:'no-store',
headers:{'Accept':'application/json'},
body:body
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok || !data.ok) {
showValidation(form, data);
return;
}
editDialog.close();
window.location.reload();
});
}).catch(function (error) {
showValidation(form, {message:error.message || String(error), fields:[]});
}).finally(function () {
if (submit) submit.disabled = false;
});
}
function openEditor(id) {
var editDialog = dialog();
var content = editDialog.querySelector('[data-edit-v2-content]');
content.innerHTML = '<p class="bratonien-base-note">Verbindung wird geladen …</p>';
if (typeof editDialog.showModal === 'function') editDialog.showModal();
else editDialog.setAttribute('open', 'open');
loadConnection(id).then(function (data) {
if (data.adapter !== 'local') throw new Error('Diese Ansicht ist nur für die bestehende Legacy-Verbindung vorgesehen.');
var legacy = data.legacy || {};
var webdav = data.webdav || {};
var storages = Array.isArray(legacy.storages) ? legacy.storages : [];
var rows = storages.map(storageRow).join('') || storageRow({});
var migrationText = webdav.migration_ready
? '<strong>Bereit.</strong> Nextcloud-Zugang und verbindungseigener Piwigo-API-Key sind vollständig gespeichert.'
: '<strong>Noch nicht bereit.</strong> Es fehlen: '+escapeHtml((webdav.migration_missing || []).join(', ') || 'WebDAV-Zugangsdaten')+'.';
content.innerHTML = '<form method="post" data-edit-v2-form>'
+ '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<input type="hidden" name="connection_id" value="'+escapeHtml(data.id)+'">'
+ '<h5>Verbindung</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Name</label><input name="connection_name" value="'+escapeHtml(data.name)+'" required>'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Nextcloud / WebDAV</h5>'
+ '<p class="bratonien-base-note">Diese Angaben werden für den neuen WebDAV-Weg benötigt und gehören zu genau dieser Verbindung.</p>'
+ '<div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Nextcloud-Adresse</label><input name="nc_nextcloud_url" value="'+escapeHtml(webdav.nextcloud_url || '')+'" placeholder="https://cloud.example.de">'
+ '<label class="bratonien-label">Nextcloud-Benutzer</label><input name="nc_nextcloud_user" value="'+escapeHtml(webdav.nextcloud_user || '')+'" autocomplete="username">'
+ '<label class="bratonien-label">Nextcloud-Passwort</label><input name="nc_nextcloud_password" type="password" autocomplete="current-password" placeholder="'+(webdav.has_nextcloud_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Piwigo API dieser Verbindung</h5>'
+ '<p class="bratonien-base-note">Der API-Key wird verbindungseigen gespeichert und beim Speichern geprüft.</p>'
+ '<div class="bratonien-form-grid">'
+ '<label class="bratonien-label">API-Schlüssel-ID</label><input name="nc_connection_api_key_id" value="'+escapeHtml(webdav.api_key_id || '')+'" autocomplete="off">'
+ '<label class="bratonien-label">API-Geheimnis</label><input name="nc_connection_api_key_secret" type="password" autocomplete="new-password" placeholder="'+(webdav.has_api_key_secret ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<p class="bratonien-base-note"><strong>WebDAV-Migration:</strong> '+migrationText+'</p>'
+ '<h5 style="margin-top:1.2rem">Bestehender Legacy-Weg</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Datenbank-Server</label><input name="nc_host" value="'+escapeHtml(legacy.host)+'" required>'
+ '<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="'+escapeHtml(legacy.port || 5432)+'" required>'
+ '<label class="bratonien-label">Datenbank</label><input name="nc_database" value="'+escapeHtml(legacy.database)+'" required>'
+ '<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" value="'+escapeHtml(legacy.user)+'" required>'
+ '<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" placeholder="'+(legacy.has_db_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Speicherorte</h5><div data-storage-list>'+rows+'</div>'
+ '<button type="button" class="buttonLike" data-add-storage>Speicherort hinzufügen</button>'
+ '<details style="margin-top:1rem"><summary>Erweiterte Legacy-Einstellungen</summary><div class="bratonien-form-grid" style="margin-top:.75rem">'
+ '<label class="bratonien-label">Source-View</label><input name="nc_source_view" value="'+escapeHtml(legacy.source_view)+'" required>'
+ '<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" value="'+escapeHtml(legacy.activity_view)+'" required>'
+ '<label class="bratonien-label">Piwigo-Galerieordner</label><input name="nc_gallery_root" value="'+escapeHtml(legacy.gallery_root)+'" required>'
+ '<label class="bratonien-label">Ruhezeit (Sek.)</label><input name="nc_quiet_seconds" type="number" min="0" value="'+escapeHtml(legacy.quiet_seconds)+'">'
+ '<label class="bratonien-label">Maximale Wartezeit (Sek.)</label><input name="nc_max_wait_seconds" type="number" min="60" value="'+escapeHtml(legacy.max_wait_seconds)+'">'
+ '<label class="bratonien-label">Vollprüfung nach (Sek.)</label><input name="nc_full_sync_seconds" type="number" min="300" value="'+escapeHtml(legacy.full_sync_seconds)+'">'
+ '</div></details>'
+ '<div class="bratonien-actions" style="margin-top:1rem"><button class="buttonLike" type="submit">Änderungen prüfen und speichern</button><button class="buttonLike" type="button" data-edit-v2-cancel>Abbrechen</button></div>'
+ '</form>';
var form = content.querySelector('[data-edit-v2-form]');
form.querySelector('[data-edit-v2-cancel]').addEventListener('click', function () { editDialog.close(); });
form.querySelector('[data-add-storage]').addEventListener('click', function () {
form.querySelector('[data-storage-list]').insertAdjacentHTML('beforeend', storageRow({}));
});
form.addEventListener('click', function (event) {
var remove = event.target.closest('[data-remove-storage]');
if (!remove) return;
var row = remove.closest('[data-storage-row]');
if (row) row.remove();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
event.stopPropagation();
submitEditor(form, editDialog);
});
}).catch(function (error) {
content.innerHTML = '<p class="bratonien-main-cache__warning"><strong>Bearbeiten nicht möglich:</strong> '+escapeHtml(error.message || String(error))+'</p>';
});
}
function rebuildLocalActions(deleteForm, id, connectionData) {
var actions = deleteForm.parentElement;
if (!actions) return;
[].slice.call(actions.querySelectorAll('form')).forEach(function (form) {
var migrate = form.querySelector('button[value="nc_connector_migrate_start"]');
var editStart = form.querySelector('button[value="nc_connector_edit_start"]');
if (migrate || editStart) form.remove();
});
[].slice.call(actions.children).forEach(function (child) {
if (child.tagName === 'BUTTON' && (child.textContent || '').trim() === 'Bearbeiten') child.remove();
if (child.dataset && child.dataset.ncMigrationInfo) child.remove();
});
var edit = document.createElement('button');
edit.type = 'button';
edit.className = 'buttonLike';
edit.textContent = 'Bearbeiten';
edit.addEventListener('click', function () { openEditor(id); });
actions.insertBefore(edit, deleteForm);
var info = document.createElement('span');
info.dataset.ncMigrationInfo = '1';
info.className = 'bratonien-base-note';
info.textContent = 'WebDAV-Migration wird geprüft …';
actions.insertBefore(info, deleteForm);
var dataPromise = connectionData ? Promise.resolve(connectionData) : loadConnection(id);
dataPromise.then(function (data) {
var webdav = data.webdav || {};
if (webdav.migration_ready) {
info.remove();
actions.insertBefore(postForm('Auf WebDAV migrieren', 'nc_connector_migrate_start', id, 'migrate'), deleteForm);
} else {
var missing = (webdav.migration_missing || []).join(', ');
info.textContent = 'WebDAV-Migration noch nicht bereit: '+(missing || 'Zugangsdaten fehlen')+'. Zuerst „Bearbeiten“.';
}
}).catch(function (error) {
info.textContent = 'WebDAV-Migration kann nicht geprüft werden: '+(error.message || String(error));
});
}
function rebuildRemoteActions(deleteForm, id) {
var actions = deleteForm.parentElement;
if (!actions) return;
[].slice.call(actions.querySelectorAll('form')).forEach(function (form) {
var oldEdit = form.querySelector('button[value="nc_connector_edit_start"]');
var oldMigrate = form.querySelector('button[value="nc_connector_migrate_start"]');
if (oldEdit || oldMigrate) form.remove();
});
[].slice.call(actions.children).forEach(function (child) {
if (child.tagName === 'BUTTON' && (child.textContent || '').trim() === 'Bearbeiten') child.remove();
if (child.dataset && child.dataset.ncMigrationInfo) child.remove();
});
actions.insertBefore(postForm('Bearbeiten', 'nc_connector_edit_start', id, 'edit'), deleteForm);
}
[].slice.call(section.querySelectorAll('button[value="nc_connector_edit_start"]')).forEach(function (button) {
var form = button.closest('form');
if (form) form.remove();
});
[].slice.call(section.querySelectorAll('button[value="nc_connector_delete"]')).forEach(function (deleteButton) {
var deleteForm = deleteButton.closest('form');
if (!deleteForm) return;
var idInput = deleteForm.querySelector('input[name="connection_id"]');
if (!idInput) return;
var id = idInput.value;
loadConnection(id).then(function (data) {
if (data.adapter === 'local') rebuildLocalActions(deleteForm, id, data);
else rebuildRemoteActions(deleteForm, id);
}).catch(function (error) {
var actions = deleteForm.parentElement;
if (!actions) return;
var info = document.createElement('span');
info.className = 'bratonien-main-cache__warning';
info.textContent = 'Verbindungstyp konnte nicht geladen werden: '+(error.message || String(error));
actions.insertBefore(info, deleteForm);
});
});
});
})();

View File

@@ -0,0 +1,90 @@
(function () {
'use strict';
function enhanceMigrationButtons(root) {
[].slice.call(root.querySelectorAll('button[value="nc_connector_migrate_start"]')).forEach(function (button) {
button.textContent = 'Nextcloud-Ordner auswählen & migrieren';
button.title = 'Die WebDAV-Quelle wird im Nextcloud-Ordnerbrowser ausgewählt. Der bestehende SMB-/Legacy-Speicher wird nicht als WebDAV-Quelle übernommen.';
});
}
function enhanceEditor(root) {
var form = root.querySelector('[data-edit-v2-form]');
if (!form || form.dataset.sourceUiEnhanced === '1') return;
form.dataset.sourceUiEnhanced = '1';
var headings = [].slice.call(form.querySelectorAll('h5'));
var legacyHeading = headings.find(function (node) {
return (node.textContent || '').trim() === 'Bestehender Legacy-Weg';
});
var storageHeading = headings.find(function (node) {
return (node.textContent || '').trim() === 'Speicherorte';
});
var storageList = form.querySelector('[data-storage-list]');
var addStorage = form.querySelector('[data-add-storage]');
if (legacyHeading && !form.querySelector('[data-webdav-source-note]')) {
var note = document.createElement('p');
note.dataset.webdavSourceNote = '1';
note.className = 'bratonien-main-cache__warning';
note.innerHTML = '<strong>Wichtig:</strong> Der folgende SMB-/lokale Speicher gehört ausschließlich zum bisherigen Legacy-Fallback. Er wird <strong>nicht</strong> als WebDAV-Quelle übernommen. Die WebDAV-Quellordner werden beim Start der Migration direkt aus Nextcloud angezeigt und ausgewählt.';
legacyHeading.insertAdjacentElement('beforebegin', note);
}
if (storageHeading && storageList && addStorage) {
storageHeading.textContent = 'Legacy-Speicher (nur Fallback)';
var details = document.createElement('details');
details.dataset.legacyStorageTechnical = '1';
details.style.marginTop = '.75rem';
var summary = document.createElement('summary');
summary.textContent = 'Technische Legacy-Speicherzuordnung bearbeiten';
details.appendChild(summary);
var info = document.createElement('p');
info.className = 'bratonien-base-note';
info.textContent = 'Diese Felder betreffen nur den alten Fallback-Weg. Für WebDAV werden keine SMB-Adressen oder lokalen Mount-Pfade eingegeben.';
details.appendChild(info);
storageHeading.insertAdjacentElement('beforebegin', details);
details.appendChild(storageHeading);
details.appendChild(storageList);
details.appendChild(addStorage);
addStorage.textContent = 'Legacy-Speicher manuell hinzufügen';
addStorage.title = 'Nur für den alten Legacy-Fallback. WebDAV-Ordner werden im Nextcloud-Ordnerbrowser ausgewählt.';
}
}
function enhance(root) {
if (!root || root.nodeType !== 1) return;
enhanceMigrationButtons(root);
if (root.matches && root.matches('#bratonien-nc-connection-edit-v2')) enhanceEditor(root);
var dialog = root.querySelector ? root.querySelector('#bratonien-nc-connection-edit-v2') : null;
if (dialog) enhanceEditor(dialog);
}
function start() {
var section = document.getElementById('nc-connector');
if (!section) return;
enhance(section);
enhanceMigrationButtons(document);
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
[].slice.call(mutation.addedNodes || []).forEach(function (node) {
if (node && node.nodeType === 1) enhance(node);
});
});
var dialog = document.getElementById('bratonien-nc-connection-edit-v2');
if (dialog) enhanceEditor(dialog);
enhanceMigrationButtons(document);
});
observer.observe(document.body, {childList:true, subtree:true});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
})();

View File

@@ -1,7 +1,7 @@
<?php <?php
/* /*
Plugin Name: Bratonien Tools Plugin Name: Bratonien Tools
Version: 0.9.4.2 Version: 0.9.6.15
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
@@ -16,6 +16,7 @@ define('BRATONIEN_TOOLS_ID', basename(dirname(__FILE__)));
define('BRATONIEN_TOOLS_PATH', PHPWG_PLUGINS_PATH . BRATONIEN_TOOLS_ID . '/'); define('BRATONIEN_TOOLS_PATH', PHPWG_PLUGINS_PATH . BRATONIEN_TOOLS_ID . '/');
require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_runtime.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_image_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/public_selection.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/public_selection.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/picture_navigation.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/picture_navigation.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/batch_titles.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/batch_titles.inc.php');
@@ -26,6 +27,8 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php');
add_event_handler('get_admin_plugin_menu_links', 'bratonien_tools_admin_menu'); add_event_handler('get_admin_plugin_menu_links', 'bratonien_tools_admin_menu');
add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4); add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4);
add_event_handler('get_src_image_url', 'bratonien_tools_filter_webdav_src_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 2);
add_event_handler('get_derivative_url', 'bratonien_tools_filter_webdav_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 4);
add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action'); add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action');
add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2); add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30); add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30);

100
nc-connector-edit-data.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
http_response_code(404);
echo json_encode(array('error'=>'Bratonien Tools ist nicht aktiv.'));
exit;
}
if (!function_exists('is_admin') || !is_admin())
{
http_response_code(403);
echo json_encode(array('error'=>'Administratorrechte erforderlich.'));
exit;
}
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector_manage.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector_connection_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector_edit.inc.php');
$id = (int)($_GET['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
http_response_code(404);
echo json_encode(array('error'=>'Connector-Verbindung wurde nicht gefunden.'));
exit;
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = array();
$migration = array('ready'=>false, 'missing'=>array());
try
{
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
if ((string)$connection['adapter'] === 'local')
{
$migration = bratonien_tools_nc_connector_migration_state($connection);
}
}
catch (Throwable $e)
{
$credentials = array();
$migration = array('ready'=>false, 'missing'=>array('gespeicherte Zugangsdaten konnten nicht gelesen werden'));
}
$storages = array();
foreach ((array)($config['storages'] ?? array()) as $storage)
{
$storages[] = array(
'storage_id'=>(string)($storage['storage_id'] ?? ''),
'source_prefix'=>(string)($storage['source_prefix'] ?? ''),
'local_mount'=>(string)($storage['local_mount'] ?? ''),
);
}
$payload = array(
'id'=>(int)$connection['id'],
'name'=>(string)$connection['name'],
'adapter'=>(string)$connection['adapter'],
'enabled'=>(bool)$connection['enabled'],
'takeover_state'=>(string)$connection['takeover_state'],
'source_mode'=>(string)($config['source_mode'] ?? ''),
'legacy'=>array(
'host'=>(string)($config['host'] ?? ''),
'port'=>(string)($config['port'] ?? '5432'),
'database'=>(string)($config['database'] ?? ''),
'user'=>(string)($config['user'] ?? ''),
'has_db_password'=>trim((string)($credentials['db_password'] ?? '')) !== '',
'source_view'=>(string)($config['source_view'] ?? ''),
'activity_view'=>(string)($config['activity_view'] ?? ''),
'gallery_root'=>(string)($config['gallery_root'] ?? ''),
'quiet_seconds'=>(int)($config['quiet_seconds'] ?? 120),
'max_wait_seconds'=>(int)($config['max_wait_seconds'] ?? 900),
'full_sync_seconds'=>(int)($config['full_sync_seconds'] ?? 86400),
'storages'=>$storages,
),
'webdav'=>array(
'nextcloud_url'=>(string)($config['nextcloud_url'] ?? ''),
'nextcloud_user'=>(string)($credentials['nextcloud_user'] ?? $config['nextcloud_access_user'] ?? $config['access_user'] ?? ''),
'has_nextcloud_password'=>(string)($credentials['nextcloud_password'] ?? '') !== '',
'api_key_id'=>(string)($credentials['api_key_id'] ?? ''),
'has_api_key_secret'=>trim((string)($credentials['api_key_secret'] ?? '')) !== '',
'migration_ready'=>!empty($migration['ready']),
'migration_missing'=>array_values((array)($migration['missing'] ?? array())),
),
);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

312
nc-connector-edit-save.php Normal file
View File

@@ -0,0 +1,312 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
function bratonien_tools_nc_edit_json($status, array $payload)
{
http_response_code((int)$status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function bratonien_tools_nc_edit_fail($message, array $fields = array(), $stage = '', $detail = '')
{
$visible = trim((string)$message);
if ($stage !== '') $visible = 'Bereich: '.$stage.' — '.$visible;
if ($detail !== '') $visible .= ' Technische Ursache: '.$detail;
bratonien_tools_nc_edit_json(422, array(
'ok'=>false,
'message'=>$visible,
'stage'=>(string)$stage,
'detail'=>(string)$detail,
'fields'=>array_values(array_unique($fields)),
));
}
function bratonien_tools_nc_edit_probe($url, $username = '', $password = '', array $headers = array())
{
if (!function_exists('curl_init'))
{
return array('ok'=>false, 'http'=>0, 'errno'=>-1, 'error'=>'cURL ist in PHP nicht verfügbar.', 'body'=>'');
}
$ch = curl_init($url);
$options = array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>true,
CURLOPT_MAXREDIRS=>3,
CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>15,
CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers),
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Editor/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
);
if ($username !== '')
{
$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
$options[CURLOPT_USERPWD] = $username.':'.$password;
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return array(
'ok'=>$body !== false && $errno === 0,
'http'=>$http,
'errno'=>$errno,
'error'=>$error,
'body'=>$body === false ? '' : (string)$body,
);
}
if (!defined('BRATONIEN_TOOLS_PATH'))
{
bratonien_tools_nc_edit_json(404, array('ok'=>false, 'message'=>'Bratonien Tools ist nicht aktiv.', 'fields'=>array()));
}
if (!function_exists('is_admin') || !is_admin())
{
bratonien_tools_nc_edit_json(403, array('ok'=>false, 'message'=>'Administratorrechte erforderlich.', 'fields'=>array()));
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST')
{
bratonien_tools_nc_edit_json(405, array('ok'=>false, 'message'=>'Nur POST ist erlaubt.', 'fields'=>array()));
}
try
{
check_pwg_token();
require_once(BRATONIEN_TOOLS_PATH.'include/tool_registry.inc.php');
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
bratonien_tools_nc_edit_fail('Die zu bearbeitende Verbindung wurde nicht gefunden.', array(), 'Verbindung');
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$nextcloud_input = trim((string)($_POST['nc_nextcloud_url'] ?? ($config['nextcloud_url'] ?? '')));
$nextcloud_user = trim((string)($_POST['nc_nextcloud_user'] ?? ($credentials['nextcloud_user'] ?? '')));
$submitted_nc_password = (string)($_POST['nc_nextcloud_password'] ?? '');
$nextcloud_password = $submitted_nc_password !== '' ? $submitted_nc_password : (string)($credentials['nextcloud_password'] ?? '');
$has_any_nextcloud = $nextcloud_input !== '' || $nextcloud_user !== '' || $nextcloud_password !== '';
if ($has_any_nextcloud)
{
$missing = array();
if ($nextcloud_input === '') $missing[] = 'nc_nextcloud_url';
if ($nextcloud_user === '') $missing[] = 'nc_nextcloud_user';
if ($nextcloud_password === '') $missing[] = 'nc_nextcloud_password';
if ($missing)
{
bratonien_tools_nc_edit_fail(
'Die WebDAV-Daten sind unvollständig. Adresse, Benutzer und Passwort werden gemeinsam benötigt.',
$missing,
'Nextcloud / WebDAV'
);
}
try
{
$candidate_urls = bratonien_tools_nc_wizard_candidate_urls($nextcloud_input);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail($e->getMessage(), array('nc_nextcloud_url'), 'Nextcloud / WebDAV');
}
$nextcloud_url = '';
$last_probe = null;
$last_status_url = '';
foreach ($candidate_urls as $candidate_url)
{
$status_url = $candidate_url.'/status.php';
$probe = bratonien_tools_nc_edit_probe($status_url);
$last_probe = $probe;
$last_status_url = $status_url;
if (!$probe['ok'] || $probe['http'] < 200 || $probe['http'] >= 300)
{
continue;
}
$status = json_decode($probe['body'], true);
if (!is_array($status) || empty($status['installed']))
{
continue;
}
$nextcloud_url = $candidate_url;
break;
}
if ($nextcloud_url === '')
{
$attempts = array();
foreach ($candidate_urls as $candidate_url)
{
$attempts[] = $candidate_url.'/status.php';
}
if (is_array($last_probe) && !$last_probe['ok'])
{
$technical = $last_probe['errno'] >= 0
? 'cURL '.$last_probe['errno'].($last_probe['error'] !== '' ? ': '.$last_probe['error'] : '')
: $last_probe['error'];
bratonien_tools_nc_edit_fail(
'Die Nextcloud-Adresse ist vom Piwigo-Server aus nicht erreichbar. Ohne angegebenes Protokoll wurden HTTPS und HTTP geprüft.',
array('nc_nextcloud_url'),
'Nextcloud / WebDAV Erreichbarkeit',
$technical.' · Geprüft: '.implode(' ; ', $attempts)
);
}
bratonien_tools_nc_edit_fail(
'Unter der angegebenen Adresse wurde keine erreichbare installierte Nextcloud gefunden. Ohne angegebenes Protokoll wurden HTTPS und HTTP geprüft.',
array('nc_nextcloud_url'),
'Nextcloud / WebDAV Erkennung',
'Geprüft: '.implode(' ; ', $attempts).($last_probe ? ' · Letzter HTTP-Status: '.$last_probe['http'] : '')
);
}
$_POST['nc_nextcloud_url'] = $nextcloud_url;
$user_url = $nextcloud_url.'/ocs/v2.php/cloud/user?format=json';
$user_probe = bratonien_tools_nc_edit_probe($user_url, $nextcloud_user, $nextcloud_password, array('OCS-APIRequest: true'));
if (!$user_probe['ok'])
{
$technical = $user_probe['errno'] >= 0
? 'cURL '.$user_probe['errno'].($user_probe['error'] !== '' ? ': '.$user_probe['error'] : '')
: $user_probe['error'];
bratonien_tools_nc_edit_fail(
'Die Nextcloud-Anmeldung konnte technisch nicht geprüft werden.',
array('nc_nextcloud_url','nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
$technical.' · Ziel: '.$user_url
);
}
if ($user_probe['http'] === 401 || $user_probe['http'] === 403)
{
bratonien_tools_nc_edit_fail(
'Nextcloud hat Benutzername oder Passwort abgelehnt.',
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
'HTTP '.$user_probe['http']
);
}
if ($user_probe['http'] < 200 || $user_probe['http'] >= 300)
{
bratonien_tools_nc_edit_fail(
'Nextcloud ist erreichbar, aber die Benutzerprüfung wurde vom Server abgelehnt.',
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
'HTTP '.$user_probe['http'].' · Ziel: '.$user_url
);
}
try
{
bratonien_tools_nc_wizard_ocs_data($user_probe['body']);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail(
'Nextcloud antwortet auf die Benutzerprüfung, die Antwort ist aber nicht gültig: '.$e->getMessage(),
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung'
);
}
}
$api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ($credentials['api_key_id'] ?? '')));
$submitted_api_secret = trim((string)($_POST['nc_connection_api_key_secret'] ?? ''));
$api_key_secret = $submitted_api_secret !== '' ? $submitted_api_secret : trim((string)($credentials['api_key_secret'] ?? ''));
if (($api_key_id === '') !== ($api_key_secret === ''))
{
$missing = $api_key_id === '' ? array('nc_connection_api_key_id') : array('nc_connection_api_key_secret');
bratonien_tools_nc_edit_fail(
'API-Schlüssel-ID und API-Geheimnis müssen gemeinsam vollständig sein.',
$missing,
'Piwigo API'
);
}
if ($api_key_id !== '')
{
try
{
bratonien_tools_nc_connector_validate_scoped_api($api_key_id, $api_key_secret);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail(
'Der Piwigo-API-Zugang konnte nicht bestätigt werden: '.$e->getMessage(),
array('nc_connection_api_key_id','nc_connection_api_key_secret'),
'Piwigo API'
);
}
}
$result = bratonien_tools_nc_connector_update_local_friendly();
bratonien_tools_nc_edit_json(200, array(
'ok'=>true,
'message'=>(string)($result['message'] ?? 'Verbindung wurde gespeichert.'),
'fields'=>array(),
));
}
catch (Throwable $e)
{
$message = trim($e->getMessage());
if ($message === '') $message = 'Die Verbindung konnte nicht gespeichert werden.';
$fields = array();
$stage = 'Verbindung';
$add = function($name) use (&$fields)
{
if (!in_array($name, $fields, true)) $fields[] = $name;
};
if (stripos($message, 'Name') !== false && stripos($message, 'Datenbankname') === false) $add('connection_name');
if (stripos($message, 'Datenbank-Server') !== false) { $add('nc_host'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbank-Port') !== false) { $add('nc_port'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbankname') !== false) { $add('nc_database'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Reader-Benutzer') !== false) { $add('nc_user'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbankpasswort') !== false) { $add('nc_db_password'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Storage-ID') !== false) { $add('nc_storage_id[]'); $stage = 'Speicherorte'; }
if (stripos($message, 'lokaler Pfad') !== false || stripos($message, 'Speicherort') !== false) { $add('nc_local_mount[]'); $stage = 'Speicherorte'; }
if (stripos($message, 'Galerieordner') !== false) { $add('nc_gallery_root'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Datenbankansichten') !== false || stripos($message, 'Source-View') !== false) { $add('nc_source_view'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Datenbankansichten') !== false || stripos($message, 'Activity-View') !== false) { $add('nc_activity_view'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Nextcloud') !== false)
{
$stage = 'Nextcloud / WebDAV';
if (stripos($message, 'Adresse') !== false) $add('nc_nextcloud_url');
if (stripos($message, 'Benutzer') !== false) $add('nc_nextcloud_user');
if (stripos($message, 'Passwort') !== false) $add('nc_nextcloud_password');
}
if (stripos($message, 'API') !== false)
{
$stage = 'Piwigo API';
$add('nc_connection_api_key_id');
$add('nc_connection_api_key_secret');
}
bratonien_tools_nc_edit_fail(
$message,
$fields,
$stage,
'Interne Prüfung: '.get_class($e)
);
}

View File

@@ -58,7 +58,6 @@ function decrypt_install_credentials($blob, $hexKey)
$decoded = json_decode($plain, true); $decoded = json_decode($plain, true);
if (!is_array($decoded)) if (!is_array($decoded))
{ {
// Backward compatibility: old connections stored only the DB password.
return array('db_password'=>$plain,'piwigo_user'=>'','piwigo_password'=>''); return array('db_password'=>$plain,'piwigo_user'=>'','piwigo_password'=>'');
} }
if (empty($decoded['db_password'])) fail_install('Datenbankpasswort fehlt in den Connector-Zugangsdaten.'); if (empty($decoded['db_password'])) fail_install('Datenbankpasswort fehlt in den Connector-Zugangsdaten.');
@@ -124,10 +123,13 @@ try
@unlink($piwigoPasswordPath); @unlink($piwigoPasswordPath);
} }
$storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount'); $storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount<TAB>include_prefix');
foreach ((array)($config['storages'] ?? array()) as $storage) foreach ((array)($config['storages'] ?? array()) as $storage)
{ {
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t".trim((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'] ?? '')."\t"
.trim((string)($storage['include_prefix'] ?? ''), '/');
} }
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); chmod($storagePath, 0600); file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); chmod($storagePath, 0600);

View File

@@ -36,6 +36,11 @@ $latest = array(
'api'=>array('state'=>'not_run','message'=>''), 'api'=>array('state'=>'not_run','message'=>''),
'fallback'=>array('state'=>'not_run','message'=>''), 'fallback'=>array('state'=>'not_run','message'=>''),
'error_detail'=>'', 'error_detail'=>'',
'route'=>'',
'route_label'=>'Noch kein Datenweg erfasst',
'route_timestamp'=>0,
'route_time_label'=>'Nicht verfügbar',
'route_detail'=>'',
); );
if (is_dir($dir)) if (is_dir($dir))
@@ -57,6 +62,54 @@ if (is_dir($dir))
$latest['connection_file'] = basename($file); $latest['connection_file'] = basename($file);
} }
} }
$route_file = $dir.'/route-status.json';
if (is_readable($route_file))
{
$route = json_decode((string)@file_get_contents($route_file), true);
if (is_array($route))
{
$route_name = (string)($route['route'] ?? '');
$route_timestamp = (int)($route['timestamp'] ?? 0);
$route_label = (string)($route['label'] ?? '');
if ($route_name === 'webdav')
{
$route_label = 'WEBDAV PRIMÄR';
}
elseif ($route_name === 'legacy_fallback')
{
$route_label = 'LEGACY-FALLBACK AKTIV';
}
elseif ($route_name === 'failed')
{
$route_label = 'FEHLER - KEIN ERFOLGREICHER DATENWEG';
}
elseif ($route_label === '')
{
$route_label = 'UNBEKANNTER DATENWEG';
}
$route_detail = trim((string)($route['detail'] ?? ''));
$latest['route'] = $route_name;
$latest['route_label'] = $route_label;
$latest['route_timestamp'] = $route_timestamp;
$latest['route_time_label'] = $route_timestamp > 0 ? date('d.m.Y H:i:s', $route_timestamp) : 'Nicht verfügbar';
$latest['route_detail'] = $route_detail;
$base_message = trim((string)($latest['message'] ?? ''));
$message_parts = array($route_label);
if ($route_name !== 'webdav' && $route_detail !== '')
{
$message_parts[] = $route_detail;
}
if ($base_message !== '')
{
$message_parts[] = $base_message;
}
$latest['message'] = implode(' · ', $message_parts);
}
}
} }
if ((int)$latest['timestamp'] > 0) if ((int)$latest['timestamp'] > 0)

1
noop2 Normal file
View File

@@ -0,0 +1 @@
x

1
noop2-remove-marker.txt Normal file
View File

@@ -0,0 +1 @@
stale branch only

86
runtime/cleanup-stale.php Normal file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$configDir = '/etc/bratonien-tools/nc-connector';
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
function cleanup_fail($message)
{
fwrite(STDERR, $message."\n");
exit(1);
}
function cleanup_tree($path)
{
$path = rtrim((string)$path, '/');
if ($path === '' || $path === '/' || !file_exists($path)) return;
if (is_link($path) || is_file($path))
{
@unlink($path);
return;
}
$items = @scandir($path);
if (!is_array($items)) return;
foreach ($items as $item)
{
if ($item === '.' || $item === '..') continue;
cleanup_tree($path.'/'.$item);
}
@rmdir($path);
}
if (!is_readable($dbConfig)) cleanup_fail('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key])) cleanup_fail('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) cleanup_fail('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$escapedTable = $db->real_escape_string($table);
$exists = $db->query("SHOW TABLES LIKE '{$escapedTable}'");
if (!$exists) cleanup_fail('Connector-Tabelle konnte nicht geprüft werden: '.$db->error);
if ($exists->num_rows === 0) exit(0);
$activeIds = array();
$result = $db->query("SELECT id FROM `{$table}`");
if (!$result) cleanup_fail('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
while ($row = $result->fetch_assoc())
{
$activeIds[(int)$row['id']] = true;
}
if (!is_dir($configDir)) exit(0);
foreach (glob($configDir.'/connection-*.conf') ?: array() as $configPath)
{
if (!preg_match('/connection-([0-9]+)\.conf$/', $configPath, $matches)) continue;
$id = (int)$matches[1];
if (isset($activeIds[$id])) continue;
echo "NC Connector: entferne verwaiste Laufzeitdateien fuer Verbindung {$id}.\n";
foreach (array('.conf','.db-password','.piwigo-password','.storages.tsv','.roots.tsv') as $suffix)
{
@unlink($configDir.'/connection-'.$id.$suffix);
}
cleanup_tree($stateRoot.'/connection-'.$id);
$statusDir = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-status';
@unlink($statusDir.'/connection-'.$id.'.json');
@unlink($statusDir.'/deleted-'.$id);
}
exit(0);

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2);
define('PHPWG_ROOT_PATH', rtrim($piwigoRoot, '/').'/');
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['SERVER_PORT'] = '80';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/cleanup-webdav-piwigo.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
function bratonien_cleanup_tree($path, $allowed_root)
{
$path = rtrim((string)$path, '/');
$allowed_root = rtrim((string)$allowed_root, '/');
if ($path === '' || $allowed_root === '' || strpos($path, $allowed_root.'/') !== 0 || !file_exists($path)) return;
if (is_link($path) || is_file($path))
{
@unlink($path);
return;
}
$items = @scandir($path);
if (!is_array($items)) return;
foreach ($items as $item)
{
if ($item === '.' || $item === '..') continue;
bratonien_cleanup_tree($path.'/'.$item, $allowed_root);
}
@rmdir($path);
}
function bratonien_webdav_site_images($site_id)
{
$rows = array();
$query = '
SELECT DISTINCT i.id, i.path, i.representative_ext
FROM '.IMAGES_TABLE.' AS i
LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id
LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id
LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id
WHERE sc.site_id = '.(int)$site_id.' OR vc.site_id = '.(int)$site_id.'
;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
$row['id'] = (int)$row['id'];
$rows[$row['id']] = $row;
}
return $rows;
}
try
{
$connection_table = function_exists('bratonien_tools_nc_connector_table')
? bratonien_tools_nc_connector_table()
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$active = array();
$exists = pwg_query("SHOW TABLES LIKE '".pwg_db_real_escape_string($connection_table)."'");
if (pwg_db_num_rows($exists))
{
$result = pwg_query("SELECT id FROM `".$connection_table."`");
while ($row = pwg_db_fetch_assoc($result))
{
$active[(int)$row['id']] = true;
}
}
$site_prefix = './_data/bratonien-tools/nc-webdav-gallery/connection-';
$result = pwg_query(
"SELECT id, galleries_url FROM ".SITES_TABLE.
" WHERE galleries_url LIKE '".pwg_db_real_escape_string($site_prefix)."%'"
);
$removed_sites = 0;
$removed_images = 0;
while ($site = pwg_db_fetch_assoc($result))
{
$site_id = (int)$site['id'];
$site_url = (string)$site['galleries_url'];
if (!preg_match('#^\./_data/bratonien-tools/nc-webdav-gallery/connection-([0-9]+)/$#', $site_url, $match)) continue;
$connection_id = (int)$match[1];
if ($connection_id < 1 || isset($active[$connection_id])) continue;
$images = bratonien_webdav_site_images($site_id);
foreach ($images as $row)
{
try
{
delete_element_derivatives($row);
}
catch (Throwable $e)
{
fwrite(STDERR, 'WebDAV-Cleanup: Derivate von Bild #'.(int)$row['id'].' konnten nicht vollständig entfernt werden: '.$e->getMessage()."\n");
}
}
delete_site($site_id);
if ($images)
{
$ids = array_keys($images);
$remaining = query2array(
'SELECT id FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $ids)).')',
null,
'id'
);
if ($remaining)
{
delete_elements(array_map('intval', $remaining), false);
}
}
$removed_sites++;
$removed_images += count($images);
echo 'NC WebDAV: entferne verwaiste Piwigo-Site '.$site_id.' für gelöschte Verbindung '.$connection_id.".\n";
$gallery_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-gallery';
$source_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-source';
$preview_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-preview';
bratonien_cleanup_tree($gallery_root.'/connection-'.$connection_id, $gallery_root);
bratonien_cleanup_tree($source_root.'/connection-'.$connection_id, $source_root);
bratonien_cleanup_tree($preview_root.'/connection-'.$connection_id, $preview_root);
$state_root = '/var/lib/bratonien-tools/nc-connector';
bratonien_cleanup_tree($state_root.'/connection-'.$connection_id, $state_root);
foreach (glob('/etc/bratonien-tools/nc-connector/webdav-connection-'.$connection_id.'.*') ?: array() as $file)
{
@unlink($file);
}
$status_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-status';
@unlink($status_root.'/connection-'.$connection_id.'.json');
@unlink($status_root.'/deleted-'.$connection_id);
}
if ($removed_sites > 0)
{
invalidate_user_cache(true);
}
echo 'NC WebDAV Piwigo-Cleanup: sites='.$removed_sites.' bilder='.$removed_images."\n";
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, 'NC WebDAV Piwigo-Cleanup: '.$e->getMessage()."\n");
exit(1);
}

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Debounce Nextcloud activity and request a periodic safety reconciliation.""" """Debounce Nextcloud activity for one connector scope."""
from __future__ import annotations from __future__ import annotations
@@ -24,54 +24,117 @@ def validate_view(name: str) -> str:
return value return value
def sql_literal(value: str) -> str:
return "'" + str(value).replace("'", "''") + "'"
def load(path: Path) -> dict[str, object]: def load(path: Path) -> dict[str, object]:
defaults: dict[str, object] = {"processed":0,"observed":0,"pending_since":0,"last_change":0,"last_full":0,"source_signature":""} defaults: dict[str, object] = {
"processed": 0, "observed": 0, "pending_since": 0,
"last_change": 0, "last_full": 0, "source_signature": "",
}
if not path.exists(): 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 {"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",""))} return {
"processed": int(data.get("processed", 0)),
"observed": int(data.get("observed", 0)),
"pending_since": int(data.get("pending_since", 0)),
"last_change": int(data.get("last_change", 0)),
"last_full": int(data.get("last_full", 0)),
"source_signature": str(data.get("source_signature", "")),
}
def save(path: Path, state: dict[str, object]) -> None: 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);handle.write("\n") json.dump(state, handle, sort_keys=True)
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["PGPASSWORD"]=args.password_file.read_text(encoding="utf-8").strip() env = os.environ.copy()
command=["psql","-XAt","-h",args.host,"-p",str(args.port),"-U",args.user,"-d",args.database,"-v","ON_ERROR_STOP=1","-c",sql] env["PGPASSWORD"] = args.password_file.read_text(encoding="utf-8").strip()
command = [
"psql", "-XAt", "-h", args.host, "-p", str(args.port),
"-U", args.user, "-d", args.database, "-v", "ON_ERROR_STOP=1", "-c", sql,
]
return subprocess.run(command, env=env, check=True, text=True, capture_output=True).stdout return subprocess.run(command, env=env, check=True, text=True, capture_output=True).stdout
def user_where(args: argparse.Namespace) -> str:
if not args.access_user:
return ""
return " WHERE lower(access_user) = lower(" + sql_literal(args.access_user) + ")"
def latest(args: argparse.Namespace) -> int: def latest(args: argparse.Namespace) -> int:
return int(query(args,f"SELECT COALESCE(MAX(activity_id), 0) FROM {validate_view(args.view)}").strip()) view = validate_view(args.view)
return int(query(args, f"SELECT COALESCE(MAX(activity_id), 0) FROM {view}{user_where(args)}").strip())
def source_signature(args: argparse.Namespace) -> str: def source_signature(args: argparse.Namespace) -> str:
if not args.source_view:return "" if args.roots_config:
payload=query(args,f"SELECT share_id, display_name, storage_id, source_path FROM {validate_view(args.source_view)} ORDER BY share_id") return hashlib.sha256(args.roots_config.read_bytes()).hexdigest()
if not args.source_view:
return ""
view = validate_view(args.source_view)
payload = query(args, f"SELECT share_id, display_name, storage_id, source_path FROM {view}{user_where(args)} 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.add_argument("action",choices=("check","commit"));parser.add_argument("--state",required=True,type=Path);parser.add_argument("--host",required=True);parser.add_argument("--port",type=int,default=5432);parser.add_argument("--database",required=True);parser.add_argument("--user",required=True);parser.add_argument("--password-file",required=True,type=Path);parser.add_argument("--view",default="piwigo_showcase_activity");parser.add_argument("--source-view",default="");parser.add_argument("--quiet",type=int,default=120);parser.add_argument("--max-wait",type=int,default=900);parser.add_argument("--full-after",type=int,default=86400);args=parser.parse_args() parser = argparse.ArgumentParser()
parser.add_argument("action", choices=("check", "commit"))
parser.add_argument("--state", required=True, type=Path)
parser.add_argument("--host", required=True)
parser.add_argument("--port", type=int, default=5432)
parser.add_argument("--database", required=True)
parser.add_argument("--user", required=True)
parser.add_argument("--password-file", required=True, type=Path)
parser.add_argument("--view", required=True)
parser.add_argument("--source-view", default="")
parser.add_argument("--roots-config", type=Path)
parser.add_argument("--access-user", 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:
validate_view(args.view) validate_view(args.view)
if args.source_view:validate_view(args.source_view) if args.source_view:
state=load(args.state);now=int(time.time());current=latest(args);current_source=source_signature(args) validate_view(args.source_view)
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 state = load(args.state)
if not int(state["last_full"]):return 0 now = int(time.time())
if current_source and current_source!=str(state["source_signature"]):return 0 current = latest(args)
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) current_source = source_signature(args)
if now-int(state["last_full"])>=args.full_after:return 0 if args.action == "commit":
if int(state["observed"])<=int(state["processed"]):return 3 state.update(processed=current, observed=current, pending_since=0, last_change=0, last_full=now, source_signature=current_source)
if now-int(state["last_change"])>=args.quiet or now-int(state["pending_since"])>=args.max_wait:return 0 save(args.state, state)
return 0
if not int(state["last_full"]):
return 0
if current_source and current_source != str(state["source_signature"]):
return 0
if current > int(state["observed"]):
state["observed"] = current
state["last_change"] = now
state["pending_since"] = int(state["pending_since"]) or now
save(args.state, state)
if now - int(state["last_full"]) >= args.full_after:
return 0
if int(state["observed"]) <= int(state["processed"]):
return 3
if now - int(state["last_change"]) >= args.quiet or now - int(state["pending_since"]) >= args.max_wait:
return 0
return 3 return 3
except Exception as error: except Exception as error:
print(f"activity-gate: {error}",file=sys.stderr);return 1 print(f"activity-gate: {error}", file=sys.stderr)
return 1
if __name__=="__main__":raise SystemExit(main()) if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,174 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
const BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION = '0.9.6.1';
$options = getopt('', array('piwigo-root:', 'connection-id:'));
$piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/');
$connection_id = (int)($options['connection-id'] ?? 0);
if ($piwigo_root === '' || $connection_id < 1)
{
fwrite(STDERR, "Parameter --piwigo-root und --connection-id werden benoetigt.\n");
exit(1);
}
if (!is_dir($piwigo_root))
{
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
exit(1);
}
define('PHPWG_ROOT_PATH', $piwigo_root.'/');
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['SERVER_PORT'] = '80';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/build-webdav-derivatives.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Derivative-Builder/'.BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION;
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
if (!function_exists('bratonien_tools_webdav_image_source_info') || !function_exists('bratonien_tools_webdav_generate_derivative'))
{
fwrite(STDERR, "Bratonien WebDAV-Bildruntime ist nicht aktiv.\n");
exit(1);
}
try
{
$variants = bratonien_tools_webdav_derivative_variants();
if (!$variants)
{
throw new RuntimeException('Keine Piwigo-Derivate konfiguriert.');
}
$images = 0;
$generated_or_ready = 0;
$identity = 0;
$metadata_repaired = 0;
$errors = 0;
$error_lines = array();
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result))
{
$image_id = (int)$row['id'];
$info = bratonien_tools_webdav_image_source_info($image_id);
if (!$info || (int)$info['connection_id'] !== $connection_id) continue;
$images++;
$preview = bratonien_tools_webdav_preview_path($info);
if (!$preview)
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.': vorbereitetes WebDAV-Preview fehlt.';
continue;
}
$preview_ext = strtolower(pathinfo($preview, PATHINFO_EXTENSION));
if (!in_array($preview_ext, array('jpg', 'jpeg', 'png', 'gif'), true))
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.': Preview-Format '.$preview_ext.' ist nicht Piwigo-kompatibel; Precache muss neu erzeugt werden.';
continue;
}
$size = @getimagesize($preview);
if (!is_array($size) || empty($size[0]) || empty($size[1]))
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.': Preview ist kein lesbares Bild: '.$preview;
continue;
}
$preview_width = (int)$size[0];
$preview_height = (int)$size[1];
if ((int)($row['width'] ?? 0) !== $preview_width || (int)($row['height'] ?? 0) !== $preview_height || (int)($row['rotation'] ?? 0) !== 0)
{
pwg_query(
'UPDATE '.IMAGES_TABLE.
' SET width='.$preview_width.', height='.$preview_height.', rotation=0'.
' WHERE id='.$image_id
);
$row['width'] = $preview_width;
$row['height'] = $preview_height;
$row['rotation'] = 0;
$metadata_repaired++;
}
$src = new SrcImage($row);
foreach ($variants as $variant_name => $params)
{
try
{
$probe = new DerivativeImage($params, $src);
if ($probe->same_as_source())
{
$identity++;
continue;
}
$detail = '';
if (bratonien_tools_webdav_generate_derivative($params, $src, $detail))
{
$generated_or_ready++;
}
else
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.' '.$variant_name.': '.($detail !== '' ? $detail : 'Derivat konnte nicht erzeugt werden.');
}
}
catch (Throwable $e)
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.' '.$variant_name.': '.get_class($e).': '.$e->getMessage();
}
}
}
if ($metadata_repaired > 0)
{
update_category('all');
invalidate_user_cache(true);
}
if ($errors > 0)
{
foreach (array_slice($error_lines, 0, 30) as $line)
{
fwrite(STDERR, $line."\n");
}
if (count($error_lines) > 30)
{
fwrite(STDERR, 'Weitere Fehler: '.(count($error_lines) - 30)."\n");
}
}
echo 'WebDAV-Derivative-Builder: version='.BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION.
' bilder='.$images.
' varianten='.count($variants).
' bereit='.$generated_or_ready.
' identisch='.$identity.
' metadaten_repariert='.$metadata_repaired.
' fehler='.$errors."\n";
exit($errors > 0 ? 1 : 0);
}
catch (Throwable $e)
{
fwrite(STDERR, 'WebDAV-Derivate: '.get_class($e).': '.$e->getMessage()."\n");
exit(1);
}

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Resolve Nextcloud view rows through explicitly configured storage mounts.""" """Resolve Nextcloud share rows through configured storage adapters."""
from __future__ import annotations from __future__ import annotations
@@ -23,93 +23,194 @@ def validate_view(name: str) -> str:
return value return value
def read_config(path: Path) -> dict[str, tuple[str, Path]]: def sql_literal(value: str) -> str:
result: dict[str, tuple[str, Path]] = {} return "'" + str(value).replace("'", "''") + "'"
def read_config(path: Path) -> dict[str, list[tuple[str, Path, str]]]:
result: dict[str, list[tuple[str, Path, str]]] = {}
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("#"):continue if not line or line.startswith("#"):
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) not in {3, 4}:
storage_id,prefix,mount=fields raise ValueError(f"{path}:{number}: expected storage_id, source_prefix, local_mount and optional include_prefix")
if not storage_id.strip():raise ValueError(f"{path}:{number}: storage_id is empty") storage_id, prefix, mount = fields[:3]
result[storage_id]=(prefix.strip("/"),Path(mount)) include_prefix = fields[3] if len(fields) == 4 else ""
storage_id = storage_id.strip()
prefix = prefix.strip("/")
include_prefix = include_prefix.strip("/")
if not storage_id:
raise ValueError(f"{path}:{number}: storage_id is empty")
if ".." in PurePosixPath(prefix).parts or ".." in PurePosixPath(include_prefix).parts:
raise ValueError(f"{path}:{number}: unsafe prefix")
result.setdefault(storage_id, []).append((prefix, Path(mount), include_prefix))
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=["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] command = [
"psql", "-X", "-A", "-F", "\t", "-t", "-v", "ON_ERROR_STOP=1",
"-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.database, "-c", sql,
]
return subprocess.run(command, env=env, check=False, text=True, capture_output=True) 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();env=os.environ.copy();env["PGPASSWORD"]=password;view=validate_view(args.view) password = args.password_file.read_text(encoding="utf-8").strip()
modern_sql=f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {view} ORDER BY share_id" env = os.environ.copy()
env["PGPASSWORD"] = password
view = validate_view(args.view)
where = ""
if args.access_user:
where = " WHERE lower(access_user) = lower(" + sql_literal(args.access_user) + ")"
modern_sql = f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {view}{where} ORDER BY share_id"
completed = run_query(args, env, modern_sql) completed = run_query(args, env, modern_sql)
if completed.returncode==0:return list(csv.reader(completed.stdout.splitlines(),delimiter="\t")) if completed.returncode == 0:
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") return list(csv.reader(completed.stdout.splitlines(), delimiter="\t"))
if args.access_user:
raise RuntimeError(completed.stderr.strip() or "Nextcloud user-filtered source query failed")
if "item_type" not in completed.stderr or "does not exist" not in completed.stderr:
raise RuntimeError(completed.stderr.strip() or "Nextcloud source view query failed")
legacy_sql = f"SELECT share_id, display_name, storage_id, source_path FROM {view} ORDER BY share_id" legacy_sql = f"SELECT share_id, display_name, storage_id, source_path FROM {view} ORDER BY share_id"
completed = run_query(args, env, legacy_sql) completed = run_query(args, env, legacy_sql)
if completed.returncode!=0:raise RuntimeError(completed.stderr.strip() or "legacy Nextcloud source view query failed") if completed.returncode != 0:
rows=[] 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"): for row in csv.reader(completed.stdout.splitlines(), delimiter="\t"):
if len(row) == 4: if len(row) == 4:
share_id,display_name,storage_id,source_path=row;rows.append([share_id,"",display_name,storage_id,source_path]) share_id, display_name, storage_id, source_path = row
else:rows.append(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:raise ValueError(f"unsafe source path: {relative}") if relative.startswith("/") or ".." in parts:
raise ValueError(f"unsafe source path: {relative}")
return root.joinpath(*parts) return root.joinpath(*parts)
def matches_prefix(path: str, prefix: str) -> bool:
return not prefix or path == prefix or path.startswith(prefix + "/")
def resolve_adapter(adapters: dict[str, list[tuple[str, Path, str]]], storage_id: str, source_path: str) -> tuple[str, Path, str] | None:
relative = source_path.strip("/")
matches: list[tuple[str, Path, str]] = []
for prefix, mount, include_prefix in adapters.get(storage_id, []):
if not matches_prefix(relative, prefix):
continue
mapped_relative = relative[len(prefix):].lstrip("/") if prefix else relative
if not matches_prefix(mapped_relative, include_prefix):
continue
matches.append((prefix, mount, include_prefix))
if not matches:
return None
matches.sort(key=lambda item: (len(item[0]), len(item[2])), reverse=True)
best_score = (len(matches[0][0]), len(matches[0][2]))
best = {(item[0], str(item[1]), item[2]): item for item in matches if (len(item[0]), len(item[2])) == best_score}
if len(best) != 1:
raise RuntimeError(f"storage adapter is ambiguous for {storage_id}")
return next(iter(best.values()))
def build(args: argparse.Namespace) -> dict[str, object]: def build(args: argparse.Namespace) -> dict[str, object]:
validate_view(args.view);adapters=read_config(args.storage_config);rows=query_rows(args) validate_view(args.view)
if not rows and not args.allow_empty:raise RuntimeError("Nextcloud returned no Showcase shares; refusing an empty manifest") adapters = read_config(args.storage_config)
manifest=[];errors=[];folder_count=0;file_count=0 rows = query_rows(args)
if not rows and not args.allow_empty:
raise RuntimeError("Nextcloud returned no matching sources; 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:errors.append(f"invalid database row with {len(row)} columns");continue if len(row) != 5:
share_id,item_type,display_name,storage_id,source_path=row;item_type=item_type.strip().lower() errors.append(f"invalid database row with {len(row)} columns")
if item_type and item_type not in {"folder","file"}:errors.append(f"share {share_id}: unsupported item_type {item_type!r}");continue 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("/") relative = source_path.strip("/")
if item_type and item_type not in {"folder", "file"}:
errors.append(f"source {share_id}: unsupported item_type {item_type!r}")
continue
try:
adapter = resolve_adapter(adapters, storage_id, relative)
except RuntimeError as error:
errors.append(f"source {share_id}: {error}")
continue
if not adapter:
continue
prefix, mount, _include_prefix = adapter
if prefix: if prefix:
expected=prefix+"/"
if relative!=prefix and not relative.startswith(expected):errors.append(f"share {share_id}: path does not match configured prefix");continue
relative = relative[len(prefix):].lstrip("/") relative = relative[len(prefix):].lstrip("/")
source = contained_join(mount, relative) source = contained_join(mount, relative)
if not mount.is_mount():errors.append(f"share {share_id}: storage mount unavailable: {mount}");continue if not mount.is_mount():
errors.append(f"source {share_id}: storage mount unavailable: {mount}")
continue
if not item_type: if not item_type:
if source.is_dir():item_type="folder" if source.is_dir():
elif source.is_file():item_type="file" item_type = "folder"
else:errors.append(f"share {share_id}: source unavailable: {source}");continue elif source.is_file():
item_type = "file"
else:
errors.append(f"source {share_id}: source unavailable: {source}")
continue
if item_type == "folder": if item_type == "folder":
if not source.is_dir():errors.append(f"share {share_id}: source directory unavailable: {source}");continue if not source.is_dir():
errors.append(f"source {share_id}: source directory unavailable: {source}")
continue
folder_count += 1 folder_count += 1
else: else:
if not source.is_file():errors.append(f"share {share_id}: source file unavailable: {source}");continue if not source.is_file():
errors.append(f"source {share_id}: source file unavailable: {source}")
continue
file_count += 1 file_count += 1
if "\t" in display_name or "\n" in display_name or "\r" in display_name:errors.append(f"share {share_id}: display name contains unsupported control characters");continue if any(char in display_name for char in ("\t", "\n", "\r")):
errors.append(f"source {share_id}: display name contains unsupported control characters")
continue
source_text = str(source) 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 if any(char in source_text for char in ("\t", "\n", "\r")):
errors.append(f"source {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))
if not manifest and rows and not args.allow_empty:
raise RuntimeError("no Nextcloud sources match the configured storage adapters")
args.output.parent.mkdir(parents=True, exist_ok=True) args.output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=args.output.parent, delete=False) as handle: 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) 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 {"sources": len(manifest), "folders": folder_count, "files": file_count, "manifest": str(args.output)}
def main() -> int: def main() -> int:
parser=argparse.ArgumentParser();parser.add_argument("--host",required=True);parser.add_argument("--port",type=int,default=5432);parser.add_argument("--database",required=True);parser.add_argument("--user",required=True);parser.add_argument("--password-file",required=True,type=Path);parser.add_argument("--view",default="piwigo_showcase_sources");parser.add_argument("--storage-config",required=True,type=Path);parser.add_argument("--output",required=True,type=Path);parser.add_argument("--allow-empty",action="store_true");args=parser.parse_args() parser = argparse.ArgumentParser()
try:print(json.dumps(build(args),ensure_ascii=False)) parser.add_argument("--host", required=True)
except Exception as error:print(f"manifest: {error}",file=sys.stderr);return 1 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", required=True)
parser.add_argument("--access-user", default="")
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__":raise SystemExit(main()) if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""Build a manifest from WebDAV-authorized Nextcloud file IDs.
The selected roots are resolved through a generic Nextcloud file view and then
mapped through configured storage adapters. No user name, path layout or storage
protocol is assumed here.
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path, PurePosixPath
IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$")
def validate_view(name: str) -> str:
value = str(name).strip()
if not IDENTIFIER.fullmatch(value):
raise ValueError(f"invalid SQL view name: {value!r}")
return value
def safe_relative(value: str) -> str:
value = str(value).strip("/")
if ".." in PurePosixPath(value).parts:
raise ValueError(f"unsafe relative path: {value!r}")
return value
def read_roots(path: Path) -> dict[int, str]:
roots: dict[int, str] = {}
with path.open(encoding="utf-8") as handle:
for number, raw in enumerate(handle, 1):
line = raw.rstrip("\n")
if not line or line.startswith("#"):
continue
fields = line.split("\t")
if len(fields) != 2:
raise ValueError(f"{path}:{number}: expected fileid and display_name")
fileid_text, display_name = fields
if not fileid_text.isdigit() or int(fileid_text) < 1:
raise ValueError(f"{path}:{number}: invalid fileid")
if any(char in display_name for char in ("\t", "\n", "\r")):
raise ValueError(f"{path}:{number}: invalid display name")
roots[int(fileid_text)] = display_name or f"Element_{fileid_text}"
if not roots:
raise ValueError("no selected Nextcloud roots configured")
return roots
def read_adapters(path: Path) -> dict[str, list[tuple[str, Path]]]:
result: dict[str, list[tuple[str, Path]]] = {}
with path.open(encoding="utf-8") as handle:
for number, raw in enumerate(handle, 1):
line = raw.rstrip("\n")
if not line or line.startswith("#"):
continue
fields = line.split("\t")
if len(fields) not in {3, 4}:
raise ValueError(f"{path}:{number}: expected storage_id, source_prefix, local_mount and optional include_prefix")
storage_id, source_prefix, local_mount = fields[:3]
storage_id = storage_id.strip()
source_prefix = safe_relative(source_prefix)
mount = Path(local_mount)
if not storage_id:
raise ValueError(f"{path}:{number}: empty storage_id")
if not mount.is_absolute():
raise ValueError(f"{path}:{number}: local_mount must be absolute")
result.setdefault(storage_id, []).append((source_prefix, mount))
if not result:
raise ValueError("no storage adapters configured")
return result
def matches_prefix(path: str, prefix: str) -> bool:
return not prefix or path == prefix or path.startswith(prefix + "/")
def resolve_adapter(adapters: dict[str, list[tuple[str, Path]]], storage_id: str, source_path: str) -> tuple[str, Path]:
relative = safe_relative(source_path)
matches = [(prefix, mount) for prefix, mount in adapters.get(storage_id, []) if matches_prefix(relative, prefix)]
if not matches:
raise RuntimeError(f"no storage adapter configured for {storage_id}")
matches.sort(key=lambda item: len(item[0]), reverse=True)
best_length = len(matches[0][0])
best = {(prefix, str(mount)): (prefix, mount) for prefix, mount in matches if len(prefix) == best_length}
if len(best) != 1:
raise RuntimeError(f"storage adapter is ambiguous for {storage_id}")
return next(iter(best.values()))
def query_rows(args: argparse.Namespace, roots: dict[int, str]) -> dict[int, tuple[str, str]]:
password = args.password_file.read_text(encoding="utf-8").strip()
env = os.environ.copy()
env["PGPASSWORD"] = password
ids = ",".join(str(fileid) for fileid in sorted(roots))
view = validate_view(args.view)
sql = f"SELECT fileid, storage_id, source_path FROM {view} WHERE fileid IN ({ids}) ORDER BY fileid"
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,
]
completed = subprocess.run(command, env=env, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(completed.stderr.strip() or "Nextcloud file view query failed")
result: dict[int, tuple[str, str]] = {}
for row in csv.reader(completed.stdout.splitlines(), delimiter="\t"):
if len(row) != 3 or not row[0].isdigit():
raise RuntimeError("invalid row returned by Nextcloud file view")
result[int(row[0])] = (row[1], row[2])
missing = sorted(set(roots) - set(result))
if missing:
raise RuntimeError("selected Nextcloud file IDs are no longer resolvable: " + ", ".join(map(str, missing)))
return result
def contained_join(root: Path, relative: str) -> Path:
relative = safe_relative(relative)
candidate = root.joinpath(*PurePosixPath(relative).parts) if relative else root
root_real = root.resolve()
candidate_real = candidate.resolve()
try:
candidate_real.relative_to(root_real)
except ValueError as error:
raise ValueError(f"resolved source escapes configured mount: {candidate}") from error
return candidate
def build(args: argparse.Namespace) -> dict[str, object]:
roots = read_roots(args.roots_config)
adapters = read_adapters(args.storage_config)
rows = query_rows(args, roots)
manifest: list[str] = []
for fileid, display_name in roots.items():
storage_id, source_path = rows[fileid]
prefix, mount = resolve_adapter(adapters, storage_id, source_path)
if not mount.is_mount():
raise RuntimeError(f"storage mount unavailable: {mount}")
relative = safe_relative(source_path)
if prefix:
relative = relative[len(prefix):].lstrip("/")
source = contained_join(mount, relative)
if source.is_dir():
item_type = "folder"
elif source.is_file():
item_type = "file"
else:
raise RuntimeError(f"selected source unavailable: {source}")
if any(char in str(source) for char in ("\t", "\n", "\r")):
raise ValueError("resolved source path contains unsupported control characters")
manifest.append(f"fileid:{fileid}\t{item_type}\t{display_name}\t{source}")
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")
temporary = Path(handle.name)
temporary.replace(args.output)
return {"roots": len(manifest), "manifest": str(args.output)}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--host", required=True)
parser.add_argument("--port", type=int, default=5432)
parser.add_argument("--database", required=True)
parser.add_argument("--user", required=True)
parser.add_argument("--password-file", required=True, type=Path)
parser.add_argument("--view", required=True)
parser.add_argument("--storage-config", required=True, type=Path)
parser.add_argument("--roots-config", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
try:
print(json.dumps(build(args), ensure_ascii=False))
except Exception as error:
print(f"selected-manifest: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,319 @@
#!/usr/bin/env python3
"""Build a placeholder-backed local source tree from Nextcloud WebDAV.
This is intentionally additive: it does not replace the existing local-storage
connector path. It creates only tiny placeholder files plus a metadata mapping;
no Nextcloud original media is downloaded.
"""
from __future__ import annotations
import argparse
import base64
import getpass
import json
import os
import shutil
import ssl
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from pathlib import Path, PurePosixPath
DAV = "DAV:"
OC = "http://owncloud.org/ns"
SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
# 1x1 transparent GIF, 34 bytes. The filename keeps the remote extension;
# the placeholder exists only so Piwigo can discover the logical image entry.
PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
def fail(message: str) -> None:
raise RuntimeError(message)
def validate_relative(value: str) -> str:
value = str(value).strip("/")
parts = PurePosixPath(value).parts
if ".." in parts:
raise ValueError(f"unsafe WebDAV path: {value!r}")
return value
def quote_path(value: str) -> str:
value = validate_relative(value)
return "/".join(urllib.parse.quote(part, safe="") for part in PurePosixPath(value).parts)
def safe_local_name(name: str) -> str:
if not name or name in {".", ".."} or "/" in name or "\x00" in name:
raise ValueError(f"unsafe remote name: {name!r}")
return name
class WebDavClient:
def __init__(self, base_url: str, user: str, password: str, timeout: int = 30) -> None:
self.base_url = base_url.rstrip("/")
self.user = user
self.password = password
self.timeout = timeout
token = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("ascii")
self.auth_header = f"Basic {token}"
self.context = ssl.create_default_context()
def collection_url(self, relative: str) -> str:
user = urllib.parse.quote(self.user, safe="")
suffix = quote_path(relative)
url = f"{self.base_url}/remote.php/dav/files/{user}/"
if suffix:
url += suffix + "/"
return url
def list_collection(self, relative: str) -> tuple[dict[str, object], list[dict[str, object]]]:
relative = validate_relative(relative)
url = self.collection_url(relative)
body = (
'<?xml version="1.0"?>'
'<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">'
'<d:prop><d:displayname/><d:resourcetype/><d:getcontenttype/>'
'<d:getcontentlength/><d:getetag/><oc:fileid/></d:prop></d:propfind>'
).encode("utf-8")
request = urllib.request.Request(url, data=body, method="PROPFIND")
request.add_header("Authorization", self.auth_header)
request.add_header("Depth", "1")
request.add_header("Content-Type", "application/xml; charset=utf-8")
try:
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
status = response.status
payload = response.read()
except urllib.error.HTTPError as error:
if error.code in {401, 403}:
fail("Nextcloud rejected the WebDAV credentials or directory access")
fail(f"Nextcloud PROPFIND failed with HTTP {error.code}")
except urllib.error.URLError as error:
fail(f"Nextcloud WebDAV is unreachable: {error.reason}")
if status != 207:
fail(f"Nextcloud PROPFIND returned HTTP {status}")
try:
root = ET.fromstring(payload)
except ET.ParseError as error:
raise RuntimeError("Nextcloud returned invalid WebDAV XML") from error
base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/")
current: dict[str, object] | None = None
children: list[dict[str, object]] = []
for response in root.findall(f"{{{DAV}}}response"):
href = response.findtext(f"{{{DAV}}}href", default="")
href_path = urllib.parse.unquote(urllib.parse.urlparse(href).path).rstrip("/")
prop = None
for propstat in response.findall(f"{{{DAV}}}propstat"):
status_text = propstat.findtext(f"{{{DAV}}}status", default="")
if " 200 " in status_text:
prop = propstat.find(f"{{{DAV}}}prop")
if prop is not None:
break
if prop is None:
continue
display = prop.findtext(f"{{{DAV}}}displayname", default="")
fileid_text = prop.findtext(f"{{{OC}}}fileid", default="").strip()
resource_type = prop.find(f"{{{DAV}}}resourcetype")
is_dir = resource_type is not None and resource_type.find(f"{{{DAV}}}collection") is not None
item = {
"display_name": display,
"fileid": int(fileid_text) if fileid_text.isdigit() else 0,
"is_dir": is_dir,
"content_type": prop.findtext(f"{{{DAV}}}getcontenttype", default=""),
"size": int(prop.findtext(f"{{{DAV}}}getcontentlength", default="0") or 0),
"etag": prop.findtext(f"{{{DAV}}}getetag", default="").strip('"'),
}
if href_path == base_path:
current = item
continue
if display:
children.append(item)
if current is None or int(current.get("fileid", 0)) < 1:
fail(f"Nextcloud returned no stable fileid for {relative or '/'}")
return current, children
def link_placeholder(seed: Path, target: Path) -> None:
target.parent.mkdir(parents=True, exist_ok=True)
try:
os.link(seed, target)
except OSError:
target.write_bytes(PLACEHOLDER)
def build_root(
client: WebDavClient,
remote_root: str,
local_root: Path,
seed: Path,
mapping: dict[str, dict[str, object]],
) -> tuple[int, int, int]:
files = 0
folders = 0
skipped = 0
stack: list[tuple[str, Path]] = [(validate_relative(remote_root), local_root)]
visited: set[str] = set()
while stack:
remote_dir, local_dir = stack.pop()
if remote_dir in visited:
continue
visited.add(remote_dir)
current, children = client.list_collection(remote_dir)
local_dir.mkdir(parents=True, exist_ok=True)
folders += 1
mapping[str(local_dir)] = {
"kind": "folder",
"fileid": int(current["fileid"]),
"webdav_path": remote_dir,
"display_name": str(current.get("display_name", "")),
}
for child in children:
name = safe_local_name(str(child["display_name"]))
child_remote = name if remote_dir == "" else f"{remote_dir}/{name}"
child_local = local_dir / name
if bool(child["is_dir"]):
stack.append((child_remote, child_local))
continue
extension = Path(name).suffix.lower()
if extension not in SUPPORTED_IMAGE_EXTENSIONS:
skipped += 1
continue
link_placeholder(seed, child_local)
files += 1
mapping[str(child_local)] = {
"kind": "file",
"fileid": int(child.get("fileid", 0)),
"webdav_path": child_remote,
"display_name": name,
"content_type": str(child.get("content_type", "")),
"size": int(child.get("size", 0)),
"etag": str(child.get("etag", "")),
}
return files, folders, skipped
def atomic_json(path: Path, payload: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True)
handle.write("\n")
temporary = Path(handle.name)
temporary.replace(path)
def atomic_text(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
handle.write(text)
temporary = Path(handle.name)
temporary.replace(path)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True)
parser.add_argument("--user", required=True)
parser.add_argument("--password-file", type=Path)
parser.add_argument("--root", action="append", required=True, help="WebDAV path relative to the authenticated user's files root")
parser.add_argument("--source-dir", required=True, type=Path)
parser.add_argument("--manifest", required=True, type=Path)
parser.add_argument("--mapping", required=True, type=Path)
parser.add_argument("--timeout", type=int, default=30)
args = parser.parse_args()
try:
if args.password_file:
password = args.password_file.read_text(encoding="utf-8").rstrip("\r\n")
else:
password = getpass.getpass("Nextcloud password: ")
if not password:
fail("Nextcloud password is empty")
source_dir = args.source_dir.resolve()
staging = source_dir.with_name(f".{source_dir.name}.next")
previous = source_dir.with_name(f".{source_dir.name}.previous")
seed = source_dir.parent / ".bratonien-webdav-placeholder.gif"
source_dir.parent.mkdir(parents=True, exist_ok=True)
seed.write_bytes(PLACEHOLDER)
os.chmod(seed, 0o644)
if staging.exists():
shutil.rmtree(staging)
staging.mkdir(parents=True)
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout))
mapping: dict[str, dict[str, object]] = {}
manifest: list[str] = []
total_files = total_folders = total_skipped = 0
used_names: set[str] = set()
for remote_root_raw in args.root:
remote_root = validate_relative(remote_root_raw)
current, _ = client.list_collection(remote_root)
fileid = int(current["fileid"])
display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user)
local_name = f"root-{fileid}"
if local_name in used_names:
fail(f"duplicate selected Nextcloud root fileid: {fileid}")
used_names.add(local_name)
local_root = staging / local_name
files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping)
total_files += files
total_folders += folders
total_skipped += skipped
manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
if previous.exists():
shutil.rmtree(previous)
if source_dir.exists():
source_dir.rename(previous)
try:
staging.rename(source_dir)
except Exception:
if source_dir.exists():
shutil.rmtree(source_dir)
if previous.exists():
previous.rename(source_dir)
raise
if previous.exists():
shutil.rmtree(previous)
final_mapping: dict[str, dict[str, object]] = {}
staging_text = str(staging)
source_text = str(source_dir)
for path, data in mapping.items():
final_path = source_text + path[len(staging_text):] if path.startswith(staging_text) else path
final_mapping[final_path] = data
atomic_text(args.manifest, "\n".join(manifest) + "\n")
atomic_json(args.mapping, {
"version": 1,
"base_url": args.base_url.rstrip("/"),
"user": args.user,
"files": final_mapping,
})
print(json.dumps({
"roots": len(args.root),
"files": total_files,
"folders": total_folders,
"skipped": total_skipped,
"source_dir": str(source_dir),
"manifest": str(args.manifest),
"mapping": str(args.mapping),
}, ensure_ascii=False))
return 0
except Exception as error:
print(f"webdav-placeholder: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -14,19 +14,13 @@ function fail_sync($message)
function http_error_detail($body) function http_error_detail($body)
{ {
$body = trim((string)$body); $body = trim((string)$body);
if ($body === '') if ($body === '') return '';
{
return '';
}
$decoded = json_decode($body, true); $decoded = json_decode($body, true);
if (is_array($decoded)) if (is_array($decoded))
{ {
$detail = (string)($decoded['message'] ?? $decoded['err'] ?? ''); $detail = (string)($decoded['message'] ?? $decoded['err'] ?? '');
if ($detail !== '') if ($detail !== '') return $detail;
{
return $detail;
}
} }
if (function_exists('simplexml_load_string')) if (function_exists('simplexml_load_string'))
@@ -38,19 +32,12 @@ function http_error_detail($body)
if ($xml !== false) if ($xml !== false)
{ {
$detail = trim((string)($xml->message ?? $xml->err ?? '')); $detail = trim((string)($xml->message ?? $xml->err ?? ''));
if ($detail !== '') if ($detail !== '') return $detail;
{
return $detail;
}
} }
} }
$plain = trim(preg_replace('/\s+/', ' ', strip_tags($body))); $plain = trim(preg_replace('/\s+/', ' ', strip_tags($body)));
if ($plain === '') return $plain === '' ? '' : mb_substr($plain, 0, 500);
{
return '';
}
return mb_substr($plain, 0, 500);
} }
function http_request($url, array $fields, array $headers = array(), $cookie_file = null) function http_request($url, array $fields, array $headers = array(), $cookie_file = null)
@@ -65,10 +52,7 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
CURLOPT_FOLLOWLOCATION => false, CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-NC-Connector', CURLOPT_USERAGENT => 'Bratonien-NC-Connector',
); );
if ($headers) if ($headers) $options[CURLOPT_HTTPHEADER] = $headers;
{
$options[CURLOPT_HTTPHEADER] = $headers;
}
if ($cookie_file !== null) if ($cookie_file !== null)
{ {
$options[CURLOPT_COOKIEJAR] = $cookie_file; $options[CURLOPT_COOKIEJAR] = $cookie_file;
@@ -81,42 +65,29 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
if ($body === false || $errno !== 0) if ($body === false || $errno !== 0) fail_sync('HTTP-Aufruf fehlgeschlagen: '.$error);
{
fail_sync('HTTP-Aufruf fehlgeschlagen: '.$error);
}
if ($http < 200 || $http >= 300) if ($http < 200 || $http >= 300)
{ {
$detail = http_error_detail((string)$body); $detail = http_error_detail((string)$body);
fail_sync('HTTP-Aufruf antwortete mit Status '.$http.($detail !== '' ? ': '.$detail : '.')); fail_sync('HTTP-Aufruf antwortete mit Status '.$http.($detail !== '' ? ': '.$detail : '.'));
} }
return (string)$body; return (string)$body;
} }
function xml_value_sync(SimpleXMLElement $node) function xml_value_sync(SimpleXMLElement $node)
{ {
$children = $node->children(); $children = $node->children();
if (count($children) === 0) if (count($children) === 0) return (string)$node;
{
return (string)$node;
}
$result = array(); $result = array();
foreach ($children as $name => $child) foreach ($children as $name => $child)
{ {
$value = xml_value_sync($child); $value = xml_value_sync($child);
if (array_key_exists($name, $result)) if (array_key_exists($name, $result))
{ {
if (!is_array($result[$name]) || !array_is_list($result[$name])) if (!is_array($result[$name]) || !array_is_list($result[$name])) $result[$name] = array($result[$name]);
{
$result[$name] = array($result[$name]);
}
$result[$name][] = $value; $result[$name][] = $value;
} }
else else $result[$name] = $value;
{
$result[$name] = $value;
}
} }
return $result; return $result;
} }
@@ -126,34 +97,22 @@ function decode_ws($body)
$decoded = json_decode((string)$body, true); $decoded = json_decode((string)$body, true);
if (!is_array($decoded)) if (!is_array($decoded))
{ {
if (!function_exists('simplexml_load_string')) if (!function_exists('simplexml_load_string')) fail_sync('Piwigo lieferte XML, aber SimpleXML ist nicht verfuegbar.');
{
fail_sync('Piwigo lieferte XML, aber SimpleXML ist nicht verfuegbar.');
}
$previous = libxml_use_internal_errors(true); $previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$body); $xml = simplexml_load_string((string)$body);
libxml_clear_errors(); libxml_clear_errors();
libxml_use_internal_errors($previous); libxml_use_internal_errors($previous);
if ($xml === false || $xml->getName() !== 'rsp') if ($xml === false || $xml->getName() !== 'rsp') fail_sync('Piwigo lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.');
{
fail_sync('Piwigo lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.');
}
$decoded = array('stat'=>(string)$xml['stat']); $decoded = array('stat'=>(string)$xml['stat']);
foreach ($xml->children() as $name => $child) foreach ($xml->children() as $name => $child)
{ {
$value = xml_value_sync($child); $value = xml_value_sync($child);
if (array_key_exists($name, $decoded)) if (array_key_exists($name, $decoded))
{ {
if (!is_array($decoded[$name]) || !array_is_list($decoded[$name])) if (!is_array($decoded[$name]) || !array_is_list($decoded[$name])) $decoded[$name] = array($decoded[$name]);
{
$decoded[$name] = array($decoded[$name]);
}
$decoded[$name][] = $value; $decoded[$name][] = $value;
} }
else else $decoded[$name] = $value;
{
$decoded[$name] = $value;
}
} }
} }
if (($decoded['stat'] ?? '') !== 'ok') if (($decoded['stat'] ?? '') !== 'ok')
@@ -161,87 +120,106 @@ function decode_ws($body)
$message = (string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.'); $message = (string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.');
fail_sync($message); fail_sync($message);
} }
if (array_key_exists('result', $decoded)) return $decoded['result'];
if (array_key_exists('result', $decoded))
{
return $decoded['result'];
}
unset($decoded['stat']); unset($decoded['stat']);
return $decoded; return $decoded;
} }
function decrypt_blob($blob, $hex_key) function decrypt_blob($blob, $hex_key)
{ {
if (!preg_match('/^[a-f0-9]{64}$/', (string)$hex_key)) if (!preg_match('/^[a-f0-9]{64}$/', (string)$hex_key)) fail_sync('Connector-Schluessel ist ungueltig.');
{
fail_sync('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_sync('Gespeicherte Zugangsdaten haben ein unbekanntes Format.');
{
fail_sync('Gespeicherte 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($hex_key), OPENSSL_RAW_DATA, $iv, $tag); $plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) if ($plain === false) fail_sync('Gespeicherte Zugangsdaten konnten nicht entschluesselt werden.');
{
fail_sync('Gespeicherte Zugangsdaten konnten nicht entschluesselt werden.');
}
return (string)$plain; return (string)$plain;
} }
function ensure_webdav_site(mysqli $db, $prefixeTable, $piwigo_root, array $connection_config, $connection_id)
{
if ((string)($connection_config['source_mode'] ?? '') !== 'webdav-placeholder') return 1;
$gallery_root = rtrim((string)($connection_config['parallel_gallery_root'] ?? ''), '/');
if ($gallery_root === '') fail_sync('WebDAV-Galeriewurzel fehlt in der Verbindungskonfiguration.');
$piwigo_root = rtrim((string)$piwigo_root, '/');
if (strpos($gallery_root, $piwigo_root.'/') !== 0)
{
fail_sync('WebDAV-Galeriewurzel liegt ausserhalb der Piwigo-Installation.');
}
if (!is_dir($gallery_root)) fail_sync('WebDAV-Galeriewurzel existiert nicht: '.$gallery_root);
$relative = ltrim(substr($gallery_root, strlen($piwigo_root)), '/');
if ($relative === '') fail_sync('WebDAV-Galeriewurzel darf nicht dem Piwigo-Hauptverzeichnis entsprechen.');
$site_url = './'.rtrim($relative, '/').'/';
$escaped = $db->real_escape_string($site_url);
$result = $db->query("SELECT id FROM `{$prefixeTable}sites` WHERE galleries_url='{$escaped}' LIMIT 1");
if (!$result) fail_sync('Piwigo-Site konnte nicht gelesen werden: '.$db->error);
if ($result->num_rows)
{
return (int)$result->fetch_assoc()['id'];
}
if (!$db->query("INSERT INTO `{$prefixeTable}sites` (galleries_url) VALUES ('{$escaped}')"))
{
fail_sync('Piwigo-Site fuer WebDAV-Verbindung #'.$connection_id.' konnte nicht angelegt werden: '.$db->error);
}
$site_id = (int)$db->insert_id;
if ($site_id < 1) fail_sync('Piwigo-Site fuer WebDAV-Verbindung #'.$connection_id.' erhielt keine gueltige ID.');
return $site_id;
}
try try
{ {
$options = getopt('', array('piwigo-root:', 'connection-id:', 'base-url:')); $options = getopt('', array('piwigo-root:', 'connection-id:', 'base-url:'));
$piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/'); $piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/');
$connection_id = (int)($options['connection-id'] ?? 0); $connection_id = (int)($options['connection-id'] ?? 0);
$base_url = rtrim((string)($options['base-url'] ?? 'http://127.0.0.1'), '/'); $base_url = rtrim((string)($options['base-url'] ?? 'http://127.0.0.1'), '/');
if ($piwigo_root === '' || $connection_id < 1) fail_sync('Parameter --piwigo-root und --connection-id werden benoetigt.');
if ($piwigo_root === '' || $connection_id < 1) if (!function_exists('curl_init')) fail_sync('PHP-cURL ist nicht verfuegbar.');
{
fail_sync('Parameter --piwigo-root und --connection-id werden benoetigt.');
}
if (!function_exists('curl_init'))
{
fail_sync('PHP-cURL ist nicht verfuegbar.');
}
$db_config = $piwigo_root.'/local/config/database.inc.php'; $db_config = $piwigo_root.'/local/config/database.inc.php';
if (!is_readable($db_config)) if (!is_readable($db_config)) fail_sync('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
{
fail_sync('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
}
$conf = array(); $conf = array();
$prefixeTable = 'piwigo_'; $prefixeTable = 'piwigo_';
require $db_config; require $db_config;
foreach (array('db_host','db_user','db_password','db_base') as $key) foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail_sync('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
{
if (!isset($conf[$key]))
{
fail_sync('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
}
}
$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_sync('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
{
fail_sync('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
}
$db->set_charset('utf8mb4'); $db->set_charset('utf8mb4');
$key_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1"); $key_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$key_result || !$key_result->num_rows) if (!$key_result || !$key_result->num_rows) fail_sync('Connector-Schluessel wurde nicht gefunden.');
{
fail_sync('Connector-Schluessel wurde nicht gefunden.');
}
$hex_key = (string)$key_result->fetch_assoc()['value']; $hex_key = (string)$key_result->fetch_assoc()['value'];
$connection_table = $prefixeTable.'bratonien_tools_nc_connections';
$connection_result = $db->query('SELECT config_json, secret_blob FROM `'.$connection_table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!$connection_result || !$connection_result->num_rows) fail_sync('Connector-Verbindung #'.$connection_id.' wurde nicht gefunden.');
$connection_row = $connection_result->fetch_assoc();
$connection_config = json_decode((string)$connection_row['config_json'], true);
if (!is_array($connection_config)) $connection_config = array();
$connection_plain = decrypt_blob((string)$connection_row['secret_blob'], $hex_key);
$connection_credentials = json_decode($connection_plain, true);
if (!is_array($connection_credentials)) $connection_credentials = array();
$site_id = ensure_webdav_site($db, $prefixeTable, $piwigo_root, $connection_config, $connection_id);
$api = array('key_id'=>'', 'key_secret'=>''); $api = array('key_id'=>'', 'key_secret'=>'');
$connection_scoped = (string)($connection_config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $connection_config);
if ($connection_scoped)
{
if (!empty($connection_config['api_enabled']))
{
$api['key_id'] = trim((string)($connection_credentials['api_key_id'] ?? ''));
$api['key_secret'] = trim((string)($connection_credentials['api_key_secret'] ?? ''));
}
}
else
{
$api_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1"); $api_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1");
if ($api_result && $api_result->num_rows) if ($api_result && $api_result->num_rows)
{ {
@@ -257,20 +235,8 @@ try
} }
} }
} }
}
$connection_table = $prefixeTable.'bratonien_tools_nc_connections';
$connection_result = $db->query('SELECT secret_blob FROM `'.$connection_table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!$connection_result || !$connection_result->num_rows)
{
fail_sync('Connector-Verbindung #'.$connection_id.' wurde nicht gefunden.');
}
$connection_blob = (string)$connection_result->fetch_assoc()['secret_blob'];
$connection_plain = decrypt_blob($connection_blob, $hex_key);
$connection_credentials = json_decode($connection_plain, true);
if (!is_array($connection_credentials))
{
$connection_credentials = array();
}
$fallback_user = (string)($connection_credentials['piwigo_user'] ?? ''); $fallback_user = (string)($connection_credentials['piwigo_user'] ?? '');
$fallback_password = (string)($connection_credentials['piwigo_password'] ?? ''); $fallback_password = (string)($connection_credentials['piwigo_password'] ?? '');
@@ -284,19 +250,17 @@ try
'Accept: application/json, text/xml;q=0.9', 'Accept: application/json, text/xml;q=0.9',
'Content-Type: application/x-www-form-urlencoded', 'Content-Type: application/x-www-form-urlencoded',
); );
decode_ws(http_request( decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id), $headers));
$base_url.'/ws.php?format=json', $orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), $headers));
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>1), // Entfernt alte technische bratonien-webdav-N Wrapper aus Site 1,
$headers // nachdem deren generierte Verzeichnisse beim Reconcile entfernt wurden.
)); if ($site_id !== 1)
$orphan = decode_ws(http_request( {
$base_url.'/ws.php?format=json', decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), $headers));
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), }
$headers
));
$added = (int)($orphan['added_orphans'] ?? 0); $added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Synchronisierung per API erfolgreich\n"; echo "Piwigo-Synchronisierung per API erfolgreich (Site $site_id)\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n"; echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
exit(0); exit(0);
} }
@@ -308,56 +272,32 @@ try
} }
else else
{ {
$api_error = 'Keine gespeicherten API-Zugangsdaten.'; $api_error = $connection_scoped ? 'Fuer diese Verbindung ist keine API konfiguriert.' : 'Keine gespeicherten API-Zugangsdaten.';
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n"); fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
} }
if ($fallback_user === '' || $fallback_password === '') if ($fallback_user === '' || $fallback_password === '') fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback fuer diese Verbindung vorhanden.');
{
fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback vorhanden.');
}
$cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-sync-'); $cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-sync-');
if ($cookie_file === false) if ($cookie_file === false) fail_sync('Temporare Piwigo-Sitzung konnte nicht angelegt werden.');
{
fail_sync('Temporare Piwigo-Sitzung konnte nicht angelegt werden.');
}
try try
{ {
decode_ws(http_request( decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password), array(), $cookie_file));
$base_url.'/ws.php?format=json',
array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password),
array(),
$cookie_file
));
http_request( http_request(
$base_url.'/admin.php?page=site_update&site=1', $base_url.'/admin.php?page=site_update&site='.$site_id,
array( array('sync'=>'files','display_info'=>1,'privacy_level'=>0,'sync_meta'=>1,'simulate'=>0,'subcats-included'=>1,'bratonien_connector'=>1,'submit'=>1),
'sync'=>'files',
'display_info'=>1,
'privacy_level'=>0,
'sync_meta'=>1,
'simulate'=>0,
'subcats-included'=>1,
'bratonien_connector'=>1,
'submit'=>1,
),
array(), array(),
$cookie_file $cookie_file
); );
$orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), array(), $cookie_file));
$orphan = decode_ws(http_request( if ($site_id !== 1)
$base_url.'/ws.php?format=json', {
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), array(), $cookie_file));
array(), }
$cookie_file
));
$added = (int)($orphan['added_orphans'] ?? 0); $added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich (Site $site_id)\n";
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n"; echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
} }
finally finally

View File

@@ -0,0 +1,290 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
const BRATONIEN_WEBDAV_PREVIEW_VERSION = 2;
const BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE = 4096;
const BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY = 88;
function fail_preview($message)
{
throw new RuntimeException($message);
}
function quote_webdav_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
function fetch_remote_blob($url, $user, $password)
{
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.6.1',
));
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $errno !== 0) fail_preview('WebDAV-Bild konnte nicht geladen werden: '.$error);
if ($http < 200 || $http >= 300) fail_preview('WebDAV-Bild antwortete mit HTTP '.$http.'.');
if ($body === '') fail_preview('WebDAV-Bild ist leer.');
return (string)$body;
}
function preview_extension_for_entry(array $entry)
{
$content_type = strtolower(trim((string)($entry['content_type'] ?? '')));
$path = strtolower((string)($entry['webdav_path'] ?? ''));
if ($content_type === 'image/png' || preg_match('/\.png$/', $path))
{
return 'png';
}
return 'jpg';
}
function preview_target_for_entry($cache_dir, $key, array $entry)
{
return $cache_dir.'/'.$key.'.'.preview_extension_for_entry($entry);
}
function remove_other_preview_format($cache_dir, $key, $keep_target)
{
foreach (array('jpg', 'png', 'webp') as $ext)
{
$candidate = $cache_dir.'/'.$key.'.'.$ext;
if ($candidate !== $keep_target && is_file($candidate)) @unlink($candidate);
}
}
function write_preview_imagick($blob, $target, $extension)
{
$image = new Imagick();
try
{
$image->readImageBlob($blob);
if (method_exists($image, 'autoOrientImage')) @$image->autoOrientImage();
$image->setIteratorIndex(0);
$image->thumbnailImage(BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE, true, true);
$image->stripImage();
if ($extension === 'png')
{
$image->setImageFormat('png');
}
else
{
$image->setImageBackgroundColor('white');
if (method_exists($image, 'mergeImageLayers'))
{
$image = $image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
}
$image->setImageFormat('jpeg');
$image->setImageCompression(Imagick::COMPRESSION_JPEG);
$image->setImageCompressionQuality(BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY);
$image->setInterlaceScheme(Imagick::INTERLACE_PLANE);
}
if (!$image->writeImage($target)) fail_preview('Imagick konnte das Preview nicht schreiben.');
}
finally
{
$image->clear();
$image->destroy();
}
}
function write_preview_gd($blob, $target, $extension)
{
$source = @imagecreatefromstring($blob);
if (!$source) fail_preview('GD konnte das Bild nicht dekodieren.');
try
{
$width = imagesx($source);
$height = imagesy($source);
if ($width < 1 || $height < 1) fail_preview('Bildabmessungen sind ungültig.');
$scale = min(1, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE / $width, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE / $height);
$target_width = max(1, (int)round($width * $scale));
$target_height = max(1, (int)round($height * $scale));
$preview = imagecreatetruecolor($target_width, $target_height);
if (!$preview) fail_preview('GD konnte die Preview-Arbeitsfläche nicht anlegen.');
try
{
if ($extension === 'png')
{
imagealphablending($preview, false);
imagesavealpha($preview, true);
$transparent = imagecolorallocatealpha($preview, 0, 0, 0, 127);
imagefilledrectangle($preview, 0, 0, $target_width, $target_height, $transparent);
}
else
{
$white = imagecolorallocate($preview, 255, 255, 255);
imagefilledrectangle($preview, 0, 0, $target_width, $target_height, $white);
}
if (!imagecopyresampled($preview, $source, 0, 0, 0, 0, $target_width, $target_height, $width, $height))
{
fail_preview('GD konnte das Preview nicht skalieren.');
}
$written = $extension === 'png'
? imagepng($preview, $target, 6)
: imagejpeg($preview, $target, BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY);
if (!$written) fail_preview('GD konnte das Preview nicht schreiben.');
}
finally
{
imagedestroy($preview);
}
}
finally
{
imagedestroy($source);
}
}
function write_preview($blob, $target, $extension)
{
$dir = dirname($target);
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) fail_preview('Preview-Verzeichnis konnte nicht angelegt werden.');
if (class_exists('Imagick'))
{
write_preview_imagick($blob, $target, $extension);
}
elseif (function_exists('imagecreatefromstring') && function_exists('imagejpeg') && function_exists('imagepng'))
{
write_preview_gd($blob, $target, $extension);
}
else
{
fail_preview('Weder Imagick noch GD ist für die Preview-Erzeugung verfügbar.');
}
clearstatcache(true, $target);
$size = @getimagesize($target);
if (!is_array($size) || empty($size[0]) || empty($size[1]) || !is_file($target) || filesize($target) < 64)
{
@unlink($target);
fail_preview('Das erzeugte Preview ist ungültig.');
}
@chmod($target, 0644);
}
try
{
$options = getopt('', array('mapping:', 'base-url:', 'user:', 'password-file:', 'cache-dir:'));
$mapping_file = (string)($options['mapping'] ?? '');
$base_url = rtrim((string)($options['base-url'] ?? ''), '/');
$user = trim((string)($options['user'] ?? ''));
$password_file = (string)($options['password-file'] ?? '');
$cache_dir = rtrim((string)($options['cache-dir'] ?? ''), '/');
if ($mapping_file === '' || !is_readable($mapping_file)) fail_preview('WebDAV-Mapping ist nicht lesbar.');
if ($base_url === '' || $user === '' || $password_file === '' || !is_readable($password_file) || $cache_dir === '') fail_preview('Preview-Parameter sind unvollständig.');
if (!function_exists('curl_init')) fail_preview('PHP-cURL ist nicht verfügbar.');
$password = rtrim((string)file_get_contents($password_file), "\r\n");
if ($password === '') fail_preview('Nextcloud-Passwort fehlt.');
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (!is_array($mapping) || !isset($mapping['files']) || !is_array($mapping['files'])) fail_preview('WebDAV-Mapping ist ungültig.');
if (!is_dir($cache_dir) && !mkdir($cache_dir, 0755, true) && !is_dir($cache_dir)) fail_preview('Preview-Cache konnte nicht angelegt werden.');
@chmod($cache_dir, 0755);
$state_file = $cache_dir.'/state.json';
$old_state = array();
if (is_readable($state_file))
{
$decoded = json_decode((string)file_get_contents($state_file), true);
if (is_array($decoded)) $old_state = $decoded;
}
$new_state = array();
$generated = 0;
$cached = 0;
$errors = 0;
foreach ($mapping['files'] as $entry)
{
if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue;
$webdav_path = trim((string)($entry['webdav_path'] ?? ''), '/');
if ($webdav_path === '') continue;
$etag = (string)($entry['etag'] ?? '');
$key = sha1($webdav_path);
$target = preview_target_for_entry($cache_dir, $key, $entry);
$extension = pathinfo($target, PATHINFO_EXTENSION);
$new_state[$key] = array(
'path' => $webdav_path,
'etag' => $etag,
'format' => $extension,
'version' => BRATONIEN_WEBDAV_PREVIEW_VERSION,
);
$old = $old_state[$key] ?? null;
if (
is_file($target)
&& is_array($old)
&& (string)($old['etag'] ?? '') === $etag
&& (string)($old['format'] ?? '') === $extension
&& (int)($old['version'] ?? 0) === BRATONIEN_WEBDAV_PREVIEW_VERSION
)
{
$cached++;
continue;
}
try
{
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.quote_webdav_path($webdav_path);
$blob = fetch_remote_blob($url, $user, $password);
write_preview($blob, $target, $extension);
remove_other_preview_format($cache_dir, $key, $target);
$generated++;
}
catch (Throwable $e)
{
$errors++;
fwrite(STDERR, $webdav_path.': '.$e->getMessage()."\n");
}
}
foreach (glob($cache_dir.'/*') ?: array() as $file)
{
if (!is_file($file) || basename($file) === 'state.json') continue;
$name = basename($file);
if (!preg_match('/^([a-f0-9]{40})\.(jpg|png|webp)$/', $name, $m)) continue;
if (!isset($new_state[$m[1]])) @unlink($file);
}
file_put_contents($state_file, json_encode($new_state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)."\n", LOCK_EX);
@chmod($state_file, 0644);
echo 'WebDAV-Previews: erzeugt='.$generated.' vorhanden='.$cached.' fehler='.$errors."\n";
exit($errors > 0 ? 1 : 0);
}
catch (Throwable $e)
{
fwrite(STDERR, 'WebDAV-Preview-Cache: '.$e->getMessage()."\n");
exit(1);
}

View File

@@ -0,0 +1,255 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
function fail_webdav_reconcile($message)
{
throw new RuntimeException($message);
}
function decrypt_webdav_credentials($blob, $hexKey)
{
if (!preg_match('/^[a-f0-9]{64}$/', (string)$hexKey)) fail_webdav_reconcile('Connector-Schluessel ist ungueltig.');
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) fail_webdav_reconcile('Connector-Zugangsdaten haben ein unbekanntes Format.');
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false || $plain === '') fail_webdav_reconcile('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
$decoded = json_decode($plain, true);
if (!is_array($decoded)) fail_webdav_reconcile('WebDAV-Verbindung besitzt kein kompatibles Secret-Format.');
return array(
'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''),
'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''),
);
}
function webdav_shell_value($value)
{
return escapeshellarg((string)$value);
}
function webdav_remove_generated_tree($path, $allowedRoot)
{
$path = rtrim((string)$path, '/');
$allowedRoot = rtrim((string)$allowedRoot, '/');
if ($path === '' || $allowedRoot === '' || strpos($path, $allowedRoot.'/') !== 0 || !file_exists($path)) return;
if (is_link($path) || is_file($path))
{
@unlink($path);
return;
}
$items = scandir($path);
if (is_array($items))
{
foreach ($items as $item)
{
if ($item === '.' || $item === '..') continue;
webdav_remove_generated_tree($path.'/'.$item, $allowedRoot);
}
}
@rmdir($path);
}
function webdav_source_fingerprint($baseUrl, $user, array $roots)
{
$normalized = array();
foreach ($roots as $root)
{
$normalized[] = array(
'fileid'=>(int)($root['fileid'] ?? 0),
'path'=>trim((string)($root['webdav_path'] ?? ''), '/'),
);
}
usort($normalized, function($a, $b)
{
$cmp = $a['fileid'] <=> $b['fileid'];
return $cmp !== 0 ? $cmp : strcmp($a['path'], $b['path']);
});
return hash('sha256', strtolower(rtrim((string)$baseUrl, '/'))."\n".strtolower(trim((string)$user))."\n".json_encode($normalized));
}
$pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$configDir = '/etc/bratonien-tools/nc-connector';
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
$publicSourceRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-source';
$publicGalleryRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-gallery';
$legacyGalleryRoot = rtrim($piwigoRoot, '/').'/galleries';
try
{
if (!is_readable($dbConfig)) fail_webdav_reconcile('Piwigo-Datenbankkonfiguration ist nicht lesbar: '.$dbConfig);
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key])) fail_webdav_reconcile('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) fail_webdav_reconcile('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
$db->set_charset('utf8mb4');
$keyResult = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$keyResult || !$keyResult->num_rows) fail_webdav_reconcile('Connector-Schluessel wurde nicht gefunden.');
$hexKey = trim((string)$keyResult->fetch_assoc()['value']);
$table = $prefixeTable.'bratonien_tools_nc_connections';
$rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC");
if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
foreach (array($configDir=>$configDir, $publicSourceRoot=>$publicSourceRoot, $publicGalleryRoot=>$publicGalleryRoot) as $dir=>$unused)
{
if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0755, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir);
}
@chmod($configDir, 0700);
@chmod($publicSourceRoot, 0755);
@chmod($publicGalleryRoot, 0755);
$known = array();
$seenFingerprints = array();
while ($row = $rows->fetch_assoc())
{
$id = (int)$row['id'];
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) $config = array();
if ((string)$row['adapter'] !== 'remote') continue;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
if (empty($config['parallel_test'])) continue;
try
{
$baseUrl = rtrim(trim((string)($config['nextcloud_url'] ?? '')), '/');
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
if ($baseUrl === '') fail_webdav_reconcile('Nextcloud-Adresse fehlt.');
if (!$roots) fail_webdav_reconcile('Keine WebDAV-Wurzeln gespeichert.');
$credentials = decrypt_webdav_credentials((string)$row['secret_blob'], $hexKey);
$user = trim($credentials['nextcloud_user']);
$password = $credentials['nextcloud_password'];
if ($user === '' || $password === '') fail_webdav_reconcile('Nextcloud-Zugangsdaten fehlen.');
$fingerprint = webdav_source_fingerprint($baseUrl, $user, $roots);
if (isset($seenFingerprints[$fingerprint]))
{
fwrite(STDERR, "NC WebDAV #{$id}: identische Quelle bereits durch Verbindung #".$seenFingerprints[$fingerprint]." abgedeckt; doppelte Runtime wird unterdrueckt.\n");
foreach (glob($configDir.'/webdav-connection-'.$id.'.*') ?: array() as $stale) @unlink($stale);
webdav_remove_generated_tree($legacyGalleryRoot.'/bratonien-webdav-'.$id, $legacyGalleryRoot);
webdav_remove_generated_tree($publicGalleryRoot.'/connection-'.$id, $publicGalleryRoot);
continue;
}
$seenFingerprints[$fingerprint] = $id;
$known[$id] = true;
$stateDir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id;
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.');
@chmod($stateDir, 0750);
$legacyDefault = $legacyGalleryRoot.'/bratonien-webdav-'.$id;
$galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
if ($galleryRoot === '' || $galleryRoot === $legacyDefault || strpos($galleryRoot, $legacyGalleryRoot.'/bratonien-webdav-') === 0)
{
$galleryRoot = $publicGalleryRoot.'/connection-'.$id;
}
if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.');
@chmod($galleryRoot, 0755);
// Alte technische Wrapper unter ./galleries duerfen nicht als Piwigo-Alben auftauchen.
webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot);
$sourceDir = $publicSourceRoot.'/connection-'.$id;
if (!is_dir($sourceDir) && !mkdir($sourceDir, 0755, true)) fail_webdav_reconcile('WebDAV-Platzhalterquelle konnte nicht angelegt werden.');
@chmod($sourceDir, 0755);
$base = $configDir.'/webdav-connection-'.$id;
$passwordPath = $base.'.nextcloud-password';
$rootsPath = $base.'.roots.tsv';
$configPath = $base.'.conf';
$statusFile = $stateDir.'/connector-status.json';
$mappingFile = $stateDir.'/webdav-map.json';
$manifestFile = $stateDir.'/webdav-manifest.tsv';
$shadowMapFile = $stateDir.'/webdav-shadow-map.json';
file_put_contents($passwordPath, $password."\n", LOCK_EX);
@chmod($passwordPath, 0600);
$rootLines = array('# webdav_path<TAB>display_name');
foreach ($roots as $root)
{
$path = trim((string)($root['webdav_path'] ?? ''), '/');
$display = trim((string)($root['display_name'] ?? ''));
if ($path === '' || $display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
$rootLines[] = $path."\t".$display;
}
file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);
@chmod($rootsPath, 0600);
$lines = array(
'PIWIGO_ROOT='.webdav_shell_value($piwigoRoot),
'CONNECTION_ID='.$id,
'SOURCE_MODE=webdav-placeholder',
'WEBDAV_BASE_URL='.webdav_shell_value($baseUrl),
'WEBDAV_USER='.webdav_shell_value($user),
'WEBDAV_PASSWORD_FILE='.webdav_shell_value($passwordPath),
'WEBDAV_ROOTS_FILE='.webdav_shell_value($rootsPath),
'WEBDAV_SOURCE_DIR='.webdav_shell_value($sourceDir),
'WEBDAV_MAPPING_FILE='.webdav_shell_value($mappingFile),
'MANIFEST='.webdav_shell_value($manifestFile),
'SHADOW_MAP_FILE='.webdav_shell_value($shadowMapFile),
'GALLERY_ROOT='.webdav_shell_value($galleryRoot),
'STATE_DIR='.webdav_shell_value($stateDir),
'STATUS_FILE='.webdav_shell_value($statusFile),
'PIWIGO_SYNC_ENABLED=1',
);
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
@chmod($configPath, 0600);
$config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile;
$config['parallel_gallery_root'] = $galleryRoot;
$config['source_fingerprint'] = $fingerprint;
$config['runtime'] = array(
'mode'=>'parallel-webdav',
'config'=>$configPath,
'piwigo_sync_enabled'=>true,
'reconciled_at'=>date('Y-m-d H:i:s'),
);
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (is_string($json))
{
$escaped = $db->real_escape_string($json);
$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} LIMIT 1");
}
}
catch (Throwable $e)
{
fwrite(STDERR, "NC WebDAV #{$id}: ".$e->getMessage()."\n");
}
}
foreach (glob($configDir.'/webdav-connection-*.conf') ?: array() as $path)
{
if (!preg_match('/webdav-connection-([0-9]+)\.conf$/', $path, $match)) continue;
$id = (int)$match[1];
if (isset($known[$id])) continue;
foreach (glob($configDir.'/webdav-connection-'.$id.'.*') ?: array() as $stale) @unlink($stale);
}
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, "NC WebDAV Reconcile: ".$e->getMessage()."\n");
exit(1);
}

336
runtime/reconcile.php Normal file
View File

@@ -0,0 +1,336 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
function fail_reconcile($message)
{
throw new RuntimeException($message);
}
function decrypt_reconcile($blob, $hexKey)
{
if (!preg_match('/^[a-f0-9]{64}$/', (string)$hexKey)) fail_reconcile('Connector-Schluessel ist ungueltig.');
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) fail_reconcile('Connector-Zugangsdaten haben ein unbekanntes Format.');
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false || $plain === '') fail_reconcile('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
$decoded = json_decode($plain, true);
if (!is_array($decoded))
{
return array('db_password'=>$plain,'piwigo_user'=>'','piwigo_password'=>'','api_key_id'=>'','api_key_secret'=>'');
}
return array(
'db_password'=>(string)($decoded['db_password'] ?? ''),
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
'api_key_id'=>(string)($decoded['api_key_id'] ?? ''),
'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''),
);
}
function encrypt_reconcile(array $credentials, $hexKey)
{
$plain = json_encode(array(
'v'=>2,
'db_password'=>(string)$credentials['db_password'],
'piwigo_user'=>(string)$credentials['piwigo_user'],
'piwigo_password'=>(string)$credentials['piwigo_password'],
'api_key_id'=>(string)$credentials['api_key_id'],
'api_key_secret'=>(string)$credentials['api_key_secret'],
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($plain)) fail_reconcile('Connector-Zugangsdaten konnten nicht serialisiert werden.');
$iv = random_bytes(12);
$tag = '';
$cipher = openssl_encrypt($plain, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
if ($cipher === false) fail_reconcile('Connector-Zugangsdaten konnten nicht verschluesselt werden.');
return base64_encode(json_encode(array(
'v'=>1,
'iv'=>base64_encode($iv),
'tag'=>base64_encode($tag),
'data'=>base64_encode($cipher),
)));
}
function write_runtime_status($piwigoRoot, $id, $stateDir, $message)
{
$payload = json_encode(array(
'state'=>'error',
'message'=>'Runtime-Vorbereitung fehlgeschlagen',
'timestamp'=>time(),
'auth_mode'=>'failed',
'api'=>array('state'=>'not_run','message'=>''),
'fallback'=>array('state'=>'not_run','message'=>''),
'error_detail'=>(string)$message,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($payload)) return;
$targets = array(rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$id.'.json');
if ($stateDir !== '') $targets[] = rtrim($stateDir, '/').'/connector-status.json';
foreach ($targets as $target)
{
$dir = dirname($target);
if (!is_dir($dir)) @mkdir($dir, 0755, true);
@file_put_contents($target, $payload."\n", LOCK_EX);
@chmod($target, 0644);
}
}
function sql_reconcile(mysqli $db, $value)
{
return $db->real_escape_string((string)$value);
}
function nextcloud_view_available(array $config, $password, $view)
{
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', (string)$view)) return false;
$spec = array(0=>array('file','/dev/null','r'),1=>array('pipe','w'),2=>array('pipe','w'));
$command = array(
'psql','-XAt','-v','ON_ERROR_STOP=1',
'-h',(string)$config['host'],'-p',(string)$config['port'],'-U',(string)$config['user'],'-d',(string)$config['database'],
'-c','SELECT 1 FROM '.$view.' LIMIT 1'
);
$env = $_ENV;
$env['PGPASSWORD'] = (string)$password;
$process = @proc_open($command, $spec, $pipes, null, $env);
if (!is_resource($process)) return false;
stream_get_contents($pipes[1]);
stream_get_contents($pipes[2]);
fclose($pipes[1]); fclose($pipes[2]);
return proc_close($process) === 0;
}
function configured_access_user(array $config)
{
foreach (array('access_user','nextcloud_access_user','showcase_user') as $key)
{
$value = trim((string)($config[$key] ?? ''));
if ($value !== '') return $value;
}
return '';
}
$pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$configDir = '/etc/bratonien-tools/nc-connector';
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
try
{
if (!is_readable($dbConfig)) fail_reconcile('Piwigo-Datenbankkonfiguration ist nicht lesbar: '.$dbConfig);
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key])) fail_reconcile('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) fail_reconcile('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
$db->set_charset('utf8mb4');
$keyResult = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$keyResult || !$keyResult->num_rows) fail_reconcile('Connector-Schluessel wurde nicht gefunden.');
$hexKey = trim((string)$keyResult->fetch_assoc()['value']);
$table = $prefixeTable.'bratonien_tools_nc_connections';
$rows = $db->query("SELECT id,name,adapter,enabled,takeover_state,config_json,secret_blob FROM `{$table}` ORDER BY id");
if (!$rows) fail_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
if (!is_dir($configDir) && !mkdir($configDir, 0700, true)) fail_reconcile('Runtime-Konfigurationsverzeichnis konnte nicht angelegt werden.');
chmod($configDir, 0700);
while ($row = $rows->fetch_assoc())
{
$id = (int)$row['id'];
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) $config = array();
$stateDir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id;
try
{
if ((string)$row['adapter'] !== 'local') continue;
$isActive = (int)$row['enabled'] === 1 && (string)$row['takeover_state'] === 'active';
$accessUser = configured_access_user($config);
$wizardConnection = (string)($config['origin'] ?? '') === 'native'
&& trim((string)($config['nextcloud_url'] ?? '')) !== ''
&& $accessUser !== '';
$verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : null;
$verificationFailed = is_array($verification) && empty($verification['ok']);
if (!$isActive && (!$wizardConnection || $verificationFailed)) continue;
foreach (array('host','port','database','user','source_view','activity_view','gallery_root') as $key)
{
if (trim((string)($config[$key] ?? '')) === '') fail_reconcile('Konfiguration unvollstaendig: '.$key.' fehlt.');
}
$storages = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array();
if (!$storages) fail_reconcile('Keine Storage-Zuordnungen gespeichert.');
$credentials = decrypt_reconcile((string)$row['secret_blob'], $hexKey);
if ($credentials['db_password'] === '') fail_reconcile('Datenbankpasswort fehlt.');
$sourceMode = trim((string)($config['source_mode'] ?? ''));
if ($sourceMode === 'user-filesystem')
{
@unlink($configDir.'/connection-'.$id.'.conf');
fail_reconcile('Diese Verbindung verwendet den verworfenen experimentellen Benutzerpfad-Modus. Bitte die Verbindung mit dem aktuellen Assistenten neu anlegen.');
}
if ($sourceMode === '' || $sourceMode === 'legacy-view')
{
$canMigrate = $accessUser !== ''
&& nextcloud_view_available($config, $credentials['db_password'], 'piwigo_connector_shares')
&& nextcloud_view_available($config, $credentials['db_password'], 'piwigo_connector_activity');
if ($canMigrate)
{
$sourceMode = 'user-shares';
$config['source_mode'] = $sourceMode;
$config['source_view'] = 'piwigo_connector_shares';
$config['activity_view'] = 'piwigo_connector_activity';
$config['access_user'] = $accessUser;
$config['nextcloud_access_user'] = $accessUser;
unset($config['showcase_user']);
echo "NC Connector: Verbindung #{$id} auf generische benutzergefilterte Quellen migriert.\n";
}
else
{
$sourceMode = 'legacy-view';
$config['source_mode'] = $sourceMode;
}
}
if (!in_array($sourceMode, array('legacy-view','user-shares','selected-fileids'), true)) fail_reconcile('Unbekannter Quellenmodus: '.$sourceMode);
if ($sourceMode !== 'legacy-view' && $accessUser === '') fail_reconcile('Für die verbindungsbezogene Quelle fehlt der Nextcloud-Benutzer.');
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
if ($sourceMode === 'selected-fileids' && !$roots) fail_reconcile('Für die Verbindung sind keine ausgewählten Nextcloud-Datei-IDs gespeichert.');
if (!$isActive && !array_key_exists('api_enabled', $config))
{
$hasFallback = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== '';
if (!$hasFallback) fail_reconcile('Die Verbindung besitzt weder einen eigenen API-Zugang noch einen eigenen Fallback.');
$credentials['api_key_id'] = '';
$credentials['api_key_secret'] = '';
$config['piwigo_auth'] = 'connection-scoped';
$config['api_enabled'] = false;
$row['secret_blob'] = encrypt_reconcile($credentials, $hexKey);
}
$connectionScoped = (string)($config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $config);
if ($connectionScoped)
{
$apiAvailable = !empty($config['api_enabled']) && $credentials['api_key_id'] !== '' && $credentials['api_key_secret'] !== '';
$fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== '';
if (!$apiAvailable && !$fallbackAvailable) fail_reconcile('Kein verbindungseigener Piwigo-Zugang gespeichert.');
}
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_reconcile('State-Verzeichnis konnte nicht angelegt werden.');
chmod($stateDir, 0750);
$base = $configDir.'/connection-'.$id;
$dbPasswordPath = $base.'.db-password';
$piwigoPasswordPath = $base.'.piwigo-password';
$storagePath = $base.'.storages.tsv';
$rootsPath = $base.'.roots.tsv';
$configPath = $base.'.conf';
$statusFile = $stateDir.'/connector-status.json';
file_put_contents($dbPasswordPath, $credentials['db_password']."\n", LOCK_EX);
chmod($dbPasswordPath, 0600);
$fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== '';
if ($fallbackAvailable)
{
file_put_contents($piwigoPasswordPath, $credentials['piwigo_password']."\n", LOCK_EX);
chmod($piwigoPasswordPath, 0600);
}
else @unlink($piwigoPasswordPath);
$storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount<TAB>include_prefix');
foreach ($storages as $storage)
{
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t"
.trim((string)($storage['source_prefix'] ?? ''), '/')."\t"
.(string)($storage['local_mount'] ?? '')."\t"
.trim((string)($storage['include_prefix'] ?? ''), '/');
}
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX);
chmod($storagePath, 0600);
if ($sourceMode === 'selected-fileids')
{
$rootLines = array('# fileid<TAB>display_name');
foreach ($roots as $root)
{
$fileid = (int)($root['fileid'] ?? 0);
$display = trim((string)($root['display_name'] ?? ''));
if ($fileid < 1 || $display === '' || preg_match('/[\t\r\n]/', $display)) fail_reconcile('Eine gespeicherte Nextcloud-Quelle ist ungueltig.');
$rootLines[] = $fileid."\t".$display;
}
file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);
chmod($rootsPath, 0600);
}
else @unlink($rootsPath);
$lines = array(
'PIWIGO_ROOT='.$piwigoRoot,
'GALLERY_ROOT='.(string)$config['gallery_root'],
'STATE_DIR='.$stateDir,
'STATUS_FILE='.$statusFile,
'NC_DB_HOST='.(string)$config['host'],
'NC_DB_PORT='.(string)$config['port'],
'NC_DB_NAME='.(string)$config['database'],
'NC_DB_USER='.(string)$config['user'],
'NC_DB_VIEW='.(string)$config['source_view'],
'NC_ACTIVITY_VIEW='.(string)$config['activity_view'],
'NC_DB_PASSWORD_FILE='.$dbPasswordPath,
'STORAGE_CONFIG='.$storagePath,
'SOURCE_MODE='.$sourceMode,
'QUIET_SECONDS='.(int)($config['quiet_seconds'] ?? 120),
'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900),
'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400),
'PIWIGO_SYNC_ENABLED=1',
);
if ($sourceMode !== 'legacy-view') $lines[] = 'ACCESS_USER='.escapeshellarg($accessUser);
if ($sourceMode === 'selected-fileids') $lines[] = 'ROOTS_CONFIG='.$rootsPath;
if ($fallbackAvailable)
{
$lines[] = 'PIWIGO_SYNC_USER='.$credentials['piwigo_user'];
$lines[] = 'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath;
}
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
chmod($configPath, 0600);
$config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile;
$config['runtime'] = array('mode'=>'shared-runner','config'=>$configPath,'reconciled_at'=>date('Y-m-d H:i:s'));
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json)) fail_reconcile('Connector-Konfiguration konnte nicht serialisiert werden.');
$now = date('Y-m-d H:i:s');
$sql = "UPDATE `{$table}` SET enabled=1,takeover_state='active',config_json='".sql_reconcile($db,$json)."',secret_blob='".sql_reconcile($db,$row['secret_blob'])."',updated='".sql_reconcile($db,$now)."' WHERE id=".$id;
if (!$db->query($sql)) fail_reconcile('Runtime-Status konnte nicht gespeichert werden: '.$db->error);
if (!$isActive) echo "NC Connector: Verbindung #{$id} ({$row['name']}) automatisch in die gemeinsame Runtime uebernommen.\n";
}
catch (Throwable $e)
{
write_runtime_status($piwigoRoot, $id, $stateDir, $e->getMessage());
fwrite(STDERR, "NC Connector #{$id}: ".$e->getMessage()."\n");
}
}
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, "NC Connector Reconcile: ".$e->getMessage()."\n");
exit(1);
}

View File

@@ -4,19 +4,238 @@ set -Eeuo pipefail
CONFIG_DIR="/etc/bratonien-tools/nc-connector" CONFIG_DIR="/etc/bratonien-tools/nc-connector"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
shopt -s nullglob shopt -s nullglob
configs=("$CONFIG_DIR"/connection-*.conf)
if [[ ${#configs[@]} -eq 0 ]]; then read_config_value() {
echo "Keine aktiven NC-Connector-Verbindungen konfiguriert." local key="$1"
local file="$2"
local value
value="$(sed -n "s/^${key}=//p" "$file" | tail -n 1)"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "$value"
}
compact_text() {
printf '%s\n' "$1" | tail -n 20 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
read_webdav_failure_detail() {
local config="$1"
local captured_output="${2:-}"
local connection_id piwigo_root status_file status_detail
connection_id="$(read_config_value CONNECTION_ID "$config")"
piwigo_root="$(read_config_value PIWIGO_ROOT "$config")"
[[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo"
status_detail=""
if [[ -n "$connection_id" ]]; then
status_file="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status/connection-${connection_id}.json"
if [[ -r "$status_file" ]]; then
status_detail="$(php -r '
$file = $argv[1];
$data = json_decode((string)@file_get_contents($file), true);
if (!is_array($data)) exit(0);
$message = trim((string)($data["message"] ?? ""));
$detail = trim((string)($data["error_detail"] ?? ""));
$parts = array();
if ($message !== "") $parts[] = $message;
if ($detail !== "") $parts[] = $detail;
if ($parts) {
$text = preg_replace("/\\s+/u", " ", implode(" - ", $parts));
echo trim((string)$text);
}
' "$status_file")"
fi
fi
if [[ -n "$captured_output" ]]; then
captured_output="$(compact_text "$captured_output")"
fi
if [[ -n "$status_detail" && -n "$captured_output" ]]; then
printf '%s' "$status_detail | Prozessausgabe: $captured_output"
elif [[ -n "$status_detail" ]]; then
printf '%s' "$status_detail"
elif [[ -n "$captured_output" ]]; then
printf '%s' "$captured_output"
elif [[ -z "$connection_id" ]]; then
printf '%s' 'WebDAV-Prozess wurde gestartet, aber die Verbindungs-ID fehlt in der Runtime-Konfiguration.'
else
printf '%s' "WebDAV-Prozess fuer Verbindung ${connection_id} endete mit Fehler, lieferte aber weder Statusdatei noch Prozessausgabe."
fi
}
write_route_status() {
local route="$1"
local label="$2"
local detail="$3"
local fallback_used="$4"
local success="$5"
[[ -n "${ROUTE_STATUS_FILE:-}" ]] || return 0
mkdir -p -- "$(dirname -- "$ROUTE_STATUS_FILE")"
php -r '
$payload = array(
"timestamp" => time(),
"route" => (string)$argv[2],
"label" => (string)$argv[3],
"detail" => (string)$argv[4],
"fallback_used" => $argv[5] === "1",
"success" => $argv[6] === "1"
);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if (!is_string($json) || file_put_contents($argv[1], $json.PHP_EOL, LOCK_EX) === false) {
fwrite(STDERR, "Route-Status konnte nicht geschrieben werden.\n");
exit(1);
}
@chmod($argv[1], 0644);
' "$ROUTE_STATUS_FILE" "$route" "$label" "$detail" "$fallback_used" "$success"
}
if ! php "$SCRIPT_DIR/reconcile.php"; then
echo "NC Connector: gespeicherte lokale Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2
exit 1
fi
if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then
echo "NC Connector: WebDAV-Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2
exit 1
fi
if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then
echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2
exit 1
fi
if ! php "$SCRIPT_DIR/cleanup-stale.php"; then
echo "NC Connector: verwaiste Laufzeitdateien konnten nicht bereinigt werden." >&2
exit 1
fi
configs=("$CONFIG_DIR"/connection-*.conf)
webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf)
if [[ ${#configs[@]} -eq 0 && ${#webdav_configs[@]} -eq 0 ]]; then
echo "Keine NC-Connector-Verbindungen konfiguriert."
exit 0 exit 0
fi fi
result=0 route_piwigo_root=""
for candidate in "${webdav_configs[@]}" "${configs[@]}"; do
[[ -f "$candidate" ]] || continue
route_piwigo_root="$(read_config_value PIWIGO_ROOT "$candidate")"
[[ -n "$route_piwigo_root" ]] && break
done
[[ -n "$route_piwigo_root" ]] || route_piwigo_root="/var/www/piwigo"
ROUTE_STATUS_FILE="${route_piwigo_root%/}/_data/bratonien-tools/nc-connector-status/route-status.json"
webdav_success=0
webdav_failed=0
webdav_failure_detail=""
if [[ ${#webdav_configs[@]} -eq 0 ]]; then
webdav_failed=1
webdav_failure_detail="Keine WebDAV-Runtime-Verbindung ist konfiguriert. Es existiert aktuell nur die Legacy-Verbindung; deshalb kann WebDAV nicht primaer laufen."
echo "NC Connector: $webdav_failure_detail" >&2
else
for config in "${webdav_configs[@]}"; do
name="$(basename "$config")"
echo "NC Connector WebDAV primaer: $name"
webdav_output=""
if webdav_output="$(env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh" 2>&1)"; then
webdav_success=1
[[ -z "$webdav_output" ]] || printf '%s\n' "$webdav_output"
else
webdav_exit=$?
webdav_failed=1
[[ -z "$webdav_output" ]] || printf '%s\n' "$webdav_output" >&2
webdav_failure_detail="$(read_webdav_failure_detail "$config" "$webdav_output")"
webdav_failure_detail="Exit-Code ${webdav_exit}: ${webdav_failure_detail}"
echo "NC Connector: WebDAV-Lauf fehlgeschlagen: $webdav_failure_detail" >&2
echo "NC Connector: Legacy-Fallback bleibt verfuegbar." >&2
fi
done
fi
if [[ "$webdav_success" -eq 1 ]]; then
write_route_status \
"webdav" \
"WebDAV (primaer)" \
"WebDAV erfolgreich. Legacy-Fallback wurde in diesem Lauf nicht ausgefuehrt." \
"0" \
"1"
echo "NC Connector: WebDAV erfolgreich; Legacy-Verbindung wird in diesem Lauf nicht ausgefuehrt."
exit 0
fi
if [[ ${#configs[@]} -eq 0 ]]; then
[[ -n "$webdav_failure_detail" ]] || webdav_failure_detail="WebDAV ist fehlgeschlagen; Ursache konnte nicht ermittelt werden."
write_route_status \
"failed" \
"FEHLER - kein Datenweg" \
"WebDAV-Fehler: $webdav_failure_detail Keine Legacy-Fallback-Verbindung vorhanden." \
"0" \
"0"
echo "NC Connector: WebDAV ist fehlgeschlagen und es ist keine Legacy-Fallback-Verbindung vorhanden." >&2
exit 1
fi
echo "NC Connector: kein erfolgreicher WebDAV-Lauf; Legacy-Fallback wird ausgefuehrt."
legacy_result=0
for config in "${configs[@]}"; do for config in "${configs[@]}"; do
echo "NC Connector: $(basename "$config")" name="$(basename "$config")"
connection_id=""
if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then
connection_id="${BASH_REMATCH[1]}"
fi
piwigo_root="$(read_config_value PIWIGO_ROOT "$config")"
[[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo"
tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status"
if [[ -n "$connection_id" && -f "$tombstone_dir/deleted-$connection_id" ]]; then
echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt."
rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \
"$CONFIG_DIR/connection-$connection_id.db-password" \
"$CONFIG_DIR/connection-$connection_id.piwigo-password" \
"$CONFIG_DIR/connection-$connection_id.storages.tsv" \
"$CONFIG_DIR/connection-$connection_id.roots.tsv"
rm -f -- "$tombstone_dir/deleted-$connection_id"
continue
fi
echo "NC Connector Legacy-Fallback: $name"
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
result=1 legacy_result=1
fi fi
done done
exit "$result" if [[ "$legacy_result" -eq 0 ]]; then
[[ -n "$webdav_failure_detail" ]] || webdav_failure_detail="WebDAV war nicht erfolgreich; Ursache konnte nicht ermittelt werden."
write_route_status \
"legacy_fallback" \
"LEGACY-FALLBACK AKTIV" \
"WebDAV-Fehler: $webdav_failure_detail Legacy-Fallback wurde erfolgreich ausgefuehrt." \
"1" \
"1"
echo "NC Connector: Legacy-Fallback erfolgreich."
exit 0
fi
[[ -n "$webdav_failure_detail" ]] || webdav_failure_detail="WebDAV war nicht erfolgreich; Ursache konnte nicht ermittelt werden."
write_route_status \
"failed" \
"FEHLER - WebDAV und Fallback" \
"WebDAV-Fehler: $webdav_failure_detail Legacy-Fallback ist ebenfalls fehlgeschlagen." \
"1" \
"0"
if [[ "$webdav_failed" -eq 1 ]]; then
echo "NC Connector: WebDAV und Legacy-Fallback sind fehlgeschlagen." >&2
fi
exit 1

205
runtime/sync-webdav.sh Normal file
View File

@@ -0,0 +1,205 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${PIWIGO_CONFIG:-}"
[[ -n "$CONFIG_FILE" && -r "$CONFIG_FILE" ]] || { echo "WebDAV-Konfiguration fehlt: ${CONFIG_FILE:-<leer>}" >&2; exit 1; }
# shellcheck source=/dev/null
source "$CONFIG_FILE"
: "${PIWIGO_ROOT:?PIWIGO_ROOT fehlt}"
: "${CONNECTION_ID:?CONNECTION_ID fehlt}"
: "${WEBDAV_BASE_URL:?WEBDAV_BASE_URL fehlt}"
: "${WEBDAV_USER:?WEBDAV_USER fehlt}"
: "${WEBDAV_PASSWORD_FILE:?WEBDAV_PASSWORD_FILE fehlt}"
: "${WEBDAV_ROOTS_FILE:?WEBDAV_ROOTS_FILE fehlt}"
: "${WEBDAV_SOURCE_DIR:?WEBDAV_SOURCE_DIR fehlt}"
: "${WEBDAV_MAPPING_FILE:?WEBDAV_MAPPING_FILE fehlt}"
: "${MANIFEST:?MANIFEST fehlt}"
: "${SHADOW_MAP_FILE:?SHADOW_MAP_FILE fehlt}"
: "${GALLERY_ROOT:?GALLERY_ROOT fehlt}"
: "${STATE_DIR:?STATE_DIR fehlt}"
: "${STATUS_FILE:?STATUS_FILE fehlt}"
[[ "${SOURCE_MODE:-}" == "webdav-placeholder" ]] || { echo "Falscher SOURCE_MODE: ${SOURCE_MODE:-<leer>}" >&2; exit 1; }
[[ -r "$WEBDAV_PASSWORD_FILE" ]] || { echo "Nextcloud-Passwortdatei fehlt." >&2; exit 1; }
[[ -r "$WEBDAV_ROOTS_FILE" ]] || { echo "WebDAV-Wurzeln fehlen." >&2; exit 1; }
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
LOCK_FILE="$STATE_DIR/webdav-sync.lock"
mkdir -p -- "$STATE_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
write_status() {
local state="$1" message="$2" detail="${3:-}" auth_mode="${4:-webdav}" api_state="${5:-not_run}" api_message="${6:-}" fallback_state="${7:-not_run}" fallback_message="${8:-}"
python3 - "$STATUS_FILE" "$PIWIGO_ROOT" "$CONNECTION_ID" "$state" "$message" "$detail" "$auth_mode" "$api_state" "$api_message" "$fallback_state" "$fallback_message" <<'PY'
import json, os, sys, tempfile, time
(status_file, piwigo_root, connection_id, state, message, detail, auth_mode,
api_state, api_message, fallback_state, fallback_message) = sys.argv[1:]
payload = {
"state": state,
"message": message,
"timestamp": int(time.time()),
"auth_mode": auth_mode,
"api": {"state": api_state, "message": api_message},
"fallback": {"state": fallback_state, "message": fallback_message},
"error_detail": detail if state == "error" else "",
}
public_file = os.path.join(piwigo_root.rstrip('/'), '_data', 'bratonien-tools', 'nc-connector-status', f'connection-{connection_id}.json')
for target in (status_file, public_file):
directory = os.path.dirname(target)
os.makedirs(directory, exist_ok=True)
fd, temporary = tempfile.mkstemp(dir=directory)
with os.fdopen(fd, 'w', encoding='utf-8') as handle:
json.dump(payload, handle, ensure_ascii=False)
handle.write('\n')
os.chmod(temporary, 0o644)
os.replace(temporary, target)
PY
}
compact_output() {
tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
failure() {
local code="$1" command="$2" line="$3"
trap - ERR
write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "Exit-Code: $code; Zeile: $line; Befehl: $command"
exit "$code"
}
trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
ROOT_ARGS=()
while IFS=$'\t' read -r path display_name; do
[[ -z "$path" || "$path" == \#* ]] && continue
ROOT_ARGS+=(--root "$path")
done < "$WEBDAV_ROOTS_FILE"
[[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; }
python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
--base-url "$WEBDAV_BASE_URL" \
--user "$WEBDAV_USER" \
--password-file "$WEBDAV_PASSWORD_FILE" \
"${ROOT_ARGS[@]}" \
--source-dir "$WEBDAV_SOURCE_DIR" \
--manifest "$MANIFEST" \
--mapping "$WEBDAV_MAPPING_FILE"
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--manifest "$MANIFEST" \
--destination "$GALLERY_ROOT" \
--state "$SHADOW_MAP_FILE"
trap - ERR
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
PREVIEW_OUTPUT=""
PREVIEW_EXIT=0
if PREVIEW_OUTPUT="$(php "$SCRIPT_DIR/lib/precache-webdav-previews.php" \
--mapping="$WEBDAV_MAPPING_FILE" \
--base-url="$WEBDAV_BASE_URL" \
--user="$WEBDAV_USER" \
--password-file="$WEBDAV_PASSWORD_FILE" \
--cache-dir="$PREVIEW_CACHE" 2>&1)"; then
PREVIEW_EXIT=0
else
PREVIEW_EXIT=$?
fi
[[ -z "$PREVIEW_OUTPUT" ]] || printf '%s\n' "$PREVIEW_OUTPUT"
if [[ "$PREVIEW_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PREVIEW_EXIT"
if [[ -n "$PREVIEW_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PREVIEW_OUTPUT" | compact_output)"
fi
write_status error "WebDAV-Vorschaubilder konnten beim Einlesen nicht erzeugt werden" "$DETAIL"
exit "$PREVIEW_EXIT"
fi
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
PIWIGO_OUTPUT=""
PIWIGO_EXIT=0
if PIWIGO_OUTPUT="$(php "$SCRIPT_DIR/lib/piwigo-sync.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID" \
--base-url="http://127.0.0.1" 2>&1)"; then
PIWIGO_EXIT=0
else
PIWIGO_EXIT=$?
fi
[[ -z "$PIWIGO_OUTPUT" ]] || printf '%s\n' "$PIWIGO_OUTPUT"
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PIWIGO_EXIT"
if [[ -n "$PIWIGO_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)"
fi
if grep -qi 'Invalid username/password' <<<"$PIWIGO_OUTPUT"; then
write_status error \
"Piwigo-Fallback fehlgeschlagen: Benutzername oder Passwort ist ungültig" \
"$DETAIL" \
"fallback" \
"not_configured" \
"Für diese Verbindung ist keine nutzbare API konfiguriert" \
"error" \
"Piwigo hat Benutzername oder Passwort des Fallback-Zugangs abgelehnt"
elif grep -qi 'Kein gespeicherter Benutzername/Passwort-Fallback\|Kein verbindungseigener Piwigo-Zugang' <<<"$PIWIGO_OUTPUT"; then
write_status error \
"Piwigo-Zugang fehlt: API und Fallback sind nicht nutzbar" \
"$DETAIL" \
"failed" \
"not_configured" \
"Für diese Verbindung ist keine nutzbare API konfiguriert" \
"not_configured" \
"Kein vollständiger Fallback-Zugang gespeichert"
else
write_status error "Piwigo-Synchronisierung des WebDAV-Shadow-Trees fehlgeschlagen" "$DETAIL"
fi
exit "$PIWIGO_EXIT"
fi
DERIVATIVE_OUTPUT=""
DERIVATIVE_EXIT=0
if DERIVATIVE_OUTPUT="$(php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID" 2>&1)"; then
DERIVATIVE_EXIT=0
else
DERIVATIVE_EXIT=$?
fi
[[ -z "$DERIVATIVE_OUTPUT" ]] || printf '%s\n' "$DERIVATIVE_OUTPUT"
if [[ "$DERIVATIVE_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $DERIVATIVE_EXIT"
if [[ -n "$DERIVATIVE_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$DERIVATIVE_OUTPUT" | compact_output)"
fi
write_status error "Piwigo-Derivate für WebDAV-Bilder konnten nicht erzeugt werden" "$DETAIL"
exit "$DERIVATIVE_EXIT"
fi
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \
"WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" \
"" \
"api" \
"ok" \
"Piwigo-API erfolgreich" \
"not_needed" \
"Fallback wurde nicht benötigt"
elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \
"WebDAV eingelesen, Piwigo über Fallback synchronisiert und Derivate erzeugt" \
"" \
"fallback" \
"not_used" \
"API war nicht nutzbar" \
"ok" \
"Benutzername/Passwort-Fallback erfolgreich"
else
write_status ok "WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt"
fi
else
write_status ok "WebDAV eingelesen und Vorschaubilder erzeugt; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync"
fi

View File

@@ -5,12 +5,27 @@ CONFIG_FILE="${PIWIGO_CONFIG:-/etc/bratonien-tools/nc-connector/connection-1.con
[[ -r "$CONFIG_FILE" ]] || { echo "Konfiguration fehlt: $CONFIG_FILE" >&2; exit 1; } [[ -r "$CONFIG_FILE" ]] || { echo "Konfiguration fehlt: $CONFIG_FILE" >&2; exit 1; }
PIWIGO_SYNC_OVERRIDE_VALUE="${PIWIGO_SYNC_OVERRIDE-}" PIWIGO_SYNC_OVERRIDE_VALUE="${PIWIGO_SYNC_OVERRIDE-}"
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$CONFIG_FILE" source "$CONFIG_FILE"
NC_ACTIVITY_VIEW="${NC_ACTIVITY_VIEW:-piwigo_showcase_activity}" : "${NC_ACTIVITY_VIEW:?NC_ACTIVITY_VIEW fehlt}"
NC_DB_VIEW="${NC_DB_VIEW:-piwigo_showcase_sources}" : "${NC_DB_VIEW:?NC_DB_VIEW fehlt}"
SOURCE_MODE="${SOURCE_MODE:-legacy-view}"
ACCESS_USER="${ACCESS_USER:-}"
ROOTS_CONFIG="${ROOTS_CONFIG:-}"
case "$SOURCE_MODE" in
legacy-view|user-shares|selected-fileids) ;;
*) echo "Unbekannter SOURCE_MODE: $SOURCE_MODE" >&2; exit 1 ;;
esac
if [[ "$SOURCE_MODE" != "legacy-view" && -z "$ACCESS_USER" ]]; then
echo "ACCESS_USER fehlt fuer die verbindungsbezogene Datenquelle." >&2
exit 1
fi
if [[ "$SOURCE_MODE" == "selected-fileids" && ( -z "$ROOTS_CONFIG" || ! -r "$ROOTS_CONFIG" ) ]]; then
echo "ROOTS_CONFIG fehlt fuer die ausgewaehlten Nextcloud-Quellen." >&2
exit 1
fi
if [[ -n "$PIWIGO_SYNC_OVERRIDE_VALUE" ]]; then if [[ -n "$PIWIGO_SYNC_OVERRIDE_VALUE" ]]; then
case "$PIWIGO_SYNC_OVERRIDE_VALUE" in case "$PIWIGO_SYNC_OVERRIDE_VALUE" in
@@ -50,13 +65,17 @@ API_MESSAGE=""
FALLBACK_STATE="not_run" FALLBACK_STATE="not_run"
FALLBACK_MESSAGE="" FALLBACK_MESSAGE=""
ERROR_DETAIL="" ERROR_DETAIL=""
ERROR_STAGE="Vorbereitung"
ERROR_MESSAGE="Synchronisierung fehlgeschlagen"
compact_output() {
tail -n 8 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
write_status() { write_status() {
local state="$1" message="$2" local state="$1" message="$2"
local error_detail="$ERROR_DETAIL" local error_detail="$ERROR_DETAIL"
if [[ "$state" != "error" ]]; then [[ "$state" == "error" ]] || error_detail=""
error_detail=""
fi
python3 - "$STATUS_FILE" "$PUBLIC_STATUS_FILE" "$state" "$message" "$AUTH_MODE" "$API_STATE" "$API_MESSAGE" "$FALLBACK_STATE" "$FALLBACK_MESSAGE" "$error_detail" <<'PY' python3 - "$STATUS_FILE" "$PUBLIC_STATUS_FILE" "$state" "$message" "$AUTH_MODE" "$API_STATE" "$API_MESSAGE" "$FALLBACK_STATE" "$FALLBACK_MESSAGE" "$error_detail" <<'PY'
import json, os, sys, tempfile, time import json, os, sys, tempfile, time
(path, public_path, state, message, auth_mode, api_state, api_message, (path, public_path, state, message, auth_mode, api_state, api_message,
@@ -86,27 +105,48 @@ PY
} }
failure() { failure() {
[[ -n "$ERROR_DETAIL" ]] || ERROR_DETAIL="Synchronisierung wurde durch einen technischen Fehler abgebrochen." local exit_code="${1:-1}"
write_status error "Synchronisierung fehlgeschlagen" local failed_command="${2:-unbekannt}"
local failed_line="${3:-?}"
trap - ERR
if [[ -z "$ERROR_DETAIL" ]]; then
ERROR_DETAIL="Schritt: $ERROR_STAGE. Exit-Code: $exit_code. Zeile: $failed_line. Fehlgeschlagener Befehl: $failed_command"
fi
write_status error "$ERROR_MESSAGE"
exit "$exit_code"
} }
trap failure ERR trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
run_stage() {
local stage="$1" message="$2"
shift 2
local output="" exit_code=0
ERROR_STAGE="$stage"
ERROR_MESSAGE="$message"
if output="$("$@" 2>&1)"; then exit_code=0; else exit_code=$?; fi
[[ -z "$output" ]] || printf '%s\n' "$output"
if [[ "$exit_code" -ne 0 ]]; then
ERROR_DETAIL="Schritt: $stage. Exit-Code: $exit_code."
[[ -z "$output" ]] || ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$output" | compact_output)"
trap - ERR
write_status error "$message"
exit "$exit_code"
fi
}
ERROR_STAGE="Lokalen Zustand prüfen"
ERROR_MESSAGE="Lokaler Connector-Zustand konnte nicht geprüft werden"
NEEDS_LOCAL_REPAIR=0 NEEDS_LOCAL_REPAIR=0
if [[ ! -s "$MAP_FILE" ]]; then if [[ ! -s "$MAP_FILE" ]]; then
NEEDS_LOCAL_REPAIR=1 NEEDS_LOCAL_REPAIR=1
else else
set +e set +e
python3 - "$MAP_FILE" "$GALLERY_ROOT" <<'PY' python3 - "$MAP_FILE" "$GALLERY_ROOT" <<'PY'
import json import json, sys
import sys
from pathlib import Path from pathlib import Path
mapping = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) mapping = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
gallery = Path(sys.argv[2]) gallery = Path(sys.argv[2])
roots = [ roots = [target for source, target in mapping.items() if source.startswith("share:") and "/" not in source]
target for source, target in mapping.items()
if source.startswith("share:") and "/" not in source
]
raise SystemExit(0 if roots and all((gallery / target).is_dir() for target in roots) else 1) raise SystemExit(0 if roots and all((gallery / target).is_dir() for target in roots) else 1)
PY PY
ROOTS_INTACT=$? ROOTS_INTACT=$?
@@ -122,90 +162,109 @@ if [[ "$NEEDS_LOCAL_REPAIR" == "0" && "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; the
[[ "$PIWIGO_ALBUMS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1 [[ "$PIWIGO_ALBUMS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1
fi fi
GATE_ARGS=(
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT"
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE"
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW"
--quiet "$QUIET_SECONDS" --max-wait "$MAX_WAIT_SECONDS" --full-after "$FULL_SYNC_SECONDS"
)
if [[ "$SOURCE_MODE" != "legacy-view" ]]; then GATE_ARGS+=(--access-user "$ACCESS_USER"); fi
if [[ "$SOURCE_MODE" == "selected-fileids" ]]; then GATE_ARGS+=(--roots-config "$ROOTS_CONFIG"); fi
if [[ "$NEEDS_LOCAL_REPAIR" == "1" ]]; then if [[ "$NEEDS_LOCAL_REPAIR" == "1" ]]; then
GATE_RESULT=0 GATE_RESULT=0
else else
if python3 "$SCRIPT_DIR/lib/activity_gate.py" check \ ERROR_STAGE="Nextcloud-Aktivitaet pruefen"
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \ ERROR_MESSAGE="Nextcloud-Aktivitaet konnte nicht geprueft werden"
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \ if GATE_OUTPUT="$(python3 "$SCRIPT_DIR/lib/activity_gate.py" check "${GATE_ARGS[@]}" 2>&1)"; then GATE_RESULT=0; else GATE_RESULT=$?; fi
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW" \ [[ -z "$GATE_OUTPUT" ]] || printf '%s\n' "$GATE_OUTPUT"
--quiet "$QUIET_SECONDS" --max-wait "$MAX_WAIT_SECONDS" --full-after "$FULL_SYNC_SECONDS"; then
GATE_RESULT=0
else
GATE_RESULT=$?
fi
fi fi
if [[ "$GATE_RESULT" == "3" ]]; then if [[ "$GATE_RESULT" == "3" ]]; then
trap - ERR trap - ERR
write_status ok "Keine Änderungen gefunden" write_status ok "Keine Aenderungen gefunden"
exit 0 exit 0
fi fi
[[ "$GATE_RESULT" == "0" ]] || { ERROR_DETAIL="Activity-Gate fehlgeschlagen (Exit-Code $GATE_RESULT)."; exit "$GATE_RESULT"; } if [[ "$GATE_RESULT" != "0" ]]; then
ERROR_DETAIL="Schritt: Nextcloud-Aktivitaet pruefen. Exit-Code: $GATE_RESULT."
[[ -z "${GATE_OUTPUT:-}" ]] || ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$GATE_OUTPUT" | compact_output)"
trap - ERR
write_status error "Nextcloud-Aktivitaet konnte nicht geprueft werden"
exit "$GATE_RESULT"
fi
python3 "$SCRIPT_DIR/lib/build_manifest.py" \ if [[ "$SOURCE_MODE" == "selected-fileids" ]]; then
run_stage "Ausgewaehlte Nextcloud-Quellen aufloesen" "Ausgewaehlte Nextcloud-Quellen konnten nicht aufgeloest werden" \
python3 "$SCRIPT_DIR/lib/build_selected_manifest.py" \
--host "$NC_DB_HOST" --port "$NC_DB_PORT" --database "$NC_DB_NAME" --user "$NC_DB_USER" \ --host "$NC_DB_HOST" --port "$NC_DB_PORT" --database "$NC_DB_NAME" --user "$NC_DB_USER" \
--password-file "$NC_DB_PASSWORD_FILE" --view "$NC_DB_VIEW" \ --password-file "$NC_DB_PASSWORD_FILE" --view "$NC_DB_VIEW" \
--storage-config "$STORAGE_CONFIG" --roots-config "$ROOTS_CONFIG" --output "$MANIFEST"
else
MANIFEST_ARGS=(
--host "$NC_DB_HOST" --port "$NC_DB_PORT" --database "$NC_DB_NAME" --user "$NC_DB_USER"
--password-file "$NC_DB_PASSWORD_FILE" --view "$NC_DB_VIEW"
--storage-config "$STORAGE_CONFIG" --output "$MANIFEST" --storage-config "$STORAGE_CONFIG" --output "$MANIFEST"
)
if [[ "$SOURCE_MODE" == "user-shares" ]]; then MANIFEST_ARGS+=(--access-user "$ACCESS_USER"); fi
run_stage "Nextcloud-Dateiliste lesen" "Dateiliste aus Nextcloud konnte nicht erstellt werden" \
python3 "$SCRIPT_DIR/lib/build_manifest.py" "${MANIFEST_ARGS[@]}"
fi
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \ run_stage "Lokalen Galeriebaum aktualisieren" "Lokaler Galeriebaum konnte nicht aktualisiert werden" \
--manifest "$MANIFEST" --destination "$GALLERY_ROOT" --state "$MAP_FILE" python3 "$SCRIPT_DIR/lib/shadow_tree.py" --manifest "$MANIFEST" --destination "$GALLERY_ROOT" --state "$MAP_FILE"
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
set +e ERROR_STAGE="Piwigo synchronisieren"
PIWIGO_OUTPUT="$(php "$SCRIPT_DIR/lib/piwigo-sync.php" \ ERROR_MESSAGE="Piwigo-Synchronisierung fehlgeschlagen"
--piwigo-root="$PIWIGO_ROOT" \ if PIWIGO_OUTPUT="$(php "$SCRIPT_DIR/lib/piwigo-sync.php" --piwigo-root="$PIWIGO_ROOT" --connection-id="$CONNECTION_ID" --base-url="http://127.0.0.1" 2>&1)"; then PIWIGO_EXIT=0; else PIWIGO_EXIT=$?; fi
--connection-id="$CONNECTION_ID" \
--base-url="http://127.0.0.1" 2>&1)"
PIWIGO_EXIT=$?
set -e
printf '%s\n' "$PIWIGO_OUTPUT" printf '%s\n' "$PIWIGO_OUTPUT"
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
AUTH_MODE="api" AUTH_MODE="api"; API_STATE="ok"; API_MESSAGE="API-Synchronisierung erfolgreich"
API_STATE="ok" FALLBACK_STATE="not_needed"; FALLBACK_MESSAGE="Fallback wurde nicht benoetigt"
API_MESSAGE="API-Synchronisierung erfolgreich"
FALLBACK_STATE="not_needed"
FALLBACK_MESSAGE="Fallback wurde nicht benötigt"
else else
API_LINE="$(grep -m1 '^Piwigo-API nicht nutzbar:' <<<"$PIWIGO_OUTPUT" || true)" API_LINE="$(grep -m1 '^Piwigo-API nicht nutzbar:' <<<"$PIWIGO_OUTPUT" || true)"
if [[ -n "$API_LINE" ]]; then
API_STATE="error" API_STATE="error"
API_MESSAGE="${API_LINE#Piwigo-API nicht nutzbar: }" if [[ -n "$API_LINE" ]]; then API_MESSAGE="${API_LINE#Piwigo-API nicht nutzbar: }"; else API_MESSAGE="API-Synchronisierung war nicht erfolgreich"; fi
else
API_STATE="error"
API_MESSAGE="API-Synchronisierung war nicht erfolgreich"
fi
if grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then if grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then
AUTH_MODE="fallback" AUTH_MODE="fallback"; FALLBACK_STATE="ok"; FALLBACK_MESSAGE="Benutzername/Passwort-Fallback erfolgreich"
FALLBACK_STATE="ok"
FALLBACK_MESSAGE="Benutzername/Passwort-Fallback erfolgreich"
elif [[ "$PIWIGO_EXIT" -ne 0 ]]; then elif [[ "$PIWIGO_EXIT" -ne 0 ]]; then
AUTH_MODE="failed" AUTH_MODE="failed"; FALLBACK_STATE="error"; FALLBACK_MESSAGE="$(tail -n 1 <<<"$PIWIGO_OUTPUT")"
FALLBACK_STATE="error"
FALLBACK_MESSAGE="$(tail -n 1 <<<"$PIWIGO_OUTPUT")"
fi fi
fi fi
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
ERROR_DETAIL="$(tail -n 3 <<<"$PIWIGO_OUTPUT" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/[[:space:]]$//')" ERROR_DETAIL="Schritt: Piwigo synchronisieren. Exit-Code: $PIWIGO_EXIT. Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)"
trap - ERR trap - ERR
write_status error "Synchronisierung fehlgeschlagen" write_status error "Piwigo-Synchronisierung fehlgeschlagen"
exit "$PIWIGO_EXIT" exit "$PIWIGO_EXIT"
fi fi
fi fi
python3 "$SCRIPT_DIR/lib/activity_gate.py" commit \ COMMIT_ARGS=(
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \ --state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT"
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \ --database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE"
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW" --view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW"
)
if [[ "$SOURCE_MODE" != "legacy-view" ]]; then COMMIT_ARGS+=(--access-user "$ACCESS_USER"); fi
if [[ "$SOURCE_MODE" == "selected-fileids" ]]; then COMMIT_ARGS+=(--roots-config "$ROOTS_CONFIG"); fi
ERROR_STAGE="Aktivitaetsstand speichern"
ERROR_MESSAGE="Aktivitaetsstand konnte nicht gespeichert werden"
if COMMIT_OUTPUT="$(python3 "$SCRIPT_DIR/lib/activity_gate.py" commit "${COMMIT_ARGS[@]}" 2>&1)"; then COMMIT_EXIT=0; else COMMIT_EXIT=$?; fi
[[ -z "$COMMIT_OUTPUT" ]] || printf '%s\n' "$COMMIT_OUTPUT"
if [[ "$COMMIT_EXIT" -ne 0 ]]; then
ERROR_DETAIL="Schritt: Aktivitaetsstand speichern. Exit-Code: $COMMIT_EXIT."
[[ -z "$COMMIT_OUTPUT" ]] || ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$COMMIT_OUTPUT" | compact_output)"
trap - ERR
write_status error "Aktivitaetsstand konnte nicht gespeichert werden"
exit "$COMMIT_EXIT"
fi
trap - ERR trap - ERR
if [[ "$AUTH_MODE" == "fallback" ]]; then if [[ "$AUTH_MODE" == "fallback" ]]; then
write_status warning "Synchronisierung erfolgreich über Fallback; API war nicht nutzbar" write_status warning "Synchronisierung erfolgreich ueber Fallback; API war nicht nutzbar"
elif [[ "$AUTH_MODE" == "api" ]]; then elif [[ "$AUTH_MODE" == "api" ]]; then
write_status ok "Synchronisierung erfolgreich über API" write_status ok "Synchronisierung erfolgreich ueber API"
else else
write_status ok "Synchronisierung erfolgreich" write_status ok "Synchronisierung erfolgreich"
fi fi

View File

@@ -9,7 +9,13 @@
.bratonien-tab-panel { width:100%; max-width:none; margin-left:0 !important; margin-right:0 !important; } .bratonien-tab-panel { width:100%; max-width:none; margin-left:0 !important; margin-right:0 !important; }
.bratonien-tab-panel[hidden] { display:none !important; } .bratonien-tab-panel[hidden] { display:none !important; }
.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} } .bratonien-edit-dialog { width:min(920px,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); }
.bratonien-edit-dialog::backdrop { background:rgba(0,0,0,.55); }
.bratonien-edit-dialog__body { padding:1.25rem 1.5rem; }
.bratonien-storage-row { display:grid; grid-template-columns:minmax(120px,.7fr) minmax(150px,1fr) minmax(240px,1.5fr) auto; gap:.5rem; align-items:end; margin:.5rem 0; }
.bratonien-storage-row label { display:flex; flex-direction:column; gap:.25rem; }
.bratonien-storage-row input { width:100%; box-sizing:border-box; }
@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}.bratonien-storage-row{grid-template-columns:1fr} }
</style> </style>
<div id="bratonien-tabs-anchor"></div> <div id="bratonien-tabs-anchor"></div>
<script> <script>
@@ -42,9 +48,172 @@
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 initial='',hash=location.hash?location.hash.slice(1):'';panels.forEach(function(item){if(item.definition.id===hash||(item.definition.sections||[]).indexOf(hash)!==-1)initial=item.definition.id;});if(!initial){try{initial=localStorage.getItem('bratonien-tools-active-tab')||'';}catch(e){}}if(!panels.some(function(i){return i.definition.id===initial;}))initial=panels[0].definition.id;activate(initial,false);
} }
function initNCWizardLifecycle(){
var section=document.getElementById('nc-connector');if(!section)return;
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 storageKey='bratonienNcWizardOpen';
var modeKey='bratonienNcWizardMode';
var resetBusy=false;
function token(){var input=section.querySelector('input[name="pwg_token"]');return input?input.value:'';}
function setOpen(value){try{if(value)sessionStorage.setItem(storageKey,'1');else sessionStorage.removeItem(storageKey);}catch(e){}}
function mode(){try{return sessionStorage.getItem(modeKey)||'';}catch(e){return '';}}
function clearMode(){try{sessionStorage.removeItem(modeKey);}catch(e){}}
function applyMode(){
if(!dialog)return;
var current=mode();
var heading=dialog.querySelector('h4');
var finish=dialog.querySelector('button[value="nc_connector_wizard_finish"]');
if(current==='edit'){
if(heading)heading.textContent='Verbindung bearbeiten';
if(finish)finish.textContent='Änderungen speichern';
}else if(current==='migrate'){
if(heading)heading.textContent='Auf WebDAV migrieren';
if(finish)finish.textContent='Migration starten';
}else{
if(heading)heading.textContent='Neue Verbindung';
if(finish)finish.textContent='Verbindung anlegen';
}
}
function showWizard(){
setOpen(true);applyMode();
if(!dialog)return;
if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();
else dialog.setAttribute('open','open');
}
function resetServer(){
var pwgToken=token();
if(!pwgToken)return Promise.reject(new Error('CSRF token missing'));
var body=new URLSearchParams();body.set('pwg_token',pwgToken);body.set('bratonien_tool','nc_connector_wizard_reset');
return fetch(window.location.href,{method:'POST',credentials:'same-origin',cache:'no-store',headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:body.toString()});
}
function closeAfterReset(){
if(resetBusy)return;resetBusy=true;setOpen(false);clearMode();
resetServer().catch(function(){}).finally(function(){resetBusy=false;if(dialog){if(typeof dialog.close==='function'&&dialog.open)dialog.close();else dialog.removeAttribute('open');}});
}
if(openButton){
openButton.addEventListener('click',function(event){
event.preventDefault();event.stopImmediatePropagation();clearMode();showWizard();
},true);
}
if(closeButton){
closeButton.addEventListener('click',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true);
}
if(dialog){
dialog.addEventListener('cancel',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true);
dialog.addEventListener('click',function(event){if(event.target===dialog){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();}},true);
[].slice.call(dialog.querySelectorAll('form[data-bratonien-wizard-form]')).forEach(function(form){
form.addEventListener('submit',function(event){
var submitter=event.submitter;
setOpen(true);
if(submitter&&submitter.hasAttribute('data-bratonien-wizard-end')){
setOpen(false);
if(submitter.value==='nc_connector_wizard_reset')clearMode();
}
},true);
});
try{if(sessionStorage.getItem(storageKey)==='1')showWizard();}catch(e){}
applyMode();
}
}
function initNCConnectionEditing(){
var section=document.getElementById('nc-connector');if(!section)return;
var modeKey='bratonienNcWizardMode';
var pwgTokenInput=section.querySelector('input[name="pwg_token"]');
var pwgToken=pwgTokenInput?pwgTokenInput.value:'';
function setMode(value){try{sessionStorage.setItem(modeKey,value);}catch(e){}}
function makePostButton(label,tool,id,modeValue){
var form=document.createElement('form');form.method='post';form.style.display='inline';
var token=document.createElement('input');token.type='hidden';token.name='pwg_token';token.value=pwgToken;form.appendChild(token);
var connection=document.createElement('input');connection.type='hidden';connection.name='connection_id';connection.value=String(id);form.appendChild(connection);
var button=document.createElement('button');button.type='submit';button.className='buttonLike';button.name='bratonien_tool';button.value=tool;button.textContent=label;
if(modeValue)form.addEventListener('submit',function(){setMode(modeValue);});
form.appendChild(button);return form;
}
function escapeHtml(value){return String(value==null?'':value).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c];});}
function localEditorDialog(){
var dialog=document.getElementById('bratonien-nc-local-edit-dialog');
if(dialog)return dialog;
dialog=document.createElement('dialog');dialog.id='bratonien-nc-local-edit-dialog';dialog.className='bratonien-edit-dialog';
dialog.innerHTML='<div class="bratonien-edit-dialog__body"><div style="display:flex;align-items:center;justify-content:space-between;gap:1rem"><div><h4 style="margin:0">Verbindung bearbeiten</h4><p class="bratonien-base-note" style="margin:.35rem 0 0">Bestehende Legacy-Verbindung. Änderungen werden in derselben Verbindung gespeichert.</p></div><button type="button" class="buttonLike" data-local-edit-close>Schließen</button></div><div data-local-edit-content style="margin-top:1rem"></div></div>';
document.body.appendChild(dialog);
dialog.querySelector('[data-local-edit-close]').addEventListener('click',function(){dialog.close();});
dialog.addEventListener('click',function(event){if(event.target===dialog)dialog.close();});
return dialog;
}
function storageRow(storage){
return '<div class="bratonien-storage-row" data-storage-row>'+
'<label>Storage-ID<input name="nc_storage_id[]" value="'+escapeHtml(storage.storage_id||'')+'" required></label>'+
'<label>Quellordner<input name="nc_source_prefix[]" value="'+escapeHtml(storage.source_prefix||'')+'" placeholder="optional"></label>'+
'<label>Lokaler Speicherpfad<input name="nc_local_mount[]" value="'+escapeHtml(storage.local_mount||'')+'" required></label>'+
'<button type="button" class="buttonLike" data-remove-storage>Entfernen</button></div>';
}
function openLocalEditor(id){
var dialog=localEditorDialog();
var content=dialog.querySelector('[data-local-edit-content]');
content.innerHTML='<p class="bratonien-base-note">Verbindung wird geladen …</p>';
if(typeof dialog.showModal==='function')dialog.showModal();else dialog.setAttribute('open','open');
fetch('plugins/bratonien_tools/nc-connector-edit-data.php?connection_id='+encodeURIComponent(id)+'&_='+Date.now(),{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
.then(function(response){return response.json().then(function(data){if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;});})
.then(function(data){
if(data.adapter!=='local')throw new Error('Diese Verbindung ist keine Legacy-Verbindung.');
var legacy=data.legacy||{};var storages=Array.isArray(legacy.storages)?legacy.storages:[];
var rows=storages.map(storageRow).join('');
if(!rows)rows=storageRow({});
content.innerHTML='<form method="post" data-local-edit-form>'+
'<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'+
'<input type="hidden" name="connection_id" value="'+escapeHtml(data.id)+'">'+
'<div class="bratonien-form-grid">'+
'<label class="bratonien-label">Name</label><input name="connection_name" value="'+escapeHtml(data.name)+'" required>'+
'<label class="bratonien-label">Datenbank-Server</label><input name="nc_host" value="'+escapeHtml(legacy.host)+'" required>'+
'<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="'+escapeHtml(legacy.port||5432)+'" required>'+
'<label class="bratonien-label">Datenbank</label><input name="nc_database" value="'+escapeHtml(legacy.database)+'" required>'+
'<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" value="'+escapeHtml(legacy.user)+'" required>'+
'<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" placeholder="leer = unverändert">'+
'</div>'+
'<h5 style="margin-top:1rem">Speicherorte</h5><p class="bratonien-base-note">Jeder Speicherort wird einzeln bearbeitet. Es ist kein Pipe-/Textformat erforderlich.</p><div data-storage-list>'+rows+'</div><button type="button" class="buttonLike" data-add-storage>Speicherort hinzufügen</button>'+
'<details style="margin-top:1rem"><summary>Erweiterte Legacy-Einstellungen</summary><div class="bratonien-form-grid" style="margin-top:.75rem">'+
'<label class="bratonien-label">Source-View</label><input name="nc_source_view" value="'+escapeHtml(legacy.source_view)+'" required>'+
'<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" value="'+escapeHtml(legacy.activity_view)+'" required>'+
'<label class="bratonien-label">Piwigo-Galerieordner</label><input name="nc_gallery_root" value="'+escapeHtml(legacy.gallery_root)+'" required>'+
'<label class="bratonien-label">Ruhezeit (Sek.)</label><input name="nc_quiet_seconds" type="number" min="0" value="'+escapeHtml(legacy.quiet_seconds)+'">'+
'<label class="bratonien-label">Maximale Wartezeit (Sek.)</label><input name="nc_max_wait_seconds" type="number" min="60" value="'+escapeHtml(legacy.max_wait_seconds)+'">'+
'<label class="bratonien-label">Vollprüfung nach (Sek.)</label><input name="nc_full_sync_seconds" type="number" min="300" value="'+escapeHtml(legacy.full_sync_seconds)+'">'+
'</div></details>'+
'<div class="bratonien-actions" style="margin-top:1rem"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_local">Änderungen speichern</button><button class="buttonLike" type="button" data-local-edit-cancel>Abbrechen</button></div></form>';
var form=content.querySelector('[data-local-edit-form]');
form.querySelector('[data-local-edit-cancel]').addEventListener('click',function(){dialog.close();});
form.querySelector('[data-add-storage]').addEventListener('click',function(){form.querySelector('[data-storage-list]').insertAdjacentHTML('beforeend',storageRow({}));});
form.addEventListener('click',function(event){var remove=event.target.closest('[data-remove-storage]');if(remove){var row=remove.closest('[data-storage-row]');if(row)row.remove();}});
})
.catch(function(error){content.innerHTML='<p class="bratonien-main-cache__warning"><strong>Bearbeiten nicht möglich:</strong> '+escapeHtml(error.message||String(error))+'</p>';});
}
[].slice.call(section.querySelectorAll('button[value="nc_connector_edit_start"]')).forEach(function(button){var form=button.closest('form');if(form)form.remove();});
[].slice.call(section.querySelectorAll('button[value="nc_connector_delete"]')).forEach(function(deleteButton){
var deleteForm=deleteButton.closest('form');var card=deleteButton.closest('details');if(!deleteForm||!card)return;
var idInput=deleteForm.querySelector('input[name="connection_id"]');if(!idInput)return;var id=idInput.value;
var actions=deleteForm.parentElement;if(!actions)return;
var text=card.textContent||'';var isLocal=text.indexOf('bestehende Legacy-Konfiguration')!==-1;
if(isLocal){
var edit=document.createElement('button');edit.type='button';edit.className='buttonLike';edit.textContent='Bearbeiten';edit.addEventListener('click',function(){openLocalEditor(id);});actions.insertBefore(edit,deleteForm);
actions.insertBefore(makePostButton('Auf WebDAV migrieren','nc_connector_migrate_start',id,'migrate'),deleteForm);
}else{
actions.insertBefore(makePostButton('Bearbeiten','nc_connector_edit_start',id,'edit'),deleteForm);
}
});
}
function initNCConnectorPolling(){ function initNCConnectorPolling(){
var section=document.getElementById('nc-connector');if(!section)return; var section=document.getElementById('nc-connector');if(!section)return;
var endpoint='plugins/bratonien_tools/nc-connector-status.php'; var endpoint='plugins/bratonien_tools/nc-connector-status.php';
var pollTimer=null;
function valueNodeForLabel(labelText){ function valueNodeForLabel(labelText){
var labels=[].slice.call(section.querySelectorAll('.bratonien-label')); var labels=[].slice.call(section.querySelectorAll('.bratonien-label'));
@@ -57,23 +226,43 @@
return null; return null;
} }
function lastResultValueNode(){
var notes=[].slice.call(section.querySelectorAll('.bratonien-base-note'));
for(var i=0;i<notes.length;i++){
var text=(notes[i].textContent||'').trim();
if(text.indexOf('Letztes Ergebnis:')===0){
return notes[i].querySelector('strong');
}
}
return null;
}
var lastRunNode=valueNodeForLabel('Letzter Lauf'); var lastRunNode=valueNodeForLabel('Letzter Lauf');
var nextRunNode=valueNodeForLabel('Nächster Lauf'); var nextRunNode=valueNodeForLabel('Nächster Lauf');
var lastResultNode=lastResultValueNode();
function render(data){
if(lastRunNode&&typeof data.last_run_label==='string'&&data.last_run_label!=='')lastRunNode.textContent=data.last_run_label;
if(nextRunNode&&typeof data.next_run_label==='string'&&data.next_run_label!=='')nextRunNode.textContent=data.next_run_label;
if(lastResultNode&&typeof data.message==='string'&&data.message!=='')lastResultNode.textContent=data.message;
}
function poll(){ function poll(){
fetch(endpoint+'?_='+Date.now(),{credentials:'same-origin',cache:'no-store'}) fetch(endpoint+'?_='+Date.now(),{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
.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(render)
if(lastRunNode&&data.last_run_label)lastRunNode.textContent=data.last_run_label;
if(nextRunNode&&data.next_run_label)nextRunNode.textContent=data.next_run_label;
})
.catch(function(){}); .catch(function(){});
} }
poll();window.setInterval(poll,5000); function schedule(){
if(pollTimer)window.clearInterval(pollTimer);
pollTimer=window.setInterval(poll,5000);
} }
function initAll(){initBratonienTabs();initNCConnectorPolling();} poll();schedule();
}
function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectionEditing();initNCConnectorPolling();}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initAll);else initAll(); if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initAll);else initAll();
})(); })();
</script> </script>

View File

@@ -32,7 +32,28 @@
<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)"> <dialog id="bratonien-nc-wizard-dialog" style="width:min(980px,calc(100vw - 3rem));max-height:88vh;overflow:auto;background:#444;color:inherit;border:1px solid #777;border-radius:4px;padding:0;box-shadow:0 18px 60px rgba(0,0,0,.55)">
<div style="padding:1.25rem 1.5rem"> <div style="padding:1.25rem 1.5rem">
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem"> <div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem">
<div><h4 style="margin:0">Neue Verbindung</h4><p class="bratonien-base-note" style="margin:.35rem 0 0"><strong>Schritt {$NC_CONNECTOR.wizard.step|escape:html} von 4</strong></p></div> <div>
<h4 style="margin:0">Neue Verbindung</h4>
<p class="bratonien-base-note" style="margin:.35rem 0 0"><strong>
{if $NC_CONNECTOR.wizard.step == 1}
Anmeldung
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
Datenbank prüfen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && !$NC_CONNECTOR.wizard.directory_selection_ready}
Speicher zuordnen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && $NC_CONNECTOR.wizard.directory_selection_ready}
Verzeichnisse auswählen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_complete}
Verbindung benennen
{elseif $NC_CONNECTOR.wizard.step == 3}
Piwigo-API
{elseif $NC_CONNECTOR.wizard.step == 4}
Abschluss
{else}
Einrichtung
{/if}
</strong></p>
</div>
<button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button> <button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button>
</div> </div>
@@ -52,30 +73,17 @@
</form> </form>
{elseif $NC_CONNECTOR.wizard.step == 2} {elseif $NC_CONNECTOR.wizard.step == 2}
<p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Der Assistent prüft jetzt, wie die freigegebenen Dateien gelesen werden können.</p> <p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Für den Datenzugriff wird die bekannte Reader-Verbindung verwendet. Verzeichnisse werden ausschließlich mit dem angemeldeten Nextcloud-Benutzer gelesen.</p>
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<span class="bratonien-label">Adresse</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong> <span class="bratonien-label">Adresse</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
<span class="bratonien-label">Version</span><strong>{if $NC_CONNECTOR.wizard.version}{$NC_CONNECTOR.wizard.version|escape:html}{else}Nicht gemeldet{/if}</strong> <span class="bratonien-label">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> <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> </div>
{if $NC_CONNECTOR.wizard.technical_stage == 'reader_credentials'} {if $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
<hr>
<h5>Datenzugriff</h5>
<p class="bratonien-base-note">Nextcloud gibt den internen Lesezugang aus Sicherheitsgründen nicht über die normale API aus. Bitte nur den dafür eingerichteten Lese-Benutzer angeben. Die üblichen Verbindungswerte prüft der Assistent selbst.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Lese-Benutzer</label><input name="nc_wizard_db_user" type="text" value="{$NC_CONNECTOR.wizard.db_user|escape:html}" required>
<label class="bratonien-label">Passwort</label><input name="nc_wizard_db_password" type="password" autocomplete="new-password" value="{$NC_CONNECTOR.wizard._db_password|escape:html}" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Automatisch prüfen</button></p>
</form>
{elseif $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
<hr> <hr>
<h5>Datenbank-Adresse prüfen</h5> <h5>Datenbank-Adresse prüfen</h5>
<p class="bratonien-base-note">Der übliche Weg hat nicht funktioniert. Deshalb werden jetzt nur die Angaben abgefragt, die davon abweichen können.</p> <p class="bratonien-base-note">Die bekannte Reader-Verbindung konnte mit der gespeicherten Adresse nicht bestätigt werden.</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} {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> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
@@ -83,16 +91,74 @@
<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">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">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">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> </div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Erneut prüfen</button></p> <p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Erneut prüfen</button></p>
</form> </form>
{elseif $NC_CONNECTOR.wizard.technical_stage == 'mounts'} {elseif $NC_CONNECTOR.wizard.technical_stage == 'mounts'}
<hr> <hr>
{if $NC_CONNECTOR.wizard.directory_selection_ready}
<h5>Verzeichnisse auswählen</h5>
<p class="bratonien-base-note">Es werden nur Verzeichnisse angezeigt, auf die <strong>{$NC_CONNECTOR.wizard.username|escape:html}</strong> in Nextcloud Zugriff hat. Mehrere Verzeichnisse können hinzugefügt werden. Bleibt die Auswahl leer, wird automatisch das Stammverzeichnis verwendet.</p>
<div class="bratonien-form-grid">
<span class="bratonien-label">Ausgewählt</span>
<div>
{if $NC_CONNECTOR.wizard.directory_selected|@count > 0}
{foreach from=$NC_CONNECTOR.wizard.directory_selected item=selected_path}
<form method="post" class="bratonien-actions" style="margin:.25rem 0" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_remove" value="{$selected_path|escape:html}">
<strong style="flex:1">{if $selected_path}{$selected_path|escape:html}{else}Stammverzeichnis{/if}</strong>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_remove">Entfernen</button>
</form>
{/foreach}
{else}
<span class="bratonien-base-note">Keine Auswahl Stammverzeichnis wird verwendet.</span>
{/if}
</div>
<span class="bratonien-label">Aktueller Ordner</span><strong>/{if $NC_CONNECTOR.wizard.directory_path}{$NC_CONNECTOR.wizard.directory_path|escape:html}{/if}</strong>
</div>
<div class="bratonien-actions" style="margin:.75rem 0">
<form method="post" 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_directory_add">Diesen Ordner hinzufügen</button>
</form>
{if $NC_CONNECTOR.wizard.directory_path}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_path" value="{$NC_CONNECTOR.wizard.directory_parent|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_browse">Eine Ebene hoch</button>
</form>
{/if}
</div>
<div style="margin:.75rem 0">
{if $NC_CONNECTOR.wizard.directory_children|@count > 0}
{foreach from=$NC_CONNECTOR.wizard.directory_children item=directory_name key=directory_path}
<form method="post" style="margin:.3rem 0" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_path" value="{$directory_path|escape:html}">
<button class="buttonLike" style="width:100%;text-align:left" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_browse">📁 {$directory_name|escape:html}</button>
</form>
{/foreach}
{else}
<p class="bratonien-base-note">Keine Unterordner vorhanden.</p>
{/if}
</div>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index}
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}">
{/foreach}
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Verzeichnisse übernehmen</button></p>
</form>
{else}
<h5>Speicherort bestätigen</h5> <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> <p class="bratonien-base-note">Die Datenquelle wurde gefunden. Ein technischer Speicherort konnte nicht automatisch zugeordnet werden.</p>
<form method="post" data-bratonien-wizard-form> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
@@ -108,29 +174,26 @@
</div> </div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Speicher prüfen</button></p> <p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Speicher prüfen</button></p>
</form> </form>
{/if}
{elseif $NC_CONNECTOR.wizard.technical_complete} {elseif $NC_CONNECTOR.wizard.technical_complete}
<hr> <hr>
<h5>Welche Freigaben sollen verwendet werden?</h5> <h5>Verbindung benennen</h5>
<p class="bratonien-base-note"><strong>Empfehlung:</strong> ein eigener Showcase-Benutzer. So bleibt die Verbindung unabhängig von persönlichen Konten.</p> <p class="bratonien-base-note">Diese Verbindung verwendet ausschließlich den in Schritt 1 angemeldeten Nextcloud-Benutzer.</p>
<form method="post" data-bratonien-wizard-form> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid"> <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_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> <span class="bratonien-label">Nextcloud-Benutzer</span><strong>{$NC_CONNECTOR.wizard.username|escape:html}</strong>
{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> </div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_select_user">Weiter</button></p> <p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_select_user">Weiter</button></p>
</form> </form>
{/if} {/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> <div class="bratonien-actions" style="margin-top:1rem">
<form method="post" 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_back">Zurück</button></form>
<form method="post" 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>
</div>
{elseif $NC_CONNECTOR.wizard.step == 3} {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> <p class="bratonien-base-note"><strong>Nextcloud ist vorbereitet.</strong> Jetzt wird der bevorzugte Piwigo-Zugang geprüft.</p>
@@ -143,13 +206,14 @@
</div> </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> <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> </form>
<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_back">Zurück</button></form>
{elseif $NC_CONNECTOR.wizard.step == 4} {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> <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"> <div class="bratonien-form-grid">
<span class="bratonien-label">Verbindung</span><strong>{$NC_CONNECTOR.wizard.connection_name|escape:html}</strong> <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">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">Nextcloud-Benutzer</span><strong>{$NC_CONNECTOR.wizard.username|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> <span class="bratonien-label">Piwigo-API</span><strong>{if $NC_CONNECTOR.wizard.api_status == 'ok'}Erfolgreich geprüft{else}Übersprungen{/if}</strong>
</div> </div>
<hr> <hr>
@@ -163,6 +227,7 @@
</div> </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> <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> </form>
<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_back">Zurück</button></form>
{/if} {/if}
</div> </div>
</dialog> </dialog>
@@ -199,14 +264,28 @@
{if $NC_CONNECTOR.connection_count > 0} {if $NC_CONNECTOR.connection_count > 0}
{foreach from=$NC_CONNECTOR.connections item=connection} {foreach from=$NC_CONNECTOR.connections item=connection}
<details style="margin:.6rem 0"> <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> <summary><strong>{$connection.display_name|escape:html}</strong> · {if $connection.enabled}aktiv{else}{$connection.takeover_state|escape:html}{/if}{if isset($connection.last_sync) && ($connection.last_sync.state == 'error' || $connection.last_sync.state == 'warning')} · Laufzeitproblem{/if}</summary>
<div style="padding:.75rem 0"> <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> <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"> <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.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} <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? Die Verbindung wird sofort aus Bratonien Tools entfernt und beim nächsten Connector-Lauf auch aus der Laufzeit entfernt. Bilder in Nextcloud oder Piwigo werden nicht gelöscht.');">Löschen</button></form>
</div> </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 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 $connection.last_sync.state == 'error'}<p class="bratonien-main-cache__warning"><strong>Laufzeitfehler:</strong> {$connection.last_sync.message|escape:html}</p>{/if}
{if $connection.last_sync.state == 'warning'}<p class="bratonien-main-cache__warning"><strong>Laufzeitwarnung:</strong> {$connection.last_sync.message|escape:html}</p>{/if}
{if $connection.last_sync.api_state == 'error'}<p class="bratonien-main-cache__warning"><strong>API:</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:</strong> {$connection.last_sync.fallback_message|escape:html}</p>{/if}
{if $connection.last_sync.error_detail}<details><summary>Technische Laufzeitdetails</summary><p class="bratonien-main-cache__warning">{$connection.last_sync.error_detail|escape:html}</p></details>{/if}
{else}
<p class="bratonien-base-note"><strong>Laufzeit:</strong> Noch kein Laufstatus für diese Verbindung vorhanden.</p>
{/if}
{if isset($connection.config.api_enabled)}
<p class="bratonien-base-note"><strong>Piwigo-Zugang dieser Verbindung:</strong> {if $connection.config.api_enabled}eigene API{elseif $connection.fallback_stored}Fallback{else}kein Zugang gespeichert{/if}</p>
{else}
<p class="bratonien-base-note"><strong>Piwigo-Zugang:</strong> bestehende Legacy-Konfiguration</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} {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> <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} {if $connection.enabled || $connection.takeover_state == 'active'}<p class="bratonien-main-cache__warning">Aktive Verbindungen können technisch nicht geändert werden.</p>{else}

173
webdav-image.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
define('BRATONIEN_TOOLS_ID', basename(__DIR__));
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php');
function bratonien_tools_webdav_image_abort($status, $message)
{
http_response_code((int)$status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function bratonien_tools_webdav_image_decrypt_secret($blob, $hex_key)
{
$hex_key = trim((string)$hex_key);
if (!preg_match('/^[a-f0-9]{64}$/', $hex_key)) return null;
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) return null;
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) return null;
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) return null;
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : null;
}
function bratonien_tools_webdav_image_quote_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
$image_id = (int)($_GET['id'] ?? 0);
if ($image_id < 1) bratonien_tools_webdav_image_abort(400, 'Bild-ID fehlt.');
$permission_condition = get_sql_condition_FandF(array('forbidden_categories'=>'category_id'), null, true);
$access_result = pwg_query('SELECT 1 FROM '.IMAGE_CATEGORY_TABLE.' WHERE image_id='.$image_id.' AND '.$permission_condition.' LIMIT 1');
if (!pwg_db_num_rows($access_result)) bratonien_tools_webdav_image_abort(403, 'Kein Zugriff auf dieses Bild.');
$source = bratonien_tools_webdav_image_source_info($image_id);
if (!$source) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Quelle für dieses Bild gefunden.');
if (!empty($_GET['preview']))
{
$preview = bratonien_tools_webdav_preview_path($source);
if ($preview)
{
$mtime = @filemtime($preview) ?: time();
$etag = sha1($preview.'|'.$mtime.'|'.(@filesize($preview) ?: 0));
header('Content-Type: '.bratonien_tools_webdav_preview_content_type($preview));
header('Content-Length: '.(string)filesize($preview));
header('ETag: "'.$etag.'"');
header('Cache-Control: private, max-age=86400, must-revalidate');
header('X-Content-Type-Options: nosniff');
$client_etag = trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? ''), " \t\r\n\"");
if ($client_etag !== '' && hash_equals($etag, $client_etag))
{
http_response_code(304);
exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'HEAD') readfile($preview);
exit;
}
}
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json, secret_blob FROM `'.$table.'` WHERE id='.(int)$source['connection_id'].' LIMIT 1');
if (!pwg_db_num_rows($result)) bratonien_tools_webdav_image_abort(404, 'WebDAV-Verbindung nicht gefunden.');
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) bratonien_tools_webdav_image_abort(500, 'WebDAV-Konfiguration ist ungültig.');
$key_result = pwg_query("SELECT value FROM ".$GLOBALS['prefixeTable']."config WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!pwg_db_num_rows($key_result)) bratonien_tools_webdav_image_abort(500, 'Connector-Schlüssel fehlt.');
$key_row = pwg_db_fetch_assoc($key_result);
$credentials = bratonien_tools_webdav_image_decrypt_secret((string)$row['secret_blob'], (string)$key_row['value']);
if (!is_array($credentials)) bratonien_tools_webdav_image_abort(500, 'WebDAV-Zugangsdaten konnten nicht gelesen werden.');
$base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/');
$user = trim((string)($credentials['nextcloud_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
$webdav_path = trim((string)$source['webdav_path'], '/');
if ($base_url === '' || $user === '' || $password === '' || $webdav_path === '')
{
bratonien_tools_webdav_image_abort(500, 'WebDAV-Bildquelle ist unvollständig.');
}
$etag = trim((string)($source['etag'] ?? ''));
if ($etag !== '')
{
header('ETag: "'.str_replace('"', '', $etag).'"');
$client_etag = trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? ''), " \t\r\n\"");
if ($client_etag !== '' && hash_equals($etag, $client_etag))
{
http_response_code(304);
exit;
}
}
header('Cache-Control: private, max-age=300, must-revalidate');
header('X-Content-Type-Options: nosniff');
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.bratonien_tools_webdav_image_quote_path($webdav_path);
$ch = curl_init($url);
$options = array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.6.1',
CURLOPT_HEADERFUNCTION => function($ch, $line)
{
$length = strlen($line);
$trimmed = trim($line);
if ($trimmed === '') return $length;
if (preg_match('#^HTTP/\S+\s+([0-9]{3})#i', $trimmed, $m))
{
http_response_code((int)$m[1]);
return $length;
}
$colon = strpos($line, ':');
if ($colon === false) return $length;
$name = strtolower(trim(substr($line, 0, $colon)));
$value = trim(substr($line, $colon + 1));
if (in_array($name, array('content-type','content-length','content-range','accept-ranges','last-modified'), true))
{
header($name.': '.$value, true);
}
return $length;
},
CURLOPT_WRITEFUNCTION => function($ch, $data)
{
echo $data;
return strlen($data);
},
);
if (!empty($_SERVER['HTTP_RANGE']))
{
$range = trim((string)$_SERVER['HTTP_RANGE']);
if (preg_match('/^bytes=(.+)$/i', $range, $m)) $options[CURLOPT_RANGE] = $m[1];
}
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'HEAD')
{
$options[CURLOPT_NOBODY] = true;
}
curl_setopt_array($ch, $options);
$ok = curl_exec($ch);
$errno = curl_errno($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($ok === false || $errno !== 0)
{
if (!headers_sent()) bratonien_tools_webdav_image_abort(502, 'Nextcloud-Bild konnte nicht geladen werden.');
exit;
}
if ($http < 200 || $http >= 400)
{
exit;
}