Compare commits

...

33 Commits

Author SHA1 Message Date
Terranom674
ad90cd678b Technische NC-Anlage uebernimmt vorhandene API einmalig 2026-08-18 16:13:05 +02:00
Terranom674
1065651bee NC-Verbindungen immer loeschbar und Laufzeitfehler sichtbar machen 2026-08-18 16:12:19 +02:00
Terranom674
4e0c9007c3 Tombstone-Pfad pro NC-Verbindung aus Konfiguration bestimmen 2026-08-18 16:10:17 +02:00
Terranom674
1485acbd45 Fallback-Handler auf verbindungsspezifische Secrets umstellen 2026-08-18 16:10:03 +02:00
Terranom674
81a28a9282 Fallback-Aenderungen erhalten verbindungseigene API 2026-08-18 16:09:31 +02:00
Terranom674
201a44a890 Piwigo Sync verwendet API der jeweiligen Verbindung 2026-08-18 16:06:41 +02:00
Terranom674
a64d5b92c5 Verbindungspruefung migriert Authentifizierung auf Verbindungsumfang 2026-08-18 16:05:22 +02:00
Terranom674
14e4943102 Bestehende native Verbindungen auf eigenen Auth-Modus migrieren 2026-08-18 16:04:54 +02:00
Terranom674
b562309c6e Geloeschte NC-Verbindungen vor Laufzeitabruf entfernen 2026-08-18 16:03:40 +02:00
Terranom674
8fc0ae96d4 NC Connector Handler auf verbindungsspezifische Laufzeit umstellen 2026-08-18 16:03:19 +02:00
Terranom674
cbcaf2d575 NC Connector Laufzeit und Authentifizierung pro Verbindung kapseln 2026-08-18 16:02:53 +02:00
Terranom674
0fa5430ff6 NC Connector Authentifizierung pro Verbindung speichern 2026-08-18 16:02:23 +02:00
Terranom674
7d0d97d844 NC-Wizard sichtbare Fenster eindeutig benennen 2026-08-18 15:45:03 +02:00
Terranom674
3325efce31 Version 0.9.4.9 2026-08-18 15:42:00 +02:00
Terranom674
8b4f3193d3 NC-Wizard Flow-Handler registrieren 2026-08-18 15:41:37 +02:00
Terranom674
47ba20e247 NC-Wizard technischen Schrittfluss konsolidieren 2026-08-18 15:41:13 +02:00
Terranom674
e44cceaacc NC-Wizard sichtbare Fenster einzeln navigierbar machen 2026-08-18 15:40:34 +02:00
Terranom674
48822bc9e9 Alten NC-Wizard AJAX-Sonderweg entfernen 2026-08-18 15:30:45 +02:00
Terranom674
d638dcc590 Alten NC-Wizard JS-Sonderweg entfernen 2026-08-18 15:30:39 +02:00
Terranom674
d520209956 Bratonien Tools 0.9.4.8 serverseitiger NC-Wizard 2026-08-18 15:30:22 +02:00
Terranom674
08b33997f5 NC-Wizard Ordnerwahl und Navigation serverseitig rendern 2026-08-18 15:29:55 +02:00
Terranom674
56b18e650c NC-Wizard serverseitige Ordneraktionen registrieren 2026-08-18 15:28:29 +02:00
Terranom674
c3d40faa1e NC-Wizard Ordnerbrowser serverseitig konsolidieren 2026-08-18 15:28:05 +02:00
Terranom674
867d8e9804 Bratonien Tools 0.9.4.7 2026-08-18 15:05:22 +02:00
Terranom674
b891e06fe5 NC-Wizard sichere WebDAV-Ordnerauswahl und Navigation 2026-08-18 15:04:33 +02:00
Terranom674
5694659004 NC-Wizard Ordner per User-WebDAV laden 2026-08-18 15:03:34 +02:00
Terranom674
eeaac9caaa NC-Wizard User-Scope und Zurück-Navigation registrieren 2026-08-18 15:03:19 +02:00
Terranom674
0a7548414b NC-Wizard strikt auf angemeldeten Nextcloud-Nutzer begrenzen 2026-08-18 15:02:56 +02:00
Terranom674
f1a117818f NC-Wizard Verzeichnisdialog und Hinweise verfeinern 2026-08-18 14:56:44 +02:00
Terranom674
c2c8652557 Automatische Syntaxpruefung fuer PHP und Python einfuehren 2026-08-18 14:55:35 +02:00
Terranom674
ea3a65664d NC-Connector Verzeichnisfilter in Runtime schreiben 2026-08-18 14:54:47 +02:00
Terranom674
c85aa2e833 Bratonien Tools 0.9.4.6 Verzeichnisauswahl laden 2026-08-18 14:54:02 +02:00
Terranom674
f671e76170 NC-Wizard dynamische Verzeichnisauswahl hinzufuegen 2026-08-18 14:53:29 +02:00
12 changed files with 907 additions and 263 deletions

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

@@ -0,0 +1,32 @@
name: Syntax Check
on:
push:
pull_request:
jobs:
syntax:
runs-on: ubuntu-latest
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: PHP installieren
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
coverage: none
- name: PHP Syntax pruefen
shell: bash
run: |
set -euo pipefail
while IFS= read -r -d '' file; do
php -l "$file"
done < <(find . -type f -name '*.php' -print0)
- name: Python Syntax pruefen
shell: bash
run: |
set -euo pipefail
python3 -m compileall -q runtime

View File

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

View File

