Compare commits

..

209 Commits

Author SHA1 Message Date
Terranom674
36349a71b1 Remove takeover state from WebDAV reconcile 2026-08-20 11:06:57 +02:00
Terranom674
df8e1f6cd0 Restore mandatory multi-version and script checks 2026-08-20 11:05:29 +02:00
Terranom674
1a6094f2e2 Record WebDAV-only connector status 2026-08-20 11:02:48 +02:00
Terranom674
40bac7b910 Document WebDAV-only connector architecture 2026-08-20 11:02:25 +02:00
Terranom674
2c10f190c3 Remove legacy connector state from admin controller 2026-08-20 11:01:43 +02:00
Terranom674
d4c195856c Remove legacy fields from WebDAV connection creation 2026-08-20 11:00:52 +02:00
Terranom674
aacbd5af1d Reduce connector model to WebDAV-only state 2026-08-20 11:00:29 +02:00
Terranom674
2e9b3ee2c6 Remove obsolete database credential field from WebDAV secrets 2026-08-20 10:59:50 +02:00
Terranom674
83f44a27b9 Remove obsolete connector WS registration and bump version 2026-08-20 10:59:28 +02:00
Terranom674
1d90754de0 Remove obsolete connector sync simulation endpoint 2026-08-20 10:51:40 +02:00
Terranom674
a4130621d0 Remove legacy local connector install tool 2026-08-20 10:50:58 +02:00
Terranom674
1f1b2dbcf6 Remove legacy connector diagnose tool 2026-08-20 10:50:47 +02:00
Terranom674
173c487474 Remove legacy local connector disable tool 2026-08-20 10:50:34 +02:00
Terranom674
edd86f59f3 Remove legacy connector normalize tool 2026-08-20 10:50:22 +02:00
Terranom674
45ab33d3c4 Remove legacy connector runtime switch 2026-08-20 10:50:14 +02:00
Terranom674
b81207e406 Remove legacy connector cleanup tool 2026-08-20 10:49:59 +02:00
Terranom674
6d33f62599 Remove legacy connector migration tool 2026-08-20 10:49:48 +02:00
Terranom674
5099b154bf Remove legacy connector cutover v2 tool 2026-08-20 10:49:35 +02:00
Terranom674
54616730ca Remove legacy connector cutover tool 2026-08-20 10:49:24 +02:00
Terranom674
09b70ee317 Remove legacy runtime status checks 2026-08-20 10:44:40 +02:00
Terranom674
01f935b769 Keep only WebDAV connection-scoped Piwigo auth helpers 2026-08-20 10:43:46 +02:00
Terranom674
03afa95dc6 Remove legacy connector UI 2026-08-20 10:43:20 +02:00
Terranom674
8588c3c1c2 Remove parallel and legacy reconcile branches 2026-08-20 10:41:14 +02:00
Terranom674
1a47e82997 Make WebDAV the sole connector mode 2026-08-20 10:40:23 +02:00
Terranom674
d2fd72bbc1 Reduce connector model to WebDAV connections 2026-08-20 10:39:58 +02:00
Terranom674
c5331d275e Remove legacy global connector authentication 2026-08-20 10:39:30 +02:00
Terranom674
7c76b4f476 Remove legacy local connector management 2026-08-20 10:39:17 +02:00
Terranom674
edfd91dc29 Remove remaining legacy connector handlers 2026-08-20 10:39:04 +02:00
Terranom674
d44f6c988f Keep connection auth WebDAV-only 2026-08-20 10:38:43 +02:00
Terranom674
471df03c76 Reduce connector wizard to WebDAV only 2026-08-20 10:38:24 +02:00
Terranom674
ea2bac5cfa Remove legacy connector tools 2026-08-20 10:35:47 +02:00
Terranom674
6ba855b76c Remove legacy wizard dispatch branches 2026-08-20 10:35:27 +02:00
Terranom674
792f7bb638 Keep wizard user scope WebDAV-only 2026-08-20 10:35:03 +02:00
Terranom674
557ad16e8a Remove legacy wizard technical flow 2026-08-20 10:34:34 +02:00
Terranom674
76f9089c00 Remove legacy wizard DB bridge 2026-08-20 10:34:21 +02:00
Terranom674
6af48796eb Remove legacy generic DB scope 2026-08-20 10:34:11 +02:00
Terranom674
b331bb087c Remove legacy takeover flow 2026-08-20 10:33:57 +02:00
Terranom674
d4ad87a6df Remove legacy local connector creation 2026-08-20 10:33:48 +02:00
Terranom674
49f30f5f17 Remove legacy Piwigo admin sync helper 2026-08-20 10:33:39 +02:00
Terranom674
ba35da9b68 Remove legacy Piwigo DB checker 2026-08-20 10:33:29 +02:00
Terranom674
d862433bdb Remove legacy selected-fileid DB manifest builder 2026-08-20 10:33:19 +02:00
Terranom674
337b5c6dc7 Remove legacy DB manifest builder 2026-08-20 10:33:04 +02:00
Terranom674
1504e1dc29 Remove legacy DB activity gate 2026-08-20 10:32:43 +02:00
Terranom674
8f0b8aabf8 Remove legacy Nextcloud DB view migration 2026-08-20 10:32:33 +02:00
Terranom674
225542acd9 Remove legacy local runtime cleanup 2026-08-20 10:32:24 +02:00
Terranom674
c152ef9808 Remove legacy local sync runtime 2026-08-20 10:32:13 +02:00
Terranom674
e5b8a01367 Remove legacy local reconcile runtime 2026-08-20 10:31:59 +02:00
Terranom674
247da33e3a Isolate Piwigo sync to WebDAV sites 2026-08-20 10:31:36 +02:00
Terranom674
198b93f4e7 Remove legacy runtime orchestration 2026-08-20 10:30:37 +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
47 changed files with 3533 additions and 4676 deletions

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

@@ -0,0 +1,82 @@
name: Syntax Check
on:
push:
pull_request:
jobs:
php-syntax:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4', '8.5']
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: PHP installieren
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
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
python3 - <<'PY'
import pathlib
import re
import sys
found = {}
for path in pathlib.Path('.').rglob('*.php'):
text = path.read_text(encoding='utf-8', errors='ignore')
for name in re.findall(r'\bfunction\s+(bratonien_tools_[A-Za-z0-9_]+)\s*\(', text):
found.setdefault(name, []).append(str(path))
duplicates = {name: paths for name, paths in found.items() if len(paths) > 1}
if duplicates:
for name, paths in sorted(duplicates.items()):
print(f'{name}: {", ".join(paths)}', file=sys.stderr)
sys.exit(1)
PY
script-syntax:
name: JavaScript, Python, Shell
runs-on: ubuntu-latest
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: JavaScript Syntax pruefen
shell: bash
run: |
set -euo pipefail
while IFS= read -r -d '' file; do
node --check "$file"
done < <(find . -type f -name '*.js' -print0)
- 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,62 @@
# Aktueller Entwicklungsstand # Aktueller Entwicklungsstand
Stand: 18.08.2026 ## Version
## Plugin **Bratonien Tools 0.9.6.2**
- Aktuelle Plugin-Version: **0.9.4.2** ## NC Connector
- Aktueller Entwicklungsblock: **NC Connector Endnutzer-Assistent und Konsolidierung**
- `0.9.3.43` markiert den abgeschlossenen vorherigen Meilenstein.
- `0.9.4.x` ist der neue Entwicklungsblock.
## Produktiver Stand Der NC Connector besitzt nur noch einen produktiven Verbindungsweg: **Nextcloud per WebDAV**.
Der lokale NC Connector ist im aktuellen Bratonien-Einsatz End-to-End funktionsfähig: ### Verbindungsmodell
- Nextcloud-Freigaben `folder` und `file` werden verarbeitet. - `adapter=remote`
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert. - `source_mode=webdav-placeholder`
- Einzeldateifreigaben werden als echte Piwigo-Orphans ohne Albumzuordnung registriert. - Nextcloud-Adresse, Benutzer und Passwort/App-Passwort werden pro Verbindung verschlüsselt gespeichert.
- Entfernte Freigaben werden aus Piwigo entfernt, Originaldateien bleiben erhalten. - Piwigo-API und Benutzername/Passwort-Fallback werden ebenfalls pro Verbindung gespeichert.
- Neue physische Connector-Alben werden privat angelegt. - Der Assistent zeigt ausschließlich die Verzeichnisse, auf die der angemeldete Nextcloud-Benutzer per WebDAV zugreifen kann.
- API-first-Synchronisierung ist produktiv für Piwigo **16.4.0** freigegeben. - Mehrere Verzeichnisse können pro Verbindung ausgewählt werden.
- Piwigo-Album- und Fotoinformationen werden nach dem direkten Sync aktualisiert.
- Der gemeinsame systemd-Timer verarbeitet aktive Verbindungen regelmäßig.
## 0.9.4.1 ### Laufzeit
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. Der gemeinsame Runner verarbeitet ausschließlich WebDAV-Verbindungen:
## 0.9.4.2 1. `runtime/reconcile-webdav.php`
2. `runtime/cleanup-webdav-piwigo.php`
3. `runtime/sync-webdav.sh` für jede vorhandene WebDAV-Konfiguration
### Verbindungsassistent Die verbindungsspezifischen Runtime-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`. Zustandsdaten liegen unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
Der Assistent wurde erneut aus Endnutzersicht konsolidiert: ### Shadow Tree und Piwigo
1. Nextcloud-Adresse + Benutzer + Passwort. `runtime/lib/build_webdav_placeholder_source.py` bildet die ausgewählten Nextcloud-Inhalte als lokale Platzhalterquelle ab. `runtime/lib/shadow_tree.py` erzeugt daraus die Dateisystemstruktur, die Piwigo als physische Site sieht.
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. Jede WebDAV-Verbindung besitzt ihre eigene Piwigo-Site. `runtime/lib/piwigo-sync.php` übergibt genau diese Site an `bratonien.nc.syncProductive` und `bratonien.nc.syncOrphans`. Es gibt keinen zusätzlichen Sync auf Site 1.
### Storage-Mappings Der Shadow Tree dient ausschließlich dazu, Piwigo die Alben und Bilder als Dateisystemstruktur bereitzustellen. Albumlogik, Dateisynchronisierung und die daraus folgenden Piwigo-Datensätze bleiben Aufgabe von Piwigo.
Das Storage-Format erlaubt jetzt einen leeren `source_prefix`. Damit kann ein kompletter Storage direkt auf einen lokalen Mount zeigen. Die frühere Fehlermeldung ### Bildauslieferung
`Storage-Zeilen muessen das Format storage_id | source_prefix | local_mount verwenden.` Die Dateien im Shadow Tree sind Platzhalter. `include/webdav_image_runtime.inc.php` erkennt die WebDAV-Zuordnung und ersetzt die Bildquelle bei der Auslieferung durch den zugehörigen Nextcloud-Inhalt. `webdav-image.php` streamt die Bilddaten serverseitig, ohne Nextcloud-Zugangsdaten an den Browser weiterzugeben.
tritt bei einem legitimen Root-Mapping nicht mehr auf. ### Löschen
### Native Aktivierung Eine WebDAV-Verbindung kann unabhängig von ihrer Reihenfolge gelöscht werden. Beim Löschen werden die zugehörige Piwigo-Site und die zugehörigen Piwigo-Datensätze entfernt. Die Originaldateien in Nextcloud bleiben unangetastet. Verwaiste WebDAV-Runtime- und Piwigo-Daten werden vom gemeinsamen Lauf bereinigt.
`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. ### Diagnose
### Dokumentation Der Status wird pro Verbindung geführt. Fehler enthalten Zeitpunkt, betroffene Verbindung, Prozesszustand und technische Details. Die Administration zeigt keine lokalen Datenbank-, Storage- oder Mount-Einstellungen mehr an.
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. ## Entfernte Connector-Komponenten
## Architektur NC Connector Nicht mehr Bestandteil des Plugins sind lokale Nextcloud-Datenbankanbindungen, PostgreSQL-Reader, Source-/Activity-Views, Storage-Mappings, Host-Mounts, lokale Reconcile-/Sync-Skripte, Cutover-/Migrationswerkzeuge und der frühere Simulations-Webservice `bratonien.nc.sync`.
- Nextcloud ist Quelle der Originalbilder. ## Prüfpflicht vor Merge
- 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 Jede Änderung wird vor dem Merge mindestens mit folgenden GitHub-Actions-Prüfungen validiert:
Priorität: - PHP-Syntax für 8.2, 8.3, 8.4 und 8.5
- Prüfung auf doppelte globale `bratonien_tools_*`-Funktionen
1. gespeicherter Piwigo-API-Key; - JavaScript-Syntax
2. bei nicht nutzbarer API gespeicherter Benutzername/Passwort-Fallback; - Python-Syntax/Compile
3. ohne beide Wege Abbruch mit Fehlerstatus. - Shell-Syntax mit `bash -n`
Produktive API-Methoden:
- `bratonien.nc.syncProductive`
- `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.
## Activity Gate und Shadow Tree
Das Activity Gate berücksichtigt:
- Aktivitätsstand;
- 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.
## Sicherheit
- Nextcloud-Originale werden nicht gelöscht.
- Connector-Zugangsdaten werden verschlüsselt gespeichert.
- Wizard-Secrets bleiben serverseitig.
- Storage-Pfade werden validiert.
- SQL-View-Namen werden validiert.
- Piwigo-API ist versionsgebunden.
- Mutierende Admin-Aktionen verwenden Post/Redirect/Get, sodass Browser-Reloads sie nicht erneut ausführen.
- Self-Updates sind an einen konkreten Commit und SHA-256 gebunden.
## Noch offen
- vollständiger Remote-Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff;
- weitere Bereinigung alter Legacy-Migrationshelfer, sobald sie nicht mehr benötigt werden;
- weitere Endnutzer-Optimierung des Assistenten auf Basis realer Fehler- und Installationspfade.
## Repository-Fallback
GitHub bleibt das führende Repository. Das private Gitea-System dient weiterhin als Mirror/Fallback.

