query('allow_prerelease', '0') === '1'; $relation = $allowPrerelease ? 'latestVersion' : 'latestStableVersion'; $query = Package::query() ->with([$relation, 'categories']) ->where('status', 'published'); if ($type = $request->query('type')) { $query->where('type', $type); } if ($keyword = trim((string) $request->query('keyword', ''))) { $query->where(function ($q) use ($keyword) { $q->where('name', 'like', '%' . $keyword . '%') ->orWhere('summary', 'like', '%' . $keyword . '%') ->orWhere('slug', 'like', '%' . $keyword . '%'); }); } if ($category = $request->query('category')) { $query->whereHas('categories', function ($q) use ($category) { $q->where('slug', $category); }); } $sort = $request->query('sort', 'latest'); if ($sort === 'downloads') { $query->orderByDesc('download_count')->orderByDesc('sort_order'); } elseif ($sort === 'name') { $query->orderBy('name'); } else { $query->orderByDesc('sort_order')->orderByDesc('updated_at'); } $size = min(max((int) $request->query('size', 20), 1), 100); $page = max((int) $request->query('page', 1), 1); $result = $query->paginate($size, ['*'], 'page', $page); return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => [ 'page' => $result->currentPage(), 'size' => $result->perPage(), 'total' => $result->total(), 'items' => collect($result->items())->map(function ($package) use ($allowPrerelease) { return RepoFormatter::packageListItem($package, $allowPrerelease); })->values()->all(), ], ]); } public function detail(string $type, string $slug): JsonResponse { $package = Package::query() ->with([ 'categories', 'screenshots', 'versions' => function ($q) { $q->orderByDesc('published_at')->orderByDesc('id'); }, ]) ->where('status', 'published') ->where('type', $type) ->where('slug', $slug) ->first(); if (!$package) { return response()->json([ 'code' => 404, 'message' => 'package not found', 'data' => null, ], 404); } return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => RepoFormatter::packageDetail($package, false), ]); } public function checkUpdates(Request $request): JsonResponse { $installed = $request->input('installed', []); $typechoVersion = (string) $request->input('typecho_version', ''); $phpVersion = (string) $request->input('php_version', ''); $allowPrerelease = (bool) $request->input('allow_prerelease', false); $updates = []; $upToDate = []; $incompatible = []; $unknown = []; foreach ($installed as $item) { $type = $item['type'] ?? ''; $slug = $item['slug'] ?? ''; $currentVersion = $item['version'] ?? ''; $package = Package::query() ->with(['latestStableVersion', 'latestVersion']) ->where('status', 'published') ->where('type', $type) ->where('slug', $slug) ->first(); $latest = $allowPrerelease ? ($package?->latestVersion ?: $package?->latestStableVersion) : $package?->latestStableVersion; if (!$package || !$latest) { $unknown[] = [ 'type' => $type, 'slug' => $slug, 'current_version' => $currentVersion, ]; continue; } if (version_compare($currentVersion, $latest->version, '>=')) { $upToDate[] = [ 'type' => $type, 'slug' => $slug, 'current_version' => $currentVersion, 'latest_version' => $latest->version, ]; continue; } $compatibilityOk = $this->isCompatible($latest, $typechoVersion, $phpVersion); if (!$compatibilityOk) { $incompatible[] = [ 'type' => $type, 'slug' => $slug, 'current_version' => $currentVersion, 'latest_version' => $latest->version, ]; continue; } $updates[] = [ 'type' => $type, 'slug' => $slug, 'name' => $package->name, 'current_version' => $currentVersion, 'latest_version' => $latest->version, 'changelog' => $latest->changelog, 'compatibility_ok' => true, 'package' => [ 'size' => (int) $latest->package_size, 'sha256' => $latest->sha256, 'download_url' => $latest->package_url, ], ]; } return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => [ 'updates' => $updates, 'up_to_date' => $upToDate, 'incompatible' => $incompatible, 'unknown' => $unknown, 'store_plugin_update' => null, ], ]); } public function download(Request $request, string $type, string $slug, string $version): JsonResponse|BinaryFileResponse|RedirectResponse { $pluginToken = (string) config('store.plugin_access_token', ''); $requestToken = (string) ($request->header('X-Store-Plugin-Token') ?: $request->query('access_token', '')); if ($pluginToken !== '' && $requestToken !== $pluginToken) { return response()->json([ 'code' => 403, 'message' => 'download forbidden', 'data' => null, ], 403); } $package = Package::query() ->where('status', 'published') ->where('type', $type) ->where('slug', $slug) ->first(); if (!$package) { return response()->json([ 'code' => 404, 'message' => 'package not found', 'data' => null, ], 404); } $versionModel = $version === 'latest' ? $package->versions()->where('is_stable', 1)->where('is_latest', 1)->first() : $package->versions()->where('version', $version)->first(); if (!$versionModel) { return response()->json([ 'code' => 404, 'message' => 'version not found', 'data' => null, ], 404); } $filePath = storage_path('app/packages/' . $type . '/' . $slug . '/' . $versionModel->version . '.zip'); $hasLocalFile = is_file($filePath); $redirect = (string) $request->query('redirect', '0') === '1'; if ($redirect) { DownloadLog::create([ 'package_id' => $package->id, 'version_id' => $versionModel->id, 'site_url' => (string) $request->query('site_url', ''), 'typecho_version' => (string) $request->query('typecho_version', ''), 'php_version' => (string) $request->query('php_version', ''), 'ip' => (string) $request->ip(), 'user_agent' => (string) $request->userAgent(), 'created_at' => now(), ]); $package->increment('download_count'); $versionModel->increment('download_count'); if ($hasLocalFile) { return response()->download($filePath, $slug . '-' . $versionModel->version . '.zip', [ 'Content-Type' => 'application/zip', ]); } if (!empty($versionModel->package_url)) { return redirect()->away($versionModel->package_url); } return response()->json([ 'code' => 404, 'message' => 'package file not found', 'data' => null, ], 404); } return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => [ 'type' => $type, 'slug' => $slug, 'version' => $versionModel->version, 'package' => [ 'download_url' => $this->buildDownloadUrl($request, $type, $slug, $versionModel->version), 'size' => (int) $versionModel->package_size, 'sha256' => $versionModel->sha256, 'filename' => $slug . '-' . $versionModel->version . '.zip', 'storage' => [ 'driver' => $hasLocalFile ? 'local' : 'remote', 'exists' => $hasLocalFile, ], ], ], ]); } private function isCompatible($version, string $typechoVersion, string $phpVersion): bool { if ($typechoVersion !== '' && $version->typecho_min !== '' && version_compare($typechoVersion, $version->typecho_min, '<')) { return false; } if ($typechoVersion !== '' && $version->typecho_max !== '') { $max = str_replace('*', '999', $version->typecho_max); if (version_compare($typechoVersion, $max, '>')) { return false; } } if ($phpVersion !== '' && $version->php_min !== '' && version_compare($phpVersion, $version->php_min, '<')) { return false; } if ($phpVersion !== '' && $version->php_max !== '') { $max = str_replace('*', '999', $version->php_max); if (version_compare($phpVersion, $max, '>')) { return false; } } return true; } private function buildDownloadUrl(Request $request, string $type, string $slug, string $version): string { $path = '/api/v1/repo/download/' . $type . '/' . $slug . '/' . $version . '?redirect=1'; $appUrl = rtrim((string) config('app.url', ''), '/'); if ($appUrl !== '') { return $appUrl . $path; } return $request->getSchemeAndHttpHost() . $path; } }