@@ -28,11 +28,23 @@ function bratonien_tools_nc_connector_create_local_api_first()
throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort muessen entweder beide angegeben oder beide leer gelassen werden.'); throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort muessen entweder beide angegeben oder beide leer gelassen werden.');
} }
$api = bratonien_tools_nc_api_credentials(); $wizard_auth_present = array_key_exists('nc_api_validated', $_POST);
$validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1'; $validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1';
if (($api['key_id'] ?? '') === '' && $fallback_user === '' && !$validated_pending_api) $api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ''));
$api_key_secret = trim((string)($_POST['nc_connection_api_key_secret'] ?? ''));
if (($api_key_id === '') !== ($api_key_secret === ''))
{ {
throw new RuntimeException('Es ist weder ein geprüfter Piwigo-API-Zugang noch ein Benutzername/Passwort-Fallback vorhanden.'); throw new RuntimeException('API-Schluessel-ID und API-Geheimnis muessen entweder beide vorhanden oder beide leer sein.');
}
if (($validated_pending_api || (!$wizard_auth_present && $fallback_user === '')) && $api_key_id === '')
{
$legacy_api = bratonien_tools_nc_api_credentials();
$api_key_id = trim((string)($legacy_api['key_id'] ?? ''));
$api_key_secret = trim((string)($legacy_api['key_secret'] ?? ''));
}
if ($api_key_id === '' && $fallback_user === '')
{
throw new RuntimeException('Fuer diese Verbindung ist weder ein eigener gepruefter API-Zugang noch ein Benutzername/Passwort-Fallback vorhanden.');
} }
$port = (int)($_POST['nc_port'] ?? 5432); $port = (int)($_POST['nc_port'] ?? 5432);
@@ -42,7 +54,7 @@ function bratonien_tools_nc_connector_create_local_api_first()
if (strpos($gallery_root, './') === 0) if (strpos($gallery_root, './') === 0)
{ {
$piwigo_root = realpath(PHPWG_ROOT_PATH); $piwigo_root = realpath(PHPWG_ROOT_PATH);
if ($piwigo_root === false) throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgelöst werden.'); if ($piwigo_root === false) throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgeloest werden.');
$gallery_root = rtrim($piwigo_root, '/').'/'.ltrim(substr($gallery_root, 2), '/'); $gallery_root = rtrim($piwigo_root, '/').'/'.ltrim(substr($gallery_root, 2), '/');
} }
if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.'); if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
@@ -60,7 +72,9 @@ function bratonien_tools_nc_connector_create_local_api_first()
'max_wait_seconds'=>max(60,(int)($_POST['nc_max_wait_seconds'] ?? 900)), 'max_wait_seconds'=>max(60,(int)($_POST['nc_max_wait_seconds'] ?? 900)),
'full_sync_seconds'=>max(300,(int)($_POST['nc_full_sync_seconds'] ?? 86400)), 'full_sync_seconds'=>max(300,(int)($_POST['nc_full_sync_seconds'] ?? 86400)),
'storages'=>bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''), 'storages'=>bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''),
'origin'=>'native','piwigo_auth'=>'api-first', 'origin'=>'native',
'piwigo_auth'=>'connection-scoped',
'api_enabled'=>$api_key_id !== '',
); );
foreach (array('nextcloud_url'=>'nc_nextcloud_url','showcase_user'=>'nc_showcase_user','nextcloud_access_user'=>'nc_access_user','nextcloud_product'=>'nc_product','nextcloud_version'=>'nc_version') as $config_key=>$post_key) foreach (array('nextcloud_url'=>'nc_nextcloud_url','showcase_user'=>'nc_showcase_user','nextcloud_access_user'=>'nc_access_user','nextcloud_product'=>'nc_product','nextcloud_version'=>'nc_version') as $config_key=>$post_key)
{ {
@@ -69,9 +83,19 @@ function bratonien_tools_nc_connector_create_local_api_first()
bratonien_tools_nc_connector_view_name($config['source_view']); bratonien_tools_nc_connector_view_name($config['source_view']);
bratonien_tools_nc_connector_view_name($config['activity_view']); bratonien_tools_nc_connector_view_name($config['activity_view']);
$secret_payload = json_encode(array(
'v'=>2,
'db_password'=>(string)$_POST['nc_db_password'],
'piwigo_user'=>$fallback_user,
'piwigo_password'=>$fallback_password,
'api_key_id'=>$api_key_id,
'api_key_secret'=>$api_key_secret,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($secret_payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
$table=bratonien_tools_nc_connector_table(); bratonien_tools_nc_connector_ensure_table(); $now=date('Y-m-d H:i:s'); $table=bratonien_tools_nc_connector_table(); bratonien_tools_nc_connector_ensure_table(); $now=date('Y-m-d H:i:s');
$connection_key='local-'.bin2hex(random_bytes(12)); $connection_key='local-'.bin2hex(random_bytes(12));
$secret_blob=bratonien_tools_nc_connector_encrypt_credentials((string)$_POST['nc_db_password'],$fallback_user,$fallback_password);
$config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); $config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
if(!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.'); if(!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
@@ -90,6 +114,6 @@ function bratonien_tools_nc_connector_create_local_api_first()
return array( return array(
'connection_id'=>$id, 'connection_id'=>$id,
'message'=>$fallback_user==='' ? 'Nextcloud-Verbindung wurde API-first ohne dauerhaft gespeicherten Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.' : 'Nextcloud-Verbindung wurde API-first mit verschluesseltem Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.' 'message'=>'Nextcloud-Verbindung wurde mit verbindungseigener Piwigo-Authentifizierung angelegt.'
); );
} }

View File

@@ -0,0 +1,26 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_save_technical_flow()
{
$result = bratonien_tools_nc_wizard_save_technical_with_known_database();
$state = bratonien_tools_nc_wizard_state();
if ((string)($state['technical_stage'] ?? '') === 'mounts')
{
if (!empty($state['directory_selection_ready']))
{
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
}
else
{
$state['mount_prompted'] = true;
}
bratonien_tools_nc_wizard_store($state);
}
return $result;
}

View File

@@ -0,0 +1,367 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_webdav_list(array $state, $path = '')
{
if (empty($state['scan_ok']) || trim((string)$state['base_url']) === '' || trim((string)$state['username']) === '' || (string)$state['_password'] === '')
{
throw new RuntimeException('Die Nextcloud-Sitzung des Assistenten ist nicht vollständig.');
}
if (!function_exists('curl_init')) throw new RuntimeException('cURL ist für die Verzeichnisauswahl nicht verfügbar.');
$path = trim((string)$path, '/');
if ($path !== '' && preg_match('#(^|/)\.\.(/|$)#', $path)) throw new RuntimeException('Ungültiger Verzeichnispfad.');
$segments = $path === '' ? array() : array_map('rawurlencode', explode('/', $path));
$user = rawurlencode((string)$state['username']);
$url = rtrim((string)$state['base_url'], '/').'/remote.php/dav/files/'.$user.'/'.implode('/', $segments);
if (substr($url, -1) !== '/') $url .= '/';
$body = '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:displayname/></d:prop></d:propfind>';
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_CUSTOMREQUEST=>'PROPFIND',
CURLOPT_POSTFIELDS=>$body,
CURLOPT_HTTPHEADER=>array('Depth: 1','Content-Type: application/xml; charset=utf-8'),
CURLOPT_HTTPAUTH=>CURLAUTH_BASIC,
CURLOPT_USERPWD=>(string)$state['username'].':'.(string)$state['_password'],
CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>20,
));
$response = curl_exec($ch);
$errno = curl_errno($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $errno !== 0) throw new RuntimeException('Nextcloud-Verzeichnisse konnten nicht geladen werden.');
if ($status === 401 || $status === 403) throw new RuntimeException('Nextcloud hat den Zugriff auf dieses Verzeichnis abgelehnt.');
if ($status !== 207) throw new RuntimeException('Nextcloud-Verzeichnisabfrage antwortete mit HTTP '.$status.'.');
libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$response);
if ($xml === false) throw new RuntimeException('Nextcloud hat eine ungültige WebDAV-Antwort geliefert.');
$xml->registerXPathNamespace('d', 'DAV:');
$children = array();
foreach ($xml->xpath('//d:response') as $item)
{
$item->registerXPathNamespace('d', 'DAV:');
$hrefs = $item->xpath('d:href');
$collections = $item->xpath('d:propstat/d:prop/d:resourcetype/d:collection');
if (!$hrefs || !$collections) continue;
$href = rawurldecode((string)$hrefs[0]);
$href_path = (string)parse_url($href, PHP_URL_PATH);
$base_path = (string)parse_url($url, PHP_URL_PATH);
if (rtrim($href_path, '/') === rtrim($base_path, '/')) continue;
$name = basename(rtrim($href_path, '/'));
if ($name === '') continue;
$child_path = $path === '' ? $name : $path.'/'.$name;
$children[$child_path] = $name;
}
natcasesort($children);
$parent = '';
if ($path !== '')
{
$parts = explode('/', $path);
array_pop($parts);
$parent = implode('/', $parts);
}
return array('current'=>$path, 'parent'=>$parent, 'children'=>$children);
}
function bratonien_tools_nc_wizard_refresh_directory_state(array &$state, $path = null)
{
if ($path === null) $path = (string)($state['directory_path'] ?? '');
$listing = bratonien_tools_nc_wizard_webdav_list($state, $path);
$state['directory_path'] = (string)$listing['current'];
$state['directory_parent'] = (string)$listing['parent'];
$state['directory_children'] = (array)$listing['children'];
if (!isset($state['directory_selected']) || !is_array($state['directory_selected'])) $state['directory_selected'] = array();
}
function bratonien_tools_nc_wizard_scan_user_scoped()
{
$state = bratonien_tools_nc_wizard_state();
$host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input']));
$username = trim((string)($_POST['nc_wizard_user'] ?? $state['username']));
$password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password'];
$state['step'] = 1;
$state['host_input'] = $host_input;
$state['username'] = $username;
$state['_password'] = $password;
bratonien_tools_nc_wizard_store($state);
if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
$base_url = '';
$status_data = null;
foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
{
try
{
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
$base_url = $candidate_url;
$status_data = $candidate_status;
break;
}
catch (Throwable $ignored) {}
}
if ($base_url === '' || !is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
$user_response = bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/user?format=json', $username, $password, array('OCS-APIRequest: true'));
if ($user_response['status'] === 401 || $user_response['status'] === 403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
if ($user_response['status'] < 200 || $user_response['status'] >= 300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$resolved_username = trim((string)($user_data['id'] ?? $username));
if ($resolved_username === '') $resolved_username = $username;
$url_host = (string)parse_url($base_url, PHP_URL_HOST);
$state = array_merge($state, array(
'step'=>2,'scan_ok'=>true,'base_url'=>$base_url,'host_input'=>$host_input,'username'=>$resolved_username,
'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''),
'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''),'product'=>(string)($status_data['productname'] ?? 'Nextcloud'),
'users'=>array(),'can_list_users'=>false,'showcase_user'=>$resolved_username,'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud',
'scan_message'=>'Nextcloud wurde erkannt und der Benutzerzugriff wurde bestätigt.','_password'=>$password,
'api_status'=>'pending','api_username'=>'','api_error'=>'','db_host'=>strtolower($url_host),'db_port'=>'5432','db_database'=>'nextcloud','db_user'=>'','db_password_set'=>false,'_db_password'=>'',
'source_view'=>'piwigo_showcase_sources','activity_view'=>'piwigo_showcase_activity','gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud',
'storages'=>array(),'storage_candidates'=>array(),'technical_stage'=>'auto_check','technical_source'=>'Automatische Prüfung','technical_error'=>'','technical_complete'=>false,
'directory_selection_ready'=>false,'directory_path'=>'','directory_parent'=>'','directory_children'=>array(),'directory_selected'=>array(),
'database_prompted'=>false,'mount_prompted'=>false,
));
if (!bratonien_tools_nc_wizard_apply_known_database_profile($state))
{
$state['technical_stage'] = 'database_details';
$state['database_prompted'] = true;
$state['technical_source'] = 'Keine bekannte Reader-Verbindung gefunden';
$state['technical_error'] = 'Für diese Nextcloud ist noch keine bekannte Datenbank-Reader-Verbindung gespeichert.';
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud wurde gefunden. Für den Datenzugriff wird eine separate Reader-Verbindung benötigt.');
}
$known_storages = isset($state['storages']) && is_array($state['storages']) ? $state['storages'] : array();
try
{
bratonien_tools_nc_wizard_finish_data_access_for_selection($state, $known_storages);
if (!empty($state['directory_selection_ready']))
{
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
}
else
{
$state['mount_prompted'] = true;
}
}
catch (Throwable $e)
{
$state['technical_complete'] = false;
$state['technical_stage'] = 'database_details';
$state['database_prompted'] = true;
$state['technical_error'] = $e->getMessage();
}
bratonien_tools_nc_wizard_store($state);
if ($state['technical_stage'] === 'database_details') return array('message'=>'Nextcloud wurde gefunden. Die bekannte Reader-Verbindung konnte noch nicht vollständig bestätigt werden.');
if (empty($state['directory_selection_ready'])) return array('message'=>'Nextcloud und Datenzugriff wurden bestätigt. Der technische Speicherort muss einmal bestätigt werden.');
return array('message'=>'Nextcloud und Datenzugriff wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers gewählt werden.');
}
function bratonien_tools_nc_wizard_directory_browse()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts' || empty($state['directory_selection_ready'])) throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
$path = trim((string)($_POST['nc_wizard_directory_path'] ?? ''), '/');
bratonien_tools_nc_wizard_refresh_directory_state($state, $path);
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Verzeichnis geöffnet.');
}
function bratonien_tools_nc_wizard_directory_add()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts' || empty($state['directory_selection_ready'])) throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
$path = trim((string)($state['directory_path'] ?? ''), '/');
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) ? $state['directory_selected'] : array();
if (!in_array($path, $selected, true)) $selected[] = $path;
$state['directory_selected'] = array_values($selected);
bratonien_tools_nc_wizard_store($state);
return array('message'=>$path === '' ? 'Stammverzeichnis ausgewählt.' : 'Verzeichnis hinzugefügt.');
}
function bratonien_tools_nc_wizard_directory_remove()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || empty($state['directory_selection_ready'])) throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
$path = trim((string)($_POST['nc_wizard_directory_remove'] ?? ''), '/');
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) ? $state['directory_selected'] : array();
$state['directory_selected'] = array_values(array_filter($selected, function($value) use ($path) { return (string)$value !== $path; }));
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Verzeichnis entfernt.');
}
function bratonien_tools_nc_wizard_save_mounts_server_side()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts') throw new RuntimeException('Die Speicher-/Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
$candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array();
if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte erkannt.');
$mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array();
// Erstes sichtbares Fenster: technische Mount-Zuordnung bestätigen.
if (empty($state['directory_selection_ready']))
{
foreach ($candidates as $index=>&$candidate)
{
$mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/');
if ($mount === '') $mount = rtrim(trim((string)($mounts[$index] ?? '')), '/');
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount);
$candidate['local_mount'] = $mount;
}
unset($candidate);
$state['storage_candidates'] = array_values($candidates);
$state['mount_prompted'] = true;
$state['directory_selection_ready'] = true;
$state['technical_complete'] = false;
$state['technical_stage'] = 'mounts';
$state['directory_path'] = '';
$state['directory_parent'] = '';
$state['directory_children'] = array();
$state['directory_selected'] = array();
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Speicherzuordnung wurde bestätigt. Jetzt können die Verzeichnisse ausgewählt werden.');
}
// Zweites sichtbares Fenster: Verzeichnisauswahl übernehmen.
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) ? $state['directory_selected'] : array();
if (!$selected) $selected = array('');
$storages = array();
foreach ($candidates as $candidate)
{
$mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/');
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount);
foreach ($selected as $directory)
{
$directory = trim((string)$directory, '/');
if ($directory !== '' && preg_match('#(^|/)\.\.(/|$)#', $directory)) throw new RuntimeException('Ungültige Verzeichnisauswahl.');
$storages[] = array(
'storage_id'=>(string)($candidate['storage_id'] ?? ''),
'source_prefix'=>trim((string)($candidate['source_prefix'] ?? ''), '/'),
'local_mount'=>$mount,
'include_prefix'=>$directory,
);
}
}
$state['storages'] = $storages;
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['technical_error'] = '';
$state['directory_selection_ready'] = false;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Verzeichnisauswahl wurde übernommen.');
}
function bratonien_tools_nc_wizard_select_current_user()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
if (empty($state['technical_complete'])) throw new RuntimeException('Die Verbindung ist noch nicht vollständig geprüft.');
$connection_name = trim((string)($_POST['nc_wizard_connection_name'] ?? $state['connection_name']));
if ($connection_name === '') throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.');
$state['connection_name'] = $connection_name;
$state['showcase_user'] = (string)$state['username'];
$state['step'] = 3;
$state['api_error'] = '';
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud-Benutzer übernommen. Jetzt folgt der Piwigo-API-Zugang.');
}
function bratonien_tools_nc_wizard_back()
{
$state = bratonien_tools_nc_wizard_state();
$step = (int)($state['step'] ?? 1);
if ($step <= 1) return array('message'=>'Bereits am Anfang des Assistenten.');
if ($step === 4)
{
// Abschluss -> API
$state['step'] = 3;
}
elseif ($step === 3)
{
// API -> Verbindungsname
$state['step'] = 2;
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['directory_selection_ready'] = false;
}
elseif (!empty($state['technical_complete']))
{
// Verbindungsname -> Verzeichnisauswahl
$state['technical_complete'] = false;
$state['technical_stage'] = 'mounts';
$state['directory_selection_ready'] = true;
bratonien_tools_nc_wizard_refresh_directory_state($state);
}
elseif ((string)($state['technical_stage'] ?? '') === 'mounts' && !empty($state['directory_selection_ready']))
{
// Verzeichnisauswahl -> vorher tatsächlich sichtbares Fenster.
if (!empty($state['mount_prompted']))
{
$state['directory_selection_ready'] = false;
}
elseif (!empty($state['database_prompted']))
{
$state['technical_stage'] = 'database_details';
$state['directory_selection_ready'] = false;
}
else
{
$state['step'] = 1;
}
}
elseif ((string)($state['technical_stage'] ?? '') === 'mounts')
{
// Mount-Zuordnung -> Datenbankfenster, falls es sichtbar war, sonst Anmeldung.
if (!empty($state['database_prompted']))
{
$state['technical_stage'] = 'database_details';
}
else
{
$state['step'] = 1;
}
}
elseif ((string)($state['technical_stage'] ?? '') === 'database_details')
{
// Datenbankfenster -> Anmeldung.
$state['step'] = 1;
}
else
{
$state['step'] = 1;
}
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Ein Fenster zurück.');
}

