['label' => 'Windows', 'ext' => 'exe', 'stored' => 'DeskHub-Setup.exe', 'download' => 'DeskHub-Setup.exe', 'magic' => 'MZ', 'mime' => 'application/vnd.microsoft.portable-executable'], 'android' => ['label' => 'Android', 'ext' => 'apk', 'stored' => 'DeskHub.apk', 'download' => 'DeskHub.apk', 'magic' => 'PK', 'mime' => 'application/vnd.android.package-archive'], 'minecraft' => ['label' => 'Minecraft', 'ext' => 'jar', 'stored' => 'DeskHub-Minecraft.jar', 'download' => 'DeskHub-Minecraft.jar', 'magic' => 'PK', 'mime' => 'application/java-archive'], 'wordpress' => ['label' => 'WordPress', 'ext' => 'zip', 'stored' => 'deskhub-tickets-wordpress.zip', 'download' => 'deskhub-tickets-wordpress.zip', 'magic' => 'PK', 'mime' => 'application/zip'], 'chrome' => ['label' => 'Chrome', 'ext' => 'zip', 'stored' => 'deskhub-chrome-erweiterung.zip', 'download' => 'deskhub-chrome-erweiterung.zip', 'magic' => 'PK', 'mime' => 'application/zip'], ]; } function app_downloads_dir(): string { return rtrim((string) (getenv('DESKHUB_APPS_DIR') ?: dirname(__DIR__) . '/app_downloads'), '/\\'); } /** Official publisher release endpoint; the access key stays on the server. */ function app_publisher_endpoint(string $platform, bool $download = false): string { if (!isset(app_platforms()[$platform]) || !function_exists('catalog_defaults')) return ''; $base = (string) (catalog_defaults()['plugins_url'] ?? ''); $parts = parse_url($base); if (!$parts || ($parts['scheme'] ?? '') !== 'https' || empty($parts['host'])) return ''; $path = rtrim(str_replace('\\', '/', dirname((string) ($parts['path'] ?? '/'))), '/'); return 'https://' . $parts['host'] . ($parts['port'] ?? null ? ':' . (int) $parts['port'] : '') . $path . '/app-release.php?platform=' . rawurlencode($platform) . ($download ? '&download=1' : ''); } function app_publisher_request(string $platform, bool $download = false) { $url = app_publisher_endpoint($platform, $download); if ($url === '') return false; $headers = function_exists('catalog_access_headers') ? catalog_access_headers($url) : []; if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => $download ? 180 : 4, CURLOPT_HTTPHEADER => $headers]); $body = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return $code === 200 ? $body : false; } $ctx = stream_context_create(['http' => ['method' => 'GET', 'timeout' => $download ? 180 : 4, 'ignore_errors' => true, 'header' => implode("\r\n", $headers)]]); $body = @file_get_contents($url, false, $ctx); $status = $http_response_header[0] ?? ''; return is_string($body) && str_contains($status, ' 200 ') ? $body : false; } function app_publisher_info(string $platform): ?array { $cacheKey = 'app_publisher_' . $platform . '_cache'; $cached = json_decode((string) get_setting($cacheKey, ''), true); if (is_array($cached) && (int) ($cached['at'] ?? 0) > time() - 60) return is_array($cached['info'] ?? null) ? $cached['info'] : null; $json = app_publisher_request($platform); $data = is_string($json) ? json_decode($json, true) : null; if (!is_array($data) || empty($data['version']) || empty($data['sha256'])) { if (is_array($cached) && is_array($cached['info'] ?? null)) return $cached['info']; set_setting($cacheKey, json_encode(['at' => time(), 'info' => null])); return null; } $p = app_platforms()[$platform] ?? null; if (!$p) return null; $info = ['platform' => $platform, 'label' => $p['label'], 'size' => (int) ($data['size'] ?? 0), 'sha256' => (string) $data['sha256'], 'modified_at' => (string) ($data['created_at'] ?? ''), 'filename' => $p['download'], 'mime' => $p['mime'], 'version' => (string) $data['version'], 'notes' => (string) ($data['notes'] ?? ''), 'source' => 'publisher']; set_setting($cacheKey, json_encode(['at' => time(), 'info' => $info], JSON_UNESCAPED_UNICODE)); return $info; } /** Legt den Ordner an und sperrt den Direktzugriff. */ function app_downloads_prepare_dir(): bool { $dir = app_downloads_dir(); if (!is_dir($dir) && !@mkdir($dir, 0755, true)) return false; $htaccess = $dir . '/.htaccess'; if (!is_file($htaccess)) { @file_put_contents($htaccess, "# Nur über staff/app_download.php (mit Anmeldung) abrufbar\n\n Require all denied\n\n\n Order deny,allow\n Deny from all\n\nOptions -Indexes\n"); } if (!is_file($dir . '/index.html')) @file_put_contents($dir . '/index.html', ''); return is_writable($dir); } /** * Angaben zu einer App oder null, wenn die Datei nicht im Ordner liegt. Version gibt es nicht: Die Dateien werden * per FTP abgelegt, angezeigt werden Datum, Größe und eine berechnete SHA-256-Prüfsumme (zwischengespeichert, * solange sich Änderungsdatum und Größe nicht ändern). */ function app_download_info(string $platform): ?array { $platforms = app_platforms(); if (!isset($platforms[$platform])) return null; $p = $platforms[$platform]; $path = app_downloads_dir() . '/' . $p['stored']; clearstatcache(true, $path); if (!is_file($path) || (int) filesize($path) < 1024) return null; $size = (int) filesize($path); $mtime = (int) filemtime($path); $cacheKey = "app_{$platform}_hash_cache"; $cached = explode(':', (string) get_setting($cacheKey, ''), 3); if (count($cached) === 3 && (int) $cached[0] === $mtime && (int) $cached[1] === $size) { $sha = $cached[2]; } else { $sha = (string) hash_file('sha256', $path); set_setting($cacheKey, "{$mtime}:{$size}:{$sha}"); } return [ 'platform' => $platform, 'label' => $p['label'], 'size' => $size, 'sha256' => $sha, 'modified_at' => date('Y-m-d H:i:s', $mtime), 'path' => $path, 'filename' => $p['download'], 'mime' => $p['mime'], ]; } function app_format_size(int $bytes): string { if ($bytes >= 1048576) return number_format($bytes / 1048576, 1, ',', '.') . ' MB'; return number_format(max(1, $bytes / 1024), 0, ',', '.') . ' KB'; } /** Minecraft-Plugin auf der Apps-Seite zeigen? Einstellung unter Einstellungen → System (Standard: aus). */ function app_minecraft_enabled(): bool { return get_setting('minecraft_enabled', '0') === '1'; }