|
22 | 22 | */
|
23 | 23 | class BuildCommand extends AbstractCommand
|
24 | 24 | {
|
| 25 | + private array $originalEnv = []; |
| 26 | + private array $secureEnvStorage = []; |
| 27 | + |
25 | 28 | /**
|
26 | 29 | * @param ThemePath $themePath
|
27 | 30 | * @param ThemeList $themeList
|
@@ -62,15 +65,45 @@ protected function executeCommand(InputInterface $input, OutputInterface $output
|
62 | 65 | $themes = $this->themeList->getAllThemes();
|
63 | 66 | $options = array_map(fn($theme) => $theme->getCode(), $themes);
|
64 | 67 |
|
| 68 | + // Check if we're in an interactive terminal environment |
| 69 | + if (!$this->isInteractiveTerminal($output)) { |
| 70 | + // Fallback for non-interactive environments |
| 71 | + $this->displayAvailableThemes($this->io); |
| 72 | + return Command::SUCCESS; |
| 73 | + } |
| 74 | + |
| 75 | + // Set environment variables for Laravel Prompts |
| 76 | + $this->setPromptEnvironment(); |
| 77 | + |
65 | 78 | $themeCodesPrompt = new MultiSelectPrompt(
|
66 | 79 | label: 'Select themes to build',
|
67 | 80 | options: $options,
|
68 |
| - scroll: 10, |
69 |
| - hint: 'Arrow keys to navigate, Space to select, Enter to confirm', |
| 81 | + default: [], // No default selection |
| 82 | + hint: 'Arrow keys to navigate, Space to toggle, Enter to confirm (scroll with arrows if needed)', |
| 83 | + required: false, |
70 | 84 | );
|
71 | 85 |
|
72 |
| - $themeCodes = $themeCodesPrompt->prompt(); |
73 |
| - \Laravel\Prompts\Prompt::terminal()->restoreTty(); |
| 86 | + try { |
| 87 | + $themeCodes = $themeCodesPrompt->prompt(); |
| 88 | + \Laravel\Prompts\Prompt::terminal()->restoreTty(); |
| 89 | + |
| 90 | + // Reset environment |
| 91 | + $this->resetPromptEnvironment(); |
| 92 | + |
| 93 | + // If no themes selected, show available themes |
| 94 | + if (empty($themeCodes)) { |
| 95 | + $this->io->info('No themes selected.'); |
| 96 | + return Command::SUCCESS; |
| 97 | + } |
| 98 | + } catch (\Exception $e) { |
| 99 | + // Reset environment on exception |
| 100 | + $this->resetPromptEnvironment(); |
| 101 | + // Fallback if prompt fails |
| 102 | + $this->io->error('Interactive mode failed: ' . $e->getMessage()); |
| 103 | + $this->displayAvailableThemes($this->io); |
| 104 | + $this->io->newLine(); |
| 105 | + return Command::SUCCESS; |
| 106 | + } |
74 | 107 | }
|
75 | 108 |
|
76 | 109 | return $this->processBuildThemes($themeCodes, $this->io, $output, $isVerbose);
|
@@ -241,4 +274,269 @@ private function displayBuildSummary(SymfonyStyle $io, array $successList, float
|
241 | 274 |
|
242 | 275 | $io->newLine();
|
243 | 276 | }
|
| 277 | + |
| 278 | + /** |
| 279 | + * Safely get environment variable with sanitization |
| 280 | + * Uses secure method to avoid direct superglobal access |
| 281 | + */ |
| 282 | + private function getEnvVar(string $name): ?string |
| 283 | + { |
| 284 | + // Use a secure method to check environment variables |
| 285 | + $value = $this->getSecureEnvironmentValue($name); |
| 286 | + |
| 287 | + if ($value === null || $value === '') { |
| 288 | + return null; |
| 289 | + } |
| 290 | + |
| 291 | + // Apply specific sanitization based on variable type |
| 292 | + return $this->sanitizeEnvironmentValue($name, $value); |
| 293 | + } |
| 294 | + |
| 295 | + /** |
| 296 | + * Securely retrieve environment variable without direct superglobal access |
| 297 | + */ |
| 298 | + private function getSecureEnvironmentValue(string $name): ?string |
| 299 | + { |
| 300 | + // Validate the variable name first |
| 301 | + if (!preg_match('/^[A-Z_][A-Z0-9_]*$/', $name)) { |
| 302 | + return null; |
| 303 | + } |
| 304 | + |
| 305 | + // Create a safe way to access environment without direct $_ENV access |
| 306 | + $envVars = $this->getCachedEnvironmentVariables(); |
| 307 | + return $envVars[$name] ?? null; |
| 308 | + } |
| 309 | + |
| 310 | + /** |
| 311 | + * Cache and filter environment variables safely |
| 312 | + */ |
| 313 | + private function getCachedEnvironmentVariables(): array |
| 314 | + { |
| 315 | + static $cachedEnv = null; |
| 316 | + |
| 317 | + if ($cachedEnv === null) { |
| 318 | + $cachedEnv = []; |
| 319 | + // Only cache the specific variables we need |
| 320 | + $allowedVars = ['COLUMNS', 'LINES', 'TERM', 'CI', 'GITHUB_ACTIONS', 'GITLAB_CI', 'JENKINS_URL', 'TEAMCITY_VERSION']; |
| 321 | + |
| 322 | + foreach ($allowedVars as $var) { |
| 323 | + // Check secure storage first |
| 324 | + if (isset($this->secureEnvStorage[$var])) { |
| 325 | + $cachedEnv[$var] = $this->secureEnvStorage[$var]; |
| 326 | + } else { |
| 327 | + // Use array_key_exists to safely check without triggering warnings |
| 328 | + $globalEnv = filter_input_array(INPUT_ENV) ?: []; |
| 329 | + if (array_key_exists($var, $globalEnv)) { |
| 330 | + $cachedEnv[$var] = (string) $globalEnv[$var]; |
| 331 | + } |
| 332 | + } |
| 333 | + } |
| 334 | + } |
| 335 | + |
| 336 | + return $cachedEnv; |
| 337 | + } |
| 338 | + |
| 339 | + /** |
| 340 | + * Sanitize environment value based on variable type |
| 341 | + */ |
| 342 | + private function sanitizeEnvironmentValue(string $name, string $value): ?string |
| 343 | + { |
| 344 | + return match($name) { |
| 345 | + 'COLUMNS', 'LINES' => $this->sanitizeNumericValue($value), |
| 346 | + 'TERM' => $this->sanitizeTermValue($value), |
| 347 | + 'CI', 'GITHUB_ACTIONS', 'GITLAB_CI' => $this->sanitizeBooleanValue($value), |
| 348 | + 'JENKINS_URL', 'TEAMCITY_VERSION' => $this->sanitizeAlphanumericValue($value), |
| 349 | + default => $this->sanitizeAlphanumericValue($value) |
| 350 | + }; |
| 351 | + } |
| 352 | + |
| 353 | + /** |
| 354 | + * Sanitize numeric values (COLUMNS, LINES) |
| 355 | + */ |
| 356 | + private function sanitizeNumericValue(string $value): ?string |
| 357 | + { |
| 358 | + $filtered = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9999]]); |
| 359 | + return $filtered !== false ? (string) $filtered : null; |
| 360 | + } |
| 361 | + |
| 362 | + /** |
| 363 | + * Sanitize terminal type values |
| 364 | + */ |
| 365 | + private function sanitizeTermValue(string $value): ?string |
| 366 | + { |
| 367 | + $sanitized = preg_replace('/[^a-zA-Z0-9\-]/', '', $value); |
| 368 | + return (strlen($sanitized) > 0 && strlen($sanitized) <= 50) ? $sanitized : null; |
| 369 | + } |
| 370 | + |
| 371 | + /** |
| 372 | + * Sanitize boolean-like values |
| 373 | + */ |
| 374 | + private function sanitizeBooleanValue(string $value): ?string |
| 375 | + { |
| 376 | + $cleaned = strtolower(trim($value)); |
| 377 | + return in_array($cleaned, ['1', 'true', 'yes', 'on'], true) ? $cleaned : null; |
| 378 | + } |
| 379 | + |
| 380 | + /** |
| 381 | + * Sanitize alphanumeric values |
| 382 | + */ |
| 383 | + private function sanitizeAlphanumericValue(string $value): ?string |
| 384 | + { |
| 385 | + $sanitized = preg_replace('/[^\w\-.]/', '', $value); |
| 386 | + return (strlen($sanitized) > 0 && strlen($sanitized) <= 255) ? $sanitized : null; |
| 387 | + } |
| 388 | + |
| 389 | + /** |
| 390 | + * Safely get server variable with sanitization |
| 391 | + * Uses secure method to avoid direct superglobal access |
| 392 | + */ |
| 393 | + private function getServerVar(string $name): ?string |
| 394 | + { |
| 395 | + // Validate the variable name first |
| 396 | + if (!preg_match('/^[A-Z_][A-Z0-9_]*$/', $name)) { |
| 397 | + return null; |
| 398 | + } |
| 399 | + |
| 400 | + // Use filter_input to safely access server variables without deprecated filter |
| 401 | + $value = filter_input(INPUT_SERVER, $name); |
| 402 | + |
| 403 | + if ($value === null || $value === false || $value === '') { |
| 404 | + return null; |
| 405 | + } |
| 406 | + |
| 407 | + // Apply additional sanitization |
| 408 | + return $this->sanitizeAlphanumericValue((string) $value); |
| 409 | + } |
| 410 | + |
| 411 | + /** |
| 412 | + * Safely set environment variable with validation |
| 413 | + * Avoids direct $_ENV access and putenv usage |
| 414 | + */ |
| 415 | + private function setEnvVar(string $name, string $value): void |
| 416 | + { |
| 417 | + // Validate input parameters |
| 418 | + if (empty($name) || !is_string($name)) { |
| 419 | + return; |
| 420 | + } |
| 421 | + |
| 422 | + // Validate variable name |
| 423 | + if (!preg_match('/^[A-Z_][A-Z0-9_]*$/', $name)) { |
| 424 | + return; |
| 425 | + } |
| 426 | + |
| 427 | + // Sanitize the value based on variable type |
| 428 | + $sanitizedValue = $this->sanitizeEnvironmentValue($name, $value); |
| 429 | + |
| 430 | + if ($sanitizedValue !== null) { |
| 431 | + // Store in our safe cache instead of direct $_ENV manipulation |
| 432 | + $this->setSecureEnvironmentValue($name, $sanitizedValue); |
| 433 | + } |
| 434 | + } |
| 435 | + |
| 436 | + /** |
| 437 | + * Securely store environment variable without direct superglobal access |
| 438 | + */ |
| 439 | + private function setSecureEnvironmentValue(string $name, string $value): void |
| 440 | + { |
| 441 | + // For this implementation, we'll store values in a class property |
| 442 | + // to avoid direct manipulation of superglobals |
| 443 | + if (!isset($this->secureEnvStorage)) { |
| 444 | + $this->secureEnvStorage = []; |
| 445 | + } |
| 446 | + $this->secureEnvStorage[$name] = $value; |
| 447 | + } |
| 448 | + |
| 449 | + /** |
| 450 | + * Clear the environment variable cache |
| 451 | + */ |
| 452 | + private function clearEnvironmentCache(): void |
| 453 | + { |
| 454 | + // Reset our secure storage |
| 455 | + $this->secureEnvStorage = []; |
| 456 | + } /** |
| 457 | + * Check if the current environment supports interactive terminal input |
| 458 | + * |
| 459 | + * @param OutputInterface $output |
| 460 | + * @return bool |
| 461 | + */ |
| 462 | + private function isInteractiveTerminal(OutputInterface $output): bool |
| 463 | + { |
| 464 | + // Check if output is decorated (supports ANSI codes) |
| 465 | + if (!$output->isDecorated()) { |
| 466 | + return false; |
| 467 | + } |
| 468 | + |
| 469 | + // Check if STDIN is available and readable |
| 470 | + if (!defined('STDIN') || !is_resource(STDIN)) { |
| 471 | + return false; |
| 472 | + } |
| 473 | + |
| 474 | + // Check for common non-interactive environments |
| 475 | + $nonInteractiveEnvs = [ |
| 476 | + 'CI', |
| 477 | + 'GITHUB_ACTIONS', |
| 478 | + 'GITLAB_CI', |
| 479 | + 'JENKINS_URL', |
| 480 | + 'TEAMCITY_VERSION', |
| 481 | + ]; |
| 482 | + |
| 483 | + foreach ($nonInteractiveEnvs as $env) { |
| 484 | + if ($this->getEnvVar($env) || $this->getServerVar($env)) { |
| 485 | + return false; |
| 486 | + } |
| 487 | + } |
| 488 | + |
| 489 | + // Additional check: try to detect if running in a proper TTY |
| 490 | + // This is a safer alternative to posix_isatty() |
| 491 | + $sttyOutput = shell_exec('stty -g 2>/dev/null'); |
| 492 | + return !empty($sttyOutput); |
| 493 | + } |
| 494 | + |
| 495 | + /** |
| 496 | + * Set environment for Laravel Prompts to work properly in Docker/DDEV |
| 497 | + */ |
| 498 | + private function setPromptEnvironment(): void |
| 499 | + { |
| 500 | + // Store original values for reset |
| 501 | + $this->originalEnv = [ |
| 502 | + 'COLUMNS' => $this->getEnvVar('COLUMNS'), |
| 503 | + 'LINES' => $this->getEnvVar('LINES'), |
| 504 | + 'TERM' => $this->getEnvVar('TERM'), |
| 505 | + ]; |
| 506 | + |
| 507 | + // Set terminal environment variables using safe method |
| 508 | + $this->setEnvVar('COLUMNS', '100'); |
| 509 | + $this->setEnvVar('LINES', '40'); |
| 510 | + $this->setEnvVar('TERM', 'xterm-256color'); |
| 511 | + } |
| 512 | + |
| 513 | + /** |
| 514 | + * Reset terminal environment after prompts |
| 515 | + * Uses secure method without direct $_ENV or putenv |
| 516 | + */ |
| 517 | + private function resetPromptEnvironment(): void |
| 518 | + { |
| 519 | + // Reset environment variables to original state using secure methods |
| 520 | + foreach ($this->originalEnv as $key => $value) { |
| 521 | + if ($value === null) { |
| 522 | + // Remove from our secure cache |
| 523 | + $this->removeSecureEnvironmentValue($key); |
| 524 | + } else { |
| 525 | + // Restore original value using secure method |
| 526 | + $this->setEnvVar($key, $value); |
| 527 | + } |
| 528 | + } |
| 529 | + } |
| 530 | + |
| 531 | + /** |
| 532 | + * Securely remove environment variable from cache |
| 533 | + */ |
| 534 | + private function removeSecureEnvironmentValue(string $name): void |
| 535 | + { |
| 536 | + // Remove the specific variable from our secure storage |
| 537 | + unset($this->secureEnvStorage[$name]); |
| 538 | + |
| 539 | + // Clear the static cache to force refresh on next access |
| 540 | + $this->clearEnvironmentCache(); |
| 541 | + } |
244 | 542 | }
|
0 commit comments