From eb3bc48cebdb4cf164d07d7fc6284ee2921b7275 Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Tue, 18 Aug 2026 16:22:19 +0200 Subject: [PATCH] Reconcile wizard connections into shared runtime --- runtime/reconcile.php | 259 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 runtime/reconcile.php diff --git a/runtime/reconcile.php b/runtime/reconcile.php new file mode 100644 index 0000000..fdded9a --- /dev/null +++ b/runtime/reconcile.php @@ -0,0 +1,259 @@ +#!/usr/bin/env php +$plain,'piwigo_user'=>'','piwigo_password'=>'','api_key_id'=>'','api_key_secret'=>''); + } + return array( + 'db_password'=>(string)($decoded['db_password'] ?? ''), + 'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''), + 'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''), + 'api_key_id'=>(string)($decoded['api_key_id'] ?? ''), + 'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''), + ); +} + +function encrypt_reconcile(array $credentials, $hexKey) +{ + $plain = json_encode(array( + 'v'=>2, + 'db_password'=>(string)$credentials['db_password'], + 'piwigo_user'=>(string)$credentials['piwigo_user'], + 'piwigo_password'=>(string)$credentials['piwigo_password'], + 'api_key_id'=>(string)$credentials['api_key_id'], + 'api_key_secret'=>(string)$credentials['api_key_secret'], + ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (!is_string($plain)) fail_reconcile('Connector-Zugangsdaten konnten nicht serialisiert werden.'); + $iv = random_bytes(12); + $tag = ''; + $cipher = openssl_encrypt($plain, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag); + if ($cipher === false) fail_reconcile('Connector-Zugangsdaten konnten nicht verschluesselt werden.'); + return base64_encode(json_encode(array( + 'v'=>1, + 'iv'=>base64_encode($iv), + 'tag'=>base64_encode($tag), + 'data'=>base64_encode($cipher), + ))); +} + +function write_runtime_status($piwigoRoot, $id, $stateDir, $message) +{ + $payload = json_encode(array( + 'state'=>'error', + 'message'=>'Runtime-Vorbereitung fehlgeschlagen', + 'timestamp'=>time(), + 'auth_mode'=>'failed', + 'api'=>array('state'=>'not_run','message'=>''), + 'fallback'=>array('state'=>'not_run','message'=>''), + 'error_detail'=>(string)$message, + ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (!is_string($payload)) return; + $targets = array(rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$id.'.json'); + if ($stateDir !== '') $targets[] = rtrim($stateDir, '/').'/connector-status.json'; + foreach ($targets as $target) + { + $dir = dirname($target); + if (!is_dir($dir)) @mkdir($dir, 0755, true); + @file_put_contents($target, $payload."\n", LOCK_EX); + @chmod($target, 0644); + } +} + +function sql_reconcile(mysqli $db, $value) +{ + return $db->real_escape_string((string)$value); +} + +$pluginRoot = dirname(__DIR__); +$piwigoRoot = dirname($pluginRoot, 2); +$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; +$configDir = '/etc/bratonien-tools/nc-connector'; +$stateRoot = '/var/lib/bratonien-tools/nc-connector'; + +try +{ + if (!is_readable($dbConfig)) fail_reconcile('Piwigo-Datenbankkonfiguration ist nicht lesbar: '.$dbConfig); + $conf = array(); + $prefixeTable = 'piwigo_'; + require $dbConfig; + foreach (array('db_host','db_user','db_password','db_base') as $key) + { + if (!isset($conf[$key])) fail_reconcile('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key); + } + + $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); + if ($db->connect_errno) fail_reconcile('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error); + $db->set_charset('utf8mb4'); + + $keyResult = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1"); + if (!$keyResult || !$keyResult->num_rows) fail_reconcile('Connector-Schluessel wurde nicht gefunden.'); + $hexKey = trim((string)$keyResult->fetch_assoc()['value']); + + $table = $prefixeTable.'bratonien_tools_nc_connections'; + $rows = $db->query("SELECT id,name,adapter,enabled,takeover_state,config_json,secret_blob FROM `{$table}` ORDER BY id"); + if (!$rows) fail_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error); + + if (!is_dir($configDir) && !mkdir($configDir, 0700, true)) fail_reconcile('Runtime-Konfigurationsverzeichnis konnte nicht angelegt werden.'); + chmod($configDir, 0700); + + while ($row = $rows->fetch_assoc()) + { + $id = (int)$row['id']; + $config = json_decode((string)$row['config_json'], true); + if (!is_array($config)) $config = array(); + $stateDir = rtrim((string)($config['state_dir'] ?? ''), '/'); + if ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id; + + try + { + if ((string)$row['adapter'] !== 'local') continue; + + $isActive = (int)$row['enabled'] === 1 && (string)$row['takeover_state'] === 'active'; + $wizardConnection = (string)($config['origin'] ?? '') === 'native' + && trim((string)($config['nextcloud_url'] ?? '')) !== '' + && trim((string)($config['showcase_user'] ?? '')) !== ''; + $verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : null; + $verificationFailed = is_array($verification) && empty($verification['ok']); + + if (!$isActive && (!$wizardConnection || $verificationFailed)) continue; + + foreach (array('host','port','database','user','source_view','activity_view','gallery_root') as $key) + { + if (trim((string)($config[$key] ?? '')) === '') fail_reconcile('Konfiguration unvollstaendig: '.$key.' fehlt.'); + } + $storages = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array(); + if (!$storages) fail_reconcile('Keine Storage-Zuordnungen gespeichert.'); + + $credentials = decrypt_reconcile((string)$row['secret_blob'], $hexKey); + if ($credentials['db_password'] === '') fail_reconcile('Datenbankpasswort fehlt.'); + + if (!$isActive && !array_key_exists('api_enabled', $config)) + { + // Alte Wizard-Verbindungen ohne API, aber mit gespeichertem Fallback, + // duerfen niemals den globalen API-Key einer anderen Verbindung erben. + $hasFallback = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== ''; + if (!$hasFallback) fail_reconcile('Die Verbindung besitzt weder einen eigenen API-Zugang noch einen eigenen Fallback.'); + $credentials['api_key_id'] = ''; + $credentials['api_key_secret'] = ''; + $config['piwigo_auth'] = 'connection-scoped'; + $config['api_enabled'] = false; + $row['secret_blob'] = encrypt_reconcile($credentials, $hexKey); + } + + $connectionScoped = (string)($config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $config); + if ($connectionScoped) + { + $apiAvailable = !empty($config['api_enabled']) && $credentials['api_key_id'] !== '' && $credentials['api_key_secret'] !== ''; + $fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== ''; + if (!$apiAvailable && !$fallbackAvailable) fail_reconcile('Kein verbindungseigener Piwigo-Zugang gespeichert.'); + } + + if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_reconcile('State-Verzeichnis konnte nicht angelegt werden.'); + chmod($stateDir, 0750); + + $base = $configDir.'/connection-'.$id; + $dbPasswordPath = $base.'.db-password'; + $piwigoPasswordPath = $base.'.piwigo-password'; + $storagePath = $base.'.storages.tsv'; + $configPath = $base.'.conf'; + $statusFile = $stateDir.'/connector-status.json'; + + file_put_contents($dbPasswordPath, $credentials['db_password']."\n", LOCK_EX); + chmod($dbPasswordPath, 0600); + + $fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== ''; + if ($fallbackAvailable) + { + file_put_contents($piwigoPasswordPath, $credentials['piwigo_password']."\n", LOCK_EX); + chmod($piwigoPasswordPath, 0600); + } + else + { + @unlink($piwigoPasswordPath); + } + + $storageLines = array('# storage_idsource_prefixlocal_mountinclude_prefix'); + foreach ($storages as $storage) + { + $storageLines[] = (string)($storage['storage_id'] ?? '')."\t" + .trim((string)($storage['source_prefix'] ?? ''), '/')."\t" + .(string)($storage['local_mount'] ?? '')."\t" + .trim((string)($storage['include_prefix'] ?? ''), '/'); + } + file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); + chmod($storagePath, 0600); + + $lines = array( + 'PIWIGO_ROOT='.$piwigoRoot, + 'GALLERY_ROOT='.(string)$config['gallery_root'], + 'STATE_DIR='.$stateDir, + 'STATUS_FILE='.$statusFile, + 'NC_DB_HOST='.(string)$config['host'], + 'NC_DB_PORT='.(string)$config['port'], + 'NC_DB_NAME='.(string)$config['database'], + 'NC_DB_USER='.(string)$config['user'], + 'NC_DB_VIEW='.(string)$config['source_view'], + 'NC_ACTIVITY_VIEW='.(string)$config['activity_view'], + 'NC_DB_PASSWORD_FILE='.$dbPasswordPath, + 'STORAGE_CONFIG='.$storagePath, + 'QUIET_SECONDS='.(int)($config['quiet_seconds'] ?? 120), + 'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900), + 'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400), + 'PIWIGO_SYNC_ENABLED=1', + ); + if ($fallbackAvailable) + { + $lines[] = 'PIWIGO_SYNC_USER='.$credentials['piwigo_user']; + $lines[] = 'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath; + } + file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); + chmod($configPath, 0600); + + $config['state_dir'] = $stateDir; + $config['status_file'] = $statusFile; + $config['runtime'] = array('mode'=>'shared-runner','config'=>$configPath,'reconciled_at'=>date('Y-m-d H:i:s')); + $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (!is_string($json)) fail_reconcile('Connector-Konfiguration konnte nicht serialisiert werden.'); + $now = date('Y-m-d H:i:s'); + $sql = "UPDATE `{$table}` SET enabled=1,takeover_state='active',config_json='".sql_reconcile($db,$json)."',secret_blob='".sql_reconcile($db,$row['secret_blob'])."',updated='".sql_reconcile($db,$now)."' WHERE id=".$id; + if (!$db->query($sql)) fail_reconcile('Runtime-Status konnte nicht gespeichert werden: '.$db->error); + + if (!$isActive) echo "NC Connector: Verbindung #{$id} ({$row['name']}) automatisch in die gemeinsame Runtime uebernommen.\n"; + } + catch (Throwable $e) + { + write_runtime_status($piwigoRoot, $id, $stateDir, $e->getMessage()); + fwrite(STDERR, "NC Connector #{$id}: ".$e->getMessage()."\n"); + } + } + + exit(0); +} +catch (Throwable $e) +{ + fwrite(STDERR, "NC Connector Reconcile: ".$e->getMessage()."\n"); + exit(1); +}