View File

@@ -23,6 +23,9 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_db_bridge.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_db_bridge.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_user_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_flow.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_connection_scope.inc.php');
function bratonien_tools_get_tools() function bratonien_tools_get_tools()
{ {
@@ -48,25 +51,29 @@ function bratonien_tools_get_tools()
'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'), 'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'),
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'), 'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'),
'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'), 'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'),
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete'), 'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_any'),
'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'), 'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'), 'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_with_known_database'), 'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_user_scoped'),
'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_with_known_database'), 'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'),
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_mounts_with_directories'), 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
'nc_connector_wizard_select_user' => array('handler' => 'bratonien_tools_nc_wizard_select_user'), 'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_mounts_server_side'),
'nc_connector_wizard_select_user' => array('handler' => 'bratonien_tools_nc_wizard_select_current_user'),
'nc_connector_wizard_api_test' => array('handler' => 'bratonien_tools_nc_wizard_api_test'), 'nc_connector_wizard_api_test' => array('handler' => 'bratonien_tools_nc_wizard_api_test'),
'nc_connector_wizard_api_skip' => array('handler' => 'bratonien_tools_nc_wizard_api_skip'), 'nc_connector_wizard_api_skip' => array('handler' => 'bratonien_tools_nc_wizard_api_skip'),
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_with_directories'), 'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_connection_scoped'),
'nc_connector_wizard_back' => array('handler' => 'bratonien_tools_nc_wizard_back'),
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'), 'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'),
'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'), 'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'),
'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_managed'), 'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'),
'nc_connector_prepare_takeover' => array('handler' => 'bratonien_tools_nc_connector_prepare_takeover'), 'nc_connector_prepare_takeover' => array('handler' => 'bratonien_tools_nc_connector_prepare_takeover'),
'nc_connector_cancel_takeover' => array('handler' => 'bratonien_tools_nc_connector_cancel_takeover'), 'nc_connector_cancel_takeover' => array('handler' => 'bratonien_tools_nc_connector_cancel_takeover'),
'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'), 'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'),
'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'), 'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'),
'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save'), 'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'),
'nc_connector_fallback_delete' => array('handler' => 'bratonien_tools_nc_connector_fallback_delete'), 'nc_connector_fallback_delete' => array('handler' => 'bratonien_tools_nc_connector_fallback_delete_scoped'),
'nc_connector_fallback_once' => array('handler' => 'bratonien_tools_nc_connector_fallback_once'), 'nc_connector_fallback_once' => array('handler' => 'bratonien_tools_nc_connector_fallback_once'),
'self_update_check' => array('handler' => 'bratonien_tools_self_update_check'), 'self_update_check' => array('handler' => 'bratonien_tools_self_update_check'),
'self_update_run' => array('handler' => 'bratonien_tools_self_update_run'), 'self_update_run' => array('handler' => 'bratonien_tools_self_update_run'),