197
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.2**
## Grundprinzip ## Grundprinzip
@@ -12,113 +12,81 @@ 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 bindet ausdrücklich ausgewählte Nextcloud-Inhalte ausschließlich per WebDAV an Piwigo an.
### 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 verschwinden aus Shadow Tree und Piwigo; die Nextcloud-Originale bleiben erhalten.
### Verbindungsassistent ### Voraussetzungen
Neue Verbindungen werden bevorzugt mit einem Endnutzer-Assistenten angelegt. Benötigt werden nur:
Ablauf: - Nextcloud-Adresse;
- Nextcloud-Benutzer bzw. App-Passwort;
- normaler WebDAV-Zugriff auf die Inhalte dieses Benutzers;
- eine normale Piwigo-Installation mit ihrer vorhandenen PHP-/Bildverarbeitungsumgebung.
1. Nextcloud-Adresse, Benutzer und Passwort eingeben. Nicht benötigt werden Nextcloud-Datenbankzugriff, `occ`-Adminzugriff, Storage-IDs, Backend-Pfade, zusätzliche Host-Mounts, FUSE, davfs oder rclone.
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-Ablauf
Die vollständige technische Maske bleibt weiterhin über **Ohne Assistent anlegen** sowie bei bestehenden Verbindungen unter **Technische Einstellungen** erreichbar. 1. Der Assistent prüft Nextcloud und liest die sichtbaren Verzeichnisse per WebDAV.
2. Ausgewählte Verzeichnisse werden rekursiv per PROPFIND eingelesen.
### Lokaler Adapter 3. Ordner, Dateiname, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie.
Der derzeit produktive Adapter verwendet: 5. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree.
6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`.
- PostgreSQL-Reader mit minimalen Leserechten; 7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert.
- Source-View `piwigo_showcase_sources`; 8. Piwigos Dateisynchronisierung erhält ausschließlich diese verbindungseigene Site.
- Activity-View `piwigo_showcase_activity`; 9. Die WebDAV-Bildzuordnung ersetzt beim Ausliefern Platzhalter-URLs durch die echte Nextcloud-Bildquelle.
- explizite Storage-Zuordnungen auf bereits vorhandene lokale Mounts; 10. `webdav-image.php` streamt benötigte Bilder serverseitig; Zugangsdaten werden dem Browser nicht offengelegt.
- 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.
### Activity Gate
Der regelmäßige Lauf berücksichtigt:
- Nextcloud-Aktivität;
- Signatur der sichtbaren Freigaben;
- Quiet-Time;
- maximale Wartezeit;
- periodischen Full-Sync;
- notwendigen Reparaturlauf bei beschädigtem Shadow Tree oder fehlenden Piwigo-Alben.
### Piwigo-Synchronisierung ### Piwigo-Synchronisierung
Der produktive Lauf ist API-first: Der Connector verwendet die vorhandene Piwigo-Dateisynchronisierung für die verbindungseigene WebDAV-Site. Der Shadow Tree ist die Dateisystemdarstellung, die Piwigo für Alben und Bilder benötigt.
1. `bratonien.nc.syncProductive` für physische Alben und Bilder; `runtime/lib/piwigo-sync.php` ruft dafür `bratonien.nc.syncProductive` auf. Die Methode führt den Piwigo-Core-Sync für die konkrete Site aus. `bratonien.nc.syncOrphans` arbeitet ebenfalls nur auf der übergebenen WebDAV-Site.
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. Die produktive Synchronisierung ist aktuell auf **Piwigo 16.4.0** abgestimmt.
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. ### Piwigo-Zugang
Der Zugang wird pro Verbindung gespeichert. Bevorzugt wird eine Piwigo-API. Wird keine API eingerichtet, kann ein Administrator-/Webmaster-Benutzer mit Passwort als Fallback verwendet werden.
### Statusanzeige
Die Administration zeigt Laufzeitstatus und Fehler pro Verbindung. Fehlerdetails bleiben der betroffenen Verbindung zugeordnet.
### Löschen einer Verbindung
Beim Löschen einer WebDAV-Verbindung werden ihre zugehörige Piwigo-Site und die dazugehörigen Piwigo-Datensätze entfernt. Nextcloud-Dateien bleiben unverändert. Runtime-, Shadowtree-, Source-, Preview- und Statusdaten werden bereinigt.
### Runtime ### Runtime
Aktive Verbindungen werden über einen gemeinsamen systemd-Timer verarbeitet: Aktive WebDAV-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. Der Lauf besteht aus:
### Statusanzeige 1. WebDAV-Verbindungen reconciliieren;
2. verwaiste WebDAV-Piwigo-Inhalte bereinigen;
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. jede vorhandene WebDAV-Verbindung synchronisieren.
### Bestehende Verbindungen
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
- Piwigo-Bildderivate gezielt leeren; Bratonien Tools kann Piwigo-Bildderivate gezielt leeren, vorhandene Bildgrößen neu erzeugen und den Cache-Aufbau als Worker-Prozess starten bzw. abbrechen. Originalbilder bleiben unangetastet.
- vorhandene Bildgrößen neu erzeugen;
- Cache-Aufbau als Worker-Prozess starten und abbrechen;
- Worker-Zahl automatisch oder manuell konfigurieren;
- Originalbilder bleiben unangetastet.
## Wasserzeichenverwaltung ## Wasserzeichenverwaltung
@@ -128,7 +96,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
@@ -153,14 +121,7 @@ Alben können in Bratonien Tools zwischen öffentlich und privat umgeschaltet we
## Geschützte Albumfreigaben ## Geschützte Albumfreigaben
Private Alben können über eigene Freigabelinks geteilt werden: Private Alben können über eigene Freigabelinks geteilt werden, optional mit Passwort und Ablaufdatum. Die Freigaben verwenden einen technischen Piwigo-Benutzer mit minimalem Albumzugriff und können widerrufen werden.
- optionales Passwort;
- optionales Ablaufdatum;
- eigener technischer Piwigo-Benutzer pro Freigabe;
- minimaler Albumzugriff;
- Hash-Speicherung von Passwörtern und Tokens;
- Widerruf und automatische Bereinigung beim Löschen eines Albums.
## Erweiterte Bildnavigation ## Erweiterte Bildnavigation
@@ -168,40 +129,28 @@ Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges B
## Selbstaktualisierung ## Selbstaktualisierung
Der integrierte Updater: Der integrierte Updater liest den Zielstand aus GitHub, bindet ein Update an einen konkreten Commit, prüft Version und SHA-256, erstellt vor dem Austausch ein Backup und verlangt Webmaster-Rechte sowie die notwendigen Servervoraussetzungen.
- liest den Zielstand aus GitHub; ## Wichtige Dateien und Verzeichnisse
- bindet ein Update an einen konkreten Commit statt an einen während des Updates beweglichen Branch-Stand;
- prüft Version und SHA-256 des Zielstands;
- erstellt vor dem Austausch ein Backup;
- nutzt Post/Redirect/Get, damit ein Browser-Reload keine schreibende Aktion erneut ausführt;
- verlangt Webmaster-Rechte, `ZipArchive` und ausreichende Schreibrechte.
## Architektur
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` WebDAV-Verbindungsmodell
- `include/nc_connector.inc.php` Datenmodell und gemeinsame Connector-Funktionen - `include/nc_connector_wizard.inc.php` Verbindungsassistent
- `include/nc_connector_wizard.inc.php` Endnutzer-Assistent und Connection-Bearbeitung - `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-Wizardablauf
- `include/nc_connector_manage.inc.php` Credential-Format, Storage-Mappings, Verifikation und Löschen - `include/nc_connector_delete_safe.inc.php` Löschen einschließlich WebDAV-Piwigo-Inhalten
- `include/nc_connector_create_api.inc.php` API-first-Verbindungserstellung - `include/nc_productive_ws.inc.php` Piwigo-Core-Dateisync
- `include/nc_connector_piwigo_api.inc.php` API-Zugang und Fallback-Verwaltung - `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung der konkreten WebDAV-Site
- `include/nc_connector_system.inc.php` Timer- und Laufzeitstatus - `include/webdav_image_runtime.inc.php` WebDAV-Bildzuordnung und URL-Filter
- `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync - `webdav-image.php` berechtigungsgeprüfter WebDAV-Bildstream
- `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung - `runtime/reconcile-webdav.php` WebDAV-Runtime-Reconcile
- `runtime/lib/activity_gate.py` Activity Gate - `runtime/cleanup-webdav-piwigo.php` Bereinigung verwaister WebDAV-Piwigo-Sites
- `runtime/lib/build_manifest.py` Auflösung der Nextcloud-Freigaben auf Storage-Mounts - `runtime/sync-webdav.sh` Ablauf einer WebDAV-Verbindung
- `runtime/lib/shadow_tree.py` atomarer Shadow Tree mit Rollback - `runtime/run-all.sh` gemeinsamer WebDAV-Runner
- `runtime/lib/piwigo-sync.php` API-first-Piwigo-Sync mit Fallback - `runtime/lib/build_webdav_placeholder_source.py` rekursiver WebDAV-Scan und Platzhalterquelle
- `runtime/run-all.sh` Multi-Connection-Runner - `runtime/lib/shadow_tree.py` atomarer Shadow Tree
- `runtime/sync.sh` Ablauf einer Verbindung - `runtime/lib/piwigo-sync.php` Piwigo-Sync mit API/Fallback
- `nc-connector-install.php` native Root-Aktivierung - `main-cache-build.php` allgemeiner Piwigo-Derivat-/Cache-Builder
- `nc-connector-disable.php` Deaktivierung
- `nc-connector-normalize.php` Normalisierung älterer aktiver Verbindungen
- `nc-connector-*-cleanup/switch/cutover` Legacy-Migrationshelfer
- `include/self_update.inc.php` Self-Updater - `include/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 +163,9 @@ 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;
- 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
`0.9.4.x` ist der aktuelle Entwicklungsblock. Der lokale NC Connector ist End-to-End produktiv im Einsatz. Der Verbindungsassistent und die Verwaltung werden weiter aus Endnutzersicht konsolidiert. Ein vollständig entfernter Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff ist noch nicht umgesetzt.

View File

@@ -119,25 +119,9 @@ foreach ($nc_connector['connections'] as &$nc_connection)
{ {
$nc_connection['last_sync'] = bratonien_tools_nc_connector_connection_last_status($nc_connection); $nc_connection['last_sync'] = bratonien_tools_nc_connector_connection_last_status($nc_connection);
$nc_connection['display_name'] = $nc_connection['name']; $nc_connection['display_name'] = $nc_connection['name'];
$storage_lines = array();
$storages = isset($nc_connection['config']['storages']) && is_array($nc_connection['config']['storages'])
? $nc_connection['config']['storages']
: array();
foreach ($storages as $storage)
{
$storage_lines[] = (string)($storage['storage_id'] ?? '').' | '.(string)($storage['source_prefix'] ?? '').' | '.(string)($storage['local_mount'] ?? '');
}
$nc_connection['storage_text'] = implode("\n", $storage_lines);
if (!empty($nc_connection['fallback_stored'])) if (!empty($nc_connection['fallback_stored']))
{ {
$nc_connection['display_name'] .= ' · Fallback gespeichert'; $nc_connection['display_name'] .= ' · Fallback gespeichert';
$nc_connection['verification_checks'][] = array(
'name' => 'Piwigo-Fallback',
'ok' => true,
'detail' => 'Benutzername/Passwort verschluesselt gespeichert',
);
} }
} }
unset($nc_connection); unset($nc_connection);
@@ -159,10 +143,6 @@ $nc_system_defaults = array(
'last_run_error_detail' => '', 'last_run_error_detail' => '',
'next_run_timestamp' => 0, 'next_run_timestamp' => 0,
'next_run_label' => 'Nicht verfügbar', 'next_run_label' => 'Nicht verfügbar',
'legacy_runtime_exists' => false,
'legacy_config_exists' => false,
'legacy_service_exists' => false,
'legacy_timer_exists' => false,
); );
$nc_connector['system'] = array_merge( $nc_connector['system'] = array_merge(
$nc_system_defaults, $nc_system_defaults,

View File

@@ -4,15 +4,6 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
/**
* NC Connector migration and verification phase.
*
* The existing /etc/piwigo-sync installation remains the production path.
* Bratonien Tools imports a copy of its configuration into its own connection
* store and verifies that copy independently. Importing and verification never
* change or stop the legacy sync.
*/
function bratonien_tools_nc_connector_table() function bratonien_tools_nc_connector_table()
{ {
if (function_exists('bratonien_tools_table')) if (function_exists('bratonien_tools_table'))
@@ -30,9 +21,8 @@ function bratonien_tools_nc_connector_ensure_table()
id int(11) NOT NULL AUTO_INCREMENT, id int(11) NOT NULL AUTO_INCREMENT,
connection_key varchar(64) NOT NULL, connection_key varchar(64) NOT NULL,
name varchar(255) NOT NULL, name varchar(255) NOT NULL,
adapter enum('local','remote') NOT NULL DEFAULT 'local', adapter enum('remote') NOT NULL DEFAULT 'remote',
enabled tinyint(1) NOT NULL DEFAULT 0, enabled tinyint(1) NOT NULL DEFAULT 1,
takeover_state enum('imported','verified','active','disabled') NOT NULL DEFAULT 'imported',
config_json mediumtext NOT NULL, config_json mediumtext NOT NULL,
secret_blob mediumtext DEFAULT NULL, secret_blob mediumtext DEFAULT NULL,
created datetime NOT NULL, created datetime NOT NULL,
@@ -64,139 +54,59 @@ function bratonien_tools_nc_connector_secret_key()
function bratonien_tools_nc_connector_encrypt_secret($plain) function bratonien_tools_nc_connector_encrypt_secret($plain)
{ {
$plain = (string)$plain; $plain = (string)$plain;
if ($plain === '') if ($plain === '') return '';
{ if (!function_exists('openssl_encrypt')) throw new RuntimeException('OpenSSL wird zum sicheren Speichern der Connector-Zugangsdaten benötigt.');
return '';
}
if (!function_exists('openssl_encrypt'))
{
throw new RuntimeException('OpenSSL wird zum sicheren Speichern der Connector-Zugangsdaten benoetigt.');
}
$iv = random_bytes(12); $iv = random_bytes(12);
$tag = ''; $tag = '';
$cipher = openssl_encrypt( $cipher = openssl_encrypt($plain, 'aes-256-gcm', bratonien_tools_nc_connector_secret_key(), OPENSSL_RAW_DATA, $iv, $tag);
$plain, if ($cipher === false) throw new RuntimeException('Connector-Zugangsdaten konnten nicht verschlüsselt werden.');
'aes-256-gcm',
bratonien_tools_nc_connector_secret_key(),
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($cipher === false)
{
throw new RuntimeException('Connector-Zugangsdaten konnten nicht verschluesselt werden.');
}
return base64_encode(json_encode(array( return base64_encode(json_encode(array(
'v' => 1, 'v'=>1,
'iv' => base64_encode($iv), 'iv'=>base64_encode($iv),
'tag' => base64_encode($tag), 'tag'=>base64_encode($tag),
'data' => base64_encode($cipher), 'data'=>base64_encode($cipher),
))); )));
} }
function bratonien_tools_nc_connector_decrypt_secret($blob) function bratonien_tools_nc_connector_decrypt_secret($blob)
{ {
$blob = trim((string)$blob); $blob = trim((string)$blob);
if ($blob === '') if ($blob === '') return '';
{ if (!function_exists('openssl_decrypt')) throw new RuntimeException('OpenSSL wird zum Lesen der Connector-Zugangsdaten benötigt.');
return '';
}
if (!function_exists('openssl_decrypt'))
{
throw new RuntimeException('OpenSSL wird zum Lesen der Connector-Zugangsdaten benoetigt.');
}
$outer = base64_decode($blob, true); $outer = base64_decode($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) throw new RuntimeException('Gespeicherte Connector-Zugangsdaten haben ein unbekanntes Format.');
{
throw new RuntimeException('Gespeicherte Connector-Zugangsdaten haben ein unbekanntes Format.');
}
$iv = base64_decode((string)($payload['iv'] ?? ''), true); $iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true); $tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true); $cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) throw new RuntimeException('Gespeicherte Connector-Zugangsdaten sind beschädigt.');
{
throw new RuntimeException('Gespeicherte Connector-Zugangsdaten sind beschaedigt.');
}
$plain = openssl_decrypt(
$cipher,
'aes-256-gcm',
bratonien_tools_nc_connector_secret_key(),
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($plain === false)
{
throw new RuntimeException('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
}
$plain = openssl_decrypt($cipher, 'aes-256-gcm', bratonien_tools_nc_connector_secret_key(), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) throw new RuntimeException('Connector-Zugangsdaten konnten nicht entschlüsselt werden.');
return (string)$plain; return (string)$plain;
} }
function bratonien_tools_nc_connector_read_config($path) function bratonien_tools_nc_connector_is_webdav(array $row)
{ {
$config = array(); $config = isset($row['config']) && is_array($row['config']) ? $row['config'] : array();
if (!is_readable($path)) return (string)($row['adapter'] ?? '') === 'remote' && (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
{
return $config;
}
$lines = @file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!is_array($lines))
{
return $config;
}
foreach ($lines as $line)
{
$line = trim($line);
if ($line === '' || $line[0] === '#')
{
continue;
}
if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches))
{
continue;
}
$value = trim($matches[2]);
$length = strlen($value);
if ($length >= 2)
{
$first = $value[0];
$last = $value[$length - 1];
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'"))
{
$value = substr($value, 1, -1);
}
}
$config[$matches[1]] = $value;
}
return $config;
} }
function bratonien_tools_nc_connector_connections() function bratonien_tools_nc_connector_connections()
{ {
bratonien_tools_nc_connector_ensure_table(); bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table(); $table = bratonien_tools_nc_connector_table();
$result = pwg_query("SELECT id, connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated FROM `$table` ORDER BY id"); $result = pwg_query("SELECT id, connection_key, name, adapter, enabled, config_json, secret_blob, created, updated FROM `$table` WHERE adapter='remote' ORDER BY id");
$connections = array(); $connections = array();
while ($row = pwg_db_fetch_assoc($result)) while ($row = pwg_db_fetch_assoc($result))
{ {
$config = json_decode((string)$row['config_json'], true); $config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
{
$config = array();
}
$verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : array();
$has_fallback = false; $has_fallback = false;
try try
@@ -205,8 +115,7 @@ function bratonien_tools_nc_connector_connections()
$credentials = json_decode($plain, true); $credentials = json_decode($plain, true);
if (is_array($credentials)) if (is_array($credentials))
{ {
$has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== '' $has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== '' && (string)($credentials['piwigo_password'] ?? '') !== '';
&& (string)($credentials['piwigo_password'] ?? '') !== '';
} }
} }
catch (Throwable $e) catch (Throwable $e)
@@ -218,17 +127,9 @@ function bratonien_tools_nc_connector_connections()
$row['id'] = (int)$row['id']; $row['id'] = (int)$row['id'];
$row['enabled'] = (bool)$row['enabled']; $row['enabled'] = (bool)$row['enabled'];
$row['config'] = $config; $row['config'] = $config;
$row['host'] = isset($config['host']) ? (string)$config['host'] : ''; $row['user'] = (string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? '');
$row['database'] = isset($config['database']) ? (string)$config['database'] : ''; $row['storage_count'] = isset($config['roots']) && is_array($config['roots']) ? count($config['roots']) : 0;
$row['user'] = isset($config['user']) ? (string)$config['user'] : '';
$row['source_view'] = isset($config['source_view']) ? (string)$config['source_view'] : '';
$row['storage_count'] = isset($config['storages']) && is_array($config['storages']) ? count($config['storages']) : 0;
$row['fallback_stored'] = $has_fallback; $row['fallback_stored'] = $has_fallback;
$row['verification'] = $verification;
$row['verified_ok'] = !empty($verification['ok']);
$row['verified_at'] = isset($verification['checked_at']) ? (string)$verification['checked_at'] : '';
$row['source_count'] = isset($verification['source_count']) ? (int)$verification['source_count'] : null;
$row['verification_checks'] = isset($verification['checks']) && is_array($verification['checks']) ? $verification['checks'] : array();
$connections[] = $row; $connections[] = $row;
} }
@@ -240,233 +141,24 @@ function bratonien_tools_nc_connector_connection($id, $with_secret = false)
bratonien_tools_nc_connector_ensure_table(); bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table(); $table = bratonien_tools_nc_connector_table();
$id = (int)$id; $id = (int)$id;
if ($id <= 0) if ($id <= 0) return null;
{
return null;
}
$columns = 'id, connection_key, name, adapter, enabled, takeover_state, config_json, created, updated'; $columns = 'id, connection_key, name, adapter, enabled, config_json, created, updated';
if ($with_secret) if ($with_secret) $columns .= ', secret_blob';
{ $result = pwg_query("SELECT $columns FROM `$table` WHERE id = $id AND adapter='remote' LIMIT 1");
$columns .= ', secret_blob'; if (!pwg_db_num_rows($result)) return null;
}
$result = pwg_query("SELECT $columns FROM `$table` WHERE id = $id LIMIT 1");
if (!pwg_db_num_rows($result))
{
return null;
}
$row = pwg_db_fetch_assoc($result); $row = pwg_db_fetch_assoc($result);
$row['id'] = (int)$row['id']; $row['id'] = (int)$row['id'];
$row['enabled'] = (bool)$row['enabled']; $row['enabled'] = (bool)$row['enabled'];
$row['config'] = json_decode((string)$row['config_json'], true); $row['config'] = json_decode((string)$row['config_json'], true);
if (!is_array($row['config'])) if (!is_array($row['config']) || (string)($row['config']['source_mode'] ?? '') !== 'webdav-placeholder') return null;
{
$row['config'] = array();
}
return $row; return $row;
} }
function bratonien_tools_nc_connector_import_bundle_path()
{
return '/tmp/bratonien-tools-nc-import.json';
}
function bratonien_tools_nc_connector_import_legacy()
{
$path = bratonien_tools_nc_connector_import_bundle_path();
if (!is_readable($path))
{
throw new RuntimeException('Kein lesbares Migrationspaket gefunden. Fuehre zuerst den angezeigten Connector-Migrationsbefehl im Piwigo-LXC aus.');
}
$raw = @file_get_contents($path);
$bundle = is_string($raw) ? json_decode($raw, true) : null;
if (!is_array($bundle) || (int)($bundle['format'] ?? 0) !== 1 || !is_array($bundle['config'] ?? null))
{
throw new RuntimeException('Das Migrationspaket ist ungueltig oder unvollstaendig.');
}
$legacy = $bundle['config'];
$required = array('NC_DB_HOST', 'NC_DB_PORT', 'NC_DB_NAME', 'NC_DB_USER', 'NC_DB_VIEW');
foreach ($required as $key)
{
if (!isset($legacy[$key]) || trim((string)$legacy[$key]) === '')
{
throw new RuntimeException('Im Migrationspaket fehlt '.$key.'.');
}
}
$password = isset($bundle['db_password']) ? (string)$bundle['db_password'] : '';
if ($password === '')
{
throw new RuntimeException('Das Migrationspaket enthaelt kein Datenbankpasswort.');
}
$config = array(
'host' => (string)$legacy['NC_DB_HOST'],
'port' => (string)$legacy['NC_DB_PORT'],
'database' => (string)$legacy['NC_DB_NAME'],
'user' => (string)$legacy['NC_DB_USER'],
'source_view' => (string)$legacy['NC_DB_VIEW'],
'activity_view' => isset($legacy['NC_ACTIVITY_VIEW']) ? (string)$legacy['NC_ACTIVITY_VIEW'] : 'piwigo_showcase_activity',
'gallery_root' => isset($legacy['GALLERY_ROOT']) ? (string)$legacy['GALLERY_ROOT'] : '',
'state_dir' => isset($legacy['STATE_DIR']) ? (string)$legacy['STATE_DIR'] : '',
'status_file' => isset($legacy['STATUS_FILE']) ? (string)$legacy['STATUS_FILE'] : '',
'quiet_seconds' => isset($legacy['QUIET_SECONDS']) ? (int)$legacy['QUIET_SECONDS'] : 120,
'max_wait_seconds' => isset($legacy['MAX_WAIT_SECONDS']) ? (int)$legacy['MAX_WAIT_SECONDS'] : 900,
'full_sync_seconds' => isset($legacy['FULL_SYNC_SECONDS']) ? (int)$legacy['FULL_SYNC_SECONDS'] : 86400,
'storages' => isset($bundle['storages']) && is_array($bundle['storages']) ? $bundle['storages'] : array(),
'imported_from' => '/etc/piwigo-sync/piwigo.conf',
'legacy_sync_enabled' => isset($legacy['PIWIGO_SYNC_ENABLED']) && (string)$legacy['PIWIGO_SYNC_ENABLED'] === '1',
);
$key_material = $config['host'].'|'.$config['database'].'|'.$config['user'].'|'.$config['source_view'];
$connection_key = 'legacy-'.substr(hash('sha256', $key_material), 0, 24);
$table = bratonien_tools_nc_connector_table();
bratonien_tools_nc_connector_ensure_table();
$now = date('Y-m-d H:i:s');
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($password);
$escaped_key = pwg_db_real_escape_string($connection_key);
$existing = pwg_query("SELECT id FROM `$table` WHERE connection_key = '$escaped_key' LIMIT 1");
if (pwg_db_num_rows($existing))
{
$row = pwg_db_fetch_assoc($existing);
$id = (int)$row['id'];
pwg_query("UPDATE `$table` SET
name = 'Bestehende Nextcloud',
adapter = 'local',
enabled = 0,
takeover_state = 'imported',
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");
}
else
{
pwg_query("INSERT INTO `$table`
(connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated)
VALUES (
'$escaped_key',
'Bestehende Nextcloud',
'local',
0,
'imported',
'".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)."'
)");
}
@unlink($path);
return array(
'message' => 'Bestehende Nextcloud-Verbindung wurde in den NC Connector importiert. Der produktive Legacy-Sync bleibt unveraendert aktiv; die importierte Verbindung ist noch nicht aktiviert.',
);
}
function bratonien_tools_nc_connector_view_name($name)
{
$name = trim((string)$name);
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/', $name))
{
throw new RuntimeException('Ungueltiger PostgreSQL-View-Name in der Connector-Konfiguration.');
}
$parts = explode('.', $name);
$quoted = array();
foreach ($parts as $part)
{
$quoted[] = '"'.str_replace('"', '""', $part).'"';
}
return implode('.', $quoted);
}
function bratonien_tools_nc_connector_psql($config, $password, $query)
{
$psql = '/usr/bin/psql';
if (!is_executable($psql))
{
throw new RuntimeException('PostgreSQL-Client /usr/bin/psql ist nicht installiert oder nicht ausfuehrbar.');
}
if (!function_exists('proc_open'))
{
throw new RuntimeException('PHP-Funktion proc_open ist deaktiviert; Connector-Verifikation kann nicht ausgefuehrt werden.');
}
$command = array(
$psql,
'-X', '-A', '-t',
'-v', 'ON_ERROR_STOP=1',
'-h', (string)$config['host'],
'-p', (string)$config['port'],
'-U', (string)$config['user'],
'-d', (string)$config['database'],
'-c', (string)$query,
);
$descriptors = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$env = array(
'PGPASSWORD' => (string)$password,
'PGCONNECT_TIMEOUT' => '5',
'LC_ALL' => 'C',
);
$process = @proc_open($command, $descriptors, $pipes, null, $env);
if (!is_resource($process))
{
throw new RuntimeException('PostgreSQL-Pruefprozess konnte nicht gestartet werden.');
}
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
$stdout = trim((string)$stdout);
$stderr = trim((string)$stderr);
if ($exit !== 0)
{
$detail = $stderr !== '' ? $stderr : 'psql Exit-Code '.$exit;
throw new RuntimeException('PostgreSQL-Abfrage fehlgeschlagen: '.$detail);
}
return $stdout;
}
function bratonien_tools_nc_connector_mount_points()
{
$mounts = array();
$content = @file_get_contents('/proc/self/mountinfo');
if (!is_string($content))
{
return $mounts;
}
foreach (preg_split('/\r\n|\r|\n/', $content) as $line)
{
if ($line === '') continue;
$parts = explode(' ', $line);
if (count($parts) < 5) continue;
$mount = str_replace(array('\\040','\\011','\\012','\\134'), array(' ',"\t","\n",'\\'), $parts[4]);
$mounts[rtrim($mount, '/')] = true;
}
return $mounts;
}
function bratonien_tools_nc_connector_status() function bratonien_tools_nc_connector_status()
{ {
$connections = bratonien_tools_nc_connector_connections(); $connections = bratonien_tools_nc_connector_connections();
$legacy_config = '/etc/piwigo-sync/piwigo.conf';
$legacy_present = is_readable($legacy_config);
$active_count = 0; $active_count = 0;
foreach ($connections as $connection) foreach ($connections as $connection)
{ {
@@ -474,11 +166,9 @@ function bratonien_tools_nc_connector_status()
} }
return array( return array(
'phase' => 'native-runtime', 'phase'=>'webdav',
'legacy_config_path' => $legacy_config, 'connections'=>$connections,
'legacy_present' => $legacy_present, 'connection_count'=>count($connections),
'connections' => $connections, 'active_count'=>$active_count,
'connection_count' => count($connections),
'active_count' => $active_count,
); );
} }

View File

@@ -1,246 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_api_credentials()
{
global $conf;
$blob = isset($conf['bratonien_nc_piwigo_api']) ? trim((string)$conf['bratonien_nc_piwigo_api']) : '';
if ($blob === '')
{
return array('key_id'=>'', 'key_secret'=>'');
}
try
{
$plain = bratonien_tools_nc_connector_decrypt_secret($blob);
$decoded = json_decode($plain, true);
if (!is_array($decoded))
{
return array('key_id'=>'', 'key_secret'=>'');
}
return array(
'key_id' => (string)($decoded['key_id'] ?? ''),
'key_secret' => (string)($decoded['key_secret'] ?? ''),
);
}
catch (Throwable $e)
{
return array('key_id'=>'', 'key_secret'=>'');
}
}
function bratonien_tools_nc_api_credentials_store($key_id, $key_secret)
{
global $conf;
$key_id = trim((string)$key_id);
$key_secret = trim((string)$key_secret);
if ($key_id === '' || $key_secret === '')
{
throw new RuntimeException('API-Schluessel-ID und Geheimnis muessen angegeben werden.');
}
$payload = json_encode(array(
'v' => 1,
'key_id' => $key_id,
'key_secret' => $key_secret,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($payload))
{
throw new RuntimeException('API-Zugangsdaten konnten nicht serialisiert werden.');
}
$blob = bratonien_tools_nc_connector_encrypt_secret($payload);
conf_update_param('bratonien_nc_piwigo_api', $blob);
$conf['bratonien_nc_piwigo_api'] = $blob;
}
function bratonien_tools_nc_connector_api_delete()
{
global $conf;
conf_update_param('bratonien_nc_piwigo_api', '');
$conf['bratonien_nc_piwigo_api'] = '';
return array('message'=>'Gespeicherte Piwigo-API-Zugangsdaten wurden geloescht.');
}
function bratonien_tools_nc_connector_auth_credentials($connection)
{
$credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? '');
return array(
'db_password' => (string)($credentials['db_password'] ?? ''),
'piwigo_user' => (string)($credentials['piwigo_user'] ?? ''),
'piwigo_password' => (string)($credentials['piwigo_password'] ?? ''),
);
}
function bratonien_tools_nc_connector_fallback_save()
{
$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 fuer den Fallback muessen 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_auth_credentials($connection);
if ($credentials['db_password'] === '')
{
throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.');
}
$blob = bratonien_tools_nc_connector_encrypt_credentials(
$credentials['db_password'],
$username,
$password
);
$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)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Piwigo-Benutzername und Passwort wurden verschluesselt als API-Fallback gespeichert.');
}
function bratonien_tools_nc_connector_fallback_delete()
{
$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_auth_credentials($connection);
if ($credentials['db_password'] === '')
{
throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.');
}
$blob = bratonien_tools_nc_connector_encrypt_credentials($credentials['db_password'], '', '');
$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)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Gespeicherter Benutzername/Passwort-Fallback wurde geloescht.');
}
function bratonien_tools_nc_connector_fallback_http_request($url, array $fields, $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 => 900,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-NC-Fallback/'.(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);
curl_close($ch);
if ($body === false || $errno !== 0)
{
throw new RuntimeException('Piwigo-Fallback konnte nicht ausgefuehrt werden: '.$error);
}
if ($http < 200 || $http >= 300)
{
throw new RuntimeException('Piwigo-Fallback antwortete mit HTTP '.$http.'.');
}
return (string)$body;
}
function bratonien_tools_nc_connector_fallback_once()
{
if (!function_exists('curl_init'))
{
throw new RuntimeException('cURL ist in PHP nicht verfuegbar.');
}
$username = trim((string)($_POST['nc_fallback_user'] ?? ''));
$password = (string)($_POST['nc_fallback_password'] ?? '');
if ($username === '' || $password === '')
{
throw new RuntimeException('Benutzername und Passwort fuer den einmaligen Fallback muessen angegeben werden.');
}
$base = rtrim(get_absolute_root_url(true), '/');
$cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-fallback-');
if ($cookie_file === false)
{
throw new RuntimeException('Temporare Fallback-Sitzung konnte nicht angelegt werden.');
}
try
{
$login_body = bratonien_tools_nc_connector_fallback_http_request(
$base.'/ws.php?format=json',
array(
'method'=>'pwg.session.login',
'username'=>$username,
'password'=>$password,
),
$cookie_file
);
$login = json_decode($login_body, true);
if (!is_array($login) || ($login['stat'] ?? '') !== 'ok')
{
throw new RuntimeException('Piwigo-Anmeldung fuer den einmaligen Fallback wurde abgelehnt.');
}
bratonien_tools_nc_connector_fallback_http_request(
$base.'/admin.php?page=site_update&site=1',
array(
'sync'=>'files',
'display_info'=>1,
'privacy_level'=>0,
'sync_meta'=>1,
'simulate'=>0,
'subcats-included'=>1,
'bratonien_connector'=>1,
'submit'=>1,
),
$cookie_file
);
$orphan_body = bratonien_tools_nc_connector_fallback_http_request(
$base.'/ws.php?format=json',
array(
'method'=>'bratonien.nc.syncOrphans',
'site_id'=>1,
'simulate'=>0,
),
$cookie_file
);
$orphan = json_decode($orphan_body, true);
if (!is_array($orphan) || ($orphan['stat'] ?? '') !== 'ok')
{
throw new RuntimeException('Orphan-Synchronisierung im einmaligen Fallback wurde abgelehnt.');
}
}
finally
{
@unlink($cookie_file);
}
return array('message'=>'Einmaliger Benutzername/Passwort-Fallback wurde ausgefuehrt. Die Zugangsdaten wurden nicht gespeichert.');
}

View File

@@ -0,0 +1,90 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_scoped_secret(array $connection)
{
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
if ((string)($connection['adapter'] ?? '') !== 'remote' || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder')
{
throw new RuntimeException('Die Verbindung ist keine WebDAV-Verbindung.');
}
$plain = bratonien_tools_nc_connector_decrypt_secret($connection['secret_blob'] ?? '');
$decoded = json_decode($plain, true);
if (!is_array($decoded))
{
throw new RuntimeException('WebDAV-Zugangsdaten haben ein unbekanntes Format.');
}
return array(
'v'=>3,
'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'] ?? ''),
);
}
function bratonien_tools_nc_connector_store_scoped_secret($id, array $connection, array $credentials)
{
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
if ((string)($connection['adapter'] ?? '') !== 'remote' || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder')
{
throw new RuntimeException('Die Verbindung ist keine WebDAV-Verbindung.');
}
if (trim((string)($credentials['nextcloud_user'] ?? '')) === '' || (string)($credentials['nextcloud_password'] ?? '') === '')
{
throw new RuntimeException('Die Nextcloud-Zugangsdaten der WebDAV-Verbindung fehlen.');
}
$credentials['v'] = 3;
unset($credentials['db_password']);
$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);
$config['piwigo_auth'] = 'connection-scoped';
$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.');
bratonien_tools_nc_connector_validate_fallback_credentials($username, $password);
$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 WebDAV-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 WebDAV-Verbindung wurde gelöscht. Ein vorhandener API-Zugang blieb unverändert.');
}

View File

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

View File

@@ -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

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

View File

@@ -7,10 +7,7 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node) function bratonien_tools_nc_connector_xml_value(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)
@@ -18,16 +15,10 @@ function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node)
$value = bratonien_tools_nc_connector_xml_value($child); $value = bratonien_tools_nc_connector_xml_value($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;
} }
@@ -35,15 +26,9 @@ function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node)
function bratonien_tools_nc_connector_decode_api_response($body, $content_type = '') function bratonien_tools_nc_connector_decode_api_response($body, $content_type = '')
{ {
$decoded = json_decode((string)$body, true); $decoded = json_decode((string)$body, true);
if (is_array($decoded)) if (is_array($decoded)) return $decoded;
{
return $decoded;
}
if (!function_exists('simplexml_load_string')) if (!function_exists('simplexml_load_string')) throw new RuntimeException('Piwigo-API lieferte XML, aber SimpleXML ist in PHP nicht verfügbar.');
{
throw new RuntimeException('Piwigo-API lieferte XML, aber SimpleXML ist in PHP 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);
@@ -52,7 +37,7 @@ function bratonien_tools_nc_connector_decode_api_response($body, $content_type =
if ($xml === false || $xml->getName() !== 'rsp') if ($xml === false || $xml->getName() !== 'rsp')
{ {
$detail = $content_type !== '' ? ' Antworttyp: '.$content_type.'.' : ''; $detail = $content_type !== '' ? ' Antworttyp: '.$content_type.'.' : '';
throw new RuntimeException('Piwigo-API lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.'.$detail); throw new RuntimeException('Piwigo-API lieferte weder gültiges JSON noch eine gültige XML-Webservice-Antwort.'.$detail);
} }
$response = array('stat'=>(string)$xml['stat']); $response = array('stat'=>(string)$xml['stat']);
@@ -61,27 +46,17 @@ function bratonien_tools_nc_connector_decode_api_response($body, $content_type =
$value = bratonien_tools_nc_connector_xml_value($child); $value = bratonien_tools_nc_connector_xml_value($child);
if (array_key_exists($name, $response)) if (array_key_exists($name, $response))
{ {
if (!is_array($response[$name]) || !array_is_list($response[$name])) if (!is_array($response[$name]) || !array_is_list($response[$name])) $response[$name] = array($response[$name]);
{
$response[$name] = array($response[$name]);
}
$response[$name][] = $value; $response[$name][] = $value;
} }
else else $response[$name] = $value;
{
$response[$name] = $value;
}
} }
return $response; return $response;
} }
function bratonien_tools_nc_connector_api_payload(array $decoded) function bratonien_tools_nc_connector_api_payload(array $decoded)
{ {
if (array_key_exists('result', $decoded)) if (array_key_exists('result', $decoded)) return $decoded['result'];
{
return $decoded['result'];
}
unset($decoded['stat']); unset($decoded['stat']);
return $decoded; return $decoded;
} }
@@ -91,66 +66,42 @@ function bratonien_tools_nc_connector_collect_method_names($value, array &$metho
if (is_string($value)) if (is_string($value))
{ {
$value = trim($value); $value = trim($value);
if ($value !== '' && preg_match('/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/', $value)) if ($value !== '' && preg_match('/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/', $value)) $methods[$value] = true;
{
$methods[$value] = true;
}
return;
}
if (!is_array($value))
{
return; return;
} }
if (!is_array($value)) return;
if (isset($value['name']) && is_string($value['name'])) if (isset($value['name']) && is_string($value['name']))
{ {
$name = trim($value['name']); $name = trim($value['name']);
if ($name !== '') if ($name !== '') $methods[$name] = true;
{
$methods[$name] = true;
}
}
foreach ($value as $entry)
{
bratonien_tools_nc_connector_collect_method_names($entry, $methods);
} }
foreach ($value as $entry) bratonien_tools_nc_connector_collect_method_names($entry, $methods);
} }
function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, $method) function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, $method)
{ {
if (!function_exists('curl_init')) if (!function_exists('curl_init')) throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der API-Key-Test kann nicht ausgeführt werden.');
{
throw new RuntimeException('cURL ist in PHP nicht verfuegbar. Der API-Key-Test kann nicht ausgefuehrt werden.');
}
$api_key_id = trim((string)$api_key_id); $api_key_id = trim((string)$api_key_id);
$api_key_secret = trim((string)$api_key_secret); $api_key_secret = trim((string)$api_key_secret);
if ($api_key_id === '' || $api_key_secret === '') if ($api_key_id === '' || $api_key_secret === '') throw new RuntimeException('API-Schlüssel-ID und Geheimnis müssen angegeben werden.');
{
throw new RuntimeException('API-Schluessel-ID und Geheimnis muessen angegeben werden.');
}
$api_key = $api_key_id.':'.$api_key_secret;
$url = rtrim(get_absolute_root_url(true), '/').'/ws.php'; $url = rtrim(get_absolute_root_url(true), '/').'/ws.php';
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt_array($ch, array( curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER=>true,
CURLOPT_POST => true, CURLOPT_POST=>true,
CURLOPT_POSTFIELDS => http_build_query(array( CURLOPT_POSTFIELDS=>http_build_query(array('method'=>(string)$method,'format'=>'json')),
'method' => (string)$method, CURLOPT_HTTPHEADER=>array(
'format' => 'json', 'X-PIWIGO-API: '.$api_key_id.':'.$api_key_secret,
)),
CURLOPT_HTTPHEADER => array(
'X-PIWIGO-API: '.$api_key,
'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',
), ),
CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_CONNECTTIMEOUT=>10,
CURLOPT_TIMEOUT => 20, CURLOPT_TIMEOUT=>20,
CURLOPT_FOLLOWLOCATION => false, CURLOPT_FOLLOWLOCATION=>false,
CURLOPT_USERAGENT => 'Bratonien-Tools-NC-Connector/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'), CURLOPT_USERAGENT=>'Bratonien-Tools-NC-WebDAV/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
)); ));
$body = curl_exec($ch); $body = curl_exec($ch);
@@ -160,14 +111,8 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s
$content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE); $content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch); curl_close($ch);
if ($body === false || $errno !== 0) if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-API konnte nicht erreicht werden: '.$error);
{ if ($http_code < 200 || $http_code >= 300) throw new RuntimeException('Piwigo-API antwortete mit HTTP '.$http_code.'.');
throw new RuntimeException('Piwigo-API konnte nicht erreicht werden: '.$error);
}
if ($http_code < 200 || $http_code >= 300)
{
throw new RuntimeException('Piwigo-API antwortete mit HTTP '.$http_code.'.');
}
$decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $content_type); $decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $content_type);
if (($decoded['stat'] ?? '') !== 'ok') if (($decoded['stat'] ?? '') !== 'ok')
@@ -175,71 +120,63 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s
$message = (string)($decoded['message'] ?? $decoded['err'] ?? 'API-Aufruf wurde abgelehnt.'); $message = (string)($decoded['message'] ?? $decoded['err'] ?? 'API-Aufruf wurde abgelehnt.');
throw new RuntimeException($message); throw new RuntimeException($message);
} }
return bratonien_tools_nc_connector_api_payload($decoded); return bratonien_tools_nc_connector_api_payload($decoded);
} }
function bratonien_tools_nc_connector_piwigo_api_test() function bratonien_tools_nc_connector_validate_fallback_credentials($username, $password)
{ {
$api_key_id = trim((string)($_POST['nc_piwigo_api_key_id'] ?? '')); if (!function_exists('curl_init')) throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der Piwigo-Fallback kann nicht geprüft werden.');
$api_key_secret = trim((string)($_POST['nc_piwigo_api_key_secret'] ?? ''));
if ($api_key_id === '') $username = trim((string)$username);
$password = (string)$password;
if ($username === '' || $password === '') throw new RuntimeException('Piwigo-Benutzername und Passwort müssen angegeben werden.');
$cookie_file = tempnam(sys_get_temp_dir(), 'br-pwg-auth-');
if ($cookie_file === false) throw new RuntimeException('Temporäre Piwigo-Sitzung konnte nicht angelegt werden.');
try
{ {
throw new RuntimeException('Piwigo-API-Schluessel-ID fehlt.'); $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-WebDAV/'.(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 geprüft werden: '.$error);
if ($http < 200 || $http >= 300) throw new RuntimeException('Piwigo-Fallback-Prüfung 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.');
} }
if ($api_key_secret === '') return bratonien_tools_nc_connector_api_payload($decoded);
{ };
throw new RuntimeException('Piwigo-API-Geheimnis fehlt.');
$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, gehört aber keinem Administrator/Webmaster.');
return array('username'=>(string)($status['username'] ?? $username),'status'=>$role);
} }
finally
$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 Benutzerstatus geliefert.'); @unlink($cookie_file);
} }
$username = (string)($status['username'] ?? $status['user'] ?? '');
$user_status = strtolower((string)($status['status'] ?? ''));
$is_admin = in_array($user_status, array('admin', 'webmaster'), true);
if (!$is_admin)
{
throw new RuntimeException('Der API-Key funktioniert, gehoert aber keinem Piwigo-Administrator/Webmaster. Er wurde nicht gespeichert.');
}
$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);
$methods = array_keys($method_map);
sort($methods, SORT_STRING);
$required_methods = array('bratonien.nc.syncProductive', 'bratonien.nc.syncOrphans');
$missing = array_values(array_diff($required_methods, $methods));
if ($missing)
{
throw new RuntimeException('Der API-Key ist gueltig, aber die benoetigten Bratonien-Sync-Methoden fehlen: '.implode(', ', $missing).'.');
}
bratonien_tools_nc_api_credentials_store($api_key_id, $api_key_secret);
$sync_candidates = array_values(array_filter($methods, function($method)
{
return preg_match('/sync|synchron|site/i', (string)$method) === 1;
}));
$result = array(
'ok' => true,
'username' => $username !== '' ? $username : 'nicht gemeldet',
'status' => $user_status !== '' ? $user_status : 'nicht gemeldet',
'admin' => true,
'method_count' => count($methods),
'sync_candidates' => $sync_candidates,
'sync_api_detected' => true,
'stored' => true,
'conclusion' => 'Der API-Key ist als bevorzugter Connector-Zugang gespeichert. Produktive API-Synchronisierung bleibt auf die im Plugin freigegebene Piwigo-Version begrenzt; bei einer nicht freigegebenen Version greift nur der konfigurierte Benutzername/Passwort-Fallback.',
);
return array(
'message' => 'Piwigo-API-Key wurde geprueft und verschluesselt als bevorzugter Connector-Zugang gespeichert.',
'nc_piwigo_api_test' => $result,
);
} }

View File

@@ -6,23 +6,17 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_nc_connector_systemctl_value(array $args) function bratonien_tools_nc_connector_systemctl_value(array $args)
{ {
if (!function_exists('proc_open')) if (!function_exists('proc_open')) return '';
{
return '';
}
$command = array_merge(array('/usr/bin/systemctl'), $args); $command = array_merge(array('/usr/bin/systemctl'), $args);
$spec = array( $spec = array(
0 => array('file', '/dev/null', 'r'), 0=>array('file','/dev/null','r'),
1 => array('pipe', 'w'), 1=>array('pipe','w'),
2 => array('pipe', 'w'), 2=>array('pipe','w'),
); );
$environment = array_merge($_ENV, array('LC_ALL'=>'C', 'LANG'=>'C')); $environment = array_merge($_ENV, array('LC_ALL'=>'C','LANG'=>'C'));
$process = @proc_open($command, $spec, $pipes, null, $environment); $process = @proc_open($command, $spec, $pipes, null, $environment);
if (!is_resource($process)) if (!is_resource($process)) return '';
{
return '';
}
$stdout = stream_get_contents($pipes[1]); $stdout = stream_get_contents($pipes[1]);
stream_get_contents($pipes[2]); stream_get_contents($pipes[2]);
fclose($pipes[1]); fclose($pipes[1]);
@@ -34,10 +28,7 @@ function bratonien_tools_nc_connector_systemctl_value(array $args)
function bratonien_tools_nc_connector_parse_systemd_time($value) function bratonien_tools_nc_connector_parse_systemd_time($value)
{ {
$value = trim((string)$value); $value = trim((string)$value);
if ($value === '' || strtolower($value) === 'n/a') if ($value === '' || strtolower($value) === 'n/a') return 0;
{
return 0;
}
$parsed = strtotime($value); $parsed = strtotime($value);
return $parsed === false ? 0 : (int)$parsed; return $parsed === false ? 0 : (int)$parsed;
} }
@@ -45,106 +36,52 @@ function bratonien_tools_nc_connector_parse_systemd_time($value)
function bratonien_tools_nc_connector_monotonic_to_timestamp($value) function bratonien_tools_nc_connector_monotonic_to_timestamp($value)
{ {
$value = trim((string)$value); $value = trim((string)$value);
if ($value === '' || $value === '0' || strtolower($value) === 'n/a') if ($value === '' || $value === '0' || strtolower($value) === 'n/a') return 0;
{ if (!preg_match('/^([0-9]+)(?:us)?$/', $value, $matches)) return 0;
return 0;
}
if (preg_match('/^([0-9]+)(?:us)?$/', $value, $matches))
{
$next_boot_seconds = ((float)$matches[1]) / 1000000; $next_boot_seconds = ((float)$matches[1]) / 1000000;
}
else
{
return 0;
}
$uptime_raw = @file_get_contents('/proc/uptime'); $uptime_raw = @file_get_contents('/proc/uptime');
if (!is_string($uptime_raw) || !preg_match('/^([0-9]+(?:\.[0-9]+)?)/', trim($uptime_raw), $uptime_match)) if (!is_string($uptime_raw) || !preg_match('/^([0-9]+(?:\.[0-9]+)?)/', trim($uptime_raw), $uptime_match)) return 0;
{
return 0;
}
$remaining = $next_boot_seconds - (float)$uptime_match[1]; $remaining = $next_boot_seconds - (float)$uptime_match[1];
if ($remaining < -1) if ($remaining < -1) return 0;
{
return 0;
}
return (int)round(time() + max(0, $remaining)); return (int)round(time() + max(0, $remaining));
} }
function bratonien_tools_nc_connector_next_from_timer_list($timer) function bratonien_tools_nc_connector_next_from_timer_list($timer)
{ {
$line = bratonien_tools_nc_connector_systemctl_value(array( $line = bratonien_tools_nc_connector_systemctl_value(array('list-timers','--all','--no-pager','--no-legend',$timer));
'list-timers', '--all', '--no-pager', '--no-legend', $timer, if ($line === '') return 0;
));
if ($line === '')
{
return 0;
}
$first_line = trim((string)strtok($line, "\n")); $first_line = trim((string)strtok($line, "\n"));
if (preg_match('/^(\S+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+\S+)/', $first_line, $matches)) if (!preg_match('/^(\S+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+\S+)/', $first_line, $matches)) return 0;
{
$parsed = strtotime($matches[1]); $parsed = strtotime($matches[1]);
return $parsed === false ? 0 : (int)$parsed; return $parsed === false ? 0 : (int)$parsed;
}
return 0;
} }
function bratonien_tools_nc_connector_connection_last_status(array $connection) function bratonien_tools_nc_connector_connection_last_status(array $connection)
{ {
$empty = array( $empty = array(
'timestamp'=>0, 'timestamp'=>0,'label'=>'Nicht verfügbar','state'=>'','message'=>'','auth_mode'=>'',
'label'=>'Nicht verfügbar', 'api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>'',
'state'=>'',
'message'=>'',
'auth_mode'=>'',
'api_state'=>'',
'api_message'=>'',
'fallback_state'=>'',
'fallback_message'=>'',
'error_detail'=>'',
); );
$connection_id = (int)($connection['id'] ?? 0); $connection_id = (int)($connection['id'] ?? 0);
$candidates = array(); $candidates = array();
if ($connection_id > 0) if ($connection_id > 0) $candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json';
{
$candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json';
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array(); $config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/'); $state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir === '' && $connection_id > 0) if ($state_dir === '' && $connection_id > 0) $state_dir = '/var/lib/bratonien-tools/nc-connector/connection-'.$connection_id;
{ if ($state_dir !== '') $candidates[] = $state_dir.'/connector-status.json';
$state_dir = '/var/lib/bratonien-tools/nc-connector/connection-'.$connection_id;
}
if ($state_dir !== '')
{
$candidates[] = $state_dir.'/connector-status.json';
}
$decoded = null; $decoded = null;
foreach ($candidates as $candidate) foreach ($candidates as $candidate)
{ {
if (!is_readable($candidate)) if (!is_readable($candidate)) continue;
{
continue;
}
$value = json_decode((string)@file_get_contents($candidate), true); $value = json_decode((string)@file_get_contents($candidate), true);
if (is_array($value)) if (is_array($value)) { $decoded = $value; break; }
{
$decoded = $value;
break;
}
}
if (!is_array($decoded))
{
return $empty;
} }
if (!is_array($decoded)) return $empty;
$timestamp = (int)($decoded['timestamp'] ?? 0); $timestamp = (int)($decoded['timestamp'] ?? 0);
$api = isset($decoded['api']) && is_array($decoded['api']) ? $decoded['api'] : array(); $api = isset($decoded['api']) && is_array($decoded['api']) ? $decoded['api'] : array();
@@ -166,22 +103,12 @@ function bratonien_tools_nc_connector_connection_last_status(array $connection)
function bratonien_tools_nc_connector_last_status(array $connections) function bratonien_tools_nc_connector_last_status(array $connections)
{ {
$latest = array('timestamp'=>0, 'state'=>'', 'message'=>'', 'auth_mode'=>'', 'api_state'=>'', 'api_message'=>'', 'fallback_state'=>'', 'fallback_message'=>'', 'error_detail'=>''); $latest = array('timestamp'=>0,'state'=>'','message'=>'','auth_mode'=>'','api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>'');
foreach ($connections as $connection) foreach ($connections as $connection)
{ {
if (empty($connection['enabled']) || (string)($connection['takeover_state'] ?? '') !== 'active')
{
continue;
}
$status = bratonien_tools_nc_connector_connection_last_status($connection); $status = bratonien_tools_nc_connector_connection_last_status($connection);
if ((int)$status['timestamp'] >= (int)$latest['timestamp']) if ((int)$status['timestamp'] >= (int)$latest['timestamp']) $latest = $status;
{
$latest = $status;
} }
}
return $latest; return $latest;
} }
@@ -190,49 +117,40 @@ function bratonien_tools_nc_connector_system_status(array $connections = array()
$timer = 'bratonien-nc-connector.timer'; $timer = 'bratonien-nc-connector.timer';
$service = 'bratonien-nc-connector.service'; $service = 'bratonien-nc-connector.service';
$next_realtime_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $timer, '--property=NextElapseUSecRealtime', '--value')); $next_realtime_raw = bratonien_tools_nc_connector_systemctl_value(array('show',$timer,'--property=NextElapseUSecRealtime','--value'));
$next_timestamp = bratonien_tools_nc_connector_parse_systemd_time($next_realtime_raw); $next_timestamp = bratonien_tools_nc_connector_parse_systemd_time($next_realtime_raw);
if ($next_timestamp <= 0) if ($next_timestamp <= 0)
{ {
$next_monotonic_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $timer, '--property=NextElapseUSecMonotonic', '--value')); $next_monotonic_raw = bratonien_tools_nc_connector_systemctl_value(array('show',$timer,'--property=NextElapseUSecMonotonic','--value'));
$next_timestamp = bratonien_tools_nc_connector_monotonic_to_timestamp($next_monotonic_raw); $next_timestamp = bratonien_tools_nc_connector_monotonic_to_timestamp($next_monotonic_raw);
} }
if ($next_timestamp <= 0) $next_timestamp = bratonien_tools_nc_connector_next_from_timer_list($timer);
if ($next_timestamp <= 0) $active = bratonien_tools_nc_connector_systemctl_value(array('is-active',$timer));
{ $enabled = bratonien_tools_nc_connector_systemctl_value(array('is-enabled',$timer));
$next_timestamp = bratonien_tools_nc_connector_next_from_timer_list($timer);
}
$active = bratonien_tools_nc_connector_systemctl_value(array('is-active', $timer));
$enabled = bratonien_tools_nc_connector_systemctl_value(array('is-enabled', $timer));
$last = bratonien_tools_nc_connector_last_status($connections); $last = bratonien_tools_nc_connector_last_status($connections);
if ($last['timestamp'] <= 0) if ($last['timestamp'] <= 0)
{ {
$last_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $service, '--property=ExecMainExitTimestamp', '--value')); $last_raw = bratonien_tools_nc_connector_systemctl_value(array('show',$service,'--property=ExecMainExitTimestamp','--value'));
$last['timestamp'] = bratonien_tools_nc_connector_parse_systemd_time($last_raw); $last['timestamp'] = bratonien_tools_nc_connector_parse_systemd_time($last_raw);
} }
return array( return array(
'timer_name' => $timer, 'timer_name'=>$timer,
'timer_active' => $active === 'active', 'timer_active'=>$active === 'active',
'timer_enabled' => $enabled === 'enabled', 'timer_enabled'=>$enabled === 'enabled',
'last_run_timestamp' => (int)$last['timestamp'], 'last_run_timestamp'=>(int)$last['timestamp'],
'last_run_label' => $last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar', 'last_run_label'=>$last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar',
'last_run_state' => (string)$last['state'], 'last_run_state'=>(string)$last['state'],
'last_run_message' => (string)$last['message'], 'last_run_message'=>(string)$last['message'],
'last_run_auth_mode' => (string)$last['auth_mode'], 'last_run_auth_mode'=>(string)$last['auth_mode'],
'last_run_api_state' => (string)$last['api_state'], 'last_run_api_state'=>(string)$last['api_state'],
'last_run_api_message' => (string)$last['api_message'], 'last_run_api_message'=>(string)$last['api_message'],
'last_run_fallback_state' => (string)$last['fallback_state'], 'last_run_fallback_state'=>(string)$last['fallback_state'],
'last_run_fallback_message' => (string)$last['fallback_message'], 'last_run_fallback_message'=>(string)$last['fallback_message'],
'last_run_error_detail' => (string)$last['error_detail'], 'last_run_error_detail'=>(string)$last['error_detail'],
'next_run_timestamp' => $next_timestamp, 'next_run_timestamp'=>$next_timestamp,
'next_run_label' => $next_timestamp > 0 ? date('d.m.Y H:i:s', $next_timestamp) : 'Nicht verfügbar', 'next_run_label'=>$next_timestamp > 0 ? date('d.m.Y H:i:s', $next_timestamp) : 'Nicht verfügbar',
'legacy_runtime_exists' => is_dir('/opt/piwigo-sync'),
'legacy_config_exists' => is_dir('/etc/piwigo-sync'),
'legacy_service_exists' => is_file('/etc/systemd/system/piwigo-sync.service'),
'legacy_timer_exists' => is_file('/etc/systemd/system/piwigo-sync.timer'),
); );
} }

View File

@@ -1,135 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
/**
* Controlled handover preparation for verified NC Connector connections.
*
* This phase intentionally does not stop, disable or modify the legacy
* piwigo-sync service. It only marks a verified Connector connection as ready
* for a later controlled takeover. The marker can be rolled back at any time.
*/
function bratonien_tools_nc_connector_ensure_takeover_state()
{
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
pwg_query("ALTER TABLE `$table` MODIFY takeover_state enum('imported','verified','ready','active','disabled') NOT NULL DEFAULT 'imported'");
}
function bratonien_tools_nc_connector_prepare_takeover()
{
$id = isset($_POST['connection_id']) ? (int)$_POST['connection_id'] : 0;
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
if ($connection['takeover_state'] !== 'verified')
{
throw new RuntimeException('Nur eine erfolgreich verifizierte Verbindung kann fuer die Uebergabe vorbereitet werden.');
}
$config = $connection['config'];
$verification = isset($config['verification']) && is_array($config['verification'])
? $config['verification']
: array();
if (empty($verification['ok']))
{
throw new RuntimeException('Die gespeicherte Verifikation ist nicht erfolgreich. Bitte die Verbindung erneut pruefen.');
}
$config['takeover'] = array(
'prepared_at' => date('Y-m-d H:i:s'),
'legacy_sync_untouched' => true,
'connector_enabled' => false,
'first_run' => array(
'status' => 'pending',
'finished_at' => null,
'detail' => '',
),
);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Uebergabestatus konnte nicht gespeichert werden.');
}
bratonien_tools_nc_connector_ensure_takeover_state();
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET
takeover_state = 'ready',
enabled = 0,
config_json = '".pwg_db_real_escape_string($config_json)."',
updated = '".pwg_db_real_escape_string($now)."'
WHERE id = ".(int)$connection['id']);
return array(
'message' => 'Die verifizierte Nextcloud-Verbindung ist jetzt fuer die kontrollierte Uebergabe vorbereitet. Der Connector wurde noch nicht aktiviert und der Legacy-Sync bleibt unveraendert Produktionsverbindung.',
);
}
function bratonien_tools_nc_connector_cancel_takeover()
{
$id = isset($_POST['connection_id']) ? (int)$_POST['connection_id'] : 0;
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
if ($connection['takeover_state'] !== 'ready')
{
throw new RuntimeException('Diese Verbindung befindet sich nicht in der Uebergabevorbereitung.');
}
$config = $connection['config'];
if (isset($config['takeover']))
{
unset($config['takeover']);
}
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Uebergabestatus konnte nicht zurueckgesetzt werden.');
}
bratonien_tools_nc_connector_ensure_takeover_state();
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET
takeover_state = 'verified',
enabled = 0,
config_json = '".pwg_db_real_escape_string($config_json)."',
updated = '".pwg_db_real_escape_string($now)."'
WHERE id = ".(int)$connection['id']);
return array(
'message' => 'Die Uebergabevorbereitung wurde zurueckgenommen. Die Verbindung bleibt verifiziert und deaktiviert; der Legacy-Sync bleibt unveraendert aktiv.',
);
}
/**
* Normalize the outcome of a Connector sync run during takeover.
*
* Both "changed" and "no_changes" are successful results. Only "error"
* represents a failed run and may trigger rollback of a controlled handover.
*/
function bratonien_tools_nc_connector_takeover_result($status, $detail = '')
{
$status = trim((string)$status);
if (!in_array($status, array('changed', 'no_changes', 'error'), true))
{
throw new RuntimeException('Unbekannter Connector-Laufstatus: '.$status);
}
return array(
'status' => $status,
'success' => $status !== 'error',
'changed' => $status === 'changed',
'finished_at' => date('Y-m-d H:i:s'),
'detail' => (string)$detail,
);
}

View File

@@ -0,0 +1,121 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
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 die WebDAV-Verbindung 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 !== '';
$config = array(
'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'=>'',
'piwigo_auth'=>'connection-scoped',
'api_enabled'=>$api_enabled,
);
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,
'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.');
$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.');
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
$connection_key = 'webdav-'.bin2hex(random_bytes(12));
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
pwg_query("INSERT INTO `$table` (connection_key,name,adapter,enabled,config_json,secret_blob,created,updated) VALUES ('"
.pwg_db_real_escape_string($connection_key)."','"
.pwg_db_real_escape_string($name)."','remote',1,'"
.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.');
$config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
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");
return array(
'connection_id'=>$id,
'message'=>'WebDAV-Verbindung wurde angelegt.',
);
}

View File

@@ -7,371 +7,213 @@ 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',
'connection_name'=>'',
'scan_message'=>'',
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
'gallery_root'=>'',
'roots'=>array(),
'technical_stage'=>'mounts',
'technical_source'=>'WebDAV',
'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(),
'api_status'=>'pending',
'api_username'=>'',
'api_error'=>'',
'_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);
if(!is_array($parts)||empty($parts['host'])) throw new RuntimeException('Die Nextcloud-Adresse ist ungültig.'); $parts = parse_url($host);
$scheme=strtolower((string)($parts['scheme']??'https')); if (!is_array($parts) || empty($parts['host'])) throw new RuntimeException('Die Nextcloud-Adresse ist ungültig.');
if(!in_array($scheme,array('http','https'),true)) throw new RuntimeException('Nextcloud muss per HTTP oder HTTPS erreichbar sein.');
$url=$scheme.'://'.$parts['host']; $scheme = strtolower((string)($parts['scheme'] ?? 'https'));
if(!empty($parts['port'])) $url.=':'.(int)$parts['port']; if (!in_array($scheme, array('http','https'), true)) throw new RuntimeException('Nextcloud muss per HTTP oder HTTPS erreichbar sein.');
if(!empty($parts['path'])&&$parts['path']!=='/') $url.='/'.trim($parts['path'],'/');
return rtrim($url,'/'); $url = $scheme.'://'.$parts['host'];
if (!empty($parts['port'])) $url .= ':'.(int)$parts['port'];
if (!empty($parts['path']) && $parts['path'] !== '/') $url .= '/'.trim($parts['path'], '/');
return rtrim($url, '/');
} }
function bratonien_tools_nc_wizard_candidate_urls($host) 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);
$options=array( $ch = curl_init($url);
CURLOPT_RETURNTRANSFER=>true,CURLOPT_FOLLOWLOCATION=>true,CURLOPT_MAXREDIRS=>3,CURLOPT_CONNECTTIMEOUT=>8,CURLOPT_TIMEOUT=>15, $options = array(
CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'),$headers), CURLOPT_RETURNTRANSFER=>true,
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version')?bratonien_tools_current_version():'dev'), 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-WebDAV/'.(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); if ($username !== '')
$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.'); $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
return array('status'=>$status,'body'=>(string)$body); $options[CURLOPT_USERPWD] = $username.':'.$password;
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $errno !== 0) throw new RuntimeException('Verbindung fehlgeschlagen.');
return array('status'=>$status, 'body'=>(string)$body);
} }
function bratonien_tools_nc_wizard_ocs_data($body) 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();
$status=strtolower(trim((string)($meta['status']??'')));$status_code=isset($meta['statuscode'])?(int)$meta['statuscode']:0; $meta = isset($decoded['ocs']['meta']) && is_array($decoded['ocs']['meta']) ? $decoded['ocs']['meta'] : array();
if($status!=='ok'&&!in_array($status_code,array(100,200),true)) $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))
{ {
$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_api_test()
{ {
$url_host=strtolower((string)parse_url($state['base_url'],PHP_URL_HOST)); $state = bratonien_tools_nc_wizard_state();
$state['db_host']=''; if ((int)$state['step'] !== 3) throw new RuntimeException('Der API-Test ist in diesem Assistentenschritt nicht verfügbar.');
$state['db_port']='5432';
$state['db_database']='nextcloud';
$state['source_view']='piwigo_showcase_sources';
$state['activity_view']='piwigo_showcase_activity';
$state['gallery_root']=rtrim(PHPWG_ROOT_PATH,'/').'/galleries/nextcloud';
$state['technical_stage']='reader_credentials';
$state['technical_source']='Noch nicht geprüft';
$state['technical_error']='';
$state['technical_complete']=false;
foreach(bratonien_tools_nc_connector_connections() as $candidate)
{
$config=isset($candidate['config'])&&is_array($candidate['config'])?$candidate['config']:array();
$candidate_url=trim((string)($config['nextcloud_url']??''));
$match=false;
if($candidate_url!=='')
{
try{$match=bratonien_tools_nc_wizard_normalize_url($candidate_url)===$state['base_url'];}catch(Throwable $ignored){$match=false;}
}
if(!$match) continue;
$full=bratonien_tools_nc_connector_connection((int)$candidate['id'],true);
if(!$full) continue;
$credentials=bratonien_tools_nc_connector_credentials_from_blob($full['secret_blob']??'');
$state['db_host']=(string)($config['host']??'');
$state['db_port']=(string)($config['port']??'5432');
$state['db_database']=(string)($config['database']??'nextcloud');
$state['db_user']=(string)($config['user']??'');
$state['_db_password']=(string)($credentials['db_password']??'');
$state['db_password_set']=$state['_db_password']!=='';
$state['source_view']=(string)($config['source_view']??$state['source_view']);
$state['activity_view']=(string)($config['activity_view']??$state['activity_view']);
$state['gallery_root']=(string)($config['gallery_root']??$state['gallery_root']);
$state['storages']=isset($config['storages'])&&is_array($config['storages'])?$config['storages']:array();
$state['storage_candidates']=$state['storages'];
$state['technical_complete']=$state['db_host']!==''&&$state['db_user']!==''&&$state['db_password_set']&&!empty($state['storages']);
$state['technical_stage']=$state['technical_complete']?'ready':'reader_credentials';
$state['technical_source']='Passende vorhandene Verbindung übernommen';
break;
}
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)
{
$config=array('host'=>$state['db_host'],'port'=>$state['db_port'],'database'=>$state['db_database'],'user'=>$state['db_user']);
$password=(string)$state['_db_password'];
bratonien_tools_nc_connector_psql($config,$password,'SELECT 1');
$source_view=bratonien_tools_nc_connector_view_name($state['source_view']);
$activity_view=bratonien_tools_nc_connector_view_name($state['activity_view']);
bratonien_tools_nc_connector_psql($config,$password,'SELECT COUNT(*) FROM '.$source_view);
bratonien_tools_nc_connector_psql($config,$password,'SELECT 1 FROM '.$activity_view.' LIMIT 1');
$rows=bratonien_tools_nc_connector_psql($config,$password,"SELECT DISTINCT storage_id::text || E'\\t' || COALESCE(source_path, '') FROM ".$source_view." ORDER BY 1");
$candidates=array();
foreach(preg_split('/\r\n|\r|\n/',trim($rows)) as $line)
{
if($line==='') continue;
$parts=explode("\t",$line,2);
if(count($parts)!==2) continue;
$storage_id=trim($parts[0]);$source_path=trim($parts[1],'/');
if($storage_id==='') continue;
$prefix=$source_path;
if(strpos($source_path,'/')!==false) $prefix=substr($source_path,0,strpos($source_path,'/'));
if(!isset($candidates[$storage_id])) $candidates[$storage_id]=array('storage_id'=>$storage_id,'source_prefix'=>$prefix,'local_mount'=>'');
}
if(!$candidates) throw new RuntimeException('Die Verbindung zur Datenquelle funktioniert, aber es wurden noch keine freigegebenen Speicherorte gefunden.');
$known=array();
foreach(bratonien_tools_nc_connector_connections() as $connection)
{
foreach(($connection['config']['storages']??array()) as $storage)
{
$key=(string)($storage['storage_id']??'').'|'.trim((string)($storage['source_prefix']??''),'/');
if($key!=='|') $known[$key]=(string)($storage['local_mount']??'');
}
}
foreach($candidates as &$candidate)
{
$key=$candidate['storage_id'].'|'.trim($candidate['source_prefix'],'/');
if(!empty($known[$key])&&is_dir($known[$key])&&is_readable($known[$key])) $candidate['local_mount']=$known[$key];
}
unset($candidate);
$state['storage_candidates']=array_values($candidates);
$state['storages']=array_values(array_filter($state['storage_candidates'],function($storage){return trim((string)($storage['local_mount']??''))!=='';}));
$state['technical_error']='';
$state['technical_source']='Datenzugriff erfolgreich geprüft';
$state['technical_complete']=count($state['storages'])===count($state['storage_candidates']);
$state['technical_stage']=$state['technical_complete']?'ready':'mounts';
}
function bratonien_tools_nc_wizard_save_technical()
{
$state=bratonien_tools_nc_wizard_state();
if(empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
$state['connection_name']=trim((string)($_POST['nc_wizard_connection_name']??$state['connection_name']));
$state['db_user']=trim((string)($_POST['nc_wizard_db_user']??$state['db_user']));
$db_password=array_key_exists('nc_wizard_db_password',$_POST)?(string)$_POST['nc_wizard_db_password']:(string)$state['_db_password'];
if($db_password!==''){$state['_db_password']=$db_password;$state['db_password_set']=true;}
$advanced=isset($_POST['nc_wizard_db_host'])||$state['technical_stage']==='database_details';
if($advanced)
{
$state['db_host']=trim((string)($_POST['nc_wizard_db_host']??$state['db_host']));
$state['db_port']=(string)max(1,min(65535,(int)($_POST['nc_wizard_db_port']??$state['db_port'])));
$state['db_database']=trim((string)($_POST['nc_wizard_db_database']??$state['db_database']));
}
else
{
$state['db_host']=(string)parse_url($state['base_url'],PHP_URL_HOST);
$state['db_port']='5432';
$state['db_database']='nextcloud';
}
$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); 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.'); $key_id = trim((string)$state['_api_key_id']);
$secret = trim((string)$state['_api_key_secret']);
if ($key_id === '' || $secret === '') throw new RuntimeException('API-Schlüssel-ID und API-Geheimnis fehlen.');
try try
{ {
bratonien_tools_nc_wizard_finish_data_access($state); $status = bratonien_tools_nc_connector_piwigo_api_request($key_id, $secret, 'pwg.session.getStatus');
$user_status = strtolower((string)($status['status'] ?? ''));
if (!in_array($user_status, array('admin','webmaster'), true)) throw new RuntimeException('Der API-Key gehört keinem Piwigo-Administrator/Webmaster.');
$method_result = bratonien_tools_nc_connector_piwigo_api_request($key_id, $secret, 'reflection.getMethodList');
$method_map = array();
bratonien_tools_nc_connector_collect_method_names($method_result, $method_map);
$missing = array_values(array_diff(array('bratonien.nc.syncProductive','bratonien.nc.syncOrphans'), array_keys($method_map)));
if ($missing) throw new RuntimeException('Benötigte Bratonien-Sync-Methoden fehlen: '.implode(', ', $missing).'.');
$state['_api_key_id'] = $key_id;
$state['_api_key_secret'] = $secret;
$state['api_status'] = 'ok';
$state['api_username'] = (string)($status['username'] ?? $status['user'] ?? '');
$state['api_error'] = '';
$state['step'] = 4;
bratonien_tools_nc_wizard_store($state); 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'=>'Piwigo-API erfolgreich geprüft. Gespeichert wird sie erst beim Abschluss des Assistenten.');
} }
catch(Throwable $e) catch (Throwable $e)
{ {
$state['technical_error']=$e->getMessage(); $state['api_status'] = 'error';
if(!$advanced) $state['api_error'] = $e->getMessage();
{
$state['technical_stage']='database_details';
bratonien_tools_nc_wizard_store($state);
throw new RuntimeException('Der übliche Datenbankzugang konnte nicht automatisch bestätigt werden. Bitte die Datenbank-Adresse prüfen.');
}
bratonien_tools_nc_wizard_store($state); bratonien_tools_nc_wizard_store($state);
throw $e; throw $e;
} }
} }
function bratonien_tools_nc_wizard_save_mounts()
{
$state=bratonien_tools_nc_wizard_state();
$candidates=is_array($state['storage_candidates'])?$state['storage_candidates']:array();
$mounts=isset($_POST['nc_wizard_storage_mount'])&&is_array($_POST['nc_wizard_storage_mount'])?$_POST['nc_wizard_storage_mount']:array();
if(!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte erkannt.');
$storages=array();
foreach($candidates as $index=>$candidate)
{
$mount=trim((string)($candidate['local_mount']??''));
if($mount==='') $mount=trim((string)($mounts[$index]??''));
if($mount===''||$mount[0]!=='/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
$mount=rtrim($mount,'/');
if(!is_dir($mount)||!is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount);
$candidate['local_mount']=$mount;$storages[]=$candidate;
}
$state['storages']=$storages;$state['storage_candidates']=$storages;$state['technical_complete']=true;$state['technical_stage']='ready';$state['technical_error']='';bratonien_tools_nc_wizard_store($state);
return array('message'=>'Speicherzuordnung wurde erfolgreich geprüft.');
}
function bratonien_tools_nc_wizard_select_user()
{
$state=bratonien_tools_nc_wizard_state();
if(empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
if(empty($state['technical_complete'])) throw new RuntimeException('Die Verbindung ist noch nicht vollständig geprüft.');
$showcase_user=trim((string)($_POST['nc_wizard_showcase_user']??''));
$connection_name=trim((string)($_POST['nc_wizard_connection_name']??$state['connection_name']));
if($showcase_user==='') throw new RuntimeException('Bitte den Benutzer auswählen, der die Showcase-Freigaben bereitstellt.');
if($connection_name==='') throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.');
$state['connection_name']=$connection_name;$state['showcase_user']=$showcase_user;$state['step']=3;$state['api_error']='';bratonien_tools_nc_wizard_store($state);
return array('message'=>'Showcase-Benutzer übernommen. Jetzt folgt der Piwigo-API-Zugang.');
}
function bratonien_tools_nc_wizard_api_test()
{
$state=bratonien_tools_nc_wizard_state();
if((int)$state['step']!==3) throw new RuntimeException('Der API-Test ist in diesem Assistentenschritt nicht verfügbar.');
$posted_id=trim((string)($_POST['nc_wizard_api_key_id']??''));$posted_secret=trim((string)($_POST['nc_wizard_api_key_secret']??''));
if($posted_id!=='')$state['_api_key_id']=$posted_id;if($posted_secret!=='')$state['_api_key_secret']=$posted_secret;bratonien_tools_nc_wizard_store($state);
$key_id=trim((string)$state['_api_key_id']);$secret=trim((string)$state['_api_key_secret']);
if($key_id===''&&$secret===''){$stored=bratonien_tools_nc_api_credentials();$key_id=trim((string)($stored['key_id']??''));$secret=trim((string)($stored['key_secret']??''));}
if($key_id===''||$secret==='') throw new RuntimeException('API-Schlüssel-ID und API-Geheimnis fehlen.');
try
{
$status=bratonien_tools_nc_connector_piwigo_api_request($key_id,$secret,'pwg.session.getStatus');
$user_status=strtolower((string)($status['status']??''));
if(!in_array($user_status,array('admin','webmaster'),true)) throw new RuntimeException('Der API-Key gehört keinem Piwigo-Administrator/Webmaster.');
$method_result=bratonien_tools_nc_connector_piwigo_api_request($key_id,$secret,'reflection.getMethodList');$method_map=array();bratonien_tools_nc_connector_collect_method_names($method_result,$method_map);
$missing=array_values(array_diff(array('bratonien.nc.syncProductive','bratonien.nc.syncOrphans'),array_keys($method_map)));
if($missing) throw new RuntimeException('Benötigte Bratonien-Sync-Methoden fehlen: '.implode(', ',$missing).'.');
$state['_api_key_id']=$key_id;$state['_api_key_secret']=$secret;$state['api_status']='ok';$state['api_username']=(string)($status['username']??$status['user']??'');$state['api_error']='';$state['step']=4;bratonien_tools_nc_wizard_store($state);
return array('message'=>'Piwigo-API erfolgreich geprüft. Gespeichert wird sie erst beim Abschluss des Assistenten.');
}
catch(Throwable $e){$state['api_status']='error';$state['api_error']=$e->getMessage();bratonien_tools_nc_wizard_store($state);throw $e;}
}
function bratonien_tools_nc_wizard_api_skip() function bratonien_tools_nc_wizard_api_skip()
{ {
$state=bratonien_tools_nc_wizard_state();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.');
return array('message'=>'Piwigo-API wurde übersprungen. Für diese Verbindung ist deshalb ein Fallback-Zugang erforderlich.');
}
function bratonien_tools_nc_wizard_finish() $state['api_status'] = 'skipped';
{ $state['api_error'] = '';
$state=bratonien_tools_nc_wizard_state(); $state['step'] = 4;
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); 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.');
$storage_lines=array(); return array('message'=>'Piwigo-API wurde übersprungen. Für diese Verbindung ist deshalb ein Fallback-Zugang erforderlich.');
foreach($state['storages'] as $storage)
{
$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);
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.');
function bratonien_tools_nc_connector_update_technical() $connection = bratonien_tools_nc_connector_connection($id, false);
{ if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$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.'); if ((string)($connection['adapter'] ?? '') !== 'remote' || (string)($connection['config']['source_mode'] ?? '') !== 'webdav-placeholder')
$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']); throw new RuntimeException('Es können nur WebDAV-Verbindungen bearbeitet werden.');
$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.'); }
$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.');
} }

View File

@@ -0,0 +1,199 @@
<?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_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_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['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);
}
else
{
$state['step'] = 1;
$state['directory_selection_ready'] = false;
}
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Ein Fenster zurück.');
}

View File

@@ -0,0 +1,200 @@
<?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'),
'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',
'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(),
'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')
{
throw new RuntimeException('Der Assistent unterstützt ausschließlich WebDAV-Verbindungen.');
}
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.');
}
function bratonien_tools_nc_wizard_finish_dispatch()
{
$state = bratonien_tools_nc_wizard_state();
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder')
{
throw new RuntimeException('Der Assistent unterstützt ausschließlich WebDAV-Verbindungen.');
}
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

@@ -1,282 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
/**
* Registers the connector-owned filesystem synchronization endpoint.
*
* Authentication and authorization are delegated to Piwigo's Web API.
* The method is administrator-only and POST-only so an API key inherits the
* permissions of its Piwigo user without storing that user's password.
*/
function bratonien_tools_register_ws_methods($arr)
{
$service = &$arr[0];
$service->addMethod(
'bratonien.nc.sync',
'bratonien_tools_ws_nc_sync',
array(
'site_id' => array(
'default' => 1,
'type' => WS_TYPE_ID,
'info' => 'Piwigo storage site to synchronize. Default: 1.',
),
'simulate' => array(
'default' => true,
'type' => WS_TYPE_BOOL,
'info' => 'Simulation only. Productive API synchronization is disabled until parity has been verified.',
),
),
'Simulates the local Piwigo filesystem synchronization used by the Bratonien NC Connector.',
null,
array(
'admin_only' => true,
'post_only' => true,
)
);
}
function bratonien_tools_ws_nc_sync_error(&$errors, $path, $type)
{
$errors[] = array(
'path' => (string)$path,
'type' => (string)$type,
);
}
/**
* Read-only parity test for Piwigo 16.4.0 admin/site_update.php with the
* connector's fixed settings:
* sync=files, privacy_level=0, sync_meta=1, subcats-included=1,
* no cat, no caddie, no meta_all, no meta_empty_overrides.
*
* The simulation deliberately performs no INSERT/UPDATE/DELETE and triggers
* no Piwigo activity/events. It only calculates what the matching core admin
* synchronization would attempt to change.
*/
function bratonien_tools_ws_nc_sync($params, &$service)
{
global $conf, $user, $logger;
if (empty($conf['enable_synchronization']))
{
return new PwgError(403, 'Piwigo filesystem synchronization is disabled.');
}
$piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : '';
if ($piwigo_version !== '16.4.0')
{
return new PwgError(
409,
'Bratonien API synchronization is not approved for Piwigo '.$piwigo_version.'. Use the administrator fallback until this Piwigo version has been verified.'
);
}
$simulate = !isset($params['simulate']) || (bool)$params['simulate'];
if (!$simulate)
{
return new PwgError(
409,
'Productive Bratonien API synchronization is not enabled yet. Run the simulation and verify parity with Piwigo admin synchronization first.'
);
}
$site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1;
if ($site_id < 1)
{
return new PwgError(400, 'Invalid site_id.');
}
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
include_once(PHPWG_ROOT_PATH.'admin/site_reader_local.php');
$query = '
SELECT galleries_url
FROM '.SITES_TABLE.'
WHERE id = '.$site_id.'
LIMIT 1';
$result = pwg_query($query);
if (!pwg_db_num_rows($result))
{
return new PwgError(404, 'Piwigo site does not exist.');
}
list($site_url) = pwg_db_fetch_row($result);
if (url_is_remote($site_url))
{
return new PwgError(400, 'Remote Piwigo sites are not supported by this synchronization method.');
}
$errors = array();
$site_reader = new LocalSiteReader($site_url);
if (!$site_reader->open())
{
return new PwgError(500, 'Piwigo could not open the configured local site.');
}
$counts = array(
'new_categories' => 0,
'del_categories' => 0,
'new_elements' => 0,
'del_elements' => 0,
'upd_elements' => 0,
'new_formats' => 0,
'del_formats' => 0,
'metadata_candidates' => 0,
);
// -------------------------------------------------------------------
// Directories / physical categories
// -------------------------------------------------------------------
$query = '
SELECT id, uppercats, global_rank, status, visible
FROM '.CATEGORIES_TABLE.'
WHERE dir IS NOT NULL
AND site_id = '.$site_id;
$db_categories = hash_from_query($query, 'id');
$db_fulldirs = get_fulldirs(array_keys($db_categories));
$basedir = preg_replace('#/*$#', '', $site_url);
$db_fulldirs = array_flip($db_fulldirs);
$fs_fulldirs = $site_reader->get_full_directories($basedir);
$next_id = pwg_db_nextval('id', CATEGORIES_TABLE);
foreach (array_diff($fs_fulldirs, array_keys($db_fulldirs)) as $fulldir)
{
$dir = basename($fulldir);
if (!preg_match($conf['sync_chars_regex'], $dir))
{
bratonien_tools_ws_nc_sync_error($errors, $fulldir, 'PWG-UPDATE-1');
continue;
}
$virtual_id = $next_id++;
$db_fulldirs[$fulldir] = $virtual_id;
$db_categories[$virtual_id] = array('id'=>$virtual_id);
$counts['new_categories']++;
}
$to_delete = array();
foreach (array_diff(array_keys($db_fulldirs), $fs_fulldirs) as $fulldir)
{
$to_delete[] = $db_fulldirs[$fulldir];
}
$counts['del_categories'] = count($to_delete);
// -------------------------------------------------------------------
// Files / images / formats
// -------------------------------------------------------------------
$fs = $site_reader->get_elements($basedir);
$cat_ids = array_diff(array_keys($db_categories), $to_delete);
$db_elements = array();
if (count($cat_ids) > 0)
{
$query = '
SELECT id, path
FROM '.IMAGES_TABLE.'
WHERE storage_category_id IN ('.wordwrap(implode(', ', $cat_ids), 160, "\n").')';
$db_elements = simple_hash_from_query($query, 'id', 'path');
}
foreach (array_diff(array_keys($fs), $db_elements) as $path)
{
$dirname = dirname($path);
if (!isset($db_fulldirs[$dirname]))
{
continue;
}
$filename = basename($path);
if (!preg_match($conf['sync_chars_regex'], $filename))
{
bratonien_tools_ws_nc_sync_error($errors, $path, 'PWG-UPDATE-1');
continue;
}
$counts['new_elements']++;
if (!empty($conf['enable_formats']) && isset($fs[$path]['formats']) && is_array($fs[$path]['formats']))
{
$counts['new_formats'] += count($fs[$path]['formats']);
}
}
if (!empty($conf['enable_formats']) && count($db_elements) > 0)
{
$db_elements_flip = array_flip($db_elements);
$existing_ids = array();
foreach (array_intersect_key($fs, $db_elements_flip) as $path => $unused)
{
$existing_ids[] = $db_elements_flip[$path];
}
if (count($existing_ids) > 0)
{
$db_formats = array();
$query = '
SELECT *
FROM '.IMAGE_FORMAT_TABLE.'
WHERE image_id IN ('.implode(',', $existing_ids).')';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
if (!isset($db_formats[$row['image_id']]))
{
$db_formats[$row['image_id']] = array();
}
$db_formats[$row['image_id']][$row['ext']] = $row['format_id'];
}
foreach ($db_formats as $image_id => $formats)
{
$path = $db_elements[$image_id];
$filesystem_formats = isset($fs[$path]['formats']) && is_array($fs[$path]['formats']) ? $fs[$path]['formats'] : array();
$counts['del_formats'] += count(array_diff_key($formats, $filesystem_formats));
}
foreach ($existing_ids as $image_id)
{
$path = $db_elements[$image_id];
$known_formats = isset($db_formats[$image_id]) ? $db_formats[$image_id] : array();
$filesystem_formats = isset($fs[$path]['formats']) && is_array($fs[$path]['formats']) ? $fs[$path]['formats'] : array();
$counts['new_formats'] += count(array_diff_key($filesystem_formats, $known_formats));
}
}
}
$counts['del_elements'] = count(array_diff($db_elements, array_keys($fs)));
$files = get_filelist('', $site_id, true, false);
$update_count = 0;
foreach ($files as $id => $file)
{
$data = $site_reader->get_element_update_attributes($file['path']);
if (is_array($data))
{
$update_count++;
}
}
$counts['upd_elements'] = $update_count;
$metadata_files = get_filelist('', $site_id, true, true);
$counts['metadata_candidates'] = count($metadata_files);
return array(
'mode' => 'simulation',
'approved_piwigo_version' => '16.4.0',
'piwigo_version' => $piwigo_version,
'site_id' => $site_id,
'site_url' => $site_url,
'counts' => $counts,
'errors' => $errors,
'error_count' => count($errors),
'database_writes' => false,
'fallback' => 'administrator',
'connected_with' => isset($_SESSION['connected_with']) ? (string)$_SESSION['connected_with'] : '',
'username' => isset($user['username']) ? (string)$user['username'] : '',
'status' => isset($user['status']) ? (string)$user['status'] : '',
);
}

View File

@@ -16,12 +16,13 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/self_update.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php'); 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_takeover.inc.php');
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_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_user_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_connection_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');
function bratonien_tools_get_tools() function bratonien_tools_get_tools()
{ {
@@ -46,27 +47,22 @@ function bratonien_tools_get_tools()
'album_share_create' => array('handler' => 'bratonien_tools_create_album_share'), 'album_share_create' => array('handler' => 'bratonien_tools_create_album_share'),
'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_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'),
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete'), '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_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'),
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan'), 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical'), 'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_mounts'), 'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
'nc_connector_wizard_select_user' => array('handler' => 'bratonien_tools_nc_wizard_select_user'), '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_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'),
'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_managed'), 'nc_connector_fallback_delete' => array('handler' => 'bratonien_tools_nc_connector_fallback_delete_scoped'),
'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_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_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save'),
'nc_connector_fallback_delete' => array('handler' => 'bratonien_tools_nc_connector_fallback_delete'),
'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,7 +1,7 @@
<?php <?php
/* /*
Plugin Name: Bratonien Tools Plugin Name: Bratonien Tools
Version: 0.9.4.3 Version: 0.9.6.2
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation. Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien Author: Bratonien
@@ -16,16 +16,18 @@ 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');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php'); 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);
@@ -33,7 +35,6 @@ add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', E
add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10); add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10);
add_event_handler('init', 'bratonien_tools_album_shares_init'); add_event_handler('init', 'bratonien_tools_album_shares_init');
add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories'); add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories');
add_event_handler('ws_add_methods', 'bratonien_tools_register_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods'); add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_productive_ws_methods'); add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_productive_ws_methods');

View File

@@ -1,284 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-cutover-v2.php <connection-id>\n");
exit(1);
}
$connectionId = (int)$argv[1];
$piwigoRoot = dirname(__DIR__, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$legacyConfig = '/etc/piwigo-sync/piwigo.conf';
$legacyRuntime = '/opt/piwigo-sync/sync.sh';
$newTimer = 'bratonien-nc-connector.timer';
$newService = 'bratonien-nc-connector.service';
$legacyTimer = 'piwigo-sync.timer';
function fail($message)
{
throw new RuntimeException($message);
}
function readKeyValueFile($path)
{
if (!is_readable($path)) fail('Konfiguration nicht lesbar: '.$path);
$result = array();
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line)
{
$line = trim($line);
if ($line === '' || $line[0] === '#') continue;
if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches)) continue;
$value = trim($matches[2]);
if (strlen($value) >= 2)
{
$first = $value[0]; $last = $value[strlen($value)-1];
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) $value = substr($value, 1, -1);
}
$result[$matches[1]] = $value;
}
return $result;
}
function runCommand(array $command, $allowFailure = false)
{
$spec = array(0=>array('file','/dev/null','r'),1=>array('pipe','w'),2=>array('pipe','w'));
$process = proc_open($command, $spec, $pipes);
if (!is_resource($process)) fail('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
$stdout = stream_get_contents($pipes[1]); $stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]); fclose($pipes[2]);
$exit = proc_close($process);
if ($exit !== 0 && !$allowFailure)
{
$detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
fail('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : ''));
}
return array('exit'=>$exit,'stdout'=>(string)$stdout,'stderr'=>(string)$stderr);
}
function decryptConnectorSecret($blob, $hexKey)
{
if (!preg_match('/^[a-f0-9]{64}$/', $hexKey)) fail('Connector-Schluessel ist ungueltig.');
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) fail('Connector-Zugangsdaten haben ein unbekanntes Format.');
$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('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
$decoded = json_decode($plain, true);
if (is_array($decoded) && array_key_exists('db_password', $decoded))
{
$password = (string)$decoded['db_password'];
if ($password === '') fail('Datenbankpasswort fehlt in den Connector-Zugangsdaten.');
return $password;
}
// Backward compatibility for older imported connections that stored only
// the database password as plaintext inside the encrypted envelope.
return (string)$plain;
}
function sqlEscape(mysqli $db, $value)
{
return $db->real_escape_string((string)$value);
}
function saveTakeoverResult(mysqli $db, $table, $connectionId, array $config, $state, $enabled)
{
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json)) fail('Connector-Status konnte nicht serialisiert werden.');
$now = date('Y-m-d H:i:s');
$sql = "UPDATE `".$table."` SET takeover_state='".sqlEscape($db, $state)."', enabled=".($enabled ? 1 : 0).", config_json='".sqlEscape($db, $json)."', updated='".sqlEscape($db, $now)."' WHERE id=".(int)$connectionId;
if (!$db->query($sql)) fail('Connector-Status konnte nicht gespeichert werden: '.$db->error);
}
$legacyTimerWasEnabled = false;
$newTimerInstalled = false;
$db = null;
$table = '';
$config = array();
try
{
if (!is_readable($dbConfig)) fail('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig);
if (!is_executable($legacyRuntime)) fail('Bestehende Sync-Runtime fehlt: '.$legacyRuntime);
$conf = array(); $prefixeTable = 'piwigo_'; require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.');
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) fail('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$result = $db->query("SELECT id, takeover_state, enabled, config_json, secret_blob FROM `".$table."` WHERE id=".$connectionId." LIMIT 1");
if (!$result || !$result->num_rows) fail('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.');
$row = $result->fetch_assoc();
if ((string)$row['takeover_state'] !== 'ready' || (int)$row['enabled'] !== 0) fail('Connector-Verbindung muss im Zustand ready und deaktiviert sein.');
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config) || empty($config['verification']['ok'])) fail('Connector-Verbindung besitzt keine erfolgreiche Verifikation.');
$keyResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$keyResult || !$keyResult->num_rows) fail('Connector-Schluessel wurde in Piwigo nicht gefunden.');
$keyRow = $keyResult->fetch_assoc();
$dbPassword = decryptConnectorSecret($row['secret_blob'], (string)$keyRow['value']);
$legacy = readKeyValueFile($legacyConfig);
$piwigoUser = trim((string)($legacy['PIWIGO_SYNC_USER'] ?? ''));
$piwigoPasswordFile = trim((string)($legacy['PIWIGO_SYNC_PASSWORD_FILE'] ?? '/etc/piwigo-sync/piwigo-password'));
if ($piwigoUser === '' || !is_readable($piwigoPasswordFile)) fail('Legacy-Piwigo-Sync-Zugangsdaten konnten fuer den einmaligen Cutover nicht gelesen werden.');
$piwigoPassword = trim((string)file_get_contents($piwigoPasswordFile));
if ($piwigoPassword === '') fail('Legacy-Piwigo-Sync-Passwort ist leer.');
foreach (array('host','port','database','user','source_view','gallery_root','state_dir') as $key)
{
if (!isset($config[$key]) || trim((string)$config[$key]) === '') fail('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
}
$runtimeDir = '/etc/bratonien-tools/nc-connector';
if (!is_dir($runtimeDir) && !mkdir($runtimeDir, 0700, true)) fail('Connector-Laufzeitverzeichnis konnte nicht angelegt werden.');
chmod($runtimeDir, 0700);
$base = $runtimeDir.'/connection-'.$connectionId;
$dbPasswordPath = $base.'.db-password';
$piwigoPasswordPath = $base.'.piwigo-password';
$storagePath = $base.'.storages.tsv';
$configPath = $base.'.conf';
$statusPath = rtrim((string)$config['state_dir'], '/').'/connector-status.json';
file_put_contents($dbPasswordPath, $dbPassword."\n", LOCK_EX);
file_put_contents($piwigoPasswordPath, $piwigoPassword."\n", LOCK_EX);
chmod($dbPasswordPath, 0600); chmod($piwigoPasswordPath, 0600);
$storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount');
foreach ((array)($config['storages'] ?? array()) as $storage)
{
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t".trim((string)($storage['source_prefix'] ?? ''), '/')."\t".(string)($storage['local_mount'] ?? '');
}
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); chmod($storagePath, 0600);
$piwigoRootConfigured = isset($legacy['PIWIGO_ROOT']) && trim((string)$legacy['PIWIGO_ROOT']) !== '' ? trim((string)$legacy['PIWIGO_ROOT']) : $piwigoRoot;
$lines = array(
'PIWIGO_ROOT='.$piwigoRootConfigured,
'GALLERY_ROOT='.(string)$config['gallery_root'],
'STATE_DIR='.(string)$config['state_dir'],
'STATUS_FILE='.$statusPath,
'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_DB_PASSWORD_FILE='.$dbPasswordPath,
'STORAGE_CONFIG='.$storagePath,
'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',
'PIWIGO_SYNC_USER='.$piwigoUser,
'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath,
);
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); chmod($configPath, 0600);
$enabledCheck = runCommand(array('systemctl', 'is-enabled', $legacyTimer), true);
$legacyTimerWasEnabled = $enabledCheck['exit'] === 0;
echo "Stoppe Legacy-Timer...\n";
runCommand(array('systemctl', 'stop', $legacyTimer));
$deadline = time() + 120;
do
{
$active = runCommand(array('systemctl', 'is-active', '--quiet', 'piwigo-sync.service'), true);
if ($active['exit'] !== 0) break;
if (time() >= $deadline) fail('Ein laufender Legacy-Sync wurde nach 120 Sekunden nicht beendet.');
sleep(2);
}
while (true);
@unlink($statusPath);
echo "Fuehre ersten Connector-Lauf aus...\n";
$firstRun = runCommand(array('env', 'PIWIGO_CONFIG='.$configPath, $legacyRuntime), true);
if ($firstRun['exit'] !== 0)
{
$detail = trim($firstRun['stderr']) !== '' ? trim($firstRun['stderr']) : trim($firstRun['stdout']);
fail('Erster Connector-Lauf ist technisch fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.'));
}
$runResult = 'no_changes'; $statusState = ''; $statusMessage = '';
if (is_readable($statusPath))
{
$status = json_decode((string)file_get_contents($statusPath), true);
if (is_array($status))
{
$statusState = trim((string)($status['state'] ?? ''));
$statusMessage = trim((string)($status['message'] ?? ''));
}
$legacyNoChangeMessage = 'Synchronisierung fehlgeschlagen; bestehende Galerie blieb unverändert';
if ($statusState === 'error' && $statusMessage !== $legacyNoChangeMessage) fail('Erster Connector-Lauf meldete einen technischen Fehler'.($statusMessage !== '' ? ': '.$statusMessage : '.'));
if ($statusState === 'ok') $runResult = 'changed';
elseif ($statusState === 'error' && $statusMessage === $legacyNoChangeMessage)
{
$runResult = 'no_changes';
echo "Hinweis: Legacy-Runtime hat den erwarteten Activity-Gate-No-Op faelschlich als error markiert; Exit-Code 0 bestaetigt den erfolgreichen No-Change-Lauf.\n";
}
}
echo 'Erster Lauf Exit-Code: '.$firstRun['exit']."\n";
echo 'Connector-Status: '.($statusState !== '' ? $statusState : 'kein Abschlussstatus').($statusMessage !== '' ? ' - '.$statusMessage : '')."\n";
echo 'Bewertung: '.$runResult."\n";
$servicePath = '/etc/systemd/system/'.$newService;
$timerPath = '/etc/systemd/system/'.$newTimer;
$service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=PIWIGO_CONFIG=".$configPath."\nExecStart=".$legacyRuntime."\n\n";
$timer = "[Unit]\nDescription=Bratonien NC Connector regelmaessig pruefen\n\n[Timer]\nOnBootSec=3min\nOnUnitActiveSec=1min\nRandomizedDelaySec=15s\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n";
file_put_contents($servicePath, $service, LOCK_EX); file_put_contents($timerPath, $timer, LOCK_EX); chmod($servicePath,0644); chmod($timerPath,0644); $newTimerInstalled=true;
runCommand(array('systemctl','daemon-reload'));
runCommand(array('systemctl','disable',$legacyTimer),true);
runCommand(array('systemctl','enable','--now',$newTimer));
$config['takeover']['cutover_at']=date('Y-m-d H:i:s');
$config['takeover']['legacy_timer_disabled']=true;
$config['takeover']['connector_timer']=$newTimer;
$config['takeover']['runtime']='legacy-runtime-transition';
$config['takeover']['first_run']=array('state'=>'success','result'=>$runResult,'status_state'=>$statusState,'status_message'=>$statusMessage,'checked_at'=>date('Y-m-d H:i:s'));
saveTakeoverResult($db,$table,$connectionId,$config,'active',true);
echo "Cutover erfolgreich.\n";
echo "Erster Connector-Lauf: ".$runResult."\n";
echo "Legacy-Timer ist deaktiviert; ".$newTimer." ist aktiv.\n";
echo "Die bisherige Sync-Runtime bleibt fuer diesen Uebergangsschritt noch als Laufzeit erhalten.\n";
}
catch (Throwable $e)
{
if ($db instanceof mysqli && $table !== '' && $config)
{
try
{
$config['takeover']['first_run']=array('state'=>'error','result'=>'error','checked_at'=>date('Y-m-d H:i:s'),'message'=>substr($e->getMessage(),0,500));
saveTakeoverResult($db,$table,$connectionId,$config,'ready',false);
}
catch (Throwable $ignored){}
}
if ($newTimerInstalled) runCommand(array('systemctl','disable','--now',$newTimer),true);
if ($legacyTimerWasEnabled) runCommand(array('systemctl','enable','--now',$legacyTimer),true);
else runCommand(array('systemctl','start',$legacyTimer),true);
fwrite(STDERR, "Cutover fehlgeschlagen: ".$e->getMessage()."\n");
fwrite(STDERR, "Legacy-Timer wurde wieder aktiviert/gestartet.\n");
exit(1);
}

View File

@@ -1,374 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: sudo php nc-connector-cutover.php <connection-id>\n");
exit(1);
}
$connectionId = (int)$argv[1];
$piwigoRoot = dirname(__DIR__, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$legacyConfig = '/etc/piwigo-sync/piwigo.conf';
$legacyRuntime = '/opt/piwigo-sync/sync.sh';
$newTimer = 'bratonien-nc-connector.timer';
$newService = 'bratonien-nc-connector.service';
$legacyTimer = 'piwigo-sync.timer';
function fail($message)
{
throw new RuntimeException($message);
}
function readKeyValueFile($path)
{
if (!is_readable($path))
{
fail('Konfiguration nicht lesbar: '.$path);
}
$result = array();
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line)
{
$line = trim($line);
if ($line === '' || $line[0] === '#')
{
continue;
}
if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches))
{
continue;
}
$value = trim($matches[2]);
if (strlen($value) >= 2)
{
$first = $value[0];
$last = $value[strlen($value)-1];
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'"))
{
$value = substr($value, 1, -1);
}
}
$result[$matches[1]] = $value;
}
return $result;
}
function runCommand(array $command, $allowFailure = false)
{
$spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$process = proc_open($command, $spec, $pipes);
if (!is_resource($process))
{
fail('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
if ($exit !== 0 && !$allowFailure)
{
$detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
fail('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : ''));
}
return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr);
}
function decryptConnectorSecret($blob, $hexKey)
{
if (!preg_match('/^[a-f0-9]{64}$/', $hexKey))
{
fail('Connector-Schluessel ist ungueltig.');
}
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1)
{
fail('Connector-Zugangsdaten haben ein unbekanntes Format.');
}
$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('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
}
return (string)$plain;
}
function sqlEscape(mysqli $db, $value)
{
return $db->real_escape_string((string)$value);
}
function saveTakeoverResult(mysqli $db, $table, $connectionId, array $config, $state, $enabled)
{
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json))
{
fail('Connector-Status konnte nicht serialisiert werden.');
}
$now = date('Y-m-d H:i:s');
$sql = "UPDATE `".$table."` SET takeover_state='".sqlEscape($db, $state)."', enabled=".($enabled ? 1 : 0).", config_json='".sqlEscape($db, $json)."', updated='".sqlEscape($db, $now)."' WHERE id=".(int)$connectionId;
if (!$db->query($sql))
{
fail('Connector-Status konnte nicht gespeichert werden: '.$db->error);
}
}
$legacyTimerWasEnabled = false;
$newTimerInstalled = false;
$db = null;
$table = '';
$config = array();
try
{
if (!is_readable($dbConfig))
{
fail('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig);
}
if (!is_executable($legacyRuntime))
{
fail('Bestehende Sync-Runtime fehlt: '.$legacyRuntime);
}
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key]))
{
fail('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.');
}
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno)
{
fail('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
}
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$result = $db->query("SELECT id, takeover_state, enabled, config_json, secret_blob FROM `".$table."` WHERE id=".$connectionId." LIMIT 1");
if (!$result || !$result->num_rows)
{
fail('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.');
}
$row = $result->fetch_assoc();
if ((string)$row['takeover_state'] !== 'ready' || (int)$row['enabled'] !== 0)
{
fail('Connector-Verbindung muss im Zustand ready und deaktiviert sein.');
}
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config) || empty($config['verification']['ok']))
{
fail('Connector-Verbindung besitzt keine erfolgreiche Verifikation.');
}
$keyResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$keyResult || !$keyResult->num_rows)
{
fail('Connector-Schluessel wurde in Piwigo nicht gefunden.');
}
$keyRow = $keyResult->fetch_assoc();
$dbPassword = decryptConnectorSecret($row['secret_blob'], (string)$keyRow['value']);
$legacy = readKeyValueFile($legacyConfig);
$piwigoUser = trim((string)($legacy['PIWIGO_SYNC_USER'] ?? ''));
$piwigoPasswordFile = trim((string)($legacy['PIWIGO_SYNC_PASSWORD_FILE'] ?? '/etc/piwigo-sync/piwigo-password'));
if ($piwigoUser === '' || !is_readable($piwigoPasswordFile))
{
fail('Legacy-Piwigo-Sync-Zugangsdaten konnten fuer den einmaligen Cutover nicht gelesen werden.');
}
$piwigoPassword = trim((string)file_get_contents($piwigoPasswordFile));
if ($piwigoPassword === '')
{
fail('Legacy-Piwigo-Sync-Passwort ist leer.');
}
foreach (array('host','port','database','user','source_view','gallery_root','state_dir') as $key)
{
if (!isset($config[$key]) || trim((string)$config[$key]) === '')
{
fail('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
}
}
$runtimeDir = '/etc/bratonien-tools/nc-connector';
if (!is_dir($runtimeDir) && !mkdir($runtimeDir, 0700, true))
{
fail('Connector-Laufzeitverzeichnis konnte nicht angelegt werden.');
}
chmod($runtimeDir, 0700);
$base = $runtimeDir.'/connection-'.$connectionId;
$dbPasswordPath = $base.'.db-password';
$piwigoPasswordPath = $base.'.piwigo-password';
$storagePath = $base.'.storages.tsv';
$configPath = $base.'.conf';
$statusPath = rtrim((string)$config['state_dir'], '/').'/connector-status.json';
file_put_contents($dbPasswordPath, $dbPassword."\n", LOCK_EX);
file_put_contents($piwigoPasswordPath, $piwigoPassword."\n", LOCK_EX);
chmod($dbPasswordPath, 0600);
chmod($piwigoPasswordPath, 0600);
$storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount');
foreach ((array)($config['storages'] ?? array()) as $storage)
{
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t".(string)($storage['source_prefix'] ?? '')."\t".(string)($storage['local_mount'] ?? '');
}
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX);
chmod($storagePath, 0600);
$piwigoRootConfigured = isset($legacy['PIWIGO_ROOT']) && trim((string)$legacy['PIWIGO_ROOT']) !== '' ? trim((string)$legacy['PIWIGO_ROOT']) : $piwigoRoot;
$lines = array(
'PIWIGO_ROOT='.$piwigoRootConfigured,
'GALLERY_ROOT='.(string)$config['gallery_root'],
'STATE_DIR='.(string)$config['state_dir'],
'STATUS_FILE='.$statusPath,
'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_DB_PASSWORD_FILE='.$dbPasswordPath,
'STORAGE_CONFIG='.$storagePath,
'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',
'PIWIGO_SYNC_USER='.$piwigoUser,
'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath,
);
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
chmod($configPath, 0600);
$enabledCheck = runCommand(array('systemctl', 'is-enabled', $legacyTimer), true);
$legacyTimerWasEnabled = $enabledCheck['exit'] === 0;
echo "Stoppe Legacy-Timer...\n";
runCommand(array('systemctl', 'stop', $legacyTimer));
$deadline = time() + 120;
do
{
$active = runCommand(array('systemctl', 'is-active', '--quiet', 'piwigo-sync.service'), true);
if ($active['exit'] !== 0)
{
break;
}
if (time() >= $deadline)
{
fail('Ein laufender Legacy-Sync wurde nach 120 Sekunden nicht beendet.');
}
sleep(2);
}
while (true);
@unlink($statusPath);
echo "Fuehre ersten Connector-Lauf aus...\n";
$firstRun = runCommand(array('env', 'PIWIGO_CONFIG='.$configPath, $legacyRuntime), true);
if ($firstRun['exit'] !== 0)
{
$detail = trim($firstRun['stderr']) !== '' ? trim($firstRun['stderr']) : trim($firstRun['stdout']);
fail('Erster Connector-Lauf ist technisch fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.'));
}
$runResult = 'no_changes';
if (is_readable($statusPath))
{
$status = json_decode((string)file_get_contents($statusPath), true);
if (!is_array($status) || (string)($status['state'] ?? '') !== 'ok')
{
fail('Erster Connector-Lauf lieferte keinen gueltigen Erfolgsstatus.');
}
$runResult = 'changed';
}
$servicePath = '/etc/systemd/system/'.$newService;
$timerPath = '/etc/systemd/system/'.$newTimer;
$service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=PIWIGO_CONFIG=".$configPath."\nExecStart=".$legacyRuntime."\n\n";
$timer = "[Unit]\nDescription=Bratonien NC Connector regelmaessig pruefen\n\n[Timer]\nOnBootSec=3min\nOnUnitActiveSec=1min\nRandomizedDelaySec=15s\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n";
file_put_contents($servicePath, $service, LOCK_EX);
file_put_contents($timerPath, $timer, LOCK_EX);
chmod($servicePath, 0644);
chmod($timerPath, 0644);
$newTimerInstalled = true;
runCommand(array('systemctl', 'daemon-reload'));
runCommand(array('systemctl', 'disable', $legacyTimer), true);
runCommand(array('systemctl', 'enable', '--now', $newTimer));
$config['takeover']['cutover_at'] = date('Y-m-d H:i:s');
$config['takeover']['legacy_timer_disabled'] = true;
$config['takeover']['connector_timer'] = $newTimer;
$config['takeover']['runtime'] = 'legacy-runtime-transition';
$config['takeover']['first_run'] = array(
'state' => 'success',
'result' => $runResult,
'checked_at' => date('Y-m-d H:i:s'),
);
saveTakeoverResult($db, $table, $connectionId, $config, 'active', true);
echo "Cutover erfolgreich.\n";
echo "Erster Connector-Lauf: ".$runResult."\n";
echo "Legacy-Timer ist deaktiviert; ".$newTimer." ist aktiv.\n";
echo "Die bisherige Sync-Runtime bleibt fuer diesen Uebergangsschritt noch als Laufzeit erhalten.\n";
}
catch (Throwable $e)
{
if ($db instanceof mysqli && $table !== '' && $config)
{
try
{
$config['takeover']['first_run'] = array(
'state' => 'error',
'result' => 'error',
'checked_at' => date('Y-m-d H:i:s'),
'message' => substr($e->getMessage(), 0, 500),
);
saveTakeoverResult($db, $table, $connectionId, $config, 'ready', false);
}
catch (Throwable $ignored)
{
}
}
if ($newTimerInstalled)
{
runCommand(array('systemctl', 'disable', '--now', $newTimer), true);
}
if ($legacyTimerWasEnabled)
{
runCommand(array('systemctl', 'enable', '--now', $legacyTimer), true);
}
else
{
runCommand(array('systemctl', 'start', $legacyTimer), true);
}
fwrite(STDERR, "Cutover fehlgeschlagen: ".$e->getMessage()."\n");
fwrite(STDERR, "Legacy-Timer wurde wieder aktiviert/gestartet.\n");
exit(1);
}

View File

@@ -1,110 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-diagnose.php <connection-id>\n");
exit(1);
}
$connectionId = (int)$argv[1];
$configPath = '/etc/bratonien-tools/nc-connector/connection-'.$connectionId.'.conf';
$runtime = '/opt/piwigo-sync/sync.sh';
$legacyTimer = 'piwigo-sync.timer';
$legacyService = 'piwigo-sync.service';
function runCommand(array $command, $allowFailure = false)
{
$spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$process = proc_open($command, $spec, $pipes);
if (!is_resource($process))
{
throw new RuntimeException('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
if ($exit !== 0 && !$allowFailure)
{
throw new RuntimeException('Befehl fehlgeschlagen: '.implode(' ', $command));
}
return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr);
}
$timerWasEnabled = false;
try
{
if (!is_readable($configPath))
{
throw new RuntimeException('Connector-Konfiguration fehlt oder ist nicht lesbar: '.$configPath.'. Fuehre zuerst den Cutover-Versuch mindestens einmal aus, damit die Laufzeitkonfiguration erzeugt wird.');
}
if (!is_executable($runtime))
{
throw new RuntimeException('Sync-Runtime fehlt: '.$runtime);
}
$enabled = runCommand(array('systemctl', 'is-enabled', $legacyTimer), true);
$timerWasEnabled = $enabled['exit'] === 0;
echo "Stoppe Legacy-Timer fuer die Diagnose...\n";
runCommand(array('systemctl', 'stop', $legacyTimer), true);
$deadline = time() + 120;
while (true)
{
$active = runCommand(array('systemctl', 'is-active', '--quiet', $legacyService), true);
if ($active['exit'] !== 0)
{
break;
}
if (time() >= $deadline)
{
throw new RuntimeException('Ein laufender Legacy-Sync wurde nach 120 Sekunden nicht beendet.');
}
sleep(2);
}
echo "Fuehre Connector-Lauf im Diagnosemodus aus...\n\n";
$result = runCommand(array('env', 'PIWIGO_CONFIG='.$configPath, 'bash', '-x', $runtime), true);
echo "===== STDOUT =====\n";
echo trim($result['stdout'])."\n";
echo "===== STDERR / TRACE =====\n";
echo trim($result['stderr'])."\n";
echo "===== ENDE =====\n";
echo 'Exit-Code: '.$result['exit']."\n";
}
catch (Throwable $e)
{
fwrite(STDERR, 'Diagnose fehlgeschlagen: '.$e->getMessage()."\n");
}
finally
{
if ($timerWasEnabled)
{
runCommand(array('systemctl', 'enable', '--now', $legacyTimer), true);
}
else
{
runCommand(array('systemctl', 'start', $legacyTimer), true);
}
echo "Legacy-Timer wurde nach der Diagnose wieder aktiviert/gestartet.\n";
}

View File

@@ -1,81 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-disable.php <connection-id>\n");
exit(1);
}
$id = (int)$argv[1];
$piwigoRoot = dirname(__DIR__, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$configDir = '/etc/bratonien-tools/nc-connector';
try
{
$conf = array();
$prefixeTable = 'piwigo_';
if (!is_readable($dbConfig)) throw new RuntimeException('Piwigo-Datenbankkonfiguration nicht lesbar.');
require $dbConfig;
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) throw new RuntimeException('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$result = $db->query("SELECT id, enabled, takeover_state, config_json FROM `".$table."` WHERE id=".$id." LIMIT 1");
if (!$result || !$result->num_rows) throw new RuntimeException('Connector-Verbindung #'.$id.' wurde nicht gefunden.');
$row = $result->fetch_assoc();
if ((int)$row['enabled'] !== 1 || (string)$row['takeover_state'] !== 'active')
{
throw new RuntimeException('Die Verbindung ist nicht aktiv.');
}
passthru('systemctl stop bratonien-nc-connector.timer', $stopExit);
if ($stopExit !== 0) throw new RuntimeException('Connector-Timer konnte nicht gestoppt werden.');
$base = $configDir.'/connection-'.$id;
foreach (array('.conf','.storages.tsv','.db-password','.piwigo-password') as $suffix)
{
$path = $base.$suffix;
if (is_file($path) && !unlink($path)) throw new RuntimeException('Datei konnte nicht entfernt werden: '.$path);
}
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) $config = array();
unset($config['runtime']);
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$now = date('Y-m-d H:i:s');
$jsonEsc = $db->real_escape_string((string)$json);
$nowEsc = $db->real_escape_string($now);
if (!$db->query("UPDATE `".$table."` SET enabled=0, takeover_state='disabled', config_json='".$jsonEsc."', updated='".$nowEsc."' WHERE id=".$id))
{
throw new RuntimeException('Connector-Status konnte nicht gespeichert werden: '.$db->error);
}
$remaining = glob($configDir.'/connection-*.conf') ?: array();
if ($remaining)
{
passthru('systemctl start bratonien-nc-connector.timer', $startExit);
if ($startExit !== 0) throw new RuntimeException('Connector-Timer konnte nicht wieder gestartet werden.');
echo "Verbindung #".$id." deaktiviert. Andere Connector-Verbindungen bleiben aktiv.\n";
}
else
{
passthru('systemctl disable bratonien-nc-connector.timer', $disableExit);
echo "Verbindung #".$id." deaktiviert. Es gibt keine weitere aktive Connector-Verbindung; der Timer bleibt gestoppt.\n";
}
}
catch (Throwable $e)
{
fwrite(STDERR, $e->getMessage()."\n");
exit(1);
}

View File

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

View File

@@ -1,183 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-legacy-cleanup.php <connection-id>\n");
exit(1);
}
$connectionId = (int)$argv[1];
$piwigoRoot = dirname(__DIR__, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$pluginRuntime = __DIR__.'/runtime/sync.sh';
$configPath = '/etc/bratonien-tools/nc-connector/connection-'.$connectionId.'.conf';
$connectorService = '/etc/systemd/system/bratonien-nc-connector.service';
$connectorTimer = 'bratonien-nc-connector.timer';
$legacyServiceName = 'piwigo-sync.service';
$legacyTimerName = 'piwigo-sync.timer';
$legacyServicePath = '/etc/systemd/system/'.$legacyServiceName;
$legacyTimerPath = '/etc/systemd/system/'.$legacyTimerName;
function cleanupFail($message)
{
throw new RuntimeException($message);
}
function cleanupRun(array $command, $allowFailure = false)
{
$spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$process = proc_open($command, $spec, $pipes);
if (!is_resource($process))
{
cleanupFail('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
if ($exit !== 0 && !$allowFailure)
{
$detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
cleanupFail('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : ''));
}
return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr);
}
function cleanupRemoveTree($path)
{
if (!file_exists($path) && !is_link($path))
{
return;
}
if (is_link($path) || is_file($path))
{
if (!@unlink($path))
{
cleanupFail('Datei konnte nicht entfernt werden: '.$path);
}
return;
}
$items = scandir($path);
if (!is_array($items))
{
cleanupFail('Verzeichnis konnte nicht gelesen werden: '.$path);
}
foreach ($items as $item)
{
if ($item === '.' || $item === '..')
{
continue;
}
cleanupRemoveTree($path.'/'.$item);
}
if (!@rmdir($path))
{
cleanupFail('Verzeichnis konnte nicht entfernt werden: '.$path);
}
}
try
{
foreach (array($dbConfig, $configPath, $connectorService, $pluginRuntime) as $required)
{
if (!is_readable($required))
{
cleanupFail('Erforderliche Connector-Datei fehlt oder ist nicht lesbar: '.$required);
}
}
$serviceDefinition = (string)file_get_contents($connectorService);
if (strpos($serviceDefinition, $pluginRuntime) === false)
{
cleanupFail('Der aktive Connector-Service verwendet noch nicht die Plugin-eigene Runtime. Legacy-Bestand wird nicht entfernt.');
}
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno)
{
cleanupFail('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
}
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$result = $db->query("SELECT takeover_state, enabled, config_json FROM `".$table."` WHERE id=".$connectionId." LIMIT 1");
if (!$result || !$result->num_rows)
{
cleanupFail('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.');
}
$row = $result->fetch_assoc();
$config = json_decode((string)$row['config_json'], true);
if ((string)$row['takeover_state'] !== 'active' || (int)$row['enabled'] !== 1 || !is_array($config))
{
cleanupFail('Legacy-Cleanup ist nur fuer eine aktive Connector-Verbindung erlaubt.');
}
if (($config['takeover']['runtime'] ?? '') !== 'plugin-runtime')
{
cleanupFail('Connector-Status bestaetigt die Plugin-Runtime noch nicht. Legacy-Bestand wird nicht entfernt.');
}
$active = cleanupRun(array('systemctl', 'is-active', $connectorTimer), true);
if ($active['exit'] !== 0)
{
cleanupFail('Der Connector-Timer ist nicht aktiv. Vor dem Cleanup muss der produktive Connector laufen.');
}
echo "Deaktiviere verbliebene Legacy-Units...\n";
cleanupRun(array('systemctl', 'disable', '--now', $legacyTimerName), true);
cleanupRun(array('systemctl', 'stop', $legacyServiceName), true);
echo "Entferne alte systemd-Units...\n";
foreach (array($legacyTimerPath, $legacyServicePath) as $path)
{
if (is_file($path) || is_link($path))
{
@unlink($path);
}
}
echo "Entferne /opt/piwigo-sync...\n";
cleanupRemoveTree('/opt/piwigo-sync');
echo "Entferne /etc/piwigo-sync...\n";
cleanupRemoveTree('/etc/piwigo-sync');
cleanupRun(array('systemctl', 'daemon-reload'));
cleanupRun(array('systemctl', 'reset-failed', $legacyServiceName), true);
$config['takeover']['legacy_cleaned_at'] = date('Y-m-d H:i:s');
$config['takeover']['legacy_cleanup'] = true;
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (is_string($json))
{
$now = date('Y-m-d H:i:s');
$escapedJson = $db->real_escape_string($json);
$escapedNow = $db->real_escape_string($now);
$db->query("UPDATE `".$table."` SET config_json='".$escapedJson."', updated='".$escapedNow."' WHERE id=".$connectionId);
}
echo "Legacy-Cleanup erfolgreich.\n";
echo "Entfernt: /opt/piwigo-sync, /etc/piwigo-sync sowie piwigo-sync.service/timer.\n";
echo "/var/lib/piwigo-sync bleibt bestehen, weil dort der aktive Connector seinen Laufzeitstatus, Name-Map und Activity-State fuehrt.\n";
}
catch (Throwable $e)
{
fwrite(STDERR, "Legacy-Cleanup abgebrochen: ".$e->getMessage()."\n");
exit(1);
}

View File

@@ -1,133 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
$configPath = '/etc/piwigo-sync/piwigo.conf';
$storagePath = '/etc/piwigo-sync/storages.tsv';
$bundlePath = '/tmp/bratonien-tools-nc-import.json';
function readLegacyConfig($path)
{
if (!is_readable($path))
{
throw new RuntimeException('Legacy-Konfiguration nicht lesbar: '.$path);
}
$config = array();
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line)
{
$line = trim($line);
if ($line === '' || $line[0] === '#')
{
continue;
}
if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches))
{
continue;
}
$value = trim($matches[2]);
if (strlen($value) >= 2)
{
$first = $value[0];
$last = $value[strlen($value)-1];
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'"))
{
$value = substr($value, 1, -1);
}
}
$config[$matches[1]] = $value;
}
return $config;
}
function readStorages($path)
{
$storages = array();
if (!is_readable($path))
{
return $storages;
}
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line)
{
if ($line === '' || $line[0] === '#')
{
continue;
}
$parts = explode("\t", $line);
if (count($parts) !== 3)
{
continue;
}
$storages[] = array(
'storage_id' => $parts[0],
'source_prefix' => $parts[1],
'local_mount' => $parts[2],
);
}
return $storages;
}
try
{
$config = readLegacyConfig($configPath);
$passwordPath = isset($config['NC_DB_PASSWORD_FILE']) ? $config['NC_DB_PASSWORD_FILE'] : '/etc/piwigo-sync/nextcloud-db-password';
if (!is_readable($passwordPath))
{
throw new RuntimeException('Nextcloud-Datenbankpasswort nicht lesbar: '.$passwordPath);
}
$password = trim((string)file_get_contents($passwordPath));
if ($password === '')
{
throw new RuntimeException('Nextcloud-Datenbankpasswort ist leer.');
}
$bundle = array(
'format' => 1,
'created_at' => gmdate('c'),
'config' => $config,
'db_password' => $password,
'storages' => readStorages($storagePath),
);
$json = json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json))
{
throw new RuntimeException('Migrationspaket konnte nicht erzeugt werden.');
}
if (file_put_contents($bundlePath, $json."\n", LOCK_EX) === false)
{
throw new RuntimeException('Migrationspaket konnte nicht geschrieben werden: '.$bundlePath);
}
@chmod($bundlePath, 0600);
if (!@chown($bundlePath, 'www-data'))
{
@unlink($bundlePath);
throw new RuntimeException('Migrationspaket konnte nicht sicher an www-data uebergeben werden.');
}
@chgrp($bundlePath, 'www-data');
echo "Migrationspaket wurde bereitgestellt.\n";
echo "Jetzt in Piwigo -> Bratonien Tools -> NC Connector wechseln und 'Bestehende Verbindung importieren' ausfuehren.\n";
echo "Der laufende piwigo-sync wurde nicht veraendert oder gestoppt.\n";
}
catch (Throwable $e)
{
fwrite(STDERR, 'Fehler: '.$e->getMessage()."\n");
exit(1);
}

View File

@@ -1,149 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-normalize.php <connection-id>\n");
exit(1);
}
$id = (int)$argv[1];
$pluginRoot = __DIR__;
$piwigoRoot = dirname(__DIR__, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$configPath = '/etc/bratonien-tools/nc-connector/connection-'.$id.'.conf';
$newState = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$servicePath = '/etc/systemd/system/bratonien-nc-connector.service';
$timer = 'bratonien-nc-connector.timer';
$service = 'bratonien-nc-connector.service';
function normalize_run(array $command, $allowFailure = false)
{
$spec = array(0=>array('file','/dev/null','r'), 1=>array('pipe','w'), 2=>array('pipe','w'));
$process = proc_open($command, $spec, $pipes);
if (!is_resource($process)) throw new RuntimeException('Prozess konnte nicht gestartet werden.');
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]); fclose($pipes[2]);
$exit = proc_close($process);
if ($exit !== 0 && !$allowFailure)
{
$detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
throw new RuntimeException('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : ''));
}
return array('exit'=>$exit, 'stdout'=>$stdout, 'stderr'=>$stderr);
}
function remove_tree($path)
{
if (!is_dir($path)) return;
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
if ($item->isDir()) @rmdir($item->getPathname()); else @unlink($item->getPathname());
}
@rmdir($path);
}
try
{
$conf = array();
$prefixeTable = 'piwigo_';
if (!is_readable($dbConfig)) throw new RuntimeException('Piwigo-Datenbankkonfiguration nicht lesbar.');
require $dbConfig;
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) throw new RuntimeException('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$result = $db->query("SELECT id, enabled, takeover_state, config_json FROM `".$table."` WHERE id=".$id." LIMIT 1");
if (!$result || !$result->num_rows) throw new RuntimeException('Connector-Verbindung #'.$id.' wurde nicht gefunden.');
$row = $result->fetch_assoc();
if ((int)$row['enabled'] !== 1 || (string)$row['takeover_state'] !== 'active') throw new RuntimeException('Die Verbindung muss aktiv sein.');
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) throw new RuntimeException('Connector-Konfiguration ist ungueltig.');
$oldState = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($oldState === '') throw new RuntimeException('Bisheriger State-Pfad fehlt.');
if ($oldState === $newState)
{
echo "Verbindung #".$id." verwendet bereits den nativen State-Pfad.\n";
exit(0);
}
if (!is_readable($configPath)) throw new RuntimeException('Aktive Runtime-Konfiguration fehlt: '.$configPath);
echo "Stoppe Connector-Timer...\n";
normalize_run(array('systemctl','stop',$timer));
$deadline = time() + 120;
while (normalize_run(array('systemctl','is-active','--quiet',$service), true)['exit'] === 0)
{
if (time() >= $deadline) throw new RuntimeException('Laufender Connector-Lauf wurde nach 120 Sekunden nicht beendet.');
sleep(2);
}
if (!is_dir(dirname($newState)) && !mkdir(dirname($newState), 0750, true)) throw new RuntimeException('Neues State-Wurzelverzeichnis konnte nicht angelegt werden.');
if (!is_dir($newState) && !mkdir($newState, 0750, true)) throw new RuntimeException('Neues State-Verzeichnis konnte nicht angelegt werden.');
if (is_dir($oldState)) normalize_run(array('cp','-a',$oldState.'/.',$newState.'/'));
chmod($newState, 0750);
$originalConfig = file_get_contents($configPath);
if (!is_string($originalConfig)) throw new RuntimeException('Runtime-Konfiguration konnte nicht gelesen werden.');
$newStatus = $newState.'/connector-status.json';
$updatedConfig = preg_replace('/^STATE_DIR=.*$/m', 'STATE_DIR='.$newState, $originalConfig, 1);
$updatedConfig = preg_replace('/^STATUS_FILE=.*$/m', 'STATUS_FILE='.$newStatus, $updatedConfig, 1);
if (!is_string($updatedConfig) || $updatedConfig === $originalConfig) throw new RuntimeException('Runtime-Konfiguration konnte nicht auf nativen State umgestellt werden.');
file_put_contents($configPath, $updatedConfig, LOCK_EX);
chmod($configPath, 0600);
echo "Teste Plugin-Runtime mit neuem State...\n";
$test = normalize_run(array('env','PIWIGO_CONFIG='.$configPath,'bash',$pluginRoot.'/runtime/sync.sh'), true);
if ($test['exit'] !== 0)
{
file_put_contents($configPath, $originalConfig, LOCK_EX);
remove_tree($newState);
normalize_run(array('systemctl','start',$timer), true);
$detail = trim($test['stderr']) !== '' ? trim($test['stderr']) : trim($test['stdout']);
throw new RuntimeException('Normalisierung abgebrochen; alter Zustand wiederhergestellt'.($detail !== '' ? ': '.$detail : '.'));
}
$serviceContent = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nExecStart=/usr/bin/env bash ".$pluginRoot."/runtime/run-all.sh\n\n";
file_put_contents($servicePath, $serviceContent, LOCK_EX);
chmod($servicePath, 0644);
$config['state_dir'] = $newState;
$config['status_file'] = $newStatus;
$config['origin'] = 'native';
$config['runtime'] = array('mode'=>'plugin-runtime', 'config'=>$configPath);
if (isset($config['takeover']) && is_array($config['takeover'])) $config['takeover']['runtime'] = 'plugin-runtime';
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$jsonEsc = $db->real_escape_string((string)$json);
$nowEsc = $db->real_escape_string(date('Y-m-d H:i:s'));
if (!$db->query("UPDATE `".$table."` SET config_json='".$jsonEsc."', updated='".$nowEsc."' WHERE id=".$id)) throw new RuntimeException('Connector-Datenbankstatus konnte nicht aktualisiert werden: '.$db->error);
normalize_run(array('systemctl','daemon-reload'));
normalize_run(array('systemctl','enable','--now',$timer));
if ($oldState !== '/var/lib' && $oldState !== '/' && strpos($oldState, '/var/lib/') === 0)
{
remove_tree($oldState);
}
echo "Normalisierung erfolgreich.\n";
echo "State: ".$newState."\n";
echo "Service: gemeinsamer Multi-Connection-Runner aus Bratonien Tools.\n";
}
catch (Throwable $e)
{
fwrite(STDERR, $e->getMessage()."\n");
exit(1);
}

View File

@@ -1,226 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-runtime-switch.php <connection-id>\n");
exit(1);
}
$connectionId = (int)$argv[1];
$piwigoRoot = dirname(__DIR__, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$pluginRuntime = __DIR__.'/runtime/sync.sh';
$configPath = '/etc/bratonien-tools/nc-connector/connection-'.$connectionId.'.conf';
$serviceName = 'bratonien-nc-connector.service';
$timerName = 'bratonien-nc-connector.timer';
$servicePath = '/etc/systemd/system/'.$serviceName;
function failRuntimeSwitch($message)
{
throw new RuntimeException($message);
}
function runRuntimeSwitch(array $command, $allowFailure = false)
{
$spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$process = proc_open($command, $spec, $pipes);
if (!is_resource($process))
{
failRuntimeSwitch('Prozess konnte nicht gestartet werden: '.implode(' ', $command));
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
if ($exit !== 0 && !$allowFailure)
{
$detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
failRuntimeSwitch('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : ''));
}
return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr);
}
function parseRuntimeConfig($path)
{
if (!is_readable($path))
{
failRuntimeSwitch('Connector-Konfiguration nicht lesbar: '.$path);
}
$result = array();
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line)
{
$line = trim($line);
if ($line === '' || $line[0] === '#')
{
continue;
}
if (preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches))
{
$result[$matches[1]] = trim($matches[2]);
}
}
return $result;
}
function dbEscapeRuntime(mysqli $db, $value)
{
return $db->real_escape_string((string)$value);
}
$oldService = null;
$db = null;
$table = '';
$config = array();
$timerStopped = false;
try
{
if (!is_file($pluginRuntime) || !is_readable($pluginRuntime))
{
failRuntimeSwitch('Plugin-Runtime fehlt: '.$pluginRuntime);
}
if (!is_readable($dbConfig))
{
failRuntimeSwitch('Piwigo-Datenbankkonfiguration nicht lesbar.');
}
if (!is_readable($servicePath))
{
failRuntimeSwitch('Bestehender Connector-Service wurde nicht gefunden: '.$servicePath);
}
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno)
{
failRuntimeSwitch('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error);
}
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$result = $db->query("SELECT takeover_state, enabled, config_json FROM `".$table."` WHERE id=".$connectionId." LIMIT 1");
if (!$result || !$result->num_rows)
{
failRuntimeSwitch('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.');
}
$row = $result->fetch_assoc();
if ((string)$row['takeover_state'] !== 'active' || (int)$row['enabled'] !== 1)
{
failRuntimeSwitch('Runtime-Switch ist nur fuer eine aktive Connector-Verbindung erlaubt.');
}
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config))
{
failRuntimeSwitch('Connector-Konfiguration ist ungueltig.');
}
$runtimeConfig = parseRuntimeConfig($configPath);
$statusPath = isset($runtimeConfig['STATUS_FILE']) ? (string)$runtimeConfig['STATUS_FILE'] : '';
if ($statusPath === '')
{
failRuntimeSwitch('STATUS_FILE fehlt in der Connector-Konfiguration.');
}
$oldService = file_get_contents($servicePath);
if (!is_string($oldService) || $oldService === '')
{
failRuntimeSwitch('Bestehende Service-Definition konnte nicht gesichert werden.');
}
echo "Stoppe Connector-Timer fuer den Runtime-Test...\n";
runRuntimeSwitch(array('systemctl', 'stop', $timerName));
$timerStopped = true;
$deadline = time() + 120;
do
{
$active = runRuntimeSwitch(array('systemctl', 'is-active', '--quiet', $serviceName), true);
if ($active['exit'] !== 0)
{
break;
}
if (time() >= $deadline)
{
failRuntimeSwitch('Ein laufender Connector-Sync wurde nach 120 Sekunden nicht beendet.');
}
sleep(2);
}
while (true);
@unlink($statusPath);
echo "Teste Plugin-eigene Sync-Runtime...\n";
$test = runRuntimeSwitch(array('env', 'PIWIGO_CONFIG='.$configPath, '/bin/bash', $pluginRuntime), true);
if ($test['exit'] !== 0)
{
$detail = trim($test['stderr']) !== '' ? trim($test['stderr']) : trim($test['stdout']);
failRuntimeSwitch('Plugin-Runtime-Test fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.'));
}
$status = is_readable($statusPath) ? json_decode((string)file_get_contents($statusPath), true) : null;
$state = is_array($status) ? trim((string)($status['state'] ?? '')) : '';
$message = is_array($status) ? trim((string)($status['message'] ?? '')) : '';
if ($state === 'error')
{
failRuntimeSwitch('Plugin-Runtime meldete einen Fehler'.($message !== '' ? ': '.$message : '.'));
}
echo 'Runtime-Test erfolgreich: '.($message !== '' ? $message : 'Exit-Code 0')."\n";
$service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=PIWIGO_CONFIG=".$configPath."\nExecStart=/bin/bash ".$pluginRuntime."\n\n";
if (file_put_contents($servicePath, $service, LOCK_EX) === false)
{
failRuntimeSwitch('Neue Service-Definition konnte nicht geschrieben werden.');
}
chmod($servicePath, 0644);
runRuntimeSwitch(array('systemctl', 'daemon-reload'));
runRuntimeSwitch(array('systemctl', 'enable', '--now', $timerName));
$timerStopped = false;
$config['takeover']['runtime'] = 'plugin-runtime';
$config['takeover']['runtime_switched_at'] = date('Y-m-d H:i:s');
$config['takeover']['runtime_path'] = $pluginRuntime;
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json))
{
failRuntimeSwitch('Connector-Status konnte nicht serialisiert werden.');
}
$now = date('Y-m-d H:i:s');
$sql = "UPDATE `".$table."` SET config_json='".dbEscapeRuntime($db, $json)."', updated='".dbEscapeRuntime($db, $now)."' WHERE id=".$connectionId;
if (!$db->query($sql))
{
failRuntimeSwitch('Connector-Status konnte nicht aktualisiert werden: '.$db->error);
}
echo "Runtime-Switch erfolgreich.\n";
echo "Der Connector verwendet jetzt ausschliesslich die Plugin-eigene Sync-Runtime.\n";
}
catch (Throwable $e)
{
if (is_string($oldService) && $oldService !== '')
{
@file_put_contents($servicePath, $oldService, LOCK_EX);
runRuntimeSwitch(array('systemctl', 'daemon-reload'), true);
}
if ($timerStopped)
{
runRuntimeSwitch(array('systemctl', 'enable', '--now', $timerName), true);
}
fwrite(STDERR, "Runtime-Switch fehlgeschlagen: ".$e->getMessage()."\n");
fwrite(STDERR, "Die bisherige Connector-Runtime wurde beibehalten/wiederhergestellt.\n");
exit(1);
}

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,77 +0,0 @@
#!/usr/bin/env python3
"""Debounce Nextcloud activity and request a periodic safety reconciliation."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
import time
from pathlib import Path
IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$")
def validate_view(name: str) -> str:
value = str(name).strip()
if not IDENTIFIER.fullmatch(value):
raise ValueError(f"invalid SQL view name: {value!r}")
return value
def load(path: Path) -> dict[str, object]:
defaults: dict[str, object] = {"processed":0,"observed":0,"pending_since":0,"last_change":0,"last_full":0,"source_signature":""}
if not path.exists():
return defaults
data=json.loads(path.read_text(encoding="utf-8"))
return {"processed":int(data.get("processed",0)),"observed":int(data.get("observed",0)),"pending_since":int(data.get("pending_since",0)),"last_change":int(data.get("last_change",0)),"last_full":int(data.get("last_full",0)),"source_signature":str(data.get("source_signature",""))}
def save(path: Path, state: dict[str, object]) -> None:
path.parent.mkdir(parents=True,exist_ok=True)
fd,name=tempfile.mkstemp(dir=path.parent)
with os.fdopen(fd,"w",encoding="utf-8") as handle:
json.dump(state,handle,sort_keys=True);handle.write("\n")
Path(name).replace(path)
def query(args: argparse.Namespace, sql: str) -> str:
env=os.environ.copy();env["PGPASSWORD"]=args.password_file.read_text(encoding="utf-8").strip()
command=["psql","-XAt","-h",args.host,"-p",str(args.port),"-U",args.user,"-d",args.database,"-v","ON_ERROR_STOP=1","-c",sql]
return subprocess.run(command,env=env,check=True,text=True,capture_output=True).stdout
def latest(args: argparse.Namespace) -> int:
return int(query(args,f"SELECT COALESCE(MAX(activity_id), 0) FROM {validate_view(args.view)}").strip())
def source_signature(args: argparse.Namespace) -> str:
if not args.source_view:return ""
payload=query(args,f"SELECT share_id, display_name, storage_id, source_path FROM {validate_view(args.source_view)} ORDER BY share_id")
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def main() -> int:
parser=argparse.ArgumentParser();parser.add_argument("action",choices=("check","commit"));parser.add_argument("--state",required=True,type=Path);parser.add_argument("--host",required=True);parser.add_argument("--port",type=int,default=5432);parser.add_argument("--database",required=True);parser.add_argument("--user",required=True);parser.add_argument("--password-file",required=True,type=Path);parser.add_argument("--view",default="piwigo_showcase_activity");parser.add_argument("--source-view",default="");parser.add_argument("--quiet",type=int,default=120);parser.add_argument("--max-wait",type=int,default=900);parser.add_argument("--full-after",type=int,default=86400);args=parser.parse_args()
try:
validate_view(args.view)
if args.source_view:validate_view(args.source_view)
state=load(args.state);now=int(time.time());current=latest(args);current_source=source_signature(args)
if args.action=="commit":state.update(processed=current,observed=current,pending_since=0,last_change=0,last_full=now,source_signature=current_source);save(args.state,state);return 0
if not int(state["last_full"]):return 0
if current_source and current_source!=str(state["source_signature"]):return 0
if current>int(state["observed"]):state["observed"]=current;state["last_change"]=now;state["pending_since"]=int(state["pending_since"]) or now;save(args.state,state)
if now-int(state["last_full"])>=args.full_after:return 0
if int(state["observed"])<=int(state["processed"]):return 3
if now-int(state["last_change"])>=args.quiet or now-int(state["pending_since"])>=args.max_wait:return 0
return 3
except Exception as error:
print(f"activity-gate: {error}",file=sys.stderr);return 1
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,115 +0,0 @@
#!/usr/bin/env python3
"""Resolve Nextcloud view rows through explicitly configured storage mounts."""
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 read_config(path: Path) -> dict[str, tuple[str, Path]]:
result: dict[str, tuple[str, Path]] = {}
with path.open(encoding="utf-8") as handle:
for number, line in enumerate(handle, 1):
line=line.rstrip("\n")
if not line or line.startswith("#"):continue
fields=line.split("\t")
if len(fields)!=3:raise ValueError(f"{path}:{number}: expected storage_id, source_prefix and local_mount")
storage_id,prefix,mount=fields
if not storage_id.strip():raise ValueError(f"{path}:{number}: storage_id is empty")
result[storage_id]=(prefix.strip("/"),Path(mount))
return result
def run_query(args: argparse.Namespace, env: dict[str,str], sql: str) -> subprocess.CompletedProcess[str]:
command=["psql","-X","-A","-F","\t","-t","-v","ON_ERROR_STOP=1","-h",args.host,"-p",str(args.port),"-U",args.user,"-d",args.database,"-c",sql]
return subprocess.run(command,env=env,check=False,text=True,capture_output=True)
def query_rows(args: argparse.Namespace) -> list[list[str]]:
password=args.password_file.read_text(encoding="utf-8").strip();env=os.environ.copy();env["PGPASSWORD"]=password;view=validate_view(args.view)
modern_sql=f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {view} ORDER BY share_id"
completed=run_query(args,env,modern_sql)
if completed.returncode==0:return list(csv.reader(completed.stdout.splitlines(),delimiter="\t"))
if "item_type" not in completed.stderr or "does not exist" not in completed.stderr:raise RuntimeError(completed.stderr.strip() or "Nextcloud source view query failed")
legacy_sql=f"SELECT share_id, display_name, storage_id, source_path FROM {view} ORDER BY share_id"
completed=run_query(args,env,legacy_sql)
if completed.returncode!=0:raise RuntimeError(completed.stderr.strip() or "legacy Nextcloud source view query failed")
rows=[]
for row in csv.reader(completed.stdout.splitlines(),delimiter="\t"):
if len(row)==4:
share_id,display_name,storage_id,source_path=row;rows.append([share_id,"",display_name,storage_id,source_path])
else:rows.append(row)
return rows
def contained_join(root: Path, relative: str) -> Path:
parts=PurePosixPath(relative).parts
if relative.startswith("/") or ".." in parts:raise ValueError(f"unsafe source path: {relative}")
return root.joinpath(*parts)
def build(args: argparse.Namespace) -> dict[str,object]:
validate_view(args.view);adapters=read_config(args.storage_config);rows=query_rows(args)
if not rows and not args.allow_empty:raise RuntimeError("Nextcloud returned no Showcase shares; refusing an empty manifest")
manifest=[];errors=[];folder_count=0;file_count=0
for row in rows:
if len(row)!=5:errors.append(f"invalid database row with {len(row)} columns");continue
share_id,item_type,display_name,storage_id,source_path=row;item_type=item_type.strip().lower()
if item_type and item_type not in {"folder","file"}:errors.append(f"share {share_id}: unsupported item_type {item_type!r}");continue
adapter=adapters.get(storage_id)
if not adapter:errors.append(f"share {share_id}: unknown storage {storage_id}");continue
prefix,mount=adapter;relative=source_path.strip("/")
if prefix:
expected=prefix+"/"
if relative!=prefix and not relative.startswith(expected):errors.append(f"share {share_id}: path does not match configured prefix");continue
relative=relative[len(prefix):].lstrip("/")
source=contained_join(mount,relative)
if not mount.is_mount():errors.append(f"share {share_id}: storage mount unavailable: {mount}");continue
if not item_type:
if source.is_dir():item_type="folder"
elif source.is_file():item_type="file"
else:errors.append(f"share {share_id}: source unavailable: {source}");continue
if item_type=="folder":
if not source.is_dir():errors.append(f"share {share_id}: source directory unavailable: {source}");continue
folder_count+=1
else:
if not source.is_file():errors.append(f"share {share_id}: source file unavailable: {source}");continue
file_count+=1
if "\t" in display_name or "\n" in display_name or "\r" in display_name:errors.append(f"share {share_id}: display name contains unsupported control characters");continue
source_text=str(source)
if "\t" in source_text or "\n" in source_text or "\r" in source_text:errors.append(f"share {share_id}: source path contains unsupported control characters");continue
manifest.append(f"{share_id}\t{item_type}\t{display_name.lstrip('/')}\t{source}")
if errors:raise RuntimeError("; ".join(errors))
if len(manifest)!=len(rows):raise RuntimeError("not all Showcase shares could be resolved")
args.output.parent.mkdir(parents=True,exist_ok=True)
with tempfile.NamedTemporaryFile("w",encoding="utf-8",dir=args.output.parent,delete=False) as handle:
handle.write("\n".join(manifest)+("\n" if manifest else ""));temporary=Path(handle.name)
temporary.replace(args.output)
return {"shares":len(manifest),"folders":folder_count,"files":file_count,"manifest":str(args.output)}
def main() -> int:
parser=argparse.ArgumentParser();parser.add_argument("--host",required=True);parser.add_argument("--port",type=int,default=5432);parser.add_argument("--database",required=True);parser.add_argument("--user",required=True);parser.add_argument("--password-file",required=True,type=Path);parser.add_argument("--view",default="piwigo_showcase_sources");parser.add_argument("--storage-config",required=True,type=Path);parser.add_argument("--output",required=True,type=Path);parser.add_argument("--allow-empty",action="store_true");args=parser.parse_args()
try:print(json.dumps(build(args),ensure_ascii=False))
except Exception as error:print(f"manifest: {error}",file=sys.stderr);return 1
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

@@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
if ($argc !== 3) {
fwrite(STDERR, "Usage: piwigo-db-check.php MAP_FILE PIWIGO_ROOT\n");
exit(2);
}
$mapFile = $argv[1];
$piwigoRoot = rtrim($argv[2], '/') . '/';
$databaseConfig = $piwigoRoot . 'local/config/database.inc.php';
if (!is_file($databaseConfig)) {
exit(1);
}
$mapping = json_decode((string) file_get_contents($mapFile), true, 512, JSON_THROW_ON_ERROR);
$expected = [];
foreach ($mapping as $source => $target) {
if (str_starts_with((string) $source, 'share:') && !str_contains((string) $source, '/')) {
$expected[] = basename((string) $target);
}
}
if ($expected === []) {
exit(1);
}
$conf = [];
$prefixeTable = '';
require $databaseConfig;
$database = new mysqli(
(string) $conf['db_host'],
(string) $conf['db_user'],
(string) $conf['db_password'],
(string) $conf['db_base']
);
$database->set_charset('utf8mb4');
$table = $database->real_escape_string((string) $prefixeTable) . 'categories';
$result = $database->query(
'SELECT dir FROM `' . $table . '` WHERE site_id = 1 AND dir IS NOT NULL'
);
$existing = [];
while ($row = $result->fetch_assoc()) {
$existing[(string) $row['dir']] = true;
}
foreach ($expected as $directory) {
if (!isset($existing[$directory])) {
exit(1);
}
}
exit(0);

View File

@@ -1,83 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use JSON::PP qw(decode_json);
use LWP::UserAgent;
use Getopt::Long;
my %opt;
GetOptions(
\%opt,
'base-url=s',
'username=s',
'password-file=s',
) or die "Ungültige Parameter\n";
for my $required (qw(base-url username password-file)) {
die "Parameter --$required fehlt\n" if !defined $opt{$required} || $opt{$required} eq '';
}
open my $password_handle, '<', $opt{'password-file'}
or die "Passwortdatei kann nicht gelesen werden: $!\n";
my $password = <$password_handle>;
close $password_handle;
defined $password or die "Passwortdatei ist leer\n";
chomp $password;
my $ua = LWP::UserAgent->new(timeout => 900);
$ua->agent('Bratonien-NC-Connector/1.0');
$ua->cookie_jar({});
my $login = $ua->post(
$opt{'base-url'}.'/ws.php?format=json',
{
method => 'pwg.session.login',
username => $opt{username},
password => $password,
},
);
die "Piwigo-Anmeldung fehlgeschlagen: ".$login->status_line."\n"
if !$login->is_success;
my $login_result = eval { decode_json($login->decoded_content) };
die "Piwigo-Anmeldung wurde abgelehnt\n"
if !$login_result || ($login_result->{stat} // '') ne 'ok';
my $sync = $ua->post(
$opt{'base-url'}.'/admin.php?page=site_update&site=1',
{
sync => 'files',
display_info => 1,
privacy_level => 0,
sync_meta => 1,
simulate => 0,
'subcats-included' => 1,
bratonien_connector => 1,
submit => 1,
},
);
die "Piwigo-Datenbanksynchronisierung fehlgeschlagen: ".$sync->status_line."\n"
if !$sync->is_success;
my $orphans = $ua->post(
$opt{'base-url'}.'/ws.php?format=json',
{
method => 'bratonien.nc.syncOrphans',
site_id => 1,
simulate => 0,
},
);
die "Piwigo-Orphan-Synchronisierung fehlgeschlagen: ".$orphans->status_line."\n"
if !$orphans->is_success;
my $orphan_result = eval { decode_json($orphans->decoded_content) };
die "Piwigo-Orphan-Synchronisierung wurde abgelehnt\n"
if !$orphan_result || ($orphan_result->{stat} // '') ne 'ok';
my $result = $orphan_result->{result} || {};
my $added = $result->{added_orphans} // 0;
my $deleted = $result->{deleted_orphans} // 0;
print "Piwigo-Datenbanksynchronisierung erfolgreich\n";
print "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";

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)
@@ -63,12 +50,9 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 900, CURLOPT_TIMEOUT => 900,
CURLOPT_FOLLOWLOCATION => false, CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-NC-Connector', CURLOPT_USERAGENT => 'Bratonien-NC-Connector-WebDAV',
); );
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,120 +120,104 @@ 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')
{
fail_sync('Verbindung #'.$connection_id.' ist keine WebDAV-Verbindung.');
}
$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'=>'');
$api_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1"); if (!empty($connection_config['api_enabled']))
if ($api_result && $api_result->num_rows)
{ {
$api_blob = (string)$api_result->fetch_assoc()['value']; $api['key_id'] = trim((string)($connection_credentials['api_key_id'] ?? ''));
if ($api_blob !== '') $api['key_secret'] = trim((string)($connection_credentials['api_key_secret'] ?? ''));
{
$api_plain = decrypt_blob($api_blob, $hex_key);
$api_decoded = json_decode($api_plain, true);
if (is_array($api_decoded))
{
$api['key_id'] = (string)($api_decoded['key_id'] ?? '');
$api['key_secret'] = (string)($api_decoded['key_secret'] ?? '');
}
}
} }
$connection_table = $prefixeTable.'bratonien_tools_nc_connections'; $fallback_user = trim((string)($connection_credentials['piwigo_user'] ?? ''));
$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_password = (string)($connection_credentials['piwigo_password'] ?? ''); $fallback_password = (string)($connection_credentials['piwigo_password'] ?? '');
$api_error = null;
if ($api['key_id'] !== '' && $api['key_secret'] !== '') if ($api['key_id'] !== '' && $api['key_secret'] !== '')
{ {
try try
@@ -284,80 +227,42 @@ 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),
$headers
));
$orphan = decode_ws(http_request(
$base_url.'/ws.php?format=json',
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);
} }
catch (Throwable $e) catch (Throwable $e)
{ {
$api_error = $e->getMessage(); fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$e->getMessage()."\n");
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
} }
} }
else else
{ {
$api_error = 'Keine gespeicherten API-Zugangsdaten.'; fwrite(STDERR, "Piwigo-API nicht nutzbar: Fuer diese Verbindung ist keine API konfiguriert.\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(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0),
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

@@ -1,17 +0,0 @@
CREATE OR REPLACE VIEW piwigo_showcase_sources AS
SELECT
s.id AS share_id,
s.file_target AS display_name,
st.id AS storage_id,
f.path AS source_path,
s.accepted,
s.permissions,
s.item_type AS item_type
FROM oc_share AS s
JOIN oc_filecache AS f ON f.fileid = s.file_source
JOIN oc_storages AS st ON st.numeric_id = f.storage
WHERE lower(s.share_with) = 'showcase'
AND s.item_type IN ('folder', 'file')
AND s.accepted = 1;
GRANT SELECT ON piwigo_showcase_sources TO piwigo_reader;

View File

@@ -0,0 +1,248 @@
#!/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';
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}` WHERE adapter='remote' ORDER BY id DESC");
if (!$rows) fail_webdav_reconcile('WebDAV-Verbindungen konnten nicht gelesen werden: '.$db->error);
foreach (array($configDir, $stateRoot, $publicSourceRoot, $publicGalleryRoot) as $dir)
{
if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0750, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir);
}
@chmod($configDir, 0700);
@chmod($stateRoot, 0750);
@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)($config['source_mode'] ?? '') !== 'webdav-placeholder') 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($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);
$galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
$expectedGalleryRoot = $publicGalleryRoot.'/connection-'.$id;
if ($galleryRoot === '' || strpos($galleryRoot, $publicGalleryRoot.'/') !== 0) $galleryRoot = $expectedGalleryRoot;
if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.');
@chmod($galleryRoot, 0755);
$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);
unset($config['parallel_test']);
$config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile;
$config['parallel_gallery_root'] = $galleryRoot;
$config['source_fingerprint'] = $fingerprint;
$config['runtime'] = array(
'mode'=>'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)) fail_webdav_reconcile('WebDAV-Runtime-Konfiguration konnte nicht serialisiert werden.');
$escaped = $db->real_escape_string($json);
if (!$db->query("UPDATE `{$table}` SET enabled=1, config_json='{$escaped}' WHERE id={$id} AND adapter='remote' LIMIT 1"))
{
fail_webdav_reconcile('WebDAV-Runtime-Status konnte nicht gespeichert werden: '.$db->error);
}
}
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);
}

View File

@@ -4,17 +4,29 @@ 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 if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then
echo "Keine aktiven NC-Connector-Verbindungen konfiguriert." 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
webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf)
if [[ ${#webdav_configs[@]} -eq 0 ]]; then
echo "Keine WebDAV-Connector-Verbindungen konfiguriert."
exit 0 exit 0
fi fi
result=0 result=0
for config in "${configs[@]}"; do for config in "${webdav_configs[@]}"; do
echo "NC Connector: $(basename "$config")" name="$(basename "$config")"
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then echo "NC Connector WebDAV: $name"
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then
result=1 result=1
fi fi
done done

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

@@ -1,211 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${PIWIGO_CONFIG:-/etc/bratonien-tools/nc-connector/connection-1.conf}"
[[ -r "$CONFIG_FILE" ]] || { echo "Konfiguration fehlt: $CONFIG_FILE" >&2; exit 1; }
PIWIGO_SYNC_OVERRIDE_VALUE="${PIWIGO_SYNC_OVERRIDE-}"
# shellcheck source=/dev/null
source "$CONFIG_FILE"
NC_ACTIVITY_VIEW="${NC_ACTIVITY_VIEW:-piwigo_showcase_activity}"
NC_DB_VIEW="${NC_DB_VIEW:-piwigo_showcase_sources}"
if [[ -n "$PIWIGO_SYNC_OVERRIDE_VALUE" ]]; then
case "$PIWIGO_SYNC_OVERRIDE_VALUE" in
0|1) PIWIGO_SYNC_ENABLED="$PIWIGO_SYNC_OVERRIDE_VALUE" ;;
*) echo "PIWIGO_SYNC_OVERRIDE muss 0 oder 1 sein." >&2; exit 1 ;;
esac
fi
install -d -m 0750 "$STATE_DIR"
MANIFEST="$STATE_DIR/manifest.tsv"
MAP_FILE="$STATE_DIR/name-map.json"
LOCK_FILE="$STATE_DIR/sync.lock"
ACTIVITY_STATE="$STATE_DIR/activity.json"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CONNECTION_ID="${CONNECTION_ID:-}"
if [[ -z "$CONNECTION_ID" ]]; then
CONFIG_BASENAME="$(basename -- "$CONFIG_FILE")"
if [[ "$CONFIG_BASENAME" =~ ^connection-([0-9]+)\.conf$ ]]; then
CONNECTION_ID="${BASH_REMATCH[1]}"
else
echo "Connector-ID konnte nicht aus $CONFIG_FILE ermittelt werden." >&2
exit 1
fi
fi
PUBLIC_STATUS_DIR="${PIWIGO_ROOT%/}/_data/bratonien-tools/nc-connector-status"
PUBLIC_STATUS_FILE="$PUBLIC_STATUS_DIR/connection-$CONNECTION_ID.json"
install -d -m 0755 "${PIWIGO_ROOT%/}/_data/bratonien-tools" "$PUBLIC_STATUS_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
AUTH_MODE=""
API_STATE="not_run"
API_MESSAGE=""
FALLBACK_STATE="not_run"
FALLBACK_MESSAGE=""
ERROR_DETAIL=""
write_status() {
local state="$1" message="$2"
local error_detail="$ERROR_DETAIL"
if [[ "$state" != "error" ]]; then
error_detail=""
fi
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
(path, public_path, state, message, auth_mode, api_state, api_message,
fallback_state, fallback_message, error_detail) = 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": error_detail,
}
def write_json(target):
os.makedirs(os.path.dirname(target), exist_ok=True)
fd, temporary = tempfile.mkstemp(dir=os.path.dirname(target))
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)
write_json(path)
write_json(public_path)
PY
}
failure() {
[[ -n "$ERROR_DETAIL" ]] || ERROR_DETAIL="Synchronisierung wurde durch einen technischen Fehler abgebrochen."
write_status error "Synchronisierung fehlgeschlagen"
}
trap failure ERR
NEEDS_LOCAL_REPAIR=0
if [[ ! -s "$MAP_FILE" ]]; then
NEEDS_LOCAL_REPAIR=1
else
set +e
python3 - "$MAP_FILE" "$GALLERY_ROOT" <<'PY'
import json
import sys
from pathlib import Path
mapping = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
gallery = Path(sys.argv[2])
roots = [
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)
PY
ROOTS_INTACT=$?
set -e
[[ "$ROOTS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1
fi
if [[ "$NEEDS_LOCAL_REPAIR" == "0" && "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
set +e
php "$SCRIPT_DIR/lib/piwigo-db-check.php" "$MAP_FILE" "$PIWIGO_ROOT"
PIWIGO_ALBUMS_INTACT=$?
set -e
[[ "$PIWIGO_ALBUMS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1
fi
if [[ "$NEEDS_LOCAL_REPAIR" == "1" ]]; then
GATE_RESULT=0
else
if python3 "$SCRIPT_DIR/lib/activity_gate.py" check \
--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"; then
GATE_RESULT=0
else
GATE_RESULT=$?
fi
fi
if [[ "$GATE_RESULT" == "3" ]]; then
trap - ERR
write_status ok "Keine Änderungen gefunden"
exit 0
fi
[[ "$GATE_RESULT" == "0" ]] || { ERROR_DETAIL="Activity-Gate fehlgeschlagen (Exit-Code $GATE_RESULT)."; exit "$GATE_RESULT"; }
python3 "$SCRIPT_DIR/lib/build_manifest.py" \
--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"
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--manifest "$MANIFEST" --destination "$GALLERY_ROOT" --state "$MAP_FILE"
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
set +e
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)"
PIWIGO_EXIT=$?
set -e
printf '%s\n' "$PIWIGO_OUTPUT"
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
AUTH_MODE="api"
API_STATE="ok"
API_MESSAGE="API-Synchronisierung erfolgreich"
FALLBACK_STATE="not_needed"
FALLBACK_MESSAGE="Fallback wurde nicht benötigt"
else
API_LINE="$(grep -m1 '^Piwigo-API nicht nutzbar:' <<<"$PIWIGO_OUTPUT" || true)"
if [[ -n "$API_LINE" ]]; then
API_STATE="error"
API_MESSAGE="${API_LINE#Piwigo-API nicht nutzbar: }"
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
AUTH_MODE="fallback"
FALLBACK_STATE="ok"
FALLBACK_MESSAGE="Benutzername/Passwort-Fallback erfolgreich"
elif [[ "$PIWIGO_EXIT" -ne 0 ]]; then
AUTH_MODE="failed"
FALLBACK_STATE="error"
FALLBACK_MESSAGE="$(tail -n 1 <<<"$PIWIGO_OUTPUT")"
fi
fi
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
ERROR_DETAIL="$(tail -n 3 <<<"$PIWIGO_OUTPUT" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/[[:space:]]$//')"
trap - ERR
write_status error "Synchronisierung fehlgeschlagen"
exit "$PIWIGO_EXIT"
fi
fi
python3 "$SCRIPT_DIR/lib/activity_gate.py" commit \
--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"
trap - ERR
if [[ "$AUTH_MODE" == "fallback" ]]; then
write_status warning "Synchronisierung erfolgreich über Fallback; API war nicht nutzbar"
elif [[ "$AUTH_MODE" == "api" ]]; then
write_status ok "Synchronisierung erfolgreich über API"
else
write_status ok "Synchronisierung erfolgreich"
fi

View File

@@ -52,6 +52,12 @@
function token(){var input=section.querySelector('input[name="pwg_token"]');return input?input.value:'';} 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 setOpen(value){try{if(value)sessionStorage.setItem(storageKey,'1');else sessionStorage.removeItem(storageKey);}catch(e){}}
function showWizard(){
setOpen(true);
if(!dialog)return;
if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();
else dialog.setAttribute('open','open');
}
function resetServer(){ function resetServer(){
var pwgToken=token(); var pwgToken=token();
if(!pwgToken)return Promise.reject(new Error('CSRF token missing')); if(!pwgToken)return Promise.reject(new Error('CSRF token missing'));
@@ -65,8 +71,7 @@
if(openButton){ if(openButton){
openButton.addEventListener('click',function(event){ openButton.addEventListener('click',function(event){
if(resetBusy)return;event.preventDefault();event.stopImmediatePropagation();resetBusy=true;setOpen(true); event.preventDefault();event.stopImmediatePropagation();showWizard();
resetServer().then(function(){window.location.reload();}).catch(function(){setOpen(false);resetBusy=false;});
},true); },true);
} }
if(closeButton){ if(closeButton){
@@ -75,12 +80,22 @@
if(dialog){ if(dialog){
dialog.addEventListener('cancel',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true); 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); dialog.addEventListener('click',function(event){if(event.target===dialog){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();}},true);
// Every wizard POST explicitly preserves the open state before the
// browser leaves the page. Validation errors therefore return to the
// same wizard step instead of closing/resetting the dialog.
[].slice.call(dialog.querySelectorAll('form[data-bratonien-wizard-form]')).forEach(function(form){
form.addEventListener('submit',function(){setOpen(true);},true);
});
try{if(sessionStorage.getItem(storageKey)==='1')showWizard();}catch(e){}
} }
} }
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'));
@@ -93,20 +108,40 @@
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);
}
poll();schedule();
} }
function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectorPolling();} function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectorPolling();}

View File

@@ -1,6 +1,6 @@
<section class="bratonien-section" id="nc-connector"> <section class="bratonien-section" id="nc-connector">
<h3>NC Connector</h3> <h3>NC Connector</h3>
<p class="bratonien-section__intro">Verbindet Nextcloud mit Piwigo und hält freigegebene Bilder automatisch aktuell.</p> <p class="bratonien-section__intro">Verbindet Nextcloud per WebDAV mit Piwigo und hält die ausgewählten Bilder automatisch aktuell.</p>
{assign var=nc_system_available value=isset($NC_CONNECTOR.system)} {assign var=nc_system_available value=isset($NC_CONNECTOR.system)}
@@ -8,7 +8,7 @@
<div class="bratonien-card"> <div class="bratonien-card">
<h4>Status</h4> <h4>Status</h4>
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<span class="bratonien-label">Verbindungen</span><strong>{$NC_CONNECTOR.connection_count|escape:html}</strong> <span class="bratonien-label">WebDAV-Verbindungen</span><strong>{$NC_CONNECTOR.connection_count|escape:html}</strong>
<span class="bratonien-label">Automatischer Abgleich</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_active && $NC_CONNECTOR.system.timer_enabled}Aktiv{else}Nicht aktiv{/if}</strong> <span class="bratonien-label">Automatischer Abgleich</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_active && $NC_CONNECTOR.system.timer_enabled}Aktiv{else}Nicht aktiv{/if}</strong>
<span class="bratonien-label">Letzter Lauf</span><strong data-nc-last-run>{if $nc_system_available}{$NC_CONNECTOR.system.last_run_label|escape:html}{else}Nicht verfügbar{/if}</strong> <span class="bratonien-label">Letzter Lauf</span><strong data-nc-last-run>{if $nc_system_available}{$NC_CONNECTOR.system.last_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
<span class="bratonien-label">Nächster Lauf</span><strong data-nc-next-run>{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if}</strong> <span class="bratonien-label">Nächster Lauf</span><strong data-nc-next-run>{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
@@ -21,10 +21,9 @@
<div class="bratonien-card"> <div class="bratonien-card">
<h4>Neue Verbindung</h4> <h4>Neue Verbindung</h4>
<p class="bratonien-base-note">Für die normale Einrichtung empfehlen wir den Assistenten.</p> <p class="bratonien-base-note">Nextcloud wird ausschließlich per WebDAV angebunden.</p>
<div class="bratonien-actions"> <div class="bratonien-actions">
<button class="buttonLike" type="button" id="bratonien-nc-wizard-open">Mit Assistent anlegen</button> <button class="buttonLike" type="button" id="bratonien-nc-wizard-open">WebDAV-Verbindung anlegen</button>
<button class="buttonLike" type="button" id="bratonien-nc-technical-open">Ohne Assistent anlegen</button>
</div> </div>
</div> </div>
</div> </div>
@@ -32,12 +31,22 @@
<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 WebDAV-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 == 'mounts'}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>
{if $NC_CONNECTOR.wizard.step == 1} {if $NC_CONNECTOR.wizard.step == 1}
<p class="bratonien-base-note">Adresse und Zugang reichen für den ersten Scan. Der Assistent prüft den erreichbaren Web-Zugang automatisch.</p> <p class="bratonien-base-note">Adresse und Nextcloud-Zugang genügen. Der Assistent prüft HTTP/HTTPS und anschließend den WebDAV-Zugriff des angemeldeten Benutzers.</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">
@@ -52,104 +61,104 @@
</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> Angezeigt werden ausschließlich Verzeichnisse des angemeldeten Nextcloud-Benutzers.</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 == 'mounts' && $NC_CONNECTOR.wizard.directory_selection_ready}
<hr> <hr>
<h5>Datenzugriff</h5> <h5>Verzeichnisse auswählen</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"> <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> <span class="bratonien-label">Ausgewählt</span>
<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> {if $NC_CONNECTOR.wizard.directory_selected|@count > 0}
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Automatisch prüfen</button></p> {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> </form>
{elseif $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
<hr>
<h5>Datenbank-Adresse prüfen</h5>
<p class="bratonien-base-note">Der übliche Weg hat nicht funktioniert. Deshalb werden jetzt nur die Angaben abgefragt, die davon abweichen können.</p>
{if $NC_CONNECTOR.wizard.technical_error}<details><summary>Technische Details</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.wizard.technical_error|escape:html}</p></details>{/if}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Datenbank-Adresse</label><input name="nc_wizard_db_host" type="text" value="{$NC_CONNECTOR.wizard.db_host|escape:html}" required>
<label class="bratonien-label">Port</label><input name="nc_wizard_db_port" type="number" min="1" max="65535" value="{$NC_CONNECTOR.wizard.db_port|escape:html}" required>
<label class="bratonien-label">Datenbank</label><input name="nc_wizard_db_database" type="text" value="{$NC_CONNECTOR.wizard.db_database|escape:html}" required>
<label class="bratonien-label">Lese-Benutzer</label><input name="nc_wizard_db_user" type="text" value="{$NC_CONNECTOR.wizard.db_user|escape:html}" required>
<label class="bratonien-label">Passwort</label><input name="nc_wizard_db_password" type="password" autocomplete="new-password" value="{$NC_CONNECTOR.wizard._db_password|escape:html}" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Erneut prüfen</button></p>
</form>
{elseif $NC_CONNECTOR.wizard.technical_stage == 'mounts'}
<hr>
<h5>Speicherort bestätigen</h5>
<p class="bratonien-base-note">Die Datenquelle wurde gefunden. Nur Speicherorte, die nicht sicher erkannt werden konnten, müssen jetzt einmalig zugeordnet werden.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index}
<span class="bratonien-label">Speicher {$storage_index+1}</span>
{if $storage.local_mount}
<strong>Automatisch erkannt</strong>
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}">
{else}
<input name="nc_wizard_storage_mount[{$storage_index|escape:html}]" type="text" placeholder="Eingebundener Speicherpfad" required>
{/if}
{/foreach} {/foreach}
{else}
<span class="bratonien-base-note">Noch kein Verzeichnis ausgewählt.</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>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Speicher prüfen</button></p>
</form>
<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}">
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Verzeichnisse übernehmen</button></p>
</form>
{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>
<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>WebDAV ist vorbereitet.</strong> Jetzt wird der Piwigo-Zugang dieser Verbindung geprüft.</p>
{if $NC_CONNECTOR.wizard.api_error}<p class="bratonien-main-cache__warning"><strong>API-Test fehlgeschlagen:</strong> {$NC_CONNECTOR.wizard.api_error|escape:html}</p>{/if} {if $NC_CONNECTOR.wizard.api_error}<p class="bratonien-main-cache__warning"><strong>API-Test fehlgeschlagen:</strong> {$NC_CONNECTOR.wizard.api_error|escape:html}</p>{/if}
<form method="post" data-bratonien-wizard-form> <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_api_key_id">API-Schlüssel-ID</label><input id="nc_wizard_api_key_id" name="nc_wizard_api_key_id" type="text" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_id|escape:html}" placeholder="leer = bereits gespeicherten Zugang testen"> <label class="bratonien-label" for="nc_wizard_api_key_id">API-Schlüssel-ID</label><input id="nc_wizard_api_key_id" name="nc_wizard_api_key_id" type="text" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_id|escape:html}">
<label class="bratonien-label" for="nc_wizard_api_key_secret">API-Geheimnis</label><input id="nc_wizard_api_key_secret" name="nc_wizard_api_key_secret" type="password" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_secret|escape:html}" placeholder="leer = bereits gespeicherten Zugang testen"> <label class="bratonien-label" for="nc_wizard_api_key_secret">API-Geheimnis</label><input id="nc_wizard_api_key_secret" name="nc_wizard_api_key_secret" type="password" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_secret|escape:html}">
</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> Erst der letzte Button legt die WebDAV-Verbindung an.</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,88 +172,47 @@
</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>
<details id="bratonien-nc-technical-create" class="bratonien-card" style="margin-top:1.5rem">
<summary style="display:none">Ohne Assistent anlegen</summary>
<h4>Technische Einrichtung</h4>
<p class="bratonien-main-cache__warning">Nur verwenden, wenn die technische Zuordnung bekannt ist. Falsche Pfade können die Synchronisierung auf den falschen Datenbestand richten.</p>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Name</label><input name="nc_name" type="text" required>
<label class="bratonien-label">PostgreSQL-Host</label><input name="nc_host" type="text" required>
<label class="bratonien-label">PostgreSQL-Port</label><input name="nc_port" type="number" min="1" max="65535" value="5432" required>
<label class="bratonien-label">Datenbank</label><input name="nc_database" type="text" value="nextcloud" required>
<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" type="text" required>
<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" required>
<label class="bratonien-label">Source-View</label><input name="nc_source_view" type="text" value="piwigo_showcase_sources" required>
<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" type="text" value="piwigo_showcase_activity" required>
<label class="bratonien-label">Piwigo-Galeriepfad</label><input name="nc_gallery_root" type="text" placeholder="/var/www/piwigo/galleries/nextcloud" required>
<label class="bratonien-label">Fallback-Benutzer</label><input name="nc_piwigo_user" type="text">
<label class="bratonien-label">Fallback-Passwort</label><input name="nc_piwigo_password" type="password" autocomplete="new-password">
<label class="bratonien-label">Ruhezeit</label><input name="nc_quiet_seconds" type="number" min="0" value="120">
<label class="bratonien-label">Maximale Wartezeit</label><input name="nc_max_wait_seconds" type="number" min="60" value="900">
<label class="bratonien-label">Vollprüfung nach</label><input name="nc_full_sync_seconds" type="number" min="300" value="86400">
</div>
<p><strong>Storage-Zuordnungen</strong></p><p class="bratonien-base-note">Format: Storage-ID | optionales Quellpräfix | lokaler Speicherpfad</p><textarea name="nc_storages" rows="4" style="width:100%" required></textarea>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_create_local">Verbindung anlegen</button></p>
</form>
</details>
<div class="bratonien-card" style="margin-top:1.5rem"> <div class="bratonien-card" style="margin-top:1.5rem">
<h4>Bestehende Verbindungen</h4> <h4>Bestehende WebDAV-Verbindungen</h4>
{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> · aktiv{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} <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('WebDAV-Verbindung wirklich löschen? Die zugehörigen Piwigo-Inhalte dieser Verbindung werden entfernt; Nextcloud-Dateien bleiben unverändert.');">Löschen</button></form>
{if !$connection.enabled && $connection.takeover_state != 'active'}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_delete" onclick="return confirm('Verbindung wirklich löschen? Bilder in Nextcloud oder Piwigo werden nicht gelöscht.');">Löschen</button></form>{/if}
</div> </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}
{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} <p class="bratonien-base-note"><strong>Letzter Abgleich:</strong> {$connection.last_sync.label|escape:html} · {$connection.last_sync.message|escape:html}</p>
<details style="margin-top:.75rem"><summary>Technische Einstellungen</summary> {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.enabled || $connection.takeover_state == 'active'}<p class="bratonien-main-cache__warning">Aktive Verbindungen können technisch nicht geändert werden.</p>{else} {if $connection.last_sync.state == 'warning'}<p class="bratonien-main-cache__warning"><strong>Laufzeitwarnung:</strong> {$connection.last_sync.message|escape:html}</p>{/if}
<p class="bratonien-main-cache__warning">Nur ändern, wenn die technische Zuordnung bekannt ist.</p> {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}
<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}"> {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}
<div class="bratonien-form-grid"> {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}
<label class="bratonien-label">PostgreSQL-Host</label><input name="nc_host" type="text" value="{$connection.config.host|escape:html}" required> {else}
<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="{$connection.config.port|escape:html}" required> <p class="bratonien-base-note"><strong>Laufzeit:</strong> Noch kein Laufstatus für diese Verbindung vorhanden.</p>
<label class="bratonien-label">Datenbank</label><input name="nc_database" type="text" value="{$connection.config.database|escape:html}" required>
<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" type="text" value="{$connection.config.user|escape:html}" required>
<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" placeholder="leer = unverändert">
<label class="bratonien-label">Source-View</label><input name="nc_source_view" type="text" value="{$connection.config.source_view|escape:html}" required>
<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" type="text" value="{$connection.config.activity_view|escape:html}" required>
<label class="bratonien-label">Piwigo-Galeriepfad</label><input name="nc_gallery_root" type="text" value="{$connection.config.gallery_root|escape:html}" required>
<label class="bratonien-label">Ruhezeit</label><input name="nc_quiet_seconds" type="number" min="0" value="{$connection.config.quiet_seconds|escape:html}">
<label class="bratonien-label">Maximale Wartezeit</label><input name="nc_max_wait_seconds" type="number" min="60" value="{$connection.config.max_wait_seconds|escape:html}">
<label class="bratonien-label">Vollprüfung nach</label><input name="nc_full_sync_seconds" type="number" min="300" value="{$connection.config.full_sync_seconds|escape:html}">
</div>
<p><strong>Storage-Zuordnungen</strong></p><p class="bratonien-base-note">Format: Storage-ID | optionales Quellpräfix | lokaler Speicherpfad</p><textarea name="nc_storages" rows="4" style="width:100%" required>{$connection.storage_text|escape:html}</textarea>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_technical">Technische Einstellungen speichern</button></p>
</form>
{/if} {/if}
</details> <p class="bratonien-base-note"><strong>Nextcloud:</strong> {$connection.config.nextcloud_url|escape:html}</p>
<p class="bratonien-base-note"><strong>Nextcloud-Benutzer:</strong> {$connection.user|escape:html}</p>
<p class="bratonien-base-note"><strong>Ausgewählte Wurzeln:</strong> {$connection.storage_count|escape:html}</p>
<p class="bratonien-base-note"><strong>Piwigo-Zugang:</strong> {if $connection.config.api_enabled}eigene API{elseif $connection.fallback_stored}Fallback{else}kein Zugang gespeichert{/if}</p>
</div> </div>
</details> </details>
{/foreach} {/foreach}
{else}<p class="bratonien-base-note">Noch keine Verbindung vorhanden.</p>{/if} {else}<p class="bratonien-base-note">Noch keine WebDAV-Verbindung vorhanden.</p>{/if}
</div> </div>
<details class="bratonien-card" style="margin-top:1.5rem"><summary>Erweiterte Piwigo-Zugänge</summary> {if $NC_CONNECTOR.connection_count > 0}
<h4>Piwigo API</h4> <details class="bratonien-card" style="margin-top:1.5rem"><summary>Fallback-Zugang einer Verbindung ändern</summary>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">API-Schlüssel-ID</label><input name="nc_piwigo_api_key_id" type="text" autocomplete="off" required><label class="bratonien-label">API-Geheimnis</label><input name="nc_piwigo_api_key_secret" type="password" autocomplete="off" required></div><div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_test">API prüfen und speichern</button><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_delete" formnovalidate>Gespeicherte API löschen</button></div></form> <form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">Verbindung</label><select name="connection_id" required>{foreach from=$NC_CONNECTOR.connections item=connection}<option value="{$connection.id|escape:html}">{$connection.name|escape:html}</option>{/foreach}</select><label class="bratonien-label">Piwigo-Benutzer</label><input name="nc_fallback_user" type="text" autocomplete="username"><label class="bratonien-label">Piwigo-Passwort</label><input name="nc_fallback_password" type="password" autocomplete="current-password"></div><div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_save">Fallback speichern</button><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_fallback_delete" formnovalidate>Fallback löschen</button></div></form>
<h4 style="margin-top:1rem">Fallback speichern</h4>
{if $NC_CONNECTOR.connection_count > 0}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">Verbindung</label><select name="connection_id" required>{foreach from=$NC_CONNECTOR.connections item=connection}<option value="{$connection.id|escape:html}">{$connection.name|escape:html}</option>{/foreach}</select><label class="bratonien-label">Piwigo-Benutzer</label><input name="nc_fallback_user" type="text" autocomplete="username"><label class="bratonien-label">Piwigo-Passwort</label><input name="nc_fallback_password" type="password" autocomplete="current-password"></div><div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_save">Fallback speichern</button><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_fallback_delete" formnovalidate>Fallback löschen</button></div></form>{/if}
<h4 style="margin-top:1rem">Einmaliger Fallback-Test</h4>
<p class="bratonien-base-note">Verwendet die eingegebenen Piwigo-Zugangsdaten einmalig und speichert sie nicht.</p>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">Piwigo-Benutzer</label><input name="nc_fallback_user" type="text" required><label class="bratonien-label">Piwigo-Passwort</label><input name="nc_fallback_password" type="password" required></div><p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_once">Einmalig testen</button></p></form>
</details> </details>
{/if}
{literal} {literal}
<script> <script>
@@ -252,8 +220,6 @@
var dialog=document.getElementById('bratonien-nc-wizard-dialog'); var dialog=document.getElementById('bratonien-nc-wizard-dialog');
var openButton=document.getElementById('bratonien-nc-wizard-open'); var openButton=document.getElementById('bratonien-nc-wizard-open');
var closeButton=document.getElementById('bratonien-nc-wizard-close'); var closeButton=document.getElementById('bratonien-nc-wizard-close');
var technicalButton=document.getElementById('bratonien-nc-technical-open');
var technical=document.getElementById('bratonien-nc-technical-create');
var key='bratonienNcWizardOpen'; var key='bratonienNcWizardOpen';
function openWizard(){try{sessionStorage.setItem(key,'1');}catch(e){} if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();else dialog.setAttribute('open','open');} function openWizard(){try{sessionStorage.setItem(key,'1');}catch(e){} if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();else dialog.setAttribute('open','open');}
function closeWizard(){try{sessionStorage.removeItem(key);}catch(e){} if(typeof dialog.close==='function')dialog.close();else dialog.removeAttribute('open');} function closeWizard(){try{sessionStorage.removeItem(key);}catch(e){} if(typeof dialog.close==='function')dialog.close();else dialog.removeAttribute('open');}
@@ -265,7 +231,6 @@
dialog.querySelectorAll('form[data-bratonien-wizard-form]').forEach(function(form){form.addEventListener('submit',function(e){var s=e.submitter;try{if(s&&s.hasAttribute('data-bratonien-wizard-end'))sessionStorage.removeItem(key);else sessionStorage.setItem(key,'1');}catch(x){}});}); dialog.querySelectorAll('form[data-bratonien-wizard-form]').forEach(function(form){form.addEventListener('submit',function(e){var s=e.submitter;try{if(s&&s.hasAttribute('data-bratonien-wizard-end'))sessionStorage.removeItem(key);else sessionStorage.setItem(key,'1');}catch(x){}});});
try{if(sessionStorage.getItem(key)==='1')openWizard();}catch(e){} try{if(sessionStorage.getItem(key)==='1')openWizard();}catch(e){}
} }
if(technicalButton&&technical)technicalButton.addEventListener('click',function(){technical.open=!technical.open;if(technical.open)technical.scrollIntoView(true);});
})(); })();
</script> </script>
{/literal} {/literal}

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;
}