resolveThemePath((string) $this->argument('theme')); $theme = $this->loader->fromPath($path); $issues = $this->validator->validate($theme); $this->info("Theme: {$theme->name()} ({$theme->key()})"); $this->line('Manifest schema: '.$theme->manifestVersion()); if ($issues === []) { $this->info('Theme validation passed with no issues.'); return self::SUCCESS; } $this->table( ['Severity', 'File', 'Message'], array_map(fn (array $issue) => [ strtoupper($issue['severity']), $issue['file'], $issue['message'], ], $issues), ); $errors = count(array_filter($issues, fn (array $issue) => $issue['severity'] === 'error')); $warnings = count($issues) - $errors; $this->line("{$errors} error(s), {$warnings} warning(s)."); return $errors > 0 ? self::FAILURE : self::SUCCESS; } catch (\Throwable $exception) { $this->error($exception->getMessage()); return self::FAILURE; } finally { if ($temporary !== null) { $this->files->deleteDirectory($temporary); } } } /** @return array{0:string,1:?string} */ private function resolveThemePath(string $input): array { if ($this->files->isDirectory($input)) { return [realpath($input) ?: $input, null]; } if ($this->files->isFile($input) && strtolower(pathinfo($input, PATHINFO_EXTENSION)) === 'zip') { $temporary = storage_path('app/stylist/validate-'.bin2hex(random_bytes(8))); $this->files->ensureDirectoryExists($temporary); $zip = new ZipArchive(); if ($zip->open($input) !== true) { throw new RuntimeException('The theme ZIP could not be opened.'); } for ($index = 0; $index < $zip->numFiles; $index++) { $name = (string) $zip->getNameIndex($index); if (str_contains($name, '..') || str_starts_with($name, '/') || str_contains($name, "\0")) { $zip->close(); throw new RuntimeException('The ZIP contains an unsafe path.'); } } $zip->extractTo($temporary); $zip->close(); $manifest = collect($this->files->allFiles($temporary)) ->first(fn ($file) => $file->getFilename() === 'theme.json'); if (! $manifest) { throw new RuntimeException('The package does not contain theme.json.'); } return [$manifest->getPath(), $temporary]; } return [$this->loader->find($input)->path(), null]; } }