View File

@@ -1,7 +1,7 @@
<?php <?php
/* /*
Plugin Name: Bratonien Tools Plugin Name: Bratonien Tools
Version: 0.9.4.5 Version: 0.9.4.9
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

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

View File

@@ -1,44 +0,0 @@
<?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('state'=>'unavailable','message'=>'Bratonien Tools ist nicht aktiv.'));
exit;
}
if (!function_exists('is_admin') || !is_admin())
{
http_response_code(403);
echo json_encode(array('state'=>'forbidden','message'=>'Administratorrechte erforderlich.'));
exit;
}
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector_wizard.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector_wizard_db_bridge.inc.php');
try
{
$state = bratonien_tools_nc_wizard_state();
echo json_encode(array(
'state'=>'ok',
'ready'=>!empty($state['directory_selection_ready']),
'storages'=>bratonien_tools_nc_wizard_directory_options($state),
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
catch (Throwable $e)
{
http_response_code(500);
echo json_encode(array('state'=>'error','message'=>$e->getMessage()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

View File

@@ -14,19 +14,13 @@ function fail_sync($message)
function http_error_detail($body) function http_error_detail($body)
{ {
$body = trim((string)$body); $body = trim((string)$body);
if ($body === '') if ($body === '') return '';
{
return '';
}
$decoded = json_decode($body, true); $decoded = json_decode($body, true);
if (is_array($decoded)) if (is_array($decoded))
{ {
$detail = (string)($decoded['message'] ?? $decoded['err'] ?? ''); $detail = (string)($decoded['message'] ?? $decoded['err'] ?? '');
if ($detail !== '') if ($detail !== '') return $detail;
{
return $detail;
}
} }
if (function_exists('simplexml_load_string')) if (function_exists('simplexml_load_string'))
@@ -38,19 +32,12 @@ function http_error_detail($body)
if ($xml !== false) if ($xml !== false)
{ {
$detail = trim((string)($xml->message ?? $xml->err ?? '')); $detail = trim((string)($xml->message ?? $xml->err ?? ''));
if ($detail !== '') if ($detail !== '') return $detail;
{
return $detail;
}
} }
} }
$plain = trim(preg_replace('/\s+/', ' ', strip_tags($body))); $plain = trim(preg_replace('/\s+/', ' ', strip_tags($body)));
if ($plain === '') return $plain === '' ? '' : mb_substr($plain, 0, 500);
{
return '';
}
return mb_substr($plain, 0, 500);
} }
function http_request($url, array $fields, array $headers = array(), $cookie_file = null) function http_request($url, array $fields, array $headers = array(), $cookie_file = null)
@@ -65,10 +52,7 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
CURLOPT_FOLLOWLOCATION => false, CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-NC-Connector', CURLOPT_USERAGENT => 'Bratonien-NC-Connector',
); );
if ($headers) if ($headers) $options[CURLOPT_HTTPHEADER] = $headers;
{
$options[CURLOPT_HTTPHEADER] = $headers;
}
if ($cookie_file !== null) if ($cookie_file !== null)
{ {
$options[CURLOPT_COOKIEJAR] = $cookie_file; $options[CURLOPT_COOKIEJAR] = $cookie_file;
@@ -81,42 +65,29 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
if ($body === false || $errno !== 0) if ($body === false || $errno !== 0) fail_sync('HTTP-Aufruf fehlgeschlagen: '.$error);
{
fail_sync('HTTP-Aufruf fehlgeschlagen: '.$error);
}
if ($http < 200 || $http >= 300) if ($http < 200 || $http >= 300)
{ {
$detail = http_error_detail((string)$body); $detail = http_error_detail((string)$body);
fail_sync('HTTP-Aufruf antwortete mit Status '.$http.($detail !== '' ? ': '.$detail : '.')); fail_sync('HTTP-Aufruf antwortete mit Status '.$http.($detail !== '' ? ': '.$detail : '.'));
} }
return (string)$body; return (string)$body;
} }
function xml_value_sync(SimpleXMLElement $node) function xml_value_sync(SimpleXMLElement $node)
{ {
$children = $node->children(); $children = $node->children();
if (count($children) === 0) if (count($children) === 0) return (string)$node;
{
return (string)$node;
}
$result = array(); $result = array();
foreach ($children as $name => $child) foreach ($children as $name => $child)
{ {
$value = xml_value_sync($child); $value = xml_value_sync($child);
if (array_key_exists($name, $result)) if (array_key_exists($name, $result))
{ {
if (!is_array($result[$name]) || !array_is_list($result[$name])) if (!is_array($result[$name]) || !array_is_list($result[$name])) $result[$name] = array($result[$name]);
{
$result[$name] = array($result[$name]);
}
$result[$name][] = $value; $result[$name][] = $value;
} }
else else $result[$name] = $value;
{
$result[$name] = $value;
}
} }
return $result; return $result;
} }
@@ -126,34 +97,22 @@ function decode_ws($body)
$decoded = json_decode((string)$body, true); $decoded = json_decode((string)$body, true);
if (!is_array($decoded)) if (!is_array($decoded))
{ {
if (!function_exists('simplexml_load_string')) if (!function_exists('simplexml_load_string')) fail_sync('Piwigo lieferte XML, aber SimpleXML ist nicht verfuegbar.');
{
fail_sync('Piwigo lieferte XML, aber SimpleXML ist nicht verfuegbar.');
}
$previous = libxml_use_internal_errors(true); $previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$body); $xml = simplexml_load_string((string)$body);
libxml_clear_errors(); libxml_clear_errors();
libxml_use_internal_errors($previous); libxml_use_internal_errors($previous);
if ($xml === false || $xml->getName() !== 'rsp') if ($xml === false || $xml->getName() !== 'rsp') fail_sync('Piwigo lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.');
{
fail_sync('Piwigo lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.');
}
$decoded = array('stat'=>(string)$xml['stat']); $decoded = array('stat'=>(string)$xml['stat']);
foreach ($xml->children() as $name => $child) foreach ($xml->children() as $name => $child)
{ {
$value = xml_value_sync($child); $value = xml_value_sync($child);
if (array_key_exists($name, $decoded)) if (array_key_exists($name, $decoded))
{ {
if (!is_array($decoded[$name]) || !array_is_list($decoded[$name])) if (!is_array($decoded[$name]) || !array_is_list($decoded[$name])) $decoded[$name] = array($decoded[$name]);
{
$decoded[$name] = array($decoded[$name]);
}
$decoded[$name][] = $value; $decoded[$name][] = $value;
} }
else else $decoded[$name] = $value;
{
$decoded[$name] = $value;
}
} }
} }
if (($decoded['stat'] ?? '') !== 'ok') if (($decoded['stat'] ?? '') !== 'ok')
@@ -161,36 +120,22 @@ function decode_ws($body)
$message = (string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.'); $message = (string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.');
fail_sync($message); fail_sync($message);
} }
if (array_key_exists('result', $decoded)) return $decoded['result'];
if (array_key_exists('result', $decoded))
{
return $decoded['result'];
}
unset($decoded['stat']); unset($decoded['stat']);
return $decoded; return $decoded;
} }
function decrypt_blob($blob, $hex_key) function decrypt_blob($blob, $hex_key)
{ {
if (!preg_match('/^[a-f0-9]{64}$/', (string)$hex_key)) if (!preg_match('/^[a-f0-9]{64}$/', (string)$hex_key)) fail_sync('Connector-Schluessel ist ungueltig.');
{
fail_sync('Connector-Schluessel ist ungueltig.');
}
$outer = base64_decode(trim((string)$blob), true); $outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null; $payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) fail_sync('Gespeicherte Zugangsdaten haben ein unbekanntes Format.');
{
fail_sync('Gespeicherte Zugangsdaten haben ein unbekanntes Format.');
}
$iv = base64_decode((string)($payload['iv'] ?? ''), true); $iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true); $tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true); $cipher = base64_decode((string)($payload['data'] ?? ''), true);
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag); $plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) if ($plain === false) fail_sync('Gespeicherte Zugangsdaten konnten nicht entschluesselt werden.');
{
fail_sync('Gespeicherte Zugangsdaten konnten nicht entschluesselt werden.');
}
return (string)$plain; return (string)$plain;
} }
@@ -200,77 +145,64 @@ try
$piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/'); $piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/');
$connection_id = (int)($options['connection-id'] ?? 0); $connection_id = (int)($options['connection-id'] ?? 0);
$base_url = rtrim((string)($options['base-url'] ?? 'http://127.0.0.1'), '/'); $base_url = rtrim((string)($options['base-url'] ?? 'http://127.0.0.1'), '/');
if ($piwigo_root === '' || $connection_id < 1) fail_sync('Parameter --piwigo-root und --connection-id werden benoetigt.');
if ($piwigo_root === '' || $connection_id < 1) if (!function_exists('curl_init')) fail_sync('PHP-cURL ist nicht verfuegbar.');
{
fail_sync('Parameter --piwigo-root und --connection-id werden benoetigt.');
}
if (!function_exists('curl_init'))
{
fail_sync('PHP-cURL ist nicht verfuegbar.');
}
$db_config = $piwigo_root.'/local/config/database.inc.php'; $db_config = $piwigo_root.'/local/config/database.inc.php';
if (!is_readable($db_config)) if (!is_readable($db_config)) fail_sync('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
{
fail_sync('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
}
$conf = array(); $conf = array();
$prefixeTable = 'piwigo_'; $prefixeTable = 'piwigo_';
require $db_config; require $db_config;
foreach (array('db_host','db_user','db_password','db_base') as $key) foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail_sync('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
{
if (!isset($conf[$key]))
{
fail_sync('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
}
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) if ($db->connect_errno) fail_sync('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
{
fail_sync('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
}
$db->set_charset('utf8mb4'); $db->set_charset('utf8mb4');
$key_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1"); $key_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$key_result || !$key_result->num_rows) if (!$key_result || !$key_result->num_rows) fail_sync('Connector-Schluessel wurde nicht gefunden.');
{
fail_sync('Connector-Schluessel wurde nicht gefunden.');
}
$hex_key = (string)$key_result->fetch_assoc()['value']; $hex_key = (string)$key_result->fetch_assoc()['value'];
$connection_table = $prefixeTable.'bratonien_tools_nc_connections';
$connection_result = $db->query('SELECT config_json, secret_blob FROM `'.$connection_table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!$connection_result || !$connection_result->num_rows) fail_sync('Connector-Verbindung #'.$connection_id.' wurde nicht gefunden.');
$connection_row = $connection_result->fetch_assoc();
$connection_config = json_decode((string)$connection_row['config_json'], true);
if (!is_array($connection_config)) $connection_config = array();
$connection_plain = decrypt_blob((string)$connection_row['secret_blob'], $hex_key);
$connection_credentials = json_decode($connection_plain, true);
if (!is_array($connection_credentials)) $connection_credentials = array();
$api = array('key_id'=>'', 'key_secret'=>''); $api = array('key_id'=>'', 'key_secret'=>'');
$api_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1"); $connection_scoped = (string)($connection_config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $connection_config);
if ($api_result && $api_result->num_rows) if ($connection_scoped)
{ {
$api_blob = (string)$api_result->fetch_assoc()['value']; if (!empty($connection_config['api_enabled']))
if ($api_blob !== '')
{ {
$api_plain = decrypt_blob($api_blob, $hex_key); $api['key_id'] = trim((string)($connection_credentials['api_key_id'] ?? ''));
$api_decoded = json_decode($api_plain, true); $api['key_secret'] = trim((string)($connection_credentials['api_key_secret'] ?? ''));
if (is_array($api_decoded)) }
}
else
{
// Nur fuer bereits aktive Altverbindungen: bisheriger globaler API-Zugang.
$api_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1");
if ($api_result && $api_result->num_rows)
{
$api_blob = (string)$api_result->fetch_assoc()['value'];
if ($api_blob !== '')
{ {
$api['key_id'] = (string)($api_decoded['key_id'] ?? ''); $api_plain = decrypt_blob($api_blob, $hex_key);
$api['key_secret'] = (string)($api_decoded['key_secret'] ?? ''); $api_decoded = json_decode($api_plain, true);
if (is_array($api_decoded))
{
$api['key_id'] = (string)($api_decoded['key_id'] ?? '');
$api['key_secret'] = (string)($api_decoded['key_secret'] ?? '');
}
} }
} }
} }
$connection_table = $prefixeTable.'bratonien_tools_nc_connections';
$connection_result = $db->query('SELECT secret_blob FROM `'.$connection_table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!$connection_result || !$connection_result->num_rows)
{
fail_sync('Connector-Verbindung #'.$connection_id.' wurde nicht gefunden.');
}
$connection_blob = (string)$connection_result->fetch_assoc()['secret_blob'];
$connection_plain = decrypt_blob($connection_blob, $hex_key);
$connection_credentials = json_decode($connection_plain, true);
if (!is_array($connection_credentials))
{
$connection_credentials = array();
}
$fallback_user = (string)($connection_credentials['piwigo_user'] ?? ''); $fallback_user = (string)($connection_credentials['piwigo_user'] ?? '');
$fallback_password = (string)($connection_credentials['piwigo_password'] ?? ''); $fallback_password = (string)($connection_credentials['piwigo_password'] ?? '');
@@ -284,16 +216,8 @@ try
'Accept: application/json, text/xml;q=0.9', 'Accept: application/json, text/xml;q=0.9',
'Content-Type: application/x-www-form-urlencoded', 'Content-Type: application/x-www-form-urlencoded',
); );
decode_ws(http_request( decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncProductive', 'site_id'=>1), $headers));
$base_url.'/ws.php?format=json', $orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), $headers));
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>1),
$headers
));
$orphan = decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0),
$headers
));
$added = (int)($orphan['added_orphans'] ?? 0); $added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Synchronisierung per API erfolgreich\n"; echo "Piwigo-Synchronisierung per API erfolgreich\n";
@@ -308,55 +232,27 @@ try
} }
else else
{ {
$api_error = 'Keine gespeicherten API-Zugangsdaten.'; $api_error = $connection_scoped ? 'Fuer diese Verbindung ist keine API konfiguriert.' : 'Keine gespeicherten API-Zugangsdaten.';
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n"); fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
} }
if ($fallback_user === '' || $fallback_password === '') if ($fallback_user === '' || $fallback_password === '') fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback fuer diese Verbindung vorhanden.');
{
fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback vorhanden.');
}
$cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-sync-'); $cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-sync-');
if ($cookie_file === false) if ($cookie_file === false) fail_sync('Temporare Piwigo-Sitzung konnte nicht angelegt werden.');
{
fail_sync('Temporare Piwigo-Sitzung konnte nicht angelegt werden.');
}
try try
{ {
decode_ws(http_request( decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password), array(), $cookie_file));
$base_url.'/ws.php?format=json',
array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password),
array(),
$cookie_file
));
http_request( http_request(
$base_url.'/admin.php?page=site_update&site=1', $base_url.'/admin.php?page=site_update&site=1',
array( array('sync'=>'files','display_info'=>1,'privacy_level'=>0,'sync_meta'=>1,'simulate'=>0,'subcats-included'=>1,'bratonien_connector'=>1,'submit'=>1),
'sync'=>'files',
'display_info'=>1,
'privacy_level'=>0,
'sync_meta'=>1,
'simulate'=>0,
'subcats-included'=>1,
'bratonien_connector'=>1,
'submit'=>1,
),
array(), array(),
$cookie_file $cookie_file
); );
$orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), array(), $cookie_file));
$orphan = decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0),
array(),
$cookie_file
));
$added = (int)($orphan['added_orphans'] ?? 0); $added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich\n"; echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n"; echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
} }

View File

@@ -13,7 +13,31 @@ fi
result=0 result=0
for config in "${configs[@]}"; do for config in "${configs[@]}"; do
echo "NC Connector: $(basename "$config")" name="$(basename "$config")"
connection_id=""
if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then
connection_id="${BASH_REMATCH[1]}"
fi
piwigo_root="$(sed -n 's/^PIWIGO_ROOT=//p' "$config" | tail -n 1)"
piwigo_root="${piwigo_root%\"}"
piwigo_root="${piwigo_root#\"}"
piwigo_root="${piwigo_root%\'}"
piwigo_root="${piwigo_root#\'}"
[[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo"
tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status"
if [[ -n "$connection_id" && -f "$tombstone_dir/deleted-$connection_id" ]]; then
echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt."
rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \
"$CONFIG_DIR/connection-$connection_id.db-password" \
"$CONFIG_DIR/connection-$connection_id.piwigo-password" \
"$CONFIG_DIR/connection-$connection_id.storages.tsv"
rm -f -- "$tombstone_dir/deleted-$connection_id"
continue
fi
echo "NC Connector: $name"
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
result=1 result=1
fi fi

View File

@@ -32,7 +32,28 @@
<dialog id="bratonien-nc-wizard-dialog" style="width:min(980px,calc(100vw - 3rem));max-height:88vh;overflow:auto;background:#444;color:inherit;border:1px solid #777;border-radius:4px;padding:0;box-shadow:0 18px 60px rgba(0,0,0,.55)"> <dialog id="bratonien-nc-wizard-dialog" style="width:min(980px,calc(100vw - 3rem));max-height:88vh;overflow:auto;background:#444;color:inherit;border:1px solid #777;border-radius:4px;padding:0;box-shadow:0 18px 60px rgba(0,0,0,.55)">
<div style="padding:1.25rem 1.5rem"> <div style="padding:1.25rem 1.5rem">
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem"> <div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem">
<div><h4 style="margin:0">Neue Verbindung</h4><p class="bratonien-base-note" style="margin:.35rem 0 0"><strong>Schritt {$NC_CONNECTOR.wizard.step|escape:html} von 4</strong></p></div> <div>
<h4 style="margin:0">Neue Verbindung</h4>
<p class="bratonien-base-note" style="margin:.35rem 0 0"><strong>
{if $NC_CONNECTOR.wizard.step == 1}
Anmeldung
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
Datenbank prüfen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && !$NC_CONNECTOR.wizard.directory_selection_ready}
Speicher zuordnen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && $NC_CONNECTOR.wizard.directory_selection_ready}
Verzeichnisse auswählen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_complete}
Verbindung benennen
{elseif $NC_CONNECTOR.wizard.step == 3}
Piwigo-API
{elseif $NC_CONNECTOR.wizard.step == 4}
Abschluss
{else}
Einrichtung
{/if}
</strong></p>
</div>
<button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button> <button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button>
</div> </div>
@@ -52,7 +73,7 @@
</form> </form>
{elseif $NC_CONNECTOR.wizard.step == 2} {elseif $NC_CONNECTOR.wizard.step == 2}
<p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Der Assistent verwendet den Zugang aus dem ersten Schritt auch für den benötigten Lesezugriff.</p> <p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Für den Datenzugriff wird die bekannte Reader-Verbindung verwendet. Verzeichnisse werden ausschließlich mit dem angemeldeten Nextcloud-Benutzer gelesen.</p>
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<span class="bratonien-label">Adresse</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong> <span class="bratonien-label">Adresse</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
<span class="bratonien-label">Version</span><strong>{if $NC_CONNECTOR.wizard.version}{$NC_CONNECTOR.wizard.version|escape:html}{else}Nicht gemeldet{/if}</strong> <span class="bratonien-label">Version</span><strong>{if $NC_CONNECTOR.wizard.version}{$NC_CONNECTOR.wizard.version|escape:html}{else}Nicht gemeldet{/if}</strong>
@@ -62,7 +83,7 @@
{if $NC_CONNECTOR.wizard.technical_stage == 'database_details'} {if $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
<hr> <hr>
<h5>Datenbank-Adresse prüfen</h5> <h5>Datenbank-Adresse prüfen</h5>
<p class="bratonien-base-note">Der Zugang aus dem ersten Schritt bleibt unverändert. Nur die Verbindungsadresse konnte nicht automatisch bestätigt werden.</p> <p class="bratonien-base-note">Die bekannte Reader-Verbindung konnte mit der gespeicherten Adresse nicht bestätigt werden.</p>
{if $NC_CONNECTOR.wizard.technical_error}<details><summary>Technische Details</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.wizard.technical_error|escape:html}</p></details>{/if} {if $NC_CONNECTOR.wizard.technical_error}<details><summary>Technische Details</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.wizard.technical_error|escape:html}</p></details>{/if}
<form method="post" data-bratonien-wizard-form> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
@@ -76,46 +97,103 @@
{elseif $NC_CONNECTOR.wizard.technical_stage == 'mounts'} {elseif $NC_CONNECTOR.wizard.technical_stage == 'mounts'}
<hr> <hr>
<h5>Speicherort bestätigen</h5> {if $NC_CONNECTOR.wizard.directory_selection_ready}
<p class="bratonien-base-note">Die Datenquelle wurde gefunden. Nur Speicherorte, die nicht sicher erkannt werden konnten, müssen jetzt einmalig zugeordnet werden.</p> <h5>Verzeichnisse auswählen</h5>
<form method="post" data-bratonien-wizard-form> <p class="bratonien-base-note">Es werden nur Verzeichnisse angezeigt, auf die <strong>{$NC_CONNECTOR.wizard.username|escape:html}</strong> in Nextcloud Zugriff hat. Mehrere Verzeichnisse können hinzugefügt werden. Bleibt die Auswahl leer, wird automatisch das Stammverzeichnis verwendet.</p>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index} <span class="bratonien-label">Ausgewählt</span>
<span class="bratonien-label">Speicher {$storage_index+1}</span> <div>
{if $storage.local_mount} {if $NC_CONNECTOR.wizard.directory_selected|@count > 0}
<strong>Automatisch erkannt</strong> {foreach from=$NC_CONNECTOR.wizard.directory_selected item=selected_path}
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}"> <form method="post" class="bratonien-actions" style="margin:.25rem 0" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_remove" value="{$selected_path|escape:html}">
<strong style="flex:1">{if $selected_path}{$selected_path|escape:html}{else}Stammverzeichnis{/if}</strong>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_remove">Entfernen</button>
</form>
{/foreach}
{else} {else}
<input name="nc_wizard_storage_mount[{$storage_index|escape:html}]" type="text" placeholder="Eingebundener Speicherpfad" required> <span class="bratonien-base-note">Keine Auswahl Stammverzeichnis wird verwendet.</span>
{/if} {/if}
{/foreach} </div>
<span class="bratonien-label">Aktueller Ordner</span><strong>/{if $NC_CONNECTOR.wizard.directory_path}{$NC_CONNECTOR.wizard.directory_path|escape:html}{/if}</strong>
</div> </div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Speicher prüfen</button></p>
</form> <div class="bratonien-actions" style="margin:.75rem 0">
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_add">Diesen Ordner hinzufügen</button>
</form>
{if $NC_CONNECTOR.wizard.directory_path}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_path" value="{$NC_CONNECTOR.wizard.directory_parent|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_browse">Eine Ebene hoch</button>
</form>
{/if}
</div>
<div style="margin:.75rem 0">
{if $NC_CONNECTOR.wizard.directory_children|@count > 0}
{foreach from=$NC_CONNECTOR.wizard.directory_children item=directory_name key=directory_path}
<form method="post" style="margin:.3rem 0" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_path" value="{$directory_path|escape:html}">
<button class="buttonLike" style="width:100%;text-align:left" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_browse">📁 {$directory_name|escape:html}</button>
</form>
{/foreach}
{else}
<p class="bratonien-base-note">Keine Unterordner vorhanden.</p>
{/if}
</div>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index}
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}">
{/foreach}
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Verzeichnisse übernehmen</button></p>
</form>
{else}
<h5>Speicherort bestätigen</h5>
<p class="bratonien-base-note">Die Datenquelle wurde gefunden. Ein technischer Speicherort konnte nicht automatisch zugeordnet werden.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index}
<span class="bratonien-label">Speicher {$storage_index+1}</span>
{if $storage.local_mount}
<strong>Automatisch erkannt</strong>
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}">
{else}
<input name="nc_wizard_storage_mount[{$storage_index|escape:html}]" type="text" placeholder="Eingebundener Speicherpfad" required>
{/if}
{/foreach}
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Speicher prüfen</button></p>
</form>
{/if}
{elseif $NC_CONNECTOR.wizard.technical_complete} {elseif $NC_CONNECTOR.wizard.technical_complete}
<hr> <hr>
<h5>Welche Freigaben sollen verwendet werden?</h5> <h5>Verbindung benennen</h5>
<p class="bratonien-base-note"><strong>Empfehlung:</strong> ein eigener Showcase-Benutzer. So bleibt die Verbindung unabhängig von persönlichen Konten.</p> <p class="bratonien-base-note">Diese Verbindung verwendet ausschließlich den in Schritt 1 angemeldeten Nextcloud-Benutzer.</p>
<form method="post" data-bratonien-wizard-form> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_connection_name">Name der Verbindung</label><input id="nc_wizard_connection_name" name="nc_wizard_connection_name" type="text" value="{$NC_CONNECTOR.wizard.connection_name|escape:html}" required> <label class="bratonien-label" for="nc_wizard_connection_name">Name der Verbindung</label><input id="nc_wizard_connection_name" name="nc_wizard_connection_name" type="text" value="{$NC_CONNECTOR.wizard.connection_name|escape:html}" required>
<label class="bratonien-label" for="nc_wizard_showcase_user">Nextcloud-Benutzer</label> <span class="bratonien-label">Nextcloud-Benutzer</span><strong>{$NC_CONNECTOR.wizard.username|escape:html}</strong>
{if $NC_CONNECTOR.wizard.can_list_users && $NC_CONNECTOR.wizard.users|@count > 0}
<select id="nc_wizard_showcase_user" name="nc_wizard_showcase_user" required>
{foreach from=$NC_CONNECTOR.wizard.users item=nc_user}<option value="{$nc_user|escape:html}"{if $nc_user == 'showcase'} selected{/if}>{$nc_user|escape:html}{if $nc_user == 'showcase'} · empfohlen{/if}</option>{/foreach}
</select>
{else}
<input id="nc_wizard_showcase_user" name="nc_wizard_showcase_user" type="text" value="{if $NC_CONNECTOR.wizard.showcase_user}{$NC_CONNECTOR.wizard.showcase_user|escape:html}{else}showcase{/if}" required>
{/if}
</div> </div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_select_user">Weiter</button></p> <p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_select_user">Weiter</button></p>
</form> </form>
{/if} {/if}
<form method="post" style="margin-top:1rem" data-bratonien-wizard-form><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_reset" data-bratonien-wizard-end>Neu beginnen</button></form> <div class="bratonien-actions" style="margin-top:1rem">
<form method="post" data-bratonien-wizard-form><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_back">Zurück</button></form>
<form method="post" data-bratonien-wizard-form><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_reset" data-bratonien-wizard-end>Neu beginnen</button></form>
</div>
{elseif $NC_CONNECTOR.wizard.step == 3} {elseif $NC_CONNECTOR.wizard.step == 3}
<p class="bratonien-base-note"><strong>Nextcloud ist vorbereitet.</strong> Jetzt wird der bevorzugte Piwigo-Zugang geprüft.</p> <p class="bratonien-base-note"><strong>Nextcloud ist vorbereitet.</strong> Jetzt wird der bevorzugte Piwigo-Zugang geprüft.</p>
@@ -128,13 +206,14 @@
</div> </div>
<div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_test">API testen</button><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_skip" formnovalidate>Überspringen</button></div> <div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_test">API testen</button><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_skip" formnovalidate>Überspringen</button></div>
</form> </form>
<form method="post" style="margin-top:1rem" data-bratonien-wizard-form><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_back">Zurück</button></form>
{elseif $NC_CONNECTOR.wizard.step == 4} {elseif $NC_CONNECTOR.wizard.step == 4}
<p class="bratonien-base-note"><strong>Fast fertig.</strong> Die Verbindung wurde noch nicht angelegt. Erst der letzte Button übernimmt die Einstellungen.</p> <p class="bratonien-base-note"><strong>Fast fertig.</strong> Die Verbindung wurde noch nicht angelegt. Erst der letzte Button übernimmt die Einstellungen.</p>
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<span class="bratonien-label">Verbindung</span><strong>{$NC_CONNECTOR.wizard.connection_name|escape:html}</strong> <span class="bratonien-label">Verbindung</span><strong>{$NC_CONNECTOR.wizard.connection_name|escape:html}</strong>
<span class="bratonien-label">Nextcloud</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong> <span class="bratonien-label">Nextcloud</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
<span class="bratonien-label">Freigaben von</span><strong>{$NC_CONNECTOR.wizard.showcase_user|escape:html}</strong> <span class="bratonien-label">Nextcloud-Benutzer</span><strong>{$NC_CONNECTOR.wizard.username|escape:html}</strong>
<span class="bratonien-label">Piwigo-API</span><strong>{if $NC_CONNECTOR.wizard.api_status == 'ok'}Erfolgreich geprüft{else}Übersprungen{/if}</strong> <span class="bratonien-label">Piwigo-API</span><strong>{if $NC_CONNECTOR.wizard.api_status == 'ok'}Erfolgreich geprüft{else}Übersprungen{/if}</strong>
</div> </div>
<hr> <hr>
@@ -148,6 +227,7 @@
</div> </div>
<div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_finish" data-bratonien-wizard-end>Verbindung anlegen</button><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_reset" formnovalidate data-bratonien-wizard-end>Abbrechen</button></div> <div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_finish" data-bratonien-wizard-end>Verbindung anlegen</button><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_reset" formnovalidate data-bratonien-wizard-end>Abbrechen</button></div>
</form> </form>
<form method="post" style="margin-top:1rem" data-bratonien-wizard-form><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_back">Zurück</button></form>
{/if} {/if}
</div> </div>
</dialog> </dialog>
@@ -184,14 +264,28 @@
{if $NC_CONNECTOR.connection_count > 0} {if $NC_CONNECTOR.connection_count > 0}
{foreach from=$NC_CONNECTOR.connections item=connection} {foreach from=$NC_CONNECTOR.connections item=connection}
<details style="margin:.6rem 0"> <details style="margin:.6rem 0">
<summary><strong>{$connection.display_name|escape:html}</strong> · {if $connection.enabled}aktiv{else}{$connection.takeover_state|escape:html}{/if}</summary> <summary><strong>{$connection.display_name|escape:html}</strong> · {if $connection.enabled}aktiv{else}{$connection.takeover_state|escape:html}{/if}{if isset($connection.last_sync) && ($connection.last_sync.state == 'error' || $connection.last_sync.state == 'warning')} · Laufzeitproblem{/if}</summary>
<div style="padding:.75rem 0"> <div style="padding:.75rem 0">
<form method="post" class="bratonien-actions"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><input name="connection_name" type="text" value="{$connection.name|escape:html}" required><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_name">Name speichern</button></form> <form method="post" class="bratonien-actions"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><input name="connection_name" type="text" value="{$connection.name|escape:html}" required><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_name">Name speichern</button></form>
<div class="bratonien-actions" style="margin-top:.6rem"> <div class="bratonien-actions" style="margin-top:.6rem">
{if $connection.adapter == 'local' && !$connection.enabled}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_verify">Verbindung prüfen</button></form>{/if} {if $connection.adapter == 'local' && !$connection.enabled}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_verify">Verbindung prüfen</button></form>{/if}
{if !$connection.enabled && $connection.takeover_state != 'active'}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_delete" onclick="return confirm('Verbindung wirklich löschen? Bilder in Nextcloud oder Piwigo werden nicht gelöscht.');">Löschen</button></form>{/if} <form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_delete" onclick="return confirm('Verbindung wirklich löschen? Die Verbindung wird sofort aus Bratonien Tools entfernt und beim nächsten Connector-Lauf auch aus der Laufzeit entfernt. Bilder in Nextcloud oder Piwigo werden nicht gelöscht.');">Löschen</button></form>
</div> </div>
{if isset($connection.last_sync) && $connection.last_sync.timestamp > 0}<p class="bratonien-base-note"><strong>Letzter Abgleich:</strong> {$connection.last_sync.label|escape:html} · {$connection.last_sync.message|escape:html}</p>{/if} {if isset($connection.last_sync) && $connection.last_sync.timestamp > 0}
<p class="bratonien-base-note"><strong>Letzter Abgleich:</strong> {$connection.last_sync.label|escape:html} · {$connection.last_sync.message|escape:html}</p>
{if $connection.last_sync.state == 'error'}<p class="bratonien-main-cache__warning"><strong>Laufzeitfehler:</strong> {$connection.last_sync.message|escape:html}</p>{/if}
{if $connection.last_sync.state == 'warning'}<p class="bratonien-main-cache__warning"><strong>Laufzeitwarnung:</strong> {$connection.last_sync.message|escape:html}</p>{/if}
{if $connection.last_sync.api_state == 'error'}<p class="bratonien-main-cache__warning"><strong>API:</strong> {$connection.last_sync.api_message|escape:html}</p>{/if}
{if $connection.last_sync.fallback_state == 'error'}<p class="bratonien-main-cache__warning"><strong>Fallback:</strong> {$connection.last_sync.fallback_message|escape:html}</p>{/if}
{if $connection.last_sync.error_detail}<details><summary>Technische Laufzeitdetails</summary><p class="bratonien-main-cache__warning">{$connection.last_sync.error_detail|escape:html}</p></details>{/if}
{else}
<p class="bratonien-base-note"><strong>Laufzeit:</strong> Noch kein Laufstatus für diese Verbindung vorhanden.</p>
{/if}
{if isset($connection.config.api_enabled)}
<p class="bratonien-base-note"><strong>Piwigo-Zugang dieser Verbindung:</strong> {if $connection.config.api_enabled}eigene API{elseif $connection.fallback_stored}Fallback{else}kein Zugang gespeichert{/if}</p>
{else}
<p class="bratonien-base-note"><strong>Piwigo-Zugang:</strong> bestehende Legacy-Konfiguration</p>
{/if}
{if $connection.verification_checks|@count > 0}<details><summary>Letzte Prüfung</summary><ul>{foreach from=$connection.verification_checks item=check}<li>{if $check.ok}{else}{/if} {$check.name|escape:html}: {$check.detail|escape:html}</li>{/foreach}</ul></details>{/if} {if $connection.verification_checks|@count > 0}<details><summary>Letzte Prüfung</summary><ul>{foreach from=$connection.verification_checks item=check}<li>{if $check.ok}{else}{/if} {$check.name|escape:html}: {$check.detail|escape:html}</li>{/foreach}</ul></details>{/if}
<details style="margin-top:.75rem"><summary>Technische Einstellungen</summary> <details style="margin-top:.75rem"><summary>Technische Einstellungen</summary>
{if $connection.enabled || $connection.takeover_state == 'active'}<p class="bratonien-main-cache__warning">Aktive Verbindungen können technisch nicht geändert werden.</p>{else} {if $connection.enabled || $connection.takeover_state == 'active'}<p class="bratonien-main-cache__warning">Aktive Verbindungen können technisch nicht geändert werden.</p>{else}