Skip to content

Commit 049c59f

Browse files
authored
Fix downloads.php crash on invalid os parameter (#1968)
1 parent 65265f9 commit 049c59f

4 files changed

Lines changed: 359 additions & 76 deletions

File tree

public/downloads.php

Lines changed: 54 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<?php
2+
use phpweb\Downloads\OptionResolver;
3+
24
$_SERVER['BASE_PAGE'] = 'downloads.php';
35
require_once __DIR__ . '/../include/prepend.inc';
46
require_once __DIR__ . '/../include/gpg-keys.inc';
@@ -7,6 +9,58 @@
79
// Try to make this page non-cached
810
header_nocache();
911

12+
$os = [
13+
'linux' => [
14+
'name' => 'Linux',
15+
'variants' => [
16+
'linux-debian' => 'Debian',
17+
'linux-fedora' => 'Fedora',
18+
'linux-redhat' => 'RedHat',
19+
'linux-ubuntu' => 'Ubuntu',
20+
'linux-docker-cli' => 'Docker (Command Line Interface)',
21+
'linux-docker-web' => 'Docker (Web Development)',
22+
],
23+
],
24+
'osx' => [
25+
'name' => 'macOS',
26+
'variants' => [
27+
'osx-homebrew' => 'Homebrew',
28+
'osx-homebrew-php' => 'Homebrew-PHP',
29+
'osx-docker-cli' => 'Docker (Command Line Interface)',
30+
'osx-docker-web' => 'Docker (Web Development)',
31+
'osx-macports' => 'MacPorts',
32+
],
33+
],
34+
'windows' => [
35+
'name' => 'Windows',
36+
'variants' => [
37+
'windows-downloads' => 'ZIP Downloads',
38+
'windows-native' => 'Single Line Installer',
39+
'windows-chocolatey' => 'Chocolatey',
40+
'windows-scoop' => 'Scoop',
41+
'windows-winget' => 'Winget',
42+
'windows-docker-cli' => 'Docker (Command Line Interface)',
43+
'windows-docker-web' => 'Docker (Web Development)',
44+
'windows-wsl-debian' => 'WSL/Debian',
45+
'windows-wsl-ubuntu' => 'WSL/Ubuntu',
46+
],
47+
],
48+
];
49+
50+
// An invalid ?os= redirects to the auto-detected results before any output.
51+
$resolution = (new OptionResolver($os))->resolve(
52+
$_GET,
53+
$_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '',
54+
$_SERVER['HTTP_USER_AGENT'] ?? '',
55+
);
56+
57+
if ($resolution->redirectQuery !== null) {
58+
header('Location: /downloads.php?' . $resolution->redirectQuery, true, 302);
59+
exit;
60+
}
61+
62+
$options = $resolution->options;
63+
1064
$SIDEBAR_DATA = '
1165
<div class="panel">
1266
<a href="/supported-versions.php">Supported Versions</a>
@@ -52,44 +106,6 @@ function option(string $value, string $desc, $attributes = []): string
52106
return '<option value="' . $value . '"' . implode(' ', array_keys(array_filter($attributes))) . '>' . $desc . '</option>';
53107
}
54108

55-
$os = [
56-
'linux' => [
57-
'name' => 'Linux',
58-
'variants' => [
59-
'linux-debian' => 'Debian',
60-
'linux-fedora' => 'Fedora',
61-
'linux-redhat' => 'RedHat',
62-
'linux-ubuntu' => 'Ubuntu',
63-
'linux-docker-cli' => 'Docker (Command Line Interface)',
64-
'linux-docker-web' => 'Docker (Web Development)',
65-
],
66-
],
67-
'osx' => [
68-
'name' => 'macOS',
69-
'variants' => [
70-
'osx-homebrew' => 'Homebrew',
71-
'osx-homebrew-php' => 'Homebrew-PHP',
72-
'osx-docker-cli' => 'Docker (Command Line Interface)',
73-
'osx-docker-web' => 'Docker (Web Development)',
74-
'osx-macports' => 'MacPorts',
75-
],
76-
],
77-
'windows' => [
78-
'name' => 'Windows',
79-
'variants' => [
80-
'windows-downloads' => 'ZIP Downloads',
81-
'windows-native' => 'Single Line Installer',
82-
'windows-chocolatey' => 'Chocolatey',
83-
'windows-scoop' => 'Scoop',
84-
'windows-winget' => 'Winget',
85-
'windows-docker-cli' => 'Docker (Command Line Interface)',
86-
'windows-docker-web' => 'Docker (Web Development)',
87-
'windows-wsl-debian' => 'WSL/Debian',
88-
'windows-wsl-ubuntu' => 'WSL/Ubuntu',
89-
],
90-
],
91-
];
92-
93109
$versions = [
94110
'8.5' => 'version 8.5',
95111
'8.4' => 'version 8.4',
@@ -98,44 +114,6 @@ function option(string $value, string $desc, $attributes = []): string
98114
'default' => 'default PHP version for OS',
99115
];
100116

101-
102-
$platform = $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '';
103-
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
104-
$auto_os = null;
105-
$auto_osvariant = null;
106-
107-
if (!empty($platform) || !empty($ua)) {
108-
$platform = strtolower(trim($platform, '"'));
109-
if ($platform === 'windows' || stripos($ua, 'Windows') !== false) {
110-
$auto_os = 'windows';
111-
} elseif ($platform === 'macos' || stripos($ua, 'Mac') !== false) {
112-
$auto_os = 'osx';
113-
} elseif ($platform === 'linux' || stripos($ua, 'Linux') !== false) {
114-
$auto_os = 'linux';
115-
if (stripos($ua, 'Ubuntu') !== false) {
116-
$auto_osvariant = 'linux-ubuntu';
117-
} elseif (stripos($ua, 'Debian') !== false) {
118-
$auto_osvariant = 'linux-debian';
119-
} elseif (stripos($ua, 'Fedora') !== false) {
120-
$auto_osvariant = 'linux-fedora';
121-
} elseif (stripos($ua, 'Red Hat') !== false || stripos($ua, 'RedHat') !== false) {
122-
$auto_osvariant = 'linux-redhat';
123-
}
124-
}
125-
}
126-
127-
$defaults = [
128-
'os' => $auto_os ?? 'linux',
129-
'version' => 'default',
130-
];
131-
132-
$options = array_merge($defaults, $_GET);
133-
134-
if ($auto_osvariant && (!array_key_exists('osvariant', $options) || !array_key_exists($options['osvariant'], $os[$options['os']]['variants']))) {
135-
$options['osvariant'] = $auto_osvariant;
136-
} elseif (!array_key_exists('osvariant', $options) || !array_key_exists($options['osvariant'], $os[$options['os']]['variants'])) {
137-
$options['osvariant'] = array_key_first($os[$options['os']]['variants']);
138-
}
139117
?>
140118
<h1>Downloads &amp; Installation Instructions</h1>
141119

src/Downloads/OptionResolver.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpweb\Downloads;
6+
7+
class OptionResolver
8+
{
9+
/**
10+
* @param array<string, array{name: string, variants: array<string, string>}> $osList
11+
*/
12+
public function __construct(private readonly array $osList)
13+
{
14+
}
15+
16+
/**
17+
* Resolve the selected download options from the request, auto-detecting the
18+
* operating system from client hints / user agent when not explicitly chosen.
19+
*
20+
* When the client supplies an invalid os parameter (e.g. bots requesting
21+
* ?os=<garbage> or ?os[]=x, which previously crashed while indexing the os
22+
* list), the returned Resolution carries a redirect query pointing at the
23+
* auto-detected results instead of silently rendering a default.
24+
*
25+
* @param array<string, mixed> $get GET parameters (os, osvariant, version, ...)
26+
*/
27+
public function resolve(array $get, string $platformHeader, string $uaHeader): Resolution
28+
{
29+
[$autoOs, $autoOsVariant] = $this->autoDetect($platformHeader, $uaHeader);
30+
31+
$defaults = [
32+
'os' => $autoOs ?? 'linux',
33+
'version' => 'default',
34+
];
35+
36+
$options = array_merge($defaults, $get);
37+
38+
$invalidOs = array_key_exists('os', $get)
39+
&& (!is_string($get['os']) || !array_key_exists($get['os'], $this->osList));
40+
41+
if ($invalidOs) {
42+
$options['os'] = $defaults['os'];
43+
}
44+
45+
if ($autoOsVariant && (!array_key_exists('osvariant', $options) || !array_key_exists($options['osvariant'], $this->osList[$options['os']]['variants']))) {
46+
$options['osvariant'] = $autoOsVariant;
47+
} elseif (!array_key_exists('osvariant', $options) || !array_key_exists($options['osvariant'], $this->osList[$options['os']]['variants'])) {
48+
$options['osvariant'] = array_key_first($this->osList[$options['os']]['variants']);
49+
}
50+
51+
return new Resolution(
52+
$options,
53+
$invalidOs ? $this->redirectQuery($options) : null,
54+
);
55+
}
56+
57+
/**
58+
* Build the canonical query string for the auto-detected results, preserving
59+
* the resolved os/variant/version so the redirect lands on a valid page.
60+
*
61+
* @param array<string, mixed> $options
62+
*/
63+
private function redirectQuery(array $options): string
64+
{
65+
$query = [];
66+
67+
foreach (['os', 'osvariant', 'version'] as $key) {
68+
if (array_key_exists($key, $options) && is_string($options[$key])) {
69+
$query[$key] = $options[$key];
70+
}
71+
}
72+
73+
return http_build_query($query);
74+
}
75+
76+
/**
77+
* @return array{0: ?string, 1: ?string} [auto os, auto os variant]
78+
*/
79+
private function autoDetect(string $platformHeader, string $uaHeader): array
80+
{
81+
$autoOs = null;
82+
$autoOsVariant = null;
83+
84+
if ($platformHeader === '' && $uaHeader === '') {
85+
return [$autoOs, $autoOsVariant];
86+
}
87+
88+
$platform = strtolower(trim($platformHeader, '"'));
89+
90+
if ($platform === 'windows' || stripos($uaHeader, 'Windows') !== false) {
91+
$autoOs = 'windows';
92+
} elseif ($platform === 'macos' || stripos($uaHeader, 'Mac') !== false) {
93+
$autoOs = 'osx';
94+
} elseif ($platform === 'linux' || stripos($uaHeader, 'Linux') !== false) {
95+
$autoOs = 'linux';
96+
if (stripos($uaHeader, 'Ubuntu') !== false) {
97+
$autoOsVariant = 'linux-ubuntu';
98+
} elseif (stripos($uaHeader, 'Debian') !== false) {
99+
$autoOsVariant = 'linux-debian';
100+
} elseif (stripos($uaHeader, 'Fedora') !== false) {
101+
$autoOsVariant = 'linux-fedora';
102+
} elseif (stripos($uaHeader, 'Red Hat') !== false || stripos($uaHeader, 'RedHat') !== false) {
103+
$autoOsVariant = 'linux-redhat';
104+
}
105+
}
106+
107+
return [$autoOs, $autoOsVariant];
108+
}
109+
}

src/Downloads/Resolution.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpweb\Downloads;
6+
7+
final class Resolution
8+
{
9+
/**
10+
* @param array<string, mixed> $options resolved download options for rendering
11+
* @param ?string $redirectQuery query string to redirect to (without leading
12+
* '?'), or null when the request should render
13+
*/
14+
public function __construct(
15+
public readonly array $options,
16+
public readonly ?string $redirectQuery = null,
17+
) {
18+
}
19+
}

0 commit comments

Comments
 (0)