pdo instanceof \PDO; } public function ensureDefaultDashboard(string $ownerKey, string $title = 'Mein Dashboard'): array { $dashboards = $this->listDashboardsForOwner($ownerKey); if ($dashboards !== []) { foreach ($dashboards as $dashboard) { if (!empty($dashboard['is_default'])) { return $dashboard; } } $this->setDefaultDashboard($ownerKey, (int) $dashboards[0]['id']); return $this->getDashboardById((int) $dashboards[0]['id']) ?? $dashboards[0]; } $id = $this->createDashboard($ownerKey, [ 'title' => $title, 'slug' => 'mein-dashboard', 'description' => 'Persönliches Nexus-Dashboard.', 'visibility' => 'private', 'is_default' => true, ]); return $this->getDashboardById($id) ?? []; } public function listDashboardsForOwner(string $ownerKey): array { if (!$this->available() || $ownerKey === '') { return []; } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_dashboards WHERE owner_key = :owner ORDER BY is_default DESC, sort_order ASC, title ASC, id ASC" ); $stmt->execute(['owner' => $ownerKey]); return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []); } public function listAccessibleDashboards(string $ownerKey, array $groups = []): array { if (!$this->available()) { return []; } $rows = []; $seen = []; foreach ($this->listDashboardsForOwner($ownerKey) as $dashboard) { $id = (int) ($dashboard['id'] ?? 0); if ($id > 0) { $rows[] = $dashboard; $seen[$id] = true; } } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_dashboards WHERE visibility = 'public' ORDER BY sort_order ASC, title ASC, id ASC" ); $stmt->execute(); foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $dashboard) { $id = (int) ($dashboard['id'] ?? 0); if ($id > 0 && !isset($seen[$id])) { $rows[] = $dashboard; $seen[$id] = true; } } if ($ownerKey !== '') { $shared = $this->listSharedDashboardIds($ownerKey, $groups); foreach ($shared as $dashboardId) { if (isset($seen[$dashboardId])) { continue; } $dashboard = $this->getDashboardById($dashboardId); if ($dashboard !== null) { $rows[] = $dashboard; $seen[$dashboardId] = true; } } } usort($rows, static function (array $left, array $right): int { return [$left['sort_order'] ?? 0, (string) ($left['title'] ?? ''), (int) ($left['id'] ?? 0)] <=> [$right['sort_order'] ?? 0, (string) ($right['title'] ?? ''), (int) ($right['id'] ?? 0)]; }); return $rows; } public function resolveHomeDashboard(?string $ownerKey, array $groups, bool $authenticated, int $preferredId = 0): ?array { if (!$this->available()) { return null; } if ($authenticated && $ownerKey !== null && $ownerKey !== '') { if ($preferredId > 0) { $preferred = $this->getDashboardById($preferredId); if ($preferred !== null && $this->canAccessDashboard($preferred, $ownerKey, $groups)) { return $preferred; } } $dashboards = $this->listAccessibleDashboards($ownerKey, $groups); foreach ($dashboards as $dashboard) { if (!empty($dashboard['is_default']) && $this->canAccessDashboard($dashboard, $ownerKey, $groups)) { return $dashboard; } } foreach ($dashboards as $dashboard) { if ($this->canAccessDashboard($dashboard, $ownerKey, $groups)) { return $dashboard; } } } $publicId = (int) (($this->settings()['global_home_dashboard_id'] ?? 0)); if ($publicId > 0) { $dashboard = $this->getDashboardById($publicId); if ($dashboard !== null && (string) ($dashboard['visibility'] ?? '') === 'public') { return $dashboard; } } foreach ($this->listPublicDashboards() as $dashboard) { return $dashboard; } return null; } public function listPublicDashboards(): array { if (!$this->available()) { return []; } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_dashboards WHERE visibility = 'public' ORDER BY is_default DESC, sort_order ASC, title ASC, id ASC" ); $stmt->execute(); return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []); } public function getDashboardById(int $id): ?array { if (!$this->available() || $id <= 0) { return null; } $stmt = $this->pdo->prepare("SELECT * FROM nexus_dashboards WHERE id = :id LIMIT 1"); $stmt->execute(['id' => $id]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); return is_array($row) ? $this->hydrateRow($row) : null; } public function createDashboard(string $ownerKey, array $data): int { if (!$this->available() || $ownerKey === '') { return 0; } $title = trim((string) ($data['title'] ?? 'Neues Dashboard')); $slug = $this->uniqueSlug('nexus_dashboards', $ownerKey, (string) ($data['slug'] ?? $title), 0); $description = trim((string) ($data['description'] ?? '')); $visibility = $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')); $sortOrder = (int) ($data['sort_order'] ?? $this->nextSortOrder('nexus_dashboards', 'owner_key', $ownerKey)); $isDefault = !empty($data['is_default']); $config = $this->encodeJson((array) ($data['config'] ?? [])); $stmt = $this->pdo->prepare( "INSERT INTO nexus_dashboards (owner_key, title, slug, description, visibility, sort_order, is_default, config, created_at, updated_at) VALUES (:owner_key, :title, :slug, :description, :visibility, :sort_order, :is_default, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ); $stmt->execute([ 'owner_key' => $ownerKey, 'title' => $title !== '' ? $title : 'Neues Dashboard', 'slug' => $slug, 'description' => $description, 'visibility' => $visibility, 'sort_order' => $sortOrder, 'is_default' => $isDefault ? 1 : 0, 'config' => $config, ]); $id = (int) $this->pdo->lastInsertId(); if ($isDefault) { $this->setDefaultDashboard($ownerKey, $id); } return $id; } public function updateDashboard(int $id, string $ownerKey, array $data): void { if (!$this->available() || $id <= 0 || $ownerKey === '') { return; } $dashboard = $this->getOwnedDashboard($id, $ownerKey); if ($dashboard === null) { return; } $title = trim((string) ($data['title'] ?? ($dashboard['title'] ?? 'Dashboard'))); $slug = $this->uniqueSlug('nexus_dashboards', $ownerKey, (string) ($data['slug'] ?? $title), $id); $description = trim((string) ($data['description'] ?? ($dashboard['description'] ?? ''))); $visibility = $this->normalizeVisibility((string) ($data['visibility'] ?? ($dashboard['visibility'] ?? 'private'))); $sortOrder = isset($data['sort_order']) ? (int) $data['sort_order'] : (int) ($dashboard['sort_order'] ?? 0); $config = $this->encodeJson((array) ($data['config'] ?? ($dashboard['config'] ?? []))); $isDefault = !empty($data['is_default']); $stmt = $this->pdo->prepare( "UPDATE nexus_dashboards SET title = :title, slug = :slug, description = :description, visibility = :visibility, sort_order = :sort_order, is_default = :is_default, config = :config, updated_at = CURRENT_TIMESTAMP WHERE id = :id AND owner_key = :owner_key" ); $stmt->execute([ 'id' => $id, 'owner_key' => $ownerKey, 'title' => $title !== '' ? $title : 'Dashboard', 'slug' => $slug, 'description' => $description, 'visibility' => $visibility, 'sort_order' => $sortOrder, 'is_default' => $isDefault ? 1 : 0, 'config' => $config, ]); if ($isDefault) { $this->setDefaultDashboard($ownerKey, $id); } } public function deleteDashboard(int $id, string $ownerKey): void { if (!$this->available() || $id <= 0 || $ownerKey === '') { return; } $dashboard = $this->getOwnedDashboard($id, $ownerKey); if ($dashboard === null) { return; } $stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_items WHERE dashboard_id = :dashboard_id"); $stmt->execute(['dashboard_id' => $id]); $stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_shares WHERE dashboard_id = :dashboard_id"); $stmt->execute(['dashboard_id' => $id]); $stmt = $this->pdo->prepare("DELETE FROM nexus_dashboards WHERE id = :id AND owner_key = :owner_key"); $stmt->execute(['id' => $id, 'owner_key' => $ownerKey]); if (!empty($dashboard['is_default'])) { $remaining = $this->listDashboardsForOwner($ownerKey); if ($remaining !== []) { $this->setDefaultDashboard($ownerKey, (int) $remaining[0]['id']); } } } public function setDefaultDashboard(string $ownerKey, int $dashboardId): void { if (!$this->available() || $ownerKey === '' || $dashboardId <= 0) { return; } $stmt = $this->pdo->prepare("UPDATE nexus_dashboards SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE owner_key = :owner_key"); $stmt->execute(['owner_key' => $ownerKey]); $stmt = $this->pdo->prepare("UPDATE nexus_dashboards SET is_default = 1, updated_at = CURRENT_TIMESTAMP WHERE id = :id AND owner_key = :owner_key"); $stmt->execute(['id' => $dashboardId, 'owner_key' => $ownerKey]); } public function listItems(int $dashboardId): array { if (!$this->available() || $dashboardId <= 0) { return []; } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_dashboard_items WHERE dashboard_id = :dashboard_id ORDER BY sort_order ASC, id ASC" ); $stmt->execute(['dashboard_id' => $dashboardId]); return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []); } public function createItem(int $dashboardId, string $ownerKey, array $data): int { if (!$this->available() || $dashboardId <= 0) { return 0; } $dashboard = $this->getOwnedDashboard($dashboardId, $ownerKey); if ($dashboard === null) { return 0; } $itemType = $this->normalizeItemType((string) ($data['item_type'] ?? 'link')); $title = trim((string) ($data['title'] ?? 'Element')); $description = trim((string) ($data['description'] ?? '')); $gridColumn = $this->nullableInt($data['grid_column'] ?? null); $gridRow = $this->nullableInt($data['grid_row'] ?? null); $columnSpan = max(1, min(4, (int) ($data['column_span'] ?? 1))); $rowSpan = max(1, min(4, (int) ($data['row_span'] ?? 1))); $sortOrder = (int) ($data['sort_order'] ?? $this->nextItemSortOrder($dashboardId)); $config = $this->encodeJson((array) ($data['config'] ?? [])); $stmt = $this->pdo->prepare( "INSERT INTO nexus_dashboard_items (dashboard_id, item_type, title, description, grid_column, grid_row, column_span, row_span, sort_order, config, created_at, updated_at) VALUES (:dashboard_id, :item_type, :title, :description, :grid_column, :grid_row, :column_span, :row_span, :sort_order, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ); $stmt->execute([ 'dashboard_id' => $dashboardId, 'item_type' => $itemType, 'title' => $title !== '' ? $title : 'Element', 'description' => $description, 'grid_column' => $gridColumn, 'grid_row' => $gridRow, 'column_span' => $columnSpan, 'row_span' => $rowSpan, 'sort_order' => $sortOrder, 'config' => $config, ]); return (int) $this->pdo->lastInsertId(); } public function deleteItem(int $itemId, int $dashboardId, string $ownerKey): void { if (!$this->available() || $itemId <= 0 || $dashboardId <= 0) { return; } $dashboard = $this->getOwnedDashboard($dashboardId, $ownerKey); if ($dashboard === null) { return; } $stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_items WHERE id = :id AND dashboard_id = :dashboard_id"); $stmt->execute([ 'id' => $itemId, 'dashboard_id' => $dashboardId, ]); } public function listIntegrationsForOwner(string $ownerKey): array { if (!$this->available() || $ownerKey === '') { return []; } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_integrations WHERE owner_key = :owner_key ORDER BY is_active DESC, name ASC, id ASC" ); $stmt->execute(['owner_key' => $ownerKey]); return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []); } public function createIntegration(string $ownerKey, array $data): int { if (!$this->available() || $ownerKey === '') { return 0; } $stmt = $this->pdo->prepare( "INSERT INTO nexus_integrations (owner_key, type, name, base_url, visibility, is_active, config, secrets, created_at, updated_at) VALUES (:owner_key, :type, :name, :base_url, :visibility, :is_active, :config, :secrets, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ); $stmt->execute([ 'owner_key' => $ownerKey, 'type' => trim((string) ($data['type'] ?? 'generic')) ?: 'generic', 'name' => trim((string) ($data['name'] ?? 'Integration')) ?: 'Integration', 'base_url' => trim((string) ($data['base_url'] ?? '')), 'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')), 'is_active' => !empty($data['is_active']) ? 1 : 0, 'config' => $this->encodeJson((array) ($data['config'] ?? [])), 'secrets' => $this->encodeJson((array) ($data['secrets'] ?? [])), ]); return (int) $this->pdo->lastInsertId(); } public function deleteIntegration(int $id, string $ownerKey): void { if (!$this->available() || $id <= 0 || $ownerKey === '') { return; } $stmt = $this->pdo->prepare("DELETE FROM nexus_integrations WHERE id = :id AND owner_key = :owner_key"); $stmt->execute(['id' => $id, 'owner_key' => $ownerKey]); } public function listPageModulesForOwner(string $ownerKey): array { if (!$this->available() || $ownerKey === '') { return []; } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_page_modules WHERE owner_key = :owner_key ORDER BY is_active DESC, title ASC, id ASC" ); $stmt->execute(['owner_key' => $ownerKey]); return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []); } public function listAccessiblePageModules(string $ownerKey, array $groups = []): array { if (!$this->available()) { return []; } $rows = []; $seen = []; foreach ($this->listPageModulesForOwner($ownerKey) as $module) { $id = (int) ($module['id'] ?? 0); if ($id > 0) { $rows[] = $module; $seen[$id] = true; } } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_page_modules WHERE visibility = 'public' ORDER BY title ASC, id ASC" ); $stmt->execute(); foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $module) { $id = (int) ($module['id'] ?? 0); if ($id > 0 && !isset($seen[$id])) { $rows[] = $module; $seen[$id] = true; } } foreach ($groups as $group) { $group = trim((string) $group); if ($group === '') { continue; } $stmt = $this->pdo->prepare( "SELECT * FROM nexus_page_modules WHERE visibility = 'group'" ); $stmt->execute(); foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $module) { $id = (int) ($module['id'] ?? 0); if ($id <= 0 || isset($seen[$id])) { continue; } if (in_array($group, (array) ($module['config']['groups'] ?? []), true)) { $rows[] = $module; $seen[$id] = true; } } } return $rows; } public function createPageModule(string $ownerKey, array $data): int { if (!$this->available() || $ownerKey === '') { return 0; } $title = trim((string) ($data['title'] ?? 'Seitenmodul')) ?: 'Seitenmodul'; $slug = $this->uniqueSlug('nexus_page_modules', $ownerKey, (string) ($data['slug'] ?? $title), 0); $stmt = $this->pdo->prepare( "INSERT INTO nexus_page_modules (owner_key, title, slug, module_type, target_url, description, visibility, open_mode, is_active, config, created_at, updated_at) VALUES (:owner_key, :title, :slug, :module_type, :target_url, :description, :visibility, :open_mode, :is_active, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ); $stmt->execute([ 'owner_key' => $ownerKey, 'title' => $title, 'slug' => $slug, 'module_type' => $this->normalizePageModuleType((string) ($data['module_type'] ?? 'link')), 'target_url' => trim((string) ($data['target_url'] ?? '')), 'description' => trim((string) ($data['description'] ?? '')), 'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')), 'open_mode' => $this->normalizeOpenMode((string) ($data['open_mode'] ?? 'embed')), 'is_active' => !empty($data['is_active']) ? 1 : 0, 'config' => $this->encodeJson((array) ($data['config'] ?? [])), ]); return (int) $this->pdo->lastInsertId(); } public function deletePageModule(int $id, string $ownerKey): void { if (!$this->available() || $id <= 0 || $ownerKey === '') { return; } $stmt = $this->pdo->prepare("DELETE FROM nexus_page_modules WHERE id = :id AND owner_key = :owner_key"); $stmt->execute(['id' => $id, 'owner_key' => $ownerKey]); } public function getPageModule(int $id, string $ownerKey = '', array $groups = []): ?array { if (!$this->available() || $id <= 0) { return null; } $stmt = $this->pdo->prepare("SELECT * FROM nexus_page_modules WHERE id = :id LIMIT 1"); $stmt->execute(['id' => $id]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!is_array($row)) { return null; } $module = $this->hydrateRow($row); if ($module['owner_key'] === $ownerKey || $module['visibility'] === 'public') { return $module; } if ($module['visibility'] === 'group' && $ownerKey !== '') { foreach ($groups as $group) { if (in_array($group, (array) ($module['config']['groups'] ?? []), true)) { return $module; } } } return null; } public function listWidgetTemplates(string $ownerKey, bool $includePublic = true): array { if (!$this->available()) { return []; } $clauses = []; $params = []; if ($ownerKey !== '') { $clauses[] = 'owner_key = :owner_key'; $params['owner_key'] = $ownerKey; } if ($includePublic) { $clauses[] = "visibility = 'public'"; } if ($clauses === []) { return []; } $stmt = $this->pdo->prepare( 'SELECT * FROM nexus_widget_templates WHERE ' . implode(' OR ', $clauses) . ' ORDER BY name ASC, id ASC' ); $stmt->execute($params); $rows = []; $seen = []; foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) { $id = (int) ($row['id'] ?? 0); if ($id > 0 && !isset($seen[$id])) { $rows[] = $row; $seen[$id] = true; } } return $rows; } public function createWidgetTemplate(string $ownerKey, array $data): int { if (!$this->available() || $ownerKey === '') { return 0; } $stmt = $this->pdo->prepare( "INSERT INTO nexus_widget_templates (owner_key, name, widget_type, description, visibility, config, created_at, updated_at) VALUES (:owner_key, :name, :widget_type, :description, :visibility, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ); $stmt->execute([ 'owner_key' => $ownerKey, 'name' => trim((string) ($data['name'] ?? '')) ?: 'Widget', 'widget_type' => $this->normalizeItemType((string) ($data['widget_type'] ?? 'link')), 'description' => trim((string) ($data['description'] ?? '')), 'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')), 'config' => $this->encodeJson((array) ($data['config'] ?? [])), ]); return (int) $this->pdo->lastInsertId(); } public function deleteWidgetTemplate(int $id, string $ownerKey): void { if (!$this->available() || $id <= 0 || $ownerKey === '') { return; } $stmt = $this->pdo->prepare("DELETE FROM nexus_widget_templates WHERE id = :id AND owner_key = :owner_key"); $stmt->execute(['id' => $id, 'owner_key' => $ownerKey]); } public function listSearchEngines(string $ownerKey, bool $includePublic = true): array { if (!$this->available()) { return []; } $clauses = []; $params = []; if ($ownerKey !== '') { $clauses[] = 'owner_key = :owner_key'; $params['owner_key'] = $ownerKey; } if ($includePublic) { $clauses[] = "visibility = 'public'"; } $stmt = $this->pdo->prepare( 'SELECT * FROM nexus_search_engines WHERE ' . implode(' OR ', $clauses) . ' ORDER BY is_default DESC, name ASC, id ASC' ); $stmt->execute($params); $rows = []; $seen = []; foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) { $id = (int) ($row['id'] ?? 0); if ($id > 0 && !isset($seen[$id])) { $rows[] = $row; $seen[$id] = true; } } return $rows; } public function createSearchEngine(string $ownerKey, array $data): int { if (!$this->available() || $ownerKey === '') { return 0; } $shortCode = $this->normalizeShortCode((string) ($data['short_code'] ?? '')); if ($shortCode === '') { $shortCode = 's' . random_int(100, 999); } $shortCode = $this->uniqueSearchShortCode($ownerKey, $shortCode, 0); $stmt = $this->pdo->prepare( "INSERT INTO nexus_search_engines (owner_key, name, short_code, engine_type, template_url, integration_id, visibility, is_default, config, created_at, updated_at) VALUES (:owner_key, :name, :short_code, :engine_type, :template_url, :integration_id, :visibility, :is_default, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ); $stmt->execute([ 'owner_key' => $ownerKey, 'name' => trim((string) ($data['name'] ?? '')) ?: 'Suche', 'short_code' => $shortCode, 'engine_type' => trim((string) ($data['engine_type'] ?? 'template')) ?: 'template', 'template_url' => trim((string) ($data['template_url'] ?? '')), 'integration_id' => !empty($data['integration_id']) ? (int) $data['integration_id'] : null, 'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')), 'is_default' => !empty($data['is_default']) ? 1 : 0, 'config' => $this->encodeJson((array) ($data['config'] ?? [])), ]); $id = (int) $this->pdo->lastInsertId(); if (!empty($data['is_default'])) { $this->setDefaultSearchEngine($ownerKey, $id); } return $id; } public function setDefaultSearchEngine(string $ownerKey, int $id): void { if (!$this->available() || $ownerKey === '' || $id <= 0) { return; } $stmt = $this->pdo->prepare("UPDATE nexus_search_engines SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE owner_key = :owner_key"); $stmt->execute(['owner_key' => $ownerKey]); $stmt = $this->pdo->prepare("UPDATE nexus_search_engines SET is_default = 1, updated_at = CURRENT_TIMESTAMP WHERE id = :id AND owner_key = :owner_key"); $stmt->execute(['id' => $id, 'owner_key' => $ownerKey]); } public function deleteSearchEngine(int $id, string $ownerKey): void { if (!$this->available() || $id <= 0 || $ownerKey === '') { return; } $stmt = $this->pdo->prepare("DELETE FROM nexus_search_engines WHERE id = :id AND owner_key = :owner_key"); $stmt->execute(['id' => $id, 'owner_key' => $ownerKey]); } public function listApps(string $ownerKey, bool $includePublic = true): array { if (!$this->available()) { return []; } $clauses = []; $params = []; if ($ownerKey !== '') { $clauses[] = 'owner_key = :owner_key'; $params['owner_key'] = $ownerKey; } if ($includePublic) { $clauses[] = "visibility = 'public'"; } $stmt = $this->pdo->prepare( 'SELECT * FROM nexus_apps WHERE ' . implode(' OR ', $clauses) . ' ORDER BY name ASC, id ASC' ); $stmt->execute($params); $rows = []; $seen = []; foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) { $id = (int) ($row['id'] ?? 0); if ($id > 0 && !isset($seen[$id])) { $rows[] = $row; $seen[$id] = true; } } return $rows; } public function createApp(string $ownerKey, array $data): int { if (!$this->available() || $ownerKey === '') { return 0; } $stmt = $this->pdo->prepare( "INSERT INTO nexus_apps (owner_key, name, description, app_url, icon_url, integration_id, visibility, config, created_at, updated_at) VALUES (:owner_key, :name, :description, :app_url, :icon_url, :integration_id, :visibility, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ); $stmt->execute([ 'owner_key' => $ownerKey, 'name' => trim((string) ($data['name'] ?? '')) ?: 'App', 'description' => trim((string) ($data['description'] ?? '')), 'app_url' => trim((string) ($data['app_url'] ?? '')), 'icon_url' => trim((string) ($data['icon_url'] ?? '')), 'integration_id' => !empty($data['integration_id']) ? (int) $data['integration_id'] : null, 'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')), 'config' => $this->encodeJson((array) ($data['config'] ?? [])), ]); return (int) $this->pdo->lastInsertId(); } public function deleteApp(int $id, string $ownerKey): void { if (!$this->available() || $id <= 0 || $ownerKey === '') { return; } $stmt = $this->pdo->prepare("DELETE FROM nexus_apps WHERE id = :id AND owner_key = :owner_key"); $stmt->execute(['id' => $id, 'owner_key' => $ownerKey]); } private function listSharedDashboardIds(string $ownerKey, array $groups): array { $result = []; $stmt = $this->pdo->prepare( "SELECT dashboard_id, share_type, share_value FROM nexus_dashboard_shares WHERE share_type = 'user' AND share_value = :share_value" ); $stmt->execute(['share_value' => $ownerKey]); foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) { $result[] = (int) ($row['dashboard_id'] ?? 0); } foreach ($groups as $group) { $group = trim((string) $group); if ($group === '') { continue; } $stmt = $this->pdo->prepare( "SELECT dashboard_id FROM nexus_dashboard_shares WHERE share_type = 'group' AND share_value = :share_value" ); $stmt->execute(['share_value' => $group]); foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) { $result[] = (int) ($row['dashboard_id'] ?? 0); } } return array_values(array_unique(array_filter($result, static fn (int $id): bool => $id > 0))); } private function getOwnedDashboard(int $id, string $ownerKey): ?array { $dashboard = $this->getDashboardById($id); if ($dashboard === null || (string) ($dashboard['owner_key'] ?? '') !== $ownerKey) { return null; } return $dashboard; } private function uniqueSlug(string $table, string $ownerKey, string $candidate, int $ignoreId): string { $slug = $this->slugify($candidate); $suffix = 1; while ($this->slugExists($table, $ownerKey, $slug, $ignoreId)) { $suffix++; $slug = $this->slugify($candidate) . '-' . $suffix; } return $slug; } private function slugExists(string $table, string $ownerKey, string $slug, int $ignoreId): bool { $sql = "SELECT id FROM {$table} WHERE owner_key = :owner_key AND slug = :slug"; if ($ignoreId > 0) { $sql .= " AND id <> :id"; } $sql .= " LIMIT 1"; $stmt = $this->pdo->prepare($sql); $params = ['owner_key' => $ownerKey, 'slug' => $slug]; if ($ignoreId > 0) { $params['id'] = $ignoreId; } $stmt->execute($params); return (bool) $stmt->fetchColumn(); } private function slugify(string $value): string { $value = trim(mb_strtolower($value)); $value = preg_replace('/[^a-z0-9]+/u', '-', $value) ?? ''; $value = trim($value, '-'); return $value !== '' ? $value : 'eintrag'; } private function nextSortOrder(string $table, string $field, string $value): int { $stmt = $this->pdo->prepare("SELECT COALESCE(MAX(sort_order), 0) + 10 FROM {$table} WHERE {$field} = :value"); $stmt->execute(['value' => $value]); return (int) $stmt->fetchColumn(); } private function nextItemSortOrder(int $dashboardId): int { $stmt = $this->pdo->prepare("SELECT COALESCE(MAX(sort_order), 0) + 10 FROM nexus_dashboard_items WHERE dashboard_id = :dashboard_id"); $stmt->execute(['dashboard_id' => $dashboardId]); return (int) $stmt->fetchColumn(); } private function normalizeVisibility(string $value): string { $value = trim($value); return in_array($value, ['private', 'group', 'public'], true) ? $value : 'private'; } private function normalizeItemType(string $value): string { $value = trim($value); return in_array($value, ['link', 'iframe', 'page_module', 'bookmark_group', 'module_link', 'app', 'widget_template'], true) ? $value : 'link'; } private function normalizePageModuleType(string $value): string { $value = trim($value); return in_array($value, ['link', 'iframe', 'bookmark_group', 'external_status'], true) ? $value : 'link'; } private function normalizeOpenMode(string $value): string { $value = trim($value); return in_array($value, ['embed', 'new_tab', 'same_tab'], true) ? $value : 'embed'; } private function encodeJson(array $value): string { $encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); return $encoded === false ? '{}' : $encoded; } private function decodeJson(mixed $value): array { if (!is_string($value) || trim($value) === '') { return []; } $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } private function hydrateRows(array $rows): array { return array_values(array_map(fn (array $row): array => $this->hydrateRow($row), $rows)); } private function hydrateRow(array $row): array { foreach (['config', 'secrets'] as $jsonField) { if (array_key_exists($jsonField, $row)) { $row[$jsonField] = $this->decodeJson($row[$jsonField]); } } foreach (['is_default', 'is_active'] as $boolField) { if (array_key_exists($boolField, $row)) { $row[$boolField] = !empty($row[$boolField]); } } return $row; } private function nullableInt(mixed $value): ?int { if ($value === null || $value === '') { return null; } return (int) $value; } private function settings(): array { return function_exists('nexus_settings') ? nexus_settings() : []; } private function canAccessDashboard(array $dashboard, string $ownerKey, array $groups): bool { if ((string) ($dashboard['owner_key'] ?? '') === $ownerKey) { return true; } $visibility = (string) ($dashboard['visibility'] ?? 'private'); if ($visibility === 'public') { return true; } if ($visibility === 'group') { $sharedIds = $this->listSharedDashboardIds($ownerKey, $groups); return in_array((int) ($dashboard['id'] ?? 0), $sharedIds, true); } return false; } private function normalizeShortCode(string $value): string { $value = strtolower(trim($value)); $value = preg_replace('/[^a-z0-9_-]+/', '', $value) ?? ''; return substr($value, 0, 24); } private function uniqueSearchShortCode(string $ownerKey, string $candidate, int $ignoreId): string { $shortCode = $candidate; $suffix = 1; while ($this->searchShortCodeExists($ownerKey, $shortCode, $ignoreId)) { $suffix++; $shortCode = substr($candidate, 0, 20) . $suffix; } return $shortCode; } private function searchShortCodeExists(string $ownerKey, string $shortCode, int $ignoreId): bool { $sql = "SELECT id FROM nexus_search_engines WHERE owner_key = :owner_key AND short_code = :short_code"; if ($ignoreId > 0) { $sql .= " AND id <> :id"; } $sql .= " LIMIT 1"; $stmt = $this->pdo->prepare($sql); $params = ['owner_key' => $ownerKey, 'short_code' => $shortCode]; if ($ignoreId > 0) { $params['id'] = $ignoreId; } $stmt->execute($params); return (bool) $stmt->fetchColumn(); } }