Compare commits

...

129 Commits

Author SHA1 Message Date
Terranom674
00d14db57d 0.9.6.27: WebDAV in bestehende Albumstruktur synchronisieren 2026-08-19 19:37:45 +02:00
Terranom674
6e52ba824c 0.9.6.27: transparentes WebDAV-Stammverzeichnis spiegeln 2026-08-19 19:35:32 +02:00
Terranom674
a1bac43150 0.9.6.27: Nextcloud-Stamm ohne Benutzerwrapper abbilden 2026-08-19 19:35:10 +02:00
Terranom674
06646ea37f 0.9.6.27: einen NC-Anlageweg und Jetzt-abgleichen
0.9.6.27: einen NC-Anlageweg und Jetzt-abgleichen
2026-08-19 19:29:08 +02:00
Terranom674
8f2b22a376 0.9.6.27: CI-Marker entfernen 2026-08-19 19:28:20 +02:00
Terranom674
2122ab88ac 0.9.6.27: CI-Stand markieren 2026-08-19 19:28:11 +02:00
Terranom674
babe95fae3 0.9.6.27: versehentliche Hilfsdatei entfernen 2026-08-19 19:28:03 +02:00
Terranom674
14a83be190 noop 2026-08-19 19:27:53 +02:00
Terranom674
a1d2163aab 0.9.6.27: CI auf einen NC-JavaScript-Pfad zuruecksetzen 2026-08-19 19:27:06 +02:00
Terranom674
a0606b2553 0.9.6.27: paralleles Run-now-JavaScript entfernen 2026-08-19 19:26:51 +02:00
Terranom674
9543af50d8 0.9.6.27: parallelen NC-Anlageweg entfernen 2026-08-19 19:26:37 +02:00
Terranom674
4af1972353 0.9.6.27: einen einzigen NC-Verbindungsweg anzeigen 2026-08-19 19:26:11 +02:00
Terranom674
0e7070fb04 0.9.6.27: parallelen sudoers-Installer entfernen 2026-08-19 19:24:41 +02:00
Terranom674
ba53bfebe9 0.9.6.27: parallele sudoers-Hilfsstruktur entfernen 2026-08-19 19:24:36 +02:00
Terranom674
4b2fc93e3d 0.9.6.27: unnoetige Markerdatei entfernen 2026-08-19 19:24:06 +02:00
Terranom674
e356d43e5b 0.9.6.27: unnoetige Run-now-Hinweisdatei entfernen 2026-08-19 19:23:56 +02:00
Terranom674
980efc6700 0.9.6.27: Run-now-Setup markieren 2026-08-19 19:23:30 +02:00
Terranom674
a814c18d18 0.9.6.27: sudoers-Setup fuer Jetzt-abgleichen vorbereiten 2026-08-19 19:23:14 +02:00
Terranom674
6737c8989d 0.9.6.27: minimale sudoers-Regel fuer manuellen Abgleich dokumentieren 2026-08-19 19:23:04 +02:00
Terranom674
2e308867ad 0.9.6.27: Hinweis zur manuellen Service-Ausloesung 2026-08-19 19:22:54 +02:00
Terranom674
1d3768b051 0.9.6.27: Jetzt-abgleichen-JavaScript in CI pruefen 2026-08-19 19:22:48 +02:00
Terranom674
cfea5546de 0.9.6.27: Version anheben 2026-08-19 19:22:32 +02:00
Terranom674
8c30edc0ee 0.9.6.27: Jetzt-abgleichen-JavaScript laden 2026-08-19 19:22:11 +02:00
Terranom674
76a182af82 0.9.6.27: Jetzt-abgleichen-Button anzeigen 2026-08-19 19:21:41 +02:00
Terranom674
21e046cf37 0.9.6.27: manuellen NC-Abgleich registrieren 2026-08-19 19:21:16 +02:00
Terranom674
7728e368db Merge pull request #15 from Terranom674/fix/remove-nc-status-card-09625
0.9.6.26: Connector-Timer nicht mehr durch Bildaufbereitung blockieren
2026-08-19 18:57:07 +02:00
Terranom674
074aa8d99a 0.9.6.26: Version anheben 2026-08-19 18:56:13 +02:00
Terranom674
030634c39d 0.9.6.26: Connector-Lauf nicht mehr durch Bildaufbereitung blockieren 2026-08-19 18:55:50 +02:00
Terranom674
dea1a0ffb2 0.9.6.26: Bildaufbereitung aus Connector-Hauptlauf auslagern 2026-08-19 18:55:30 +02:00
Terranom674
3a3539e55b 0.9.6.25: NC-Status fuer laufenden Sync korrigieren
0.9.6.25: NC-Status fuer laufenden Sync korrigieren
2026-08-19 18:45:58 +02:00
Terranom674
49e74a019b CI: entfernte NC-Migrations-JavaScriptdatei nicht mehr pruefen 2026-08-19 18:45:22 +02:00
Terranom674
f2f7d39b22 Version 0.9.6.25 2026-08-19 18:44:19 +02:00
Terranom674
2c5bcf6ba2 NC-Status: laufenden Service und naechsten Lauf korrekt anzeigen 2026-08-19 18:43:46 +02:00
Terranom674
2d29cfacfe 0.9.6.24: Migration vollstaendig aus aktivem Connectorpfad entfernen 2026-08-19 18:34:43 +02:00
Terranom674
c5e1b33c2f 0.9.6.24: Uebergabe- und Migrationslogik entfernen 2026-08-19 18:34:21 +02:00
Terranom674
675ccd1df8 0.9.6.24: Migrations-UI-Datei entfernen 2026-08-19 18:34:11 +02:00
Terranom674
aefbe76595 0.9.6.24: WebDAV-Runtime von Migration entkoppeln 2026-08-19 18:33:41 +02:00
Terranom674
65cbc0060f 0.9.6.24: Runtime-Migrationsrouting entfernen 2026-08-19 18:33:04 +02:00
Terranom674
5dce180217 0.9.6.24: Migrationsstatus aus Edit-API entfernen 2026-08-19 18:31:51 +02:00
Terranom674
25ee22b1ef 0.9.6.24: Migrations-UI aus Verbindungseditor entfernen 2026-08-19 18:31:28 +02:00
Terranom674
992ddd3b9b 0.9.6.24: Migrationsfunktionen aus Registry entfernen 2026-08-19 18:30:30 +02:00
Terranom674
0f85958dff 0.9.6.24: WebDAV ohne Migrationspaarung 2026-08-19 18:27:44 +02:00
Terranom674
82af31435e 0.9.6.24: gekoppelte Migrationsloeschung entfernen 2026-08-19 18:25:01 +02:00
Terranom674
462197fc78 0.9.6.23: Versionsnummer aktualisieren 2026-08-19 18:20:30 +02:00
Terranom674
cb445c3f04 0.9.6.23: Status logischer NC-Verbindungen korrekt aggregieren 2026-08-19 18:20:10 +02:00
Terranom674
e1306c110c Version auf 0.9.6.22 anheben 2026-08-19 18:10:19 +02:00
Terranom674
bbae8ac984 Migration als einmaligen Uebergang begrenzen 2026-08-19 18:10:01 +02:00
Terranom674
5a7f72b24e Fallback im NC-Verbindungseditor bearbeitbar machen 2026-08-19 18:07:08 +02:00
Terranom674
09027322ef Piwigo-Fallback im Editor pruefen und speichern 2026-08-19 18:06:34 +02:00
Terranom674
a38152108c Piwigo-Fallback im Verbindungseditor bereitstellen 2026-08-19 18:05:50 +02:00
Terranom674
036f2a51e6 Version auf 0.9.6.21 anheben 2026-08-19 18:04:00 +02:00
Terranom674
8cabeb3079 Wizard-Dialog ueber Fehler und Redirects offen halten 2026-08-19 18:03:38 +02:00
Terranom674
0767c4fd8f Gekoppelte NC-Verbindung als Einheit loeschen 2026-08-19 18:03:04 +02:00
Terranom674
de649235ff NC Connector als logische Verbindungen darstellen 2026-08-19 18:02:44 +02:00
Terranom674
3896933081 Merge pull request #13 from Terranom674/fix/nc-ui-mutation-loop-09620
0.9.6.20: NC-Connector UI-Hänger beheben
2026-08-19 17:38:42 +02:00
Terranom674
72a3267ed3 Bump version to 0.9.6.20 2026-08-19 17:32:12 +02:00
Terranom674
99479d5d03 Fix NC connector MutationObserver loop 2026-08-19 17:31:57 +02:00
Terranom674
51089c3446 Merge 0.9.6.19 migration stabilization
WebDAV migration now routes per connection, preserves exact legacy fallbacks, supports API or connection-scoped fallback auth, and passes the final PHP 8.2-8.5 plus script syntax matrix.
2026-08-19 16:11:18 +02:00
Terranom674
c8d846f289 Bump version to 0.9.6.19 2026-08-19 16:09:24 +02:00
Terranom674
2603a8929c 0.9.6.19 preserve fallback-only migration auth 2026-08-19 16:08:57 +02:00
Terranom674
afd8620859 0.9.6.19 allow migration with connection-scoped fallback 2026-08-19 16:08:24 +02:00
Terranom674
72db8b48eb 0.9.6.19 make migration pairing atomic 2026-08-19 16:06:29 +02:00
Terranom674
1a0f8c61e8 0.9.6.19 test supported PHP branches and all admin JS 2026-08-19 16:05:19 +02:00
Terranom674
d4e3e7faab 0.9.6.19 route WebDAV and legacy per connection 2026-08-19 16:04:27 +02:00
Terranom674
03e92b74a7 0.9.6.19 expose exact migration fallback to runtime 2026-08-19 16:03:50 +02:00
Terranom674
88c787c956 0.9.6.19 preserve Nextcloud root selection 2026-08-19 16:02:02 +02:00
Terranom674
3f5f8dc4b4 0.9.6.19 pair migration with selected legacy connection 2026-08-19 16:01:16 +02:00
Terranom674
495edbbb70 Merge pull request #11 from Terranom674/fix/migration-runtime-09618
0.9.6.18: Migration wirklich abschließen und Root-Runtime erzeugen
2026-08-19 15:31:33 +02:00
Terranom674
3d5e6c82ea 0.9.6.18 bump version for migration runtime fix 2026-08-19 15:29:30 +02:00
Terranom674
ae815bc077 0.9.6.18 make selected path lead directly to final migration confirmation 2026-08-19 15:28:41 +02:00
Terranom674
936a1cf3c4 0.9.6.18 allow Nextcloud root in WebDAV runtime 2026-08-19 15:27:39 +02:00
Terranom674
ff0c1e0bec 0.9.6.18 complete migration after WebDAV source selection 2026-08-19 15:26:54 +02:00
Terranom674
a047fc0d83 Merge pull request #10 from Terranom674/fix/root-selection-09617
0.9.6.17: Nextcloud-Stammverzeichnis als WebDAV-Quelle zulassen
2026-08-19 15:19:39 +02:00
Terranom674
2029059720 0.9.6.17 bump version for root selection fix 2026-08-19 15:18:27 +02:00
Terranom674
1eca236509 0.9.6.17 preserve root selection when creating WebDAV connection 2026-08-19 15:17:57 +02:00
Terranom674
e464668343 0.9.6.17 allow Nextcloud root as WebDAV source 2026-08-19 15:17:23 +02:00
Terranom674
47813b0b0e Merge pull request #9 from Terranom674/fix/edit-webdav-path-picker-09616
0.9.6.16: automatische WebDAV-Pfadauswahl direkt beim Bearbeiten
2026-08-19 15:02:21 +02:00
Terranom674
26af1b3019 0.9.6.16 bump version for automatic WebDAV path picker 2026-08-19 15:01:18 +02:00
Terranom674
b1b75676a8 0.9.6.16 prepare WebDAV source picker from edited legacy connection 2026-08-19 15:00:48 +02:00
Terranom674
12880b6c41 0.9.6.16 add automatic Nextcloud path picker to editor 2026-08-19 15:00:27 +02:00
Terranom674
32abc7250d Merge pull request #8 from Terranom674/fix/webdav-source-selection-09615
0.9.6.15: WebDAV-Quellordner klar von Legacy-SMB trennen
2026-08-19 14:44:34 +02:00
Terranom674
76de2b037c 0.9.6.15 bump version 2026-08-19 14:43:23 +02:00
Terranom674
e2e82162c6 0.9.6.15 load source selection clarification UI 2026-08-19 14:42:54 +02:00
Terranom674
7a9eb1baa6 0.9.6.15 separate legacy storage from WebDAV source selection 2026-08-19 14:42:28 +02:00
Terranom674
e800f11f64 Merge pull request #7 from Terranom674/fix/adapter-ui-detection-09614
0.9.6.14: Verbindungstyp nicht mehr aus UI-Text erraten
2026-08-19 14:34:37 +02:00
Terranom674
4808f08590 0.9.6.14 bump version for adapter-safe edit actions 2026-08-19 14:33:33 +02:00
Terranom674
6fd89df5d0 0.9.6.14 determine connection type from backend, not display text 2026-08-19 14:33:03 +02:00
Terranom674
99126088e7 Remove accidental noop file 2026-08-19 14:31:02 +02:00
Terranom674
e53a3848bb noop 2026-08-19 14:30:49 +02:00
Terranom674
0f0c71acaf Merge pull request #6 from Terranom674/fix/webdav-scheme-fallback-09613
0.9.6.13: Nextcloud-Protokoll automatisch erkennen
2026-08-19 14:21:56 +02:00
Terranom674
9847690f50 0.9.6.13 bump version for WebDAV scheme detection 2026-08-19 14:21:02 +02:00
Terranom674
a2f5dc3919 0.9.6.13 try HTTP when scheme is omitted 2026-08-19 14:20:37 +02:00
Terranom674
cd3bf7c8ee Merge pull request #5 from Terranom674/fix/dialog-diagnostics-09612
0.9.6.12: konkrete Fehlerursachen im Verbindungsdialog
2026-08-19 14:14:55 +02:00
Terranom674
79ea403555 0.9.6.12 bump version for actionable diagnostics 2026-08-19 14:14:03 +02:00
Terranom674
33783dd30f 0.9.6.12 include stage and technical cause in visible error 2026-08-19 14:13:35 +02:00
Terranom674
ca6a5cf96f 0.9.6.12 return actionable connection diagnostics 2026-08-19 14:12:58 +02:00
Terranom674
62ada617fb Merge pull request #4 from Terranom674/fix/dialog-validation-09611
0.9.6.11: Dialog bei Fehler offen halten und Felder markieren
2026-08-19 13:59:01 +02:00
Terranom674
898f9b1bc9 0.9.6.11 bump version for dialog validation 2026-08-19 13:58:08 +02:00
Terranom674
5b48e1f6f2 0.9.6.11 keep edit dialog open and mark invalid fields 2026-08-19 13:57:39 +02:00
Terranom674
fdf4584028 0.9.6.11 keep connection editor open on validation errors 2026-08-19 13:56:59 +02:00
Terranom674
880744a8b7 Merge pull request #3 from Terranom674/fix/connection-editing-09610
0.9.6.10: vollständige Verbindungsdaten und migrationsfähiger WebDAV-Start
2026-08-19 13:49:19 +02:00
Terranom674
2ed84ad089 0.9.6.10 lint standalone connector editor JavaScript 2026-08-19 13:48:26 +02:00
Terranom674
60c0a51f01 0.9.6.10 bump plugin version 2026-08-19 13:48:07 +02:00
Terranom674
84d263b16b 0.9.6.10 load complete connector editor on admin page 2026-08-19 13:47:48 +02:00
Terranom674
94c81789ed 0.9.6.10 rebuild connection editor around complete connection data 2026-08-19 13:46:26 +02:00
Terranom674
e3b1f06be3 0.9.6.10 expose complete safe edit metadata and migration readiness 2026-08-19 13:44:46 +02:00
Terranom674
15a3ceb01c 0.9.6.10 complete legacy edit credentials and migration readiness 2026-08-19 13:44:26 +02:00
Terranom674
0f5812a179 0.9.6.9: Verbindung bearbeiten und Migration trennen
* 0.9.6.9 separate edit and migration flows

* 0.9.6.9 register separate edit and migration actions

* 0.9.6.9 add safe connection edit data endpoint

* 0.9.6.9 make connection editing explicit and in-place

* Bump version to 0.9.6.9

* 0.9.6.9 catch duplicate functions and admin JavaScript syntax
2026-08-19 13:35:12 +02:00
Terranom674
377b29a127 Bump Bratonien Tools to 0.9.6.8 2026-08-19 13:21:11 +02:00
Terranom674
d9696172c5 Fix duplicate connector edit handlers 2026-08-19 13:20:55 +02:00
Terranom674
3f2a85fb0b Bump version to 0.9.6.7 2026-08-19 13:17:34 +02:00
Terranom674
c882ba994b Expose editor for every connector connection 2026-08-19 13:17:13 +02:00
Terranom674
cd2c9e350b Make WebDAV connections editable in place 2026-08-19 13:16:29 +02:00
Terranom674
394f375e91 Register connection editor 2026-08-19 13:16:00 +02:00
Terranom674
3b1ccd99a5 Add end-user connection editor 2026-08-19 13:15:30 +02:00
Terranom674
04d88cd40f Bump version to 0.9.6.6 2026-08-19 13:05:02 +02:00
Terranom674
a4f953551c Report real WebDAV failure cause and missing WebDAV configuration 2026-08-19 13:04:36 +02:00
Terranom674
b4161471ca Bump version to 0.9.6.5 2026-08-19 13:00:28 +02:00
Terranom674
71fdadb7f8 Show WebDAV failure reason in connector status 2026-08-19 13:00:10 +02:00
Terranom674
98134b5ab8 Expose WebDAV failure reason before legacy fallback 2026-08-19 12:59:55 +02:00
Terranom674
5f82f2ad71 Bump version to 0.9.6.4 2026-08-19 12:56:17 +02:00
Terranom674
494f8bf192 Show active connector route in status polling 2026-08-19 12:55:41 +02:00
Terranom674
c46f9bb5bd Bump Bratonien Tools to 0.9.6.3 2026-08-19 12:12:57 +02:00
Terranom674
992cb55714 0.9.6.2 show WebDAV vs legacy fallback 2026-08-19 11:43:12 +02:00
Terranom674
46782ea9a3 0.9.6.2 expose active connector route 2026-08-19 11:42:48 +02:00
Terranom674
d03410c9ef 0.9.6.2 make WebDAV primary and legacy local true fallback 2026-08-19 11:24:59 +02:00
Terranom674
aa2a6e4c02 0.9.6.2 pair WebDAV migration with legacy fallback 2026-08-19 11:11:39 +02:00
Terranom674
ebdd77fb0a Release 0.9.6.2 migration phase one 2026-08-19 11:07:01 +02:00
Terranom674
6c36aaeb8a Begin 0.9.6.2 legacy connector migration 2026-08-19 11:06:07 +02:00
25 changed files with 2160 additions and 648 deletions

View File

@@ -5,8 +5,12 @@ on:
pull_request: pull_request:
jobs: jobs:
syntax: php-syntax:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version: ['8.2', '8.3', '8.4', '8.5']
steps: steps:
- name: Repository auschecken - name: Repository auschecken
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -14,7 +18,7 @@ jobs:
- name: PHP installieren - name: PHP installieren
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: '8.2' php-version: ${{ matrix.php-version }}
coverage: none coverage: none
- name: PHP Syntax pruefen - name: PHP Syntax pruefen
@@ -25,6 +29,31 @@ jobs:
php -l "$file" php -l "$file"
done < <(find . -type f -name '*.php' -print0) done < <(find . -type f -name '*.php' -print0)
- name: Doppelte Bratonien-Funktionen pruefen
shell: bash
run: |
set -euo pipefail
duplicates="$(grep -RhoE 'function[[:space:]]+bratonien_tools_[A-Za-z0-9_]+' --include='*.php' . | awk '{print $2}' | sort | uniq -d || true)"
if [[ -n "$duplicates" ]]; then
echo "Doppelte globale Bratonien-Funktionen gefunden:" >&2
printf '%s\n' "$duplicates" >&2
exit 1
fi
script-syntax:
runs-on: ubuntu-latest
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: Admin JavaScript Syntax pruefen
shell: bash
run: |
set -euo pipefail
sed -n '/<script>/,/<\/script>/p' template/admin_tabs.tpl | sed '1d;$d' > /tmp/bratonien-admin-tabs.js
node --check /tmp/bratonien-admin-tabs.js
node --check js/nc_connector_edit_v2.js
- name: Python Syntax pruefen - name: Python Syntax pruefen
shell: bash shell: bash
run: | run: |

View File

@@ -9,6 +9,74 @@ check_status(ACCESS_ADMINISTRATOR);
require_once(BRATONIEN_TOOLS_PATH . 'include/tool_registry.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/tool_registry.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.inc.php');
function bratonien_tools_nc_connector_admin_connections(array $connections)
{
$by_id = array();
foreach ($connections as $connection)
{
$by_id[(int)$connection['id']] = $connection;
}
$hidden_legacy_ids = array();
$logical_remote = array();
foreach ($connections as $connection)
{
if ((string)($connection['adapter'] ?? '') !== 'remote') continue;
$migration = isset($connection['config']['migration']) && is_array($connection['config']['migration'])
? $connection['config']['migration']
: array();
if ((string)($migration['role'] ?? '') !== 'webdav-primary-candidate') continue;
$legacy_id = (int)($migration['legacy_fallback_connection_id'] ?? 0);
if ($legacy_id < 1 || empty($by_id[$legacy_id]) || (string)$by_id[$legacy_id]['adapter'] !== 'local') continue;
$legacy = $by_id[$legacy_id];
$hidden_legacy_ids[$legacy_id] = true;
$connection['logical_connection'] = true;
$connection['legacy_fallback_connection_id'] = $legacy_id;
$connection['enabled'] = !empty($connection['enabled']) || !empty($legacy['enabled']);
if ($connection['enabled']) $connection['takeover_state'] = 'active';
$connection['fallback_stored'] = !empty($connection['fallback_stored']) || !empty($legacy['fallback_stored']);
if (trim((string)$connection['name']) === '') $connection['name'] = (string)$legacy['name'];
$logical_remote[(int)$connection['id']] = $connection;
}
$visible = array();
foreach ($connections as $connection)
{
$id = (int)$connection['id'];
if (isset($hidden_legacy_ids[$id])) continue;
if (isset($logical_remote[$id])) $connection = $logical_remote[$id];
$visible[] = $connection;
}
return $visible;
}
function bratonien_tools_nc_connector_admin_last_status(array $connection)
{
$latest = bratonien_tools_nc_connector_connection_last_status($connection);
$legacy_id = (int)($connection['legacy_fallback_connection_id'] ?? 0);
if ($legacy_id < 1)
{
return $latest;
}
$legacy = bratonien_tools_nc_connector_connection($legacy_id, false);
if (!$legacy)
{
return $latest;
}
$legacy_status = bratonien_tools_nc_connector_connection_last_status($legacy);
if ((int)($legacy_status['timestamp'] ?? 0) > (int)($latest['timestamp'] ?? 0))
{
return $legacy_status;
}
return $latest;
}
$tools = bratonien_tools_get_tools(); $tools = bratonien_tools_get_tools();
$messages = array(); $messages = array();
$errors = array(); $errors = array();
@@ -94,6 +162,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bratonien_tool']))
); );
$redirect_url = get_root_url().'admin.php?page=plugin-'.BRATONIEN_TOOLS_ID; $redirect_url = get_root_url().'admin.php?page=plugin-'.BRATONIEN_TOOLS_ID;
$wizard_action = strpos($tool_id, 'nc_connector_wizard_') === 0
|| $tool_id === 'nc_connector_migrate_start'
|| $tool_id === 'nc_connector_edit_start';
if ($wizard_action)
{
$wizard_closed = $tool_id === 'nc_connector_wizard_reset'
|| ($tool_id === 'nc_connector_wizard_finish' && empty($errors));
if (($tool_id === 'nc_connector_migrate_start' || $tool_id === 'nc_connector_edit_start') && !empty($errors))
{
$wizard_closed = true;
}
$redirect_url .= '&nc_wizard='.($wizard_closed ? 'closed' : 'open');
}
if (!headers_sent()) if (!headers_sent())
{ {
header('Location: '.$redirect_url, true, 303); header('Location: '.$redirect_url, true, 303);
@@ -115,9 +196,17 @@ $asset_environment = bratonien_tools_get_asset_environment();
$album_shares = bratonien_tools_get_album_shares(); $album_shares = bratonien_tools_get_album_shares();
$private_albums = bratonien_tools_get_private_albums(); $private_albums = bratonien_tools_get_private_albums();
$nc_connector = bratonien_tools_nc_connector_status(); $nc_connector = bratonien_tools_nc_connector_status();
$nc_connector_runtime_connections = $nc_connector['connections'];
$nc_connector['connections'] = bratonien_tools_nc_connector_admin_connections($nc_connector_runtime_connections);
$nc_connector['connection_count'] = count($nc_connector['connections']);
$nc_connector['active_count'] = 0;
foreach ($nc_connector['connections'] as $visible_connection)
{
if (!empty($visible_connection['enabled'])) $nc_connector['active_count']++;
}
foreach ($nc_connector['connections'] as &$nc_connection) 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_admin_last_status($nc_connection);
$nc_connection['display_name'] = $nc_connection['name']; $nc_connection['display_name'] = $nc_connection['name'];
$storage_lines = array(); $storage_lines = array();
@@ -166,7 +255,7 @@ $nc_system_defaults = array(
); );
$nc_connector['system'] = array_merge( $nc_connector['system'] = array_merge(
$nc_system_defaults, $nc_system_defaults,
bratonien_tools_nc_connector_system_status($nc_connector['connections']) bratonien_tools_nc_connector_system_status($nc_connector_runtime_connections)
); );
$album_lock_page_number = isset($_GET['br_album_page']) ? max(1, (int)$_GET['br_album_page']) : 1; $album_lock_page_number = isset($_GET['br_album_page']) ? max(1, (int)$_GET['br_album_page']) : 1;
$album_lock_search = isset($_GET['br_album_search']) ? trim((string)$_GET['br_album_search']) : ''; $album_lock_search = isset($_GET['br_album_search']) ? trim((string)$_GET['br_album_search']) : '';

View File

@@ -5,6 +5,11 @@ if (!defined('PHPWG_ROOT_PATH'))
} }
function bratonien_tools_nc_connector_create_local_api_first() function bratonien_tools_nc_connector_create_local_api_first()
{
throw new RuntimeException('Neue Verbindungen über die alte lokale PostgreSQL-/Storage-Struktur sind ab 0.9.6.2 gesperrt. Bestehende lokale Verbindungen bleiben für die schrittweise Migration weiterhin aktiv. Neue Verbindungen müssen über den WebDAV-Assistenten angelegt werden.');
}
function bratonien_tools_nc_connector_create_local_api_first_legacy()
{ {
$required = array( $required = array(
'nc_name' => 'Name', 'nc_name' => 'Name',

View File

@@ -36,14 +36,7 @@ function bratonien_tools_nc_connector_remove_webdav_piwigo_content(array $connec
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$image_rows = array(); $image_rows = array();
$query = ' $query = '\nSELECT DISTINCT i.id, i.path, i.representative_ext\n FROM '.IMAGES_TABLE.' AS i\n LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id\n LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id\n LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id\n WHERE sc.site_id = '.$site_id.' OR vc.site_id = '.$site_id.'\n;';
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); $result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result)) while ($row = pwg_db_fetch_assoc($result))
{ {
@@ -84,10 +77,8 @@ SELECT DISTINCT i.id, i.path, i.representative_ext
} }
/** /**
* Delete a connector connection from Piwigo without depending on the web * Delete exactly the connection selected by the user.
* server being allowed to write into root-owned runtime directories. * No other connector record is implicitly removed.
* 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() function bratonien_tools_nc_connector_delete_safe()
{ {
@@ -112,10 +103,7 @@ function bratonien_tools_nc_connector_delete_safe()
$status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status'; $status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status';
$public_status = $status_dir.'/connection-'.$id.'.json'; $public_status = $status_dir.'/connection-'.$id.'.json';
if (is_file($public_status)) if (is_file($public_status)) @unlink($public_status);
{
@unlink($public_status);
}
if (is_dir($status_dir) || @mkdir($status_dir, 0755, true)) if (is_dir($status_dir) || @mkdir($status_dir, 0755, true))
{ {
@@ -125,11 +113,11 @@ function bratonien_tools_nc_connector_delete_safe()
if ((int)$cleanup['site_id'] > 0) if ((int)$cleanup['site_id'] > 0)
{ {
return array( 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.', 'message'=>'Verbindung wurde gelöscht. Die zugehörigen Piwigo-Alben und '.(int)$cleanup['images'].' Bilder wurden aus Piwigo entfernt. Nextcloud-Dateien blieben unverändert.',
); );
} }
return array( return array(
'message'=>'Connector-Verbindung wurde gelöscht. Verbliebene Laufzeitdateien werden vor dem nächsten Connector-Lauf automatisch entfernt. Quelldateien blieben unverändert.', 'message'=>'Verbindung wurde gelöscht. Verbliebene Laufzeitdaten werden automatisch bereinigt. Quelldateien blieben unverändert.',
); );
} }

View File

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

View File

@@ -170,7 +170,7 @@ function bratonien_tools_nc_connector_last_status(array $connections)
foreach ($connections as $connection) foreach ($connections as $connection)
{ {
if (empty($connection['enabled']) || (string)($connection['takeover_state'] ?? '') !== 'active') if (empty($connection['enabled']))
{ {
continue; continue;
} }
@@ -190,6 +190,13 @@ 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';
$active = bratonien_tools_nc_connector_systemctl_value(array('is-active', $timer));
$enabled = bratonien_tools_nc_connector_systemctl_value(array('is-enabled', $timer));
$service_active_raw = bratonien_tools_nc_connector_systemctl_value(array('is-active', $service));
$service_active = in_array($service_active_raw, array('active', 'activating'), true);
$service_started_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $service, '--property=ActiveEnterTimestamp', '--value'));
$service_started_timestamp = bratonien_tools_nc_connector_parse_systemd_time($service_started_raw);
$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);
@@ -204,9 +211,6 @@ function bratonien_tools_nc_connector_system_status(array $connections = array()
$next_timestamp = bratonien_tools_nc_connector_next_from_timer_list($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)
{ {
@@ -214,10 +218,32 @@ function bratonien_tools_nc_connector_system_status(array $connections = array()
$last['timestamp'] = bratonien_tools_nc_connector_parse_systemd_time($last_raw); $last['timestamp'] = bratonien_tools_nc_connector_parse_systemd_time($last_raw);
} }
if ($next_timestamp > 0)
{
$next_label = date('d.m.Y H:i:s', $next_timestamp);
}
elseif ($service_active && $active === 'active' && $enabled === 'enabled')
{
$next_label = 'Nach Abschluss des aktuellen Laufs';
}
elseif ($active === 'active' && $enabled === 'enabled')
{
$next_label = 'Wird von systemd neu geplant';
}
else
{
$next_label = 'Nicht verfügbar';
}
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',
'service_active' => $service_active,
'current_run_timestamp' => $service_started_timestamp,
'current_run_label' => $service_active
? ($service_started_timestamp > 0 ? 'Läuft seit '.date('d.m.Y H:i:s', $service_started_timestamp) : 'Läuft gerade')
: 'Kein Lauf aktiv',
'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'],
@@ -229,7 +255,7 @@ function bratonien_tools_nc_connector_system_status(array $connections = array()
'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_label,
'legacy_runtime_exists' => is_dir('/opt/piwigo-sync'), 'legacy_runtime_exists' => is_dir('/opt/piwigo-sync'),
'legacy_config_exists' => is_dir('/etc/piwigo-sync'), 'legacy_config_exists' => is_dir('/etc/piwigo-sync'),
'legacy_service_exists' => is_file('/etc/systemd/system/piwigo-sync.service'), 'legacy_service_exists' => is_file('/etc/systemd/system/piwigo-sync.service'),

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

@@ -5,10 +5,8 @@ if (!defined('PHPWG_ROOT_PATH'))
} }
/** /**
* Create a disabled WebDAV-backed connector beside the existing local modes. * Create or update one independent WebDAV connector connection.
* * No migration pair, successor or implicit legacy fallback is created.
* This deliberately does not activate, migrate or modify any existing
* connection. It only persists the data required for the parallel WebDAV path.
*/ */
function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
{ {
@@ -16,7 +14,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
if (empty($state['scan_ok']) || empty($state['technical_complete'])) if (empty($state['scan_ok']) || empty($state['technical_complete']))
{ {
throw new RuntimeException('Die WebDAV-Verbindung wurde im Assistenten noch nicht vollständig vorbereitet.'); throw new RuntimeException('Die WebDAV-Verbindung wurde im Assistenten noch nicht vollstaendig vorbereitet.');
} }
$base_url = rtrim(trim((string)($state['base_url'] ?? '')), '/'); $base_url = rtrim(trim((string)($state['base_url'] ?? '')), '/');
@@ -24,7 +22,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$password = (string)($state['_password'] ?? ''); $password = (string)($state['_password'] ?? '');
if ($base_url === '' || $username === '' || $password === '') if ($base_url === '' || $username === '' || $password === '')
{ {
throw new RuntimeException('Für den WebDAV-Testpfad fehlen Nextcloud-Adresse oder Zugangsdaten.'); throw new RuntimeException('Fuer den WebDAV-Zugang fehlen Nextcloud-Adresse oder Zugangsdaten.');
} }
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) $selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
@@ -33,17 +31,26 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids']) $selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids'])
? $state['directory_selected_fileids'] ? $state['directory_selected_fileids']
: array(); : array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis für den WebDAV-Testpfad auswählen.'); if (!$selected)
{
$selected = array('');
}
$roots = array(); $roots = array();
foreach ($selected as $path) foreach ($selected as $path)
{ {
$fileid = isset($selected_ids[$path]) ? (int)$selected_ids[$path] : 0; $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.'); if ($fileid < 1)
{
throw new RuntimeException('Fuer ein ausgewaehltes Nextcloud-Verzeichnis fehlt die eindeutige Datei-ID.');
}
$display_name = $path === ''
? (trim((string)($state['display_name'] ?? '')) !== '' ? trim((string)$state['display_name']) : $username)
: basename($path);
$roots[] = array( $roots[] = array(
'fileid'=>$fileid, 'fileid'=>$fileid,
'display_name'=>basename($path), 'display_name'=>$display_name,
'webdav_path'=>$path, 'webdav_path'=>$path,
); );
} }
@@ -59,6 +66,19 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$fallback_password = (string)($state['_fallback_password'] ?? ''); $fallback_password = (string)($state['_fallback_password'] ?? '');
$api_enabled = $api_key_id !== '' && $api_key_secret !== ''; $api_enabled = $api_key_id !== '' && $api_key_secret !== '';
$editing_id = (int)($state['editing_connection_id'] ?? 0);
$editing_mode = (string)($state['editing_mode'] ?? '');
$editing_connection = $editing_id > 0 ? bratonien_tools_nc_connector_connection($editing_id, false) : null;
$editing_remote = $editing_connection
&& (string)$editing_connection['adapter'] === 'remote'
&& (string)($editing_connection['config']['source_mode'] ?? '') === 'webdav-placeholder'
&& $editing_mode === 'update';
if ($editing_mode === 'migrate')
{
throw new RuntimeException('Die Migrationsfunktion wurde entfernt. Bitte eine normale WebDAV-Verbindung anlegen.');
}
$config = array( $config = array(
'origin'=>'native', 'origin'=>'native',
'source_mode'=>'webdav-placeholder', 'source_mode'=>'webdav-placeholder',
@@ -73,11 +93,23 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
'quiet_seconds'=>120, 'quiet_seconds'=>120,
'max_wait_seconds'=>900, 'max_wait_seconds'=>900,
'full_sync_seconds'=>86400, 'full_sync_seconds'=>86400,
'parallel_test'=>true,
'piwigo_auth'=>'connection-scoped', 'piwigo_auth'=>'connection-scoped',
'api_enabled'=>$api_enabled, 'api_enabled'=>$api_enabled,
); );
if ($editing_remote)
{
$old_config = is_array($editing_connection['config'] ?? null) ? $editing_connection['config'] : array();
foreach (array('state_dir','status_file','parallel_gallery_root','source_fingerprint','runtime') as $preserve)
{
if (array_key_exists($preserve, $old_config)) $config[$preserve] = $old_config[$preserve];
}
foreach (array('quiet_seconds','max_wait_seconds','full_sync_seconds') as $preserve)
{
if (isset($old_config[$preserve])) $config[$preserve] = (int)$old_config[$preserve];
}
}
foreach (array('product'=>'nextcloud_product', 'version'=>'nextcloud_version') as $state_key=>$config_key) foreach (array('product'=>'nextcloud_product', 'version'=>'nextcloud_version') as $state_key=>$config_key)
{ {
$value = trim((string)($state[$state_key] ?? '')); $value = trim((string)($state[$state_key] ?? ''));
@@ -96,15 +128,31 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($secret_payload)) throw new RuntimeException('WebDAV-Zugangsdaten konnten nicht serialisiert werden.'); 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(); bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table(); $table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s'); $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); $secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
if ($editing_remote)
{
$config['state_dir'] = trim((string)($config['state_dir'] ?? '')) !== '' ? $config['state_dir'] : '/var/lib/bratonien-tools/nc-connector/connection-'.$editing_id;
$config['status_file'] = trim((string)($config['status_file'] ?? '')) !== '' ? $config['status_file'] : $config['state_dir'].'/connector-status.json';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nicht serialisiert werden.');
pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$editing_id." AND adapter='remote' LIMIT 1");
unset($_SESSION['bratonien_nc_wizard']);
return array(
'connection_id'=>$editing_id,
'message'=>'WebDAV-Verbindung wurde gespeichert.',
);
}
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nicht serialisiert werden.');
$connection_key = 'webdav-'.bin2hex(random_bytes(12));
pwg_query("INSERT INTO `$table` (connection_key,name,adapter,enabled,takeover_state,config_json,secret_blob,created,updated) VALUES ('" pwg_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($connection_key)."','"
.pwg_db_real_escape_string($name)."','remote',0,'disabled','" .pwg_db_real_escape_string($name)."','remote',0,'disabled','"
@@ -114,20 +162,18 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
.pwg_db_real_escape_string($now)."')"); .pwg_db_real_escape_string($now)."')");
$id = (int)pwg_db_insert_id(); $id = (int)pwg_db_insert_id();
if ($id < 1) throw new RuntimeException('Die WebDAV-Testverbindung konnte nicht eindeutig angelegt werden.'); 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['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json'; $config['status_file'] = $config['state_dir'].'/connector-status.json';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) if (!is_string($config_json)) throw new RuntimeException('WebDAV-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." AND adapter='remote' LIMIT 1");
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
throw new RuntimeException('Die WebDAV-Testverbindung konnte nach dem Anlegen nicht serialisiert werden.'); unset($_SESSION['bratonien_nc_wizard']);
}
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." LIMIT 1");
return array( return array(
'connection_id'=>$id, 'connection_id'=>$id,
'message'=>'Parallele WebDAV-Testverbindung wurde deaktiviert angelegt. Bestehende Verbindungen blieben unverändert.', 'message'=>'WebDAV-Verbindung wurde angelegt.',
); );
} }

View File

@@ -126,7 +126,6 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) $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_values(array_unique(array_map(function($path) { return trim((string)$path, '/'); }, $state['directory_selected'])))
: array(); : array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected) if (!$selected)
{ {
throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.'); throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
@@ -143,9 +142,12 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
{ {
throw new RuntimeException('Für eine Auswahl fehlt die eindeutige Nextcloud-Datei-ID. Bitte das Verzeichnis erneut auswählen.'); throw new RuntimeException('Für eine Auswahl fehlt die eindeutige Nextcloud-Datei-ID. Bitte das Verzeichnis erneut auswählen.');
} }
$display_name = $path === ''
? (trim((string)($state['display_name'] ?? '')) !== '' ? trim((string)$state['display_name']) : (string)$state['username'])
: basename($path);
$roots[] = array( $roots[] = array(
'fileid'=>$fileid, 'fileid'=>$fileid,
'display_name'=>basename($path), 'display_name'=>$display_name,
'webdav_path'=>$path, 'webdav_path'=>$path,
); );
} }
@@ -156,9 +158,35 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$state['technical_source'] = 'WebDAV-Verzeichnisse ausgewählt'; $state['technical_source'] = 'WebDAV-Verzeichnisse ausgewählt';
$state['technical_error'] = ''; $state['technical_error'] = '';
$state['directory_selection_ready'] = false; $state['directory_selection_ready'] = false;
if ((string)($state['editing_mode'] ?? '') === 'migrate')
{
$editing_id = (int)($state['editing_connection_id'] ?? 0);
$connection = $editing_id > 0 ? bratonien_tools_nc_connector_connection($editing_id, true) : null;
if (!$connection || (string)$connection['adapter'] !== 'local')
{
throw new RuntimeException('Die zu migrierende Legacy-Verbindung ist nicht mehr verfügbar.');
}
$migration = bratonien_tools_nc_connector_migration_state($connection);
if (empty($migration['ready']))
{
throw new RuntimeException('Die WebDAV-Migration kann noch nicht abgeschlossen werden. Es fehlen: '.implode(', ', $migration['missing']).'.');
}
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$state['_api_key_id'] = (string)($credentials['api_key_id'] ?? '');
$state['_api_key_secret'] = (string)($credentials['api_key_secret'] ?? '');
$state['api_status'] = !empty($migration['api_available']) ? 'ok' : 'skipped';
$state['api_error'] = '';
$state['step'] = 4;
bratonien_tools_nc_wizard_store($state); bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den parallelen Testweg vorbereitet.'); return array('message'=>'WebDAV-Quelle wurde übernommen. Mit „Migration starten“ wird jetzt der WebDAV-Nachfolger angelegt; die Legacy-Verbindung bleibt als Fallback erhalten.');
}
bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den nächsten Einrichtungsschritt vorbereitet.');
} }
function bratonien_tools_nc_wizard_finish_dispatch() function bratonien_tools_nc_wizard_finish_dispatch()

View File

@@ -18,7 +18,7 @@ function bratonien_tools_register_nc_productive_ws_methods($arr)
'info' => 'Piwigo storage site to synchronize. Default: 1.', 'info' => 'Piwigo storage site to synchronize. Default: 1.',
), ),
), ),
'Runs the approved direct Bratonien filesystem synchronization for the NC Connector.', 'Synchronizes the NC Connector into the existing Piwigo album hierarchy.',
null, null,
array( array(
'admin_only' => true, 'admin_only' => true,
@@ -35,6 +35,105 @@ function bratonien_tools_nc_productive_error(&$errors, $path, $type)
); );
} }
function bratonien_tools_nc_relative_path($basedir, $path)
{
$basedir = rtrim(str_replace('\\', '/', (string)$basedir), '/');
$path = str_replace('\\', '/', (string)$path);
if ($path === $basedir) return '';
if (strpos($path, $basedir.'/') !== 0)
{
throw new RuntimeException('WebDAV-Pfad liegt ausserhalb der Connector-Wurzel: '.$path);
}
return trim(substr($path, strlen($basedir)), '/');
}
function bratonien_tools_nc_find_album($parent_id, $dir, $name, $excluded_site_id)
{
$where_parent = $parent_id === null ? 'id_uppercat IS NULL' : 'id_uppercat='.(int)$parent_id;
$dir_sql = pwg_db_real_escape_string((string)$dir);
$name_sql = pwg_db_real_escape_string((string)$name);
$query = '
SELECT id, dir, name
FROM '.CATEGORIES_TABLE.'
WHERE '.$where_parent.'
AND (site_id IS NULL OR site_id <> '.(int)$excluded_site_id.')
AND (
dir = \''.$dir_sql.'\'
OR LOWER(name) = LOWER(\''.$name_sql.'\')
)
ORDER BY CASE WHEN dir = \''.$dir_sql.'\' THEN 0 ELSE 1 END, id
;';
$result = pwg_query($query);
$ids = array();
while ($row = pwg_db_fetch_assoc($result))
{
$ids[] = (int)$row['id'];
}
if (count($ids) > 1)
{
throw new RuntimeException('Albumzuordnung ist mehrdeutig fuer "'.$name.'".');
}
return count($ids) === 1 ? $ids[0] : null;
}
function bratonien_tools_nc_ensure_album_path($relative_dir, $excluded_site_id, array &$cache, array &$created_ids)
{
$relative_dir = trim((string)$relative_dir, '/');
if ($relative_dir === '') return null;
if (isset($cache[$relative_dir])) return $cache[$relative_dir];
$parts = explode('/', $relative_dir);
$parent_id = null;
$path = '';
foreach ($parts as $part)
{
if ($part === '') continue;
$path = $path === '' ? $part : $path.'/'.$part;
if (isset($cache[$path]))
{
$parent_id = $cache[$path];
continue;
}
$display_name = str_replace('_', ' ', $part);
$album_id = bratonien_tools_nc_find_album($parent_id, $part, $display_name, $excluded_site_id);
if ($album_id === null)
{
$created = create_virtual_category($display_name, $parent_id);
if (!is_array($created) || empty($created['id']))
{
$detail = is_array($created) && !empty($created['error']) ? (string)$created['error'] : 'unbekannter Fehler';
throw new RuntimeException('Album "'.$display_name.'" konnte nicht angelegt werden: '.$detail);
}
$album_id = (int)$created['id'];
pwg_query('UPDATE '.CATEGORIES_TABLE." SET status='private' WHERE id=".$album_id.' LIMIT 1');
add_permission_on_category(array($album_id), get_admins());
$created_ids[] = $album_id;
}
$cache[$path] = $album_id;
$parent_id = $album_id;
}
return $parent_id;
}
function bratonien_tools_nc_managed_images($basedir)
{
$prefix = rtrim((string)$basedir, '/').'/';
$escaped = pwg_db_real_escape_string(addcslashes($prefix, '_%\\'));
$query = "SELECT id, path FROM ".IMAGES_TABLE." WHERE path LIKE '".$escaped."%' ESCAPE '\\\\'";
return simple_hash_from_query($query, 'id', 'path');
}
function bratonien_tools_nc_remove_storage_categories($site_id)
{
$ids = query2array('SELECT id FROM '.CATEGORIES_TABLE.' WHERE site_id='.(int)$site_id.' AND dir IS NOT NULL', null, 'id');
if (!$ids) return 0;
delete_categories(array_map('intval', $ids));
return count($ids);
}
function bratonien_tools_ws_nc_sync_productive($params, &$service) function bratonien_tools_ws_nc_sync_productive($params, &$service)
{ {
global $conf, $user; global $conf, $user;
@@ -42,181 +141,114 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
$piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : ''; $piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : '';
if ($piwigo_version !== '16.4.0') if ($piwigo_version !== '16.4.0')
{ {
return new PwgError( return new PwgError(409, 'Bratonien API synchronization is not approved for Piwigo '.$piwigo_version.'.');
409,
'Bratonien API synchronization is not approved for Piwigo '.$piwigo_version.'. Use the administrator fallback until this Piwigo version has been verified.'
);
} }
if (empty($conf['enable_synchronization'])) if (empty($conf['enable_synchronization']))
{ {
return new PwgError(403, 'Piwigo filesystem synchronization is disabled.'); return new PwgError(403, 'Piwigo filesystem synchronization is disabled.');
} }
$site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1; $site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1;
if ($site_id < 1) if ($site_id < 1) return new PwgError(400, 'Invalid site_id.');
{
return new PwgError(400, 'Invalid site_id.');
}
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
include_once(PHPWG_ROOT_PATH.'admin/site_reader_local.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('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.');
if (!pwg_db_num_rows($result))
{
return new PwgError(404, 'Piwigo site does not exist.');
}
list($site_url) = pwg_db_fetch_row($result); list($site_url) = pwg_db_fetch_row($result);
if (url_is_remote($site_url)) if (url_is_remote($site_url)) return new PwgError(400, 'Remote Piwigo sites are not supported.');
{
return new PwgError(400, 'Remote Piwigo sites are not supported by this synchronization method.');
}
$site_reader = new LocalSiteReader($site_url); $site_reader = new LocalSiteReader($site_url);
if (!$site_reader->open()) if (!$site_reader->open()) return new PwgError(500, 'Piwigo could not open the configured local site.');
{
return new PwgError(500, 'Piwigo could not open the configured local site.');
}
list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()')); $basedir = preg_replace('#/*$#', '', (string)$site_url);
$errors = array(); $errors = array();
$counts = array( $counts = array(
'new_categories' => 0, 'reused_categories'=>0,
'del_categories' => 0, 'new_categories'=>0,
'new_elements' => 0, 'removed_duplicate_categories'=>0,
'del_elements' => 0, 'new_elements'=>0,
'upd_elements' => 0, 'del_elements'=>0,
'new_formats' => 0, 'upd_elements'=>0,
'del_formats' => 0,
'metadata_candidates' => 0,
'metadata_updated' => 0,
); );
try try
{ {
$query = 'SELECT id, id_uppercat, uppercats, global_rank, status, visible FROM '.CATEGORIES_TABLE.' WHERE dir IS NOT NULL AND site_id = '.$site_id; // Older WebDAV builds created a complete physical album tree for their own
$db_categories = hash_from_query($query, 'id'); // Piwigo site. Remove that technical duplicate first. The WebDAV files are
$db_fulldirs = get_fulldirs(array_keys($db_categories)); // placeholders and are rebuilt independently, therefore database deletion
$basedir = preg_replace('#/*$#', '', $site_url); // must not remove the actual Nextcloud originals.
$db_fulldirs = array_flip($db_fulldirs); $counts['removed_duplicate_categories'] = bratonien_tools_nc_remove_storage_categories($site_id);
$fs_fulldirs = $site_reader->get_full_directories($basedir);
$next_rank = array('NULL'=>1); list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()'));
$result = pwg_query('SELECT id FROM '.CATEGORIES_TABLE); $fs_dirs = $site_reader->get_full_directories($basedir);
while ($row = pwg_db_fetch_assoc($result)) usort($fs_dirs, function($a, $b)
{ {
$next_rank[$row['id']] = 1; return substr_count((string)$a, '/') <=> substr_count((string)$b, '/');
} });
$result = pwg_query('SELECT id_uppercat, MAX(`rank`)+1 AS next_rank FROM '.CATEGORIES_TABLE.' GROUP BY id_uppercat');
while ($row = pwg_db_fetch_assoc($result))
{
$key = empty($row['id_uppercat']) ? 'NULL' : $row['id_uppercat'];
$next_rank[$key] = (int)$row['next_rank'];
}
$next_id = pwg_db_nextval('id', CATEGORIES_TABLE); $album_cache = array();
$category_inserts = array(); $created_ids = array();
$dir_to_album = array();
foreach (array_diff($fs_fulldirs, array_keys($db_fulldirs)) as $fulldir) foreach ($fs_dirs as $full_dir)
{ {
$dir = basename($fulldir); $relative = bratonien_tools_nc_relative_path($basedir, $full_dir);
if (!preg_match($conf['sync_chars_regex'], $dir)) if ($relative === '') continue;
$before = count($created_ids);
$album_id = bratonien_tools_nc_ensure_album_path($relative, $site_id, $album_cache, $created_ids);
if ($album_id !== null)
{ {
bratonien_tools_nc_productive_error($errors, $fulldir, 'PWG-UPDATE-1'); $dir_to_album[$full_dir] = $album_id;
continue; if (count($created_ids) === $before) $counts['reused_categories']++;
}
$insert = array(
'id' => $next_id++,
'dir' => $dir,
'name' => str_replace('_', ' ', $dir),
'site_id' => $site_id,
'commentable' => boolean_to_string($conf['newcat_default_commentable']),
'status' => 'private',
'visible' => boolean_to_string($conf['newcat_default_visible']),
);
$parent_path = dirname($fulldir);
if (isset($db_fulldirs[$parent_path]))
{
$parent = $db_fulldirs[$parent_path];
$insert['id_uppercat'] = $parent;
$insert['uppercats'] = $db_categories[$parent]['uppercats'].','.$insert['id'];
$insert['rank'] = $next_rank[$parent]++;
$insert['global_rank'] = $db_categories[$parent]['global_rank'].'.'.$insert['rank'];
if ((string)$db_categories[$parent]['visible'] === 'false')
{
$insert['visible'] = 'false';
} }
} }
else $counts['new_categories'] = count($created_ids);
{
$insert['uppercats'] = (string)$insert['id'];
$insert['rank'] = $next_rank['NULL']++;
$insert['global_rank'] = (string)$insert['rank'];
}
$category_inserts[] = $insert;
$db_categories[$insert['id']] = array(
'id' => $insert['id'],
'id_uppercat' => $insert['id_uppercat'] ?? null,
'uppercats' => $insert['uppercats'],
'global_rank' => $insert['global_rank'],
'status' => 'private',
'visible' => $insert['visible'],
);
$db_fulldirs[$fulldir] = $insert['id'];
$next_rank[$insert['id']] = 1;
}
if ($category_inserts)
{
mass_inserts(
CATEGORIES_TABLE,
array('id','dir','name','site_id','id_uppercat','uppercats','commentable','visible','status','rank','global_rank'),
$category_inserts
);
$category_ids = array_map(function ($row) { return (int)$row['id']; }, $category_inserts);
pwg_activity('album', $category_ids, 'add', array('sync'=>true));
add_permission_on_category($category_ids, get_admins());
$counts['new_categories'] = count($category_ids);
}
$to_delete_categories = array();
foreach (array_diff(array_keys($db_fulldirs), $fs_fulldirs) as $fulldir)
{
$to_delete_categories[] = (int)$db_fulldirs[$fulldir];
unset($db_fulldirs[$fulldir]);
}
if ($to_delete_categories)
{
delete_categories($to_delete_categories);
$counts['del_categories'] = count($to_delete_categories);
}
$fs = $site_reader->get_elements($basedir); $fs = $site_reader->get_elements($basedir);
$cat_ids = array_diff(array_keys($db_categories), $to_delete_categories); $db_elements = bratonien_tools_nc_managed_images($basedir);
$db_elements = array(); $db_by_path = array_flip($db_elements);
if ($cat_ids)
$to_delete = array();
foreach ($db_elements as $id=>$path)
{ {
$query = 'SELECT id, path FROM '.IMAGES_TABLE.' WHERE storage_category_id IN ('.implode(',', array_map('intval', $cat_ids)).')'; if (!array_key_exists($path, $fs)) $to_delete[] = (int)$id;
$db_elements = simple_hash_from_query($query, 'id', 'path'); }
if ($to_delete)
{
delete_elements($to_delete, false);
$counts['del_elements'] = count($to_delete);
foreach ($to_delete as $id)
{
if (isset($db_elements[$id])) unset($db_by_path[$db_elements[$id]], $db_elements[$id]);
}
} }
$next_element_id = pwg_db_nextval('id', IMAGES_TABLE); $next_element_id = pwg_db_nextval('id', IMAGES_TABLE);
$image_inserts = array(); $image_inserts = array();
$image_links = array(); $image_links = array();
$format_inserts = array(); $new_ids = array();
$new_image_ids = array(); $all_ids = array();
foreach (array_diff(array_keys($fs), $db_elements) as $path) foreach ($fs as $path=>$file_info)
{ {
$dirname = dirname($path); $dirname = dirname($path);
if (!isset($db_fulldirs[$dirname])) $relative_dir = bratonien_tools_nc_relative_path($basedir, $dirname);
$category_id = null;
if ($relative_dir !== '')
{ {
$category_id = $dir_to_album[$dirname] ?? bratonien_tools_nc_ensure_album_path($relative_dir, $site_id, $album_cache, $created_ids);
}
if (isset($db_by_path[$path]))
{
$id = (int)$db_by_path[$path];
$all_ids[] = $id;
pwg_query('DELETE FROM '.IMAGE_CATEGORY_TABLE.' WHERE image_id='.$id);
if ($category_id !== null)
{
single_insert(IMAGE_CATEGORY_TABLE, array('image_id'=>$id, 'category_id'=>$category_id));
}
continue; continue;
} }
@@ -229,116 +261,41 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
$id = $next_element_id++; $id = $next_element_id++;
$image_inserts[] = array( $image_inserts[] = array(
'id' => $id, 'id'=>$id,
'file' => $filename, 'file'=>$filename,
'name' => get_name_from_file($filename), 'name'=>get_name_from_file($filename),
'date_available' => $dbnow, 'date_available'=>$dbnow,
'path' => $path, 'path'=>$path,
'representative_ext' => $fs[$path]['representative_ext'], 'representative_ext'=>$file_info['representative_ext'],
'storage_category_id' => $db_fulldirs[$dirname], 'storage_category_id'=>null,
'added_by' => (int)$user['id'], 'added_by'=>(int)$user['id'],
); );
$image_links[] = array('image_id'=>$id, 'category_id'=>$db_fulldirs[$dirname]); if ($category_id !== null)
$new_image_ids[] = $id;
if (!empty($conf['enable_formats']) && !empty($fs[$path]['formats']))
{ {
foreach ($fs[$path]['formats'] as $ext => $filesize) $image_links[] = array('image_id'=>$id, 'category_id'=>$category_id);
{
$format_inserts[] = array('image_id'=>$id, 'ext'=>$ext, 'filesize'=>$filesize);
}
} }
$new_ids[] = $id;
$all_ids[] = $id;
} }
if ($image_inserts) if ($image_inserts)
{ {
mass_inserts(IMAGES_TABLE, array_keys($image_inserts[0]), $image_inserts); mass_inserts(IMAGES_TABLE, array_keys($image_inserts[0]), $image_inserts);
mass_inserts(IMAGE_CATEGORY_TABLE, array_keys($image_links[0]), $image_links); if ($image_links) mass_inserts(IMAGE_CATEGORY_TABLE, array_keys($image_links[0]), $image_links);
pwg_activity('photo', $new_image_ids, 'add', array('sync'=>true)); pwg_activity('photo', $new_ids, 'add', array('sync'=>true));
$counts['new_elements'] = count($image_inserts); $counts['new_elements'] = count($new_ids);
}
if ($format_inserts)
{
mass_inserts(IMAGE_FORMAT_TABLE, array_keys($format_inserts[0]), $format_inserts);
$counts['new_formats'] += count($format_inserts);
} }
if (!empty($conf['enable_formats']) && $db_elements) // Refresh the normal file attributes for connector-managed images only.
{
$db_elements_flip = array_flip($db_elements);
$existing_ids = array();
foreach (array_intersect_key($fs, $db_elements_flip) as $path => $unused)
{
$existing_ids[] = (int)$db_elements_flip[$path];
}
if ($existing_ids)
{
$db_formats = array();
$result = pwg_query('SELECT * FROM '.IMAGE_FORMAT_TABLE.' WHERE image_id IN ('.implode(',', $existing_ids).')');
while ($row = pwg_db_fetch_assoc($result))
{
$db_formats[$row['image_id']][$row['ext']] = $row['format_id'];
}
$formats_to_delete = array();
$formats_to_insert = array();
foreach ($existing_ids as $image_id)
{
$path = $db_elements[$image_id];
$known = $db_formats[$image_id] ?? array();
$present = $fs[$path]['formats'] ?? array();
foreach (array_diff_key($known, $present) as $format_id)
{
$formats_to_delete[] = (int)$format_id;
}
foreach (array_diff_key($present, $known) as $ext => $filesize)
{
$formats_to_insert[] = array('image_id'=>$image_id, 'ext'=>$ext, 'filesize'=>$filesize);
}
}
if ($formats_to_delete)
{
pwg_query('DELETE FROM '.IMAGE_FORMAT_TABLE.' WHERE format_id IN ('.implode(',', $formats_to_delete).')');
$counts['del_formats'] = count($formats_to_delete);
}
if ($formats_to_insert)
{
mass_inserts(IMAGE_FORMAT_TABLE, array_keys($formats_to_insert[0]), $formats_to_insert);
$counts['new_formats'] += count($formats_to_insert);
}
}
}
$to_delete_elements = array();
foreach (array_diff($db_elements, array_keys($fs)) as $path)
{
$id = array_search($path, $db_elements, true);
if ($id !== false)
{
$to_delete_elements[] = (int)$id;
}
}
if ($to_delete_elements)
{
delete_elements($to_delete_elements);
$counts['del_elements'] = count($to_delete_elements);
}
update_category('all');
update_global_rank();
$files = get_filelist('', $site_id, true, false);
$updates = array(); $updates = array();
foreach ($files as $id => $file) foreach ($all_ids as $id)
{ {
$data = $site_reader->get_element_update_attributes($file['path']); $path_result = pwg_query('SELECT path FROM '.IMAGES_TABLE.' WHERE id='.(int)$id.' LIMIT 1');
if (!is_array($data)) if (!pwg_db_num_rows($path_result)) continue;
{ list($path) = pwg_db_fetch_row($path_result);
continue; $data = $site_reader->get_element_update_attributes($path);
} if (!is_array($data)) continue;
$data['id'] = $id; $data['id'] = (int)$id;
$updates[] = $data; $updates[] = $data;
} }
if ($updates) if ($updates)
@@ -351,94 +308,25 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
} }
$counts['upd_elements'] = count($updates); $counts['upd_elements'] = count($updates);
$metadata_files = get_filelist('', $site_id, true, true);
$counts['metadata_candidates'] = count($metadata_files);
$metadata_updates = array();
$tags_of = array();
foreach ($metadata_files as $id => $element_infos)
{
$data = $site_reader->get_element_metadata($element_infos);
if (!is_array($data))
{
bratonien_tools_nc_productive_error($errors, $element_infos['path'], 'PWG-ERROR-NO-FS');
continue;
}
$data['date_metadata_update'] = $dbnow;
$data['id'] = $id;
$metadata_updates[] = $data;
foreach (array('keywords','tags') as $key)
{
if (!isset($data[$key]))
{
continue;
}
$tags_of[$id] = $tags_of[$id] ?? array();
foreach (explode(',', $data[$key]) as $tag_name)
{
$tags_of[$id][] = tag_id_from_tag_name($tag_name);
}
}
}
if ($metadata_updates)
{
mass_updates(
IMAGES_TABLE,
array(
'primary'=>array('id'),
'update'=>array_unique(array_merge(
array_diff($site_reader->get_metadata_attributes(), array('keywords','tags')),
array('date_metadata_update')
)),
),
$metadata_updates,
MASS_UPDATES_SKIP_EMPTY
);
}
if ($tags_of)
{
set_tags_of($tags_of);
}
$counts['metadata_updated'] = count($metadata_updates);
// Mirror Piwigo 16.4.0 Maintenance -> "Update albums informations".
// This repairs the derived album hierarchy and counters that the direct
// API sync otherwise bypasses when no admin maintenance page is invoked.
images_integrity(); images_integrity();
categories_integrity(); categories_integrity();
update_uppercats(); update_uppercats();
update_category('all'); update_category('all');
update_global_rank(); update_global_rank();
invalidate_user_cache(true);
// Mirror Piwigo 16.4.0 Maintenance -> "Update photos information".
// This finalizes physical paths, ratings and derived photo information.
images_integrity();
update_path();
include_once(PHPWG_ROOT_PATH.'include/functions_rate.inc.php');
update_rating_score();
invalidate_user_cache(); invalidate_user_cache();
}
catch (Throwable $e)
{
return new PwgError(500, 'Bratonien direct synchronization failed: '.$e->getMessage());
}
return array( return array(
'mode' => 'productive', 'mode'=>'productive',
'engine' => 'bratonien-direct', 'piwigo_version'=>$piwigo_version,
'approved_piwigo_version' => '16.4.0', 'site_id'=>$site_id,
'piwigo_version' => $piwigo_version, 'site_url'=>$site_url,
'site_id' => $site_id, 'counts'=>$counts,
'site_url' => $site_url, 'errors'=>$errors,
'counts' => $counts, 'database_writes'=>array_sum($counts) > 0,
'errors' => $errors,
'error_count' => count($errors),
'database_writes' => true,
'username' => isset($user['username']) ? (string)$user['username'] : '',
'status' => isset($user['status']) ? (string)$user['status'] : '',
); );
}
catch (Throwable $error)
{
return new PwgError(500, 'Bratonien NC synchronization failed: '.$error->getMessage());
}
} }

View File

@@ -4,6 +4,17 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
if (isset($GLOBALS['template']) && is_object($GLOBALS['template']) && method_exists($GLOBALS['template'], 'func_combine_script'))
{
$script_version = function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : '0.9.6.27';
$GLOBALS['template']->func_combine_script(array(
'id'=>'bratonien_nc_connector_edit_v2',
'path'=>BRATONIEN_TOOLS_PATH.'js/nc_connector_edit_v2.js',
'load'=>'footer',
'version'=>$script_version,
));
}
require_once(BRATONIEN_TOOLS_PATH . 'tools/image_cache.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/image_cache.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php');
@@ -17,7 +28,6 @@ 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_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_auth.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php');
@@ -30,6 +40,59 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_generic_scope.inc.php'
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_edit.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.inc.php');
function bratonien_tools_nc_connector_run_now()
{
$service = 'bratonien-nc-connector.service';
$active = bratonien_tools_nc_connector_systemctl_value(array('is-active', $service));
if ($active === 'active' || $active === 'activating')
{
return array('message'=>'Der NC-Abgleich läuft bereits.');
}
if (!function_exists('proc_open'))
{
throw new RuntimeException('Der NC-Abgleich konnte nicht gestartet werden: proc_open ist nicht verfügbar.');
}
$commands = array(
array('/usr/bin/sudo', '-n', '/usr/bin/systemctl', 'start', '--no-block', $service),
array('/usr/bin/systemctl', 'start', '--no-block', $service),
);
$last_error = '';
foreach ($commands as $command)
{
if (!is_executable($command[0]))
{
continue;
}
$spec = array(
0=>array('file','/dev/null','r'),
1=>array('pipe','w'),
2=>array('pipe','w'),
);
$process = @proc_open($command, $spec, $pipes, null, array_merge($_ENV, array('LC_ALL'=>'C','LANG'=>'C')));
if (!is_resource($process))
{
continue;
}
$stdout = trim((string)stream_get_contents($pipes[1]));
$stderr = trim((string)stream_get_contents($pipes[2]));
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
if ($exit === 0)
{
return array('message'=>'NC-Abgleich wurde gestartet.');
}
$last_error = $stderr !== '' ? $stderr : $stdout;
}
throw new RuntimeException('Der NC-Abgleich konnte nicht gestartet werden'.($last_error !== '' ? ': '.$last_error : '.'));
}
function bratonien_tools_get_tools() function bratonien_tools_get_tools()
{ {
@@ -54,11 +117,13 @@ 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_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'),
'nc_connector_edit_start' => array('handler' => 'bratonien_tools_nc_connector_edit_start'),
'nc_connector_update_local' => array('handler' => 'bratonien_tools_nc_connector_update_local_friendly'),
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'), 'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'),
'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'), 'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'), 'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
'nc_connector_run_now' => array('handler' => 'bratonien_tools_nc_connector_run_now'),
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'), 'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'),
'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'), 'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'),
'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'), 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
@@ -71,10 +136,7 @@ function bratonien_tools_get_tools()
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'), '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_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_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'), 'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'),
'nc_connector_prepare_takeover' => array('handler' => 'bratonien_tools_nc_connector_prepare_takeover'),
'nc_connector_cancel_takeover' => array('handler' => 'bratonien_tools_nc_connector_cancel_takeover'),
'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'), 'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'),
'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'), 'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'),
'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'), 'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'),

View File

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

236
js/nc_connector_edit_v2.js Normal file
View File

@@ -0,0 +1,236 @@
(function () {
'use strict';
function ready(callback) {
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', callback);
else callback();
}
ready(function () {
var section = document.getElementById('nc-connector');
if (!section) return;
var tokenInput = section.querySelector('input[name="pwg_token"]');
var pwgToken = tokenInput ? tokenInput.value : '';
var technicalButton = document.getElementById('bratonien-nc-technical-open');
if (technicalButton) technicalButton.remove();
var technicalCreate = document.getElementById('bratonien-nc-technical-create');
if (technicalCreate) technicalCreate.remove();
var statusHeading = Array.prototype.find.call(section.querySelectorAll('h4'), function (heading) {
return heading.textContent.trim() === 'Status';
});
var statusCard = statusHeading ? statusHeading.closest('.bratonien-card') : null;
if (statusCard && pwgToken && !statusCard.querySelector('[data-nc-run-now]')) {
var runForm = document.createElement('form');
runForm.method = 'post';
runForm.className = 'bratonien-actions';
runForm.style.marginTop = '1rem';
runForm.setAttribute('data-nc-run-now', '1');
runForm.innerHTML = '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_run_now">Jetzt abgleichen</button>';
runForm.addEventListener('submit', function () {
var button = runForm.querySelector('button');
if (button) {
button.disabled = true;
button.textContent = 'Abgleich wird gestartet …';
}
});
statusCard.appendChild(runForm);
}
function escapeHtml(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c];
});
}
function loadConnection(id) {
return fetch('plugins/bratonien_tools/nc-connector-edit-data.php?connection_id='+encodeURIComponent(id)+'&_='+Date.now(), {
credentials:'same-origin', cache:'no-store', headers:{'Accept':'application/json'}
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok) throw new Error(data.error || ('HTTP '+response.status));
return data;
});
});
}
function createPostForm(label, tool, id) {
var form = document.createElement('form');
form.method = 'post';
form.style.display = 'inline';
form.innerHTML = '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<input type="hidden" name="connection_id" value="'+escapeHtml(id)+'">'
+ '<button class="buttonLike" type="submit" name="bratonien_tool" value="'+escapeHtml(tool)+'">'+escapeHtml(label)+'</button>';
return form;
}
function dialog() {
var node = document.getElementById('bratonien-nc-connection-edit-v2');
if (node) return node;
node = document.createElement('dialog');
node.id = 'bratonien-nc-connection-edit-v2';
node.className = 'bratonien-edit-dialog';
node.innerHTML = '<div class="bratonien-edit-dialog__body">'
+ '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem">'
+ '<div><h4 style="margin:0">Verbindung bearbeiten</h4><p class="bratonien-base-note" style="margin:.35rem 0 0">Die Einstellungen gelten nur für diese Verbindung.</p></div>'
+ '<button type="button" class="buttonLike" data-edit-close>Schließen</button></div>'
+ '<div data-edit-content style="margin-top:1rem"></div></div>';
document.body.appendChild(node);
node.querySelector('[data-edit-close]').addEventListener('click', function () { node.close(); });
node.addEventListener('click', function (event) { if (event.target === node) node.close(); });
return node;
}
function storageRow(storage) {
storage = storage || {};
return '<div class="bratonien-storage-row" data-storage-row>'
+ '<label>Storage-ID<input name="nc_storage_id[]" value="'+escapeHtml(storage.storage_id || '')+'" required></label>'
+ '<label>Quellordner<input name="nc_source_prefix[]" value="'+escapeHtml(storage.source_prefix || '')+'" placeholder="optional"></label>'
+ '<label>Lokaler Speicherpfad<input name="nc_local_mount[]" value="'+escapeHtml(storage.local_mount || '')+'" required></label>'
+ '<button type="button" class="buttonLike" data-remove-storage>Entfernen</button></div>';
}
function showError(form, data) {
var old = form.querySelector('[data-edit-error]');
if (old) old.remove();
var box = document.createElement('div');
box.dataset.editError = '1';
box.className = 'bratonien-main-cache__warning';
box.style.margin = '0 0 1rem';
box.textContent = (data && data.message) ? data.message : 'Die Verbindung konnte nicht gespeichert werden.';
form.insertBefore(box, form.firstChild);
}
function submitLocal(form, editDialog) {
if (!form.reportValidity()) return;
var button = form.querySelector('button[type="submit"]');
if (button) button.disabled = true;
fetch('plugins/bratonien_tools/nc-connector-edit-save.php', {
method:'POST', credentials:'same-origin', cache:'no-store', headers:{'Accept':'application/json'}, body:new FormData(form)
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok || !data.ok) { showError(form, data); return; }
editDialog.close();
window.location.reload();
});
}).catch(function (error) {
showError(form, {message:error.message || String(error)});
}).finally(function () {
if (button) button.disabled = false;
});
}
function openLocalEditor(id) {
var editDialog = dialog();
var content = editDialog.querySelector('[data-edit-content]');
content.innerHTML = '<p class="bratonien-base-note">Verbindung wird geladen …</p>';
if (typeof editDialog.showModal === 'function') editDialog.showModal();
else editDialog.setAttribute('open', 'open');
loadConnection(id).then(function (data) {
if (data.adapter !== 'local') throw new Error('Diese Verbindung wird über den WebDAV-Assistenten bearbeitet.');
var legacy = data.legacy || {};
var webdav = data.webdav || {};
var storages = Array.isArray(legacy.storages) ? legacy.storages : [];
var rows = storages.map(storageRow).join('') || storageRow({});
content.innerHTML = '<form method="post" data-edit-form>'
+ '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<input type="hidden" name="connection_id" value="'+escapeHtml(data.id)+'">'
+ '<h5>Verbindung</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Name</label><input name="connection_name" value="'+escapeHtml(data.name)+'" required>'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Nextcloud</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Nextcloud-Adresse</label><input name="nc_nextcloud_url" value="'+escapeHtml(webdav.nextcloud_url || '')+'">'
+ '<label class="bratonien-label">Nextcloud-Benutzer</label><input name="nc_nextcloud_user" value="'+escapeHtml(webdav.nextcloud_user || '')+'" autocomplete="username">'
+ '<label class="bratonien-label">Nextcloud-Passwort</label><input name="nc_nextcloud_password" type="password" autocomplete="current-password" placeholder="'+(webdav.has_nextcloud_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Piwigo-Zugang</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">API-Schlüssel-ID</label><input name="nc_connection_api_key_id" value="'+escapeHtml(webdav.api_key_id || '')+'" autocomplete="off">'
+ '<label class="bratonien-label">API-Geheimnis</label><input name="nc_connection_api_key_secret" type="password" autocomplete="new-password" placeholder="'+(webdav.has_api_key_secret ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '<label class="bratonien-label">Fallback-Benutzer</label><input name="nc_fallback_user" value="'+escapeHtml(webdav.fallback_user || '')+'" autocomplete="username">'
+ '<label class="bratonien-label">Fallback-Passwort</label><input name="nc_fallback_password" type="password" autocomplete="current-password" placeholder="'+(webdav.has_fallback_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Lokaler Connector</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Datenbank-Server</label><input name="nc_host" value="'+escapeHtml(legacy.host)+'" required>'
+ '<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="'+escapeHtml(legacy.port || 5432)+'" required>'
+ '<label class="bratonien-label">Datenbank</label><input name="nc_database" value="'+escapeHtml(legacy.database)+'" required>'
+ '<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" value="'+escapeHtml(legacy.user)+'" required>'
+ '<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" placeholder="'+(legacy.has_db_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Speicherorte</h5><div data-storage-list>'+rows+'</div>'
+ '<button type="button" class="buttonLike" data-add-storage>Speicherort hinzufügen</button>'
+ '<details style="margin-top:1rem"><summary>Erweiterte Einstellungen</summary><div class="bratonien-form-grid" style="margin-top:.75rem">'
+ '<label class="bratonien-label">Source-View</label><input name="nc_source_view" value="'+escapeHtml(legacy.source_view)+'" required>'
+ '<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" value="'+escapeHtml(legacy.activity_view)+'" required>'
+ '<label class="bratonien-label">Piwigo-Galerieordner</label><input name="nc_gallery_root" value="'+escapeHtml(legacy.gallery_root)+'" required>'
+ '<label class="bratonien-label">Ruhezeit (Sek.)</label><input name="nc_quiet_seconds" type="number" min="0" value="'+escapeHtml(legacy.quiet_seconds)+'">'
+ '<label class="bratonien-label">Maximale Wartezeit (Sek.)</label><input name="nc_max_wait_seconds" type="number" min="60" value="'+escapeHtml(legacy.max_wait_seconds)+'">'
+ '<label class="bratonien-label">Vollprüfung nach (Sek.)</label><input name="nc_full_sync_seconds" type="number" min="300" value="'+escapeHtml(legacy.full_sync_seconds)+'">'
+ '</div></details>'
+ '<div class="bratonien-actions" style="margin-top:1rem"><button class="buttonLike" type="submit">Änderungen prüfen und speichern</button><button class="buttonLike" type="button" data-cancel>Abbrechen</button></div>'
+ '</form>';
var form = content.querySelector('[data-edit-form]');
form.querySelector('[data-cancel]').addEventListener('click', function () { editDialog.close(); });
form.querySelector('[data-add-storage]').addEventListener('click', function () {
form.querySelector('[data-storage-list]').insertAdjacentHTML('beforeend', storageRow({}));
});
form.addEventListener('click', function (event) {
var remove = event.target.closest('[data-remove-storage]');
if (remove) {
var row = remove.closest('[data-storage-row]');
if (row) row.remove();
}
});
form.addEventListener('submit', function (event) {
event.preventDefault();
submitLocal(form, editDialog);
});
}).catch(function (error) {
content.innerHTML = '<p class="bratonien-main-cache__warning"><strong>Bearbeiten nicht möglich:</strong> '+escapeHtml(error.message || String(error))+'</p>';
});
}
[].slice.call(section.querySelectorAll('button[value="nc_connector_edit_start"]')).forEach(function (button) {
var form = button.closest('form');
if (form) form.remove();
});
[].slice.call(section.querySelectorAll('button[value="nc_connector_migrate_start"]')).forEach(function (button) {
var form = button.closest('form');
if (form) form.remove();
});
[].slice.call(section.querySelectorAll('button[value="nc_connector_delete"]')).forEach(function (deleteButton) {
var deleteForm = deleteButton.closest('form');
if (!deleteForm || !deleteForm.parentElement) return;
var idInput = deleteForm.querySelector('input[name="connection_id"]');
if (!idInput) return;
var id = idInput.value;
var actions = deleteForm.parentElement;
loadConnection(id).then(function (data) {
var edit;
if (data.adapter === 'local') {
edit = document.createElement('button');
edit.type = 'button';
edit.className = 'buttonLike';
edit.textContent = 'Bearbeiten';
edit.addEventListener('click', function () { openLocalEditor(id); });
} else {
edit = createPostForm('Bearbeiten', 'nc_connector_edit_start', id);
}
actions.insertBefore(edit, deleteForm);
}).catch(function (error) {
var info = document.createElement('span');
info.className = 'bratonien-main-cache__warning';
info.textContent = 'Verbindung konnte nicht geladen werden: '+(error.message || String(error));
actions.insertBefore(info, deleteForm);
});
});
});
})();

View File

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

View File

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

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

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

View File

@@ -0,0 +1,104 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
function bratonien_tools_nc_source_picker_json($status, array $payload)
{
http_response_code((int)$status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function bratonien_tools_nc_source_picker_fail($message, array $fields = array())
{
bratonien_tools_nc_source_picker_json(422, array(
'ok'=>false,
'message'=>(string)$message,
'fields'=>array_values(array_unique($fields)),
));
}
if (!defined('BRATONIEN_TOOLS_PATH'))
{
bratonien_tools_nc_source_picker_json(404, array('ok'=>false, 'message'=>'Bratonien Tools ist nicht aktiv.', 'fields'=>array()));
}
if (!function_exists('is_admin') || !is_admin())
{
bratonien_tools_nc_source_picker_json(403, array('ok'=>false, 'message'=>'Administratorrechte erforderlich.', 'fields'=>array()));
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST')
{
bratonien_tools_nc_source_picker_json(405, array('ok'=>false, 'message'=>'Nur POST ist erlaubt.', 'fields'=>array()));
}
try
{
check_pwg_token();
require_once(BRATONIEN_TOOLS_PATH.'include/tool_registry.inc.php');
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
bratonien_tools_nc_source_picker_fail('Die zu bearbeitende Verbindung wurde nicht gefunden.');
}
if ((string)$connection['adapter'] !== 'local')
{
bratonien_tools_nc_source_picker_fail('Die automatische Migrationsauswahl ist nur für die bestehende Legacy-Verbindung vorgesehen.');
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$base_url = trim((string)($config['nextcloud_url'] ?? ''));
$username = trim((string)($credentials['nextcloud_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
$missing = array();
if ($base_url === '') $missing[] = 'nc_nextcloud_url';
if ($username === '') $missing[] = 'nc_nextcloud_user';
if ($password === '') $missing[] = 'nc_nextcloud_password';
if ($missing)
{
bratonien_tools_nc_source_picker_fail(
'Für die automatische Ordnerauswahl müssen Nextcloud-Adresse, Benutzer und Passwort gespeichert sein.',
$missing
);
}
bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection($connection, 'migrate');
$state = bratonien_tools_nc_wizard_state();
if ((int)($state['step'] ?? 0) !== 2 || empty($state['scan_ok']) || empty($state['directory_selection_ready']))
{
$detail = trim((string)($state['technical_error'] ?? ''));
bratonien_tools_nc_source_picker_fail(
$detail !== '' ? 'Die Nextcloud-Ordner konnten nicht geladen werden: '.$detail : 'Die Nextcloud-Ordner konnten nicht geladen werden.',
array('nc_nextcloud_url','nc_nextcloud_user','nc_nextcloud_password')
);
}
bratonien_tools_nc_source_picker_json(200, array(
'ok'=>true,
'message'=>'Nextcloud-Ordner wurden geladen. Die Auswahl wird geöffnet.',
'fields'=>array(),
));
}
catch (Throwable $e)
{
$message = trim($e->getMessage());
if ($message === '') $message = 'Die Nextcloud-Ordnerauswahl konnte nicht vorbereitet werden.';
bratonien_tools_nc_source_picker_fail(
$message,
array('nc_nextcloud_url','nc_nextcloud_user','nc_nextcloud_password')
);
}

View File

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

View File

@@ -0,0 +1,35 @@
#!/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_MAPPING_FILE:?WEBDAV_MAPPING_FILE fehlt}"
: "${STATE_DIR:?STATE_DIR fehlt}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
LOCK_FILE="$STATE_DIR/webdav-media.lock"
mkdir -p -- "$STATE_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
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"
php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID"

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Build a placeholder-backed local source tree from Nextcloud WebDAV. """Build a placeholder-backed local source tree from Nextcloud WebDAV.
This is intentionally additive: it does not replace the existing local-storage This creates only tiny placeholder files plus a metadata mapping; no Nextcloud
connector path. It creates only tiny placeholder files plus a metadata mapping; original media is downloaded. The authenticated Nextcloud user is never used as
no Nextcloud original media is downloaded. an album name. Selecting the user's WebDAV root mirrors its children directly.
""" """
from __future__ import annotations from __future__ import annotations
@@ -26,8 +26,6 @@ from pathlib import Path, PurePosixPath
DAV = "DAV:" DAV = "DAV:"
OC = "http://owncloud.org/ns" OC = "http://owncloud.org/ns"
SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} 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==") PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
@@ -148,13 +146,7 @@ def link_placeholder(seed: Path, target: Path) -> None:
target.write_bytes(PLACEHOLDER) target.write_bytes(PLACEHOLDER)
def build_root( def build_root(client: WebDavClient, remote_root: str, local_root: Path, seed: Path, mapping: dict[str, dict[str, object]]) -> tuple[int, int, int]:
client: WebDavClient,
remote_root: str,
local_root: Path,
seed: Path,
mapping: dict[str, dict[str, object]],
) -> tuple[int, int, int]:
files = 0 files = 0
folders = 0 folders = 0
skipped = 0 skipped = 0
@@ -253,17 +245,19 @@ def main() -> int:
mapping: dict[str, dict[str, object]] = {} mapping: dict[str, dict[str, object]] = {}
manifest: list[str] = [] manifest: list[str] = []
total_files = total_folders = total_skipped = 0 total_files = total_folders = total_skipped = 0
used_names: set[str] = set() used_fileids: set[int] = set()
for remote_root_raw in args.root: for remote_root_raw in args.root:
remote_root = validate_relative(remote_root_raw) remote_root = validate_relative(remote_root_raw)
current, _ = client.list_collection(remote_root) current, _ = client.list_collection(remote_root)
fileid = int(current["fileid"]) fileid = int(current["fileid"])
display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user) if fileid in used_fileids:
local_name = f"root-{fileid}"
if local_name in used_names:
fail(f"duplicate selected Nextcloud root fileid: {fileid}") fail(f"duplicate selected Nextcloud root fileid: {fileid}")
used_names.add(local_name) used_fileids.add(fileid)
# An explicitly selected folder keeps its own name. The authenticated
# user's WebDAV root is transparent and must never become an album.
display = "" if remote_root == "" else (str(current.get("display_name", "")).strip() or PurePosixPath(remote_root).name)
local_name = f"root-{fileid}"
local_root = staging / local_name local_root = staging / local_name
files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping) files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping)
total_files += files total_files += files

View File

@@ -79,15 +79,23 @@ def preferred_target(source_key: str, raw_name: str, parent_target: Path, old_ma
return safe_name(raw_name) return safe_name(raw_name)
def mirror_directory(source: Path, target: Path, source_key: str, target_key: Path, old_map: dict[str, str], new_map: dict[str, str]) -> None: def mirror_directory(
source: Path,
target: Path,
source_key: str,
target_key: Path,
old_map: dict[str, str],
new_map: dict[str, str],
used: set[str] | None = None,
) -> None:
target.mkdir(parents=True, exist_ok=True) target.mkdir(parents=True, exist_ok=True)
used: set[str] = set() used_names = used if used is not None else set()
for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)): for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)):
if child.is_symlink(): if child.is_symlink():
continue continue
child_source_key = f"{source_key}/{child.name}" child_source_key = f"{source_key}/{child.name}"
preferred = preferred_target(child_source_key, child.name, target_key, old_map) preferred = preferred_target(child_source_key, child.name, target_key, old_map)
child_name = unique_name(preferred, used, child.is_file()) child_name = unique_name(preferred, used_names, child.is_file())
child_target = target / child_name child_target = target / child_name
child_target_key = target_key / child_name child_target_key = target_key / child_name
new_map[child_source_key] = child_target_key.as_posix() new_map[child_source_key] = child_target_key.as_posix()
@@ -120,11 +128,26 @@ def build(manifest: Path, destination: Path, state_file: Path) -> None:
try: try:
used_roots: set[str] = set() used_roots: set[str] = set()
transparent_roots = 0
for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])): for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])):
source = Path(entry["source_path"]) source = Path(entry["source_path"])
source_key = f"share:{entry['share_id']}" source_key = f"share:{entry['share_id']}"
preferred = preferred_target(source_key, entry["display_name"], Path("."), old_map)
is_file_share = entry["item_type"] == "file" is_file_share = entry["item_type"] == "file"
# Empty display_name is intentional: it represents the authenticated
# user's WebDAV root. Its children belong directly at destination;
# the Nextcloud username must never become an album wrapper.
if not is_file_share and entry["display_name"] == "":
transparent_roots += 1
if transparent_roots > 1:
raise ValueError("only one transparent WebDAV root is allowed")
if not source.is_dir():
raise FileNotFoundError(f"source is not a readable directory: {source}")
new_map[source_key] = "."
mirror_directory(source, staging, source_key, Path("."), old_map, new_map, used_roots)
continue
preferred = preferred_target(source_key, entry["display_name"], Path("."), old_map)
root_name = unique_name(preferred, used_roots, is_file_share) root_name = unique_name(preferred, used_roots, is_file_share)
root_key = Path(root_name) root_key = Path(root_name)
new_map[source_key] = root_key.as_posix() new_map[source_key] = root_key.as_posix()

View File

@@ -107,7 +107,7 @@ try
$rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC"); $rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC");
if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error); if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
foreach (array($configDir=>$configDir, $publicSourceRoot=>$publicSourceRoot, $publicGalleryRoot=>$publicGalleryRoot) as $dir=>$unused) foreach (array($configDir, $publicSourceRoot, $publicGalleryRoot) as $dir)
{ {
if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0755, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir); if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0755, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir);
} }
@@ -125,7 +125,6 @@ try
if (!is_array($config)) $config = array(); if (!is_array($config)) $config = array();
if ((string)$row['adapter'] !== 'remote') continue; if ((string)$row['adapter'] !== 'remote') continue;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue; if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
if (empty($config['parallel_test'])) continue;
try try
{ {
@@ -164,8 +163,6 @@ try
} }
if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.'); if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.');
@chmod($galleryRoot, 0755); @chmod($galleryRoot, 0755);
// Alte technische Wrapper unter ./galleries duerfen nicht als Piwigo-Alben auftauchen.
webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot); webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot);
$sourceDir = $publicSourceRoot.'/connection-'.$id; $sourceDir = $publicSourceRoot.'/connection-'.$id;
@@ -189,8 +186,9 @@ try
{ {
$path = trim((string)($root['webdav_path'] ?? ''), '/'); $path = trim((string)($root['webdav_path'] ?? ''), '/');
$display = trim((string)($root['display_name'] ?? '')); $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.'); $runtimePath = $path === '' ? '/' : $path;
$rootLines[] = $path."\t".$display; if ($display === '' || preg_match('/[\t\r\n]/', $runtimePath.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
$rootLines[] = $runtimePath."\t".$display;
} }
file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX); file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);
@chmod($rootsPath, 0600); @chmod($rootsPath, 0600);
@@ -215,21 +213,23 @@ try
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
@chmod($configPath, 0600); @chmod($configPath, 0600);
unset($config['migration'], $config['parallel_test']);
$config['state_dir'] = $stateDir; $config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile; $config['status_file'] = $statusFile;
$config['parallel_gallery_root'] = $galleryRoot; $config['parallel_gallery_root'] = $galleryRoot;
$config['source_fingerprint'] = $fingerprint; $config['source_fingerprint'] = $fingerprint;
$config['runtime'] = array( $config['runtime'] = array(
'mode'=>'parallel-webdav', 'mode'=>'webdav',
'config'=>$configPath, 'config'=>$configPath,
'piwigo_sync_enabled'=>true, 'piwigo_sync_enabled'=>true,
'reconciled_at'=>date('Y-m-d H:i:s'), 'reconciled_at'=>date('Y-m-d H:i:s'),
); );
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (is_string($json)) if (!is_string($json)) fail_webdav_reconcile('WebDAV-Runtime-Konfiguration konnte nicht serialisiert werden.');
{
$escaped = $db->real_escape_string($json); $escaped = $db->real_escape_string($json);
$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} LIMIT 1"); if (!$db->query("UPDATE `{$table}` SET 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) catch (Throwable $e)

View File

@@ -5,29 +5,44 @@ 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
if ! php "$SCRIPT_DIR/reconcile.php"; then read_config_value() {
echo "NC Connector: gespeicherte lokale Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2 local key="$1"
exit 1 local file="$2"
fi local value
value="$(sed -n "s/^${key}=//p" "$file" | tail -n 1)"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "$value"
}
if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then write_route_status() {
echo "NC Connector: WebDAV-Testverbindungen konnten nicht mit der parallelen Runtime abgeglichen werden." >&2 local route="$1"
exit 1 local label="$2"
fi local detail="$3"
local success="$4"
[[ -n "${ROUTE_STATUS_FILE:-}" ]] || return 0
mkdir -p -- "$(dirname -- "$ROUTE_STATUS_FILE")"
php -r '
$payload = array(
"timestamp" => time(),
"route" => (string)$argv[2],
"label" => (string)$argv[3],
"detail" => (string)$argv[4],
"fallback_used" => false,
"success" => $argv[5] === "1"
);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if (!is_string($json) || file_put_contents($argv[1], $json.PHP_EOL, LOCK_EX) === false) exit(1);
@chmod($argv[1], 0644);
' "$ROUTE_STATUS_FILE" "$route" "$label" "$detail" "$success"
}
# Nach dem Reconcile kennt Piwigo alle noch aktiven WebDAV-Verbindungen. Jetzt php "$SCRIPT_DIR/reconcile.php"
# werden Sites, Alben, Bilddatensaetze und Derivate geloeschter Verbindungen php "$SCRIPT_DIR/reconcile-webdav.php"
# entfernt. Das erfasst auch Verbindungen, die bereits vor diesem Update php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"
# geloescht wurden. php "$SCRIPT_DIR/cleanup-stale.php"
if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then
echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2
exit 1
fi
if ! php "$SCRIPT_DIR/cleanup-stale.php"; then
echo "NC Connector: verwaiste Laufzeitdateien konnten nicht bereinigt werden." >&2
exit 1
fi
configs=("$CONFIG_DIR"/connection-*.conf) configs=("$CONFIG_DIR"/connection-*.conf)
webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf) webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf)
@@ -37,37 +52,63 @@ if [[ ${#configs[@]} -eq 0 && ${#webdav_configs[@]} -eq 0 ]]; then
exit 0 exit 0
fi fi
result=0 route_piwigo_root=""
for candidate in "${webdav_configs[@]}" "${configs[@]}"; do
[[ -f "$candidate" ]] || continue
route_piwigo_root="$(read_config_value PIWIGO_ROOT "$candidate")"
[[ -n "$route_piwigo_root" ]] && break
done
[[ -n "$route_piwigo_root" ]] || route_piwigo_root="/var/www/piwigo"
ROUTE_STATUS_FILE="${route_piwigo_root%/}/_data/bratonien-tools/nc-connector-status/route-status.json"
failure_count=0
webdav_count=0
local_count=0
summary_parts=()
# WebDAV zuerst: Der parallele Shadow Tree muss bereits vollständig stehen,
# bevor der weiterhin produktive lokale Weg seine normale Piwigo-
# Dateisynchronisierung ausführt. So wird der neue Baum im selben Minutenlauf
# sichtbar, ohne dass der WebDAV-Zweig selbst eine zweite globale Piwigo-
# Synchronisierung startet.
for config in "${webdav_configs[@]}"; do for config in "${webdav_configs[@]}"; do
[[ -f "$config" ]] || continue
name="$(basename "$config")" name="$(basename "$config")"
echo "NC Connector WebDAV parallel: $name" connection_id="$(read_config_value CONNECTION_ID "$config")"
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then if [[ ! "$connection_id" =~ ^[0-9]+$ ]] || [[ "$connection_id" -lt 1 ]]; then
result=1 echo "NC Connector: $name besitzt keine gueltige Verbindungs-ID." >&2
failure_count=$((failure_count + 1))
summary_parts+=("$name: ungueltige Verbindungs-ID")
continue
fi
webdav_count=$((webdav_count + 1))
echo "NC Connector WebDAV #$connection_id: $name"
output=""
if output="$(env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh" 2>&1)"; then
[[ -z "$output" ]] || printf '%s\n' "$output"
summary_parts+=("WebDAV #$connection_id erfolgreich")
else
code=$?
[[ -z "$output" ]] || printf '%s\n' "$output" >&2
failure_count=$((failure_count + 1))
summary_parts+=("WebDAV #$connection_id fehlgeschlagen (Exit $code)")
fi fi
done done
for config in "${configs[@]}"; do for config in "${configs[@]}"; do
[[ -f "$config" ]] || continue
name="$(basename "$config")" name="$(basename "$config")"
connection_id="" connection_id="0"
if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then
connection_id="${BASH_REMATCH[1]}" connection_id="${BASH_REMATCH[1]}"
fi fi
if [[ "$connection_id" -lt 1 ]]; then
echo "NC Connector: $name besitzt keine gueltige Verbindungs-ID." >&2
failure_count=$((failure_count + 1))
summary_parts+=("$name: ungueltige Verbindungs-ID")
continue
fi
piwigo_root="$(sed -n 's/^PIWIGO_ROOT=//p' "$config" | tail -n 1)" piwigo_root="$(read_config_value PIWIGO_ROOT "$config")"
piwigo_root="${piwigo_root%\"}"
piwigo_root="${piwigo_root#\"}"
piwigo_root="${piwigo_root%\'}"
piwigo_root="${piwigo_root#\'}"
[[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo" [[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo"
tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status" tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status"
if [[ -f "$tombstone_dir/deleted-$connection_id" ]]; then
if [[ -n "$connection_id" && -f "$tombstone_dir/deleted-$connection_id" ]]; then
echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt." echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt."
rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \ rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \
"$CONFIG_DIR/connection-$connection_id.db-password" \ "$CONFIG_DIR/connection-$connection_id.db-password" \
@@ -78,10 +119,35 @@ for config in "${configs[@]}"; do
continue continue
fi fi
echo "NC Connector lokal: $name" local_count=$((local_count + 1))
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then echo "NC Connector Local #$connection_id: $name"
result=1 if env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
summary_parts+=("Local #$connection_id erfolgreich")
else
failure_count=$((failure_count + 1))
summary_parts+=("Local #$connection_id fehlgeschlagen")
fi fi
done done
exit "$result" summary_detail="$(IFS='; '; printf '%s' "${summary_parts[*]}")"
[[ -n "$summary_detail" ]] || summary_detail="Keine Verbindung wurde ausgefuehrt."
if [[ "$failure_count" -eq 0 ]]; then
if [[ "$webdav_count" -gt 0 && "$local_count" -gt 0 ]]; then
route="mixed"
label="WebDAV + Local"
elif [[ "$webdav_count" -gt 0 ]]; then
route="webdav"
label="WebDAV"
else
route="local"
label="Local"
fi
write_route_status "$route" "$label" "$summary_detail" "1"
echo "NC Connector: alle Verbindungen wurden erfolgreich verarbeitet."
exit 0
fi
write_route_status "failed" "FEHLER - mindestens eine Verbindung" "$summary_detail" "0"
echo "NC Connector: mindestens eine Verbindung ist fehlgeschlagen." >&2
exit 1

View File

@@ -59,10 +59,6 @@ for target in (status_file, public_file):
PY PY
} }
compact_output() {
tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
failure() { failure() {
local code="$1" command="$2" line="$3" local code="$1" command="$2" line="$3"
trap - ERR trap - ERR
@@ -94,28 +90,6 @@ python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--state "$SHADOW_MAP_FILE" --state "$SHADOW_MAP_FILE"
trap - ERR 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 if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
PIWIGO_OUTPUT="" PIWIGO_OUTPUT=""
@@ -133,7 +107,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PIWIGO_EXIT" DETAIL="Exit-Code: $PIWIGO_EXIT"
if [[ -n "$PIWIGO_OUTPUT" ]]; then if [[ -n "$PIWIGO_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)" DETAIL+="; Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')"
fi fi
if grep -qi 'Invalid username/password' <<<"$PIWIGO_OUTPUT"; then if grep -qi 'Invalid username/password' <<<"$PIWIGO_OUTPUT"; then
@@ -160,28 +134,21 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
exit "$PIWIGO_EXIT" exit "$PIWIGO_EXIT"
fi fi
DERIVATIVE_OUTPUT="" MEDIA_UNIT="bratonien-nc-media-${CONNECTION_ID}-$(date +%s)"
DERIVATIVE_EXIT=0 if ! systemd-run \
if DERIVATIVE_OUTPUT="$(php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \ --quiet \
--piwigo-root="$PIWIGO_ROOT" \ --collect \
--connection-id="$CONNECTION_ID" 2>&1)"; then --unit="$MEDIA_UNIT" \
DERIVATIVE_EXIT=0 --property=RuntimeMaxSec=30min \
else --setenv="PIWIGO_CONFIG=$CONFIG_FILE" \
DERIVATIVE_EXIT=$? /usr/bin/env bash "$SCRIPT_DIR/build-webdav-media.sh"; then
fi write_status error "Bildaufbereitung konnte nicht im Hintergrund gestartet werden"
[[ -z "$DERIVATIVE_OUTPUT" ]] || printf '%s\n' "$DERIVATIVE_OUTPUT" exit 1
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 fi
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \ write_status ok \
"WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" \ "WebDAV eingelesen und Piwigo synchronisiert; Bildaufbereitung läuft im Hintergrund" \
"" \ "" \
"api" \ "api" \
"ok" \ "ok" \
@@ -190,7 +157,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"Fallback wurde nicht benötigt" "Fallback wurde nicht benötigt"
elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \ write_status ok \
"WebDAV eingelesen, Piwigo über Fallback synchronisiert und Derivate erzeugt" \ "WebDAV eingelesen und Piwigo über Fallback synchronisiert; Bildaufbereitung läuft im Hintergrund" \
"" \ "" \
"fallback" \ "fallback" \
"not_used" \ "not_used" \
@@ -198,8 +165,8 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"ok" \ "ok" \
"Benutzername/Passwort-Fallback erfolgreich" "Benutzername/Passwort-Fallback erfolgreich"
else else
write_status ok "WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" write_status ok "WebDAV eingelesen und Piwigo synchronisiert; Bildaufbereitung läuft im Hintergrund"
fi fi
else else
write_status ok "WebDAV eingelesen und Vorschaubilder erzeugt; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync" write_status ok "WebDAV eingelesen; Piwigo-Synchronisierung ist für diese Verbindung deaktiviert"
fi fi

View File

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