Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 54 additions & 76 deletions downloads.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php
use phpweb\Downloads\OptionResolver;

$_SERVER['BASE_PAGE'] = 'downloads.php';
include_once __DIR__ . '/include/prepend.inc';
include_once __DIR__ . '/include/gpg-keys.inc';
Expand All @@ -7,6 +9,58 @@
// Try to make this page non-cached
header_nocache();

$os = [
'linux' => [
'name' => 'Linux',
'variants' => [
'linux-debian' => 'Debian',
'linux-fedora' => 'Fedora',
'linux-redhat' => 'RedHat',
'linux-ubuntu' => 'Ubuntu',
'linux-docker-cli' => 'Docker (Command Line Interface)',
'linux-docker-web' => 'Docker (Web Development)',
],
],
'osx' => [
'name' => 'macOS',
'variants' => [
'osx-homebrew' => 'Homebrew',
'osx-homebrew-php' => 'Homebrew-PHP',
'osx-docker-cli' => 'Docker (Command Line Interface)',
'osx-docker-web' => 'Docker (Web Development)',
'osx-macports' => 'MacPorts',
],
],
'windows' => [
'name' => 'Windows',
'variants' => [
'windows-downloads' => 'ZIP Downloads',
'windows-native' => 'Single Line Installer',
'windows-chocolatey' => 'Chocolatey',
'windows-scoop' => 'Scoop',
'windows-winget' => 'Winget',
'windows-docker-cli' => 'Docker (Command Line Interface)',
'windows-docker-web' => 'Docker (Web Development)',
'windows-wsl-debian' => 'WSL/Debian',
'windows-wsl-ubuntu' => 'WSL/Ubuntu',
],
],
];

// An invalid ?os= redirects to the auto-detected results before any output.
$resolution = (new OptionResolver($os))->resolve(
$_GET,
$_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '',
$_SERVER['HTTP_USER_AGENT'] ?? '',
);

if ($resolution->redirectQuery !== null) {
header('Location: /downloads.php?' . $resolution->redirectQuery, true, 302);
exit;
}

$options = $resolution->options;

$SIDEBAR_DATA = '
<div class="panel">
<a href="/supported-versions.php">Supported Versions</a>
Expand Down Expand Up @@ -52,44 +106,6 @@ function option(string $value, string $desc, $attributes = []): string
return '<option value="' . $value . '"' . implode(' ', array_keys(array_filter($attributes))) . '>' . $desc . '</option>';
}

$os = [
'linux' => [
'name' => 'Linux',
'variants' => [
'linux-debian' => 'Debian',
'linux-fedora' => 'Fedora',
'linux-redhat' => 'RedHat',
'linux-ubuntu' => 'Ubuntu',
'linux-docker-cli' => 'Docker (Command Line Interface)',
'linux-docker-web' => 'Docker (Web Development)',
],
],
'osx' => [
'name' => 'macOS',
'variants' => [
'osx-homebrew' => 'Homebrew',
'osx-homebrew-php' => 'Homebrew-PHP',
'osx-docker-cli' => 'Docker (Command Line Interface)',
'osx-docker-web' => 'Docker (Web Development)',
'osx-macports' => 'MacPorts',
],
],
'windows' => [
'name' => 'Windows',
'variants' => [
'windows-downloads' => 'ZIP Downloads',
'windows-native' => 'Single Line Installer',
'windows-chocolatey' => 'Chocolatey',
'windows-scoop' => 'Scoop',
'windows-winget' => 'Winget',
'windows-docker-cli' => 'Docker (Command Line Interface)',
'windows-docker-web' => 'Docker (Web Development)',
'windows-wsl-debian' => 'WSL/Debian',
'windows-wsl-ubuntu' => 'WSL/Ubuntu',
],
],
];

$versions = [
'8.5' => 'version 8.5',
'8.4' => 'version 8.4',
Expand All @@ -98,44 +114,6 @@ function option(string $value, string $desc, $attributes = []): string
'default' => 'default PHP version for OS',
];


$platform = $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$auto_os = null;
$auto_osvariant = null;

if (!empty($platform) || !empty($ua)) {
$platform = strtolower(trim($platform, '"'));
if ($platform === 'windows' || stripos($ua, 'Windows') !== false) {
$auto_os = 'windows';
} elseif ($platform === 'macos' || stripos($ua, 'Mac') !== false) {
$auto_os = 'osx';
} elseif ($platform === 'linux' || stripos($ua, 'Linux') !== false) {
$auto_os = 'linux';
if (stripos($ua, 'Ubuntu') !== false) {
$auto_osvariant = 'linux-ubuntu';
} elseif (stripos($ua, 'Debian') !== false) {
$auto_osvariant = 'linux-debian';
} elseif (stripos($ua, 'Fedora') !== false) {
$auto_osvariant = 'linux-fedora';
} elseif (stripos($ua, 'Red Hat') !== false || stripos($ua, 'RedHat') !== false) {
$auto_osvariant = 'linux-redhat';
}
}
}

$defaults = [
'os' => $auto_os ?? 'linux',
'version' => 'default',
];

$options = array_merge($defaults, $_GET);

if ($auto_osvariant && (!array_key_exists('osvariant', $options) || !array_key_exists($options['osvariant'], $os[$options['os']]['variants']))) {
$options['osvariant'] = $auto_osvariant;
} elseif (!array_key_exists('osvariant', $options) || !array_key_exists($options['osvariant'], $os[$options['os']]['variants'])) {
$options['osvariant'] = array_key_first($os[$options['os']]['variants']);
}
?>
<h1>Downloads &amp; Installation Instructions</h1>

Expand Down
109 changes: 109 additions & 0 deletions src/Downloads/OptionResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace phpweb\Downloads;

class OptionResolver
{
/**
* @param array<string, array{name: string, variants: array<string, string>}> $osList
*/
public function __construct(private readonly array $osList)
{
}

/**
* Resolve the selected download options from the request, auto-detecting the
* operating system from client hints / user agent when not explicitly chosen.
*
* When the client supplies an invalid os parameter (e.g. bots requesting
* ?os=<garbage> or ?os[]=x, which previously crashed while indexing the os
* list), the returned Resolution carries a redirect query pointing at the
* auto-detected results instead of silently rendering a default.
*
* @param array<string, mixed> $get GET parameters (os, osvariant, version, ...)
*/
public function resolve(array $get, string $platformHeader, string $uaHeader): Resolution
{
[$autoOs, $autoOsVariant] = $this->autoDetect($platformHeader, $uaHeader);

$defaults = [
'os' => $autoOs ?? 'linux',
'version' => 'default',
];

$options = array_merge($defaults, $get);

$invalidOs = array_key_exists('os', $get)
&& (!is_string($get['os']) || !array_key_exists($get['os'], $this->osList));

if ($invalidOs) {
$options['os'] = $defaults['os'];
}

if ($autoOsVariant && (!array_key_exists('osvariant', $options) || !array_key_exists($options['osvariant'], $this->osList[$options['os']]['variants']))) {
$options['osvariant'] = $autoOsVariant;
} elseif (!array_key_exists('osvariant', $options) || !array_key_exists($options['osvariant'], $this->osList[$options['os']]['variants'])) {
$options['osvariant'] = array_key_first($this->osList[$options['os']]['variants']);
}

return new Resolution(
$options,
$invalidOs ? $this->redirectQuery($options) : null,
);
}

/**
* Build the canonical query string for the auto-detected results, preserving
* the resolved os/variant/version so the redirect lands on a valid page.
*
* @param array<string, mixed> $options
*/
private function redirectQuery(array $options): string
{
$query = [];

foreach (['os', 'osvariant', 'version'] as $key) {
if (array_key_exists($key, $options) && is_string($options[$key])) {
$query[$key] = $options[$key];
}
}

return http_build_query($query);
}

/**
* @return array{0: ?string, 1: ?string} [auto os, auto os variant]
*/
private function autoDetect(string $platformHeader, string $uaHeader): array
{
$autoOs = null;
$autoOsVariant = null;

if ($platformHeader === '' && $uaHeader === '') {
return [$autoOs, $autoOsVariant];
}

$platform = strtolower(trim($platformHeader, '"'));

if ($platform === 'windows' || stripos($uaHeader, 'Windows') !== false) {
$autoOs = 'windows';
} elseif ($platform === 'macos' || stripos($uaHeader, 'Mac') !== false) {
$autoOs = 'osx';
} elseif ($platform === 'linux' || stripos($uaHeader, 'Linux') !== false) {
$autoOs = 'linux';
if (stripos($uaHeader, 'Ubuntu') !== false) {
$autoOsVariant = 'linux-ubuntu';
} elseif (stripos($uaHeader, 'Debian') !== false) {
$autoOsVariant = 'linux-debian';
} elseif (stripos($uaHeader, 'Fedora') !== false) {
$autoOsVariant = 'linux-fedora';
} elseif (stripos($uaHeader, 'Red Hat') !== false || stripos($uaHeader, 'RedHat') !== false) {
$autoOsVariant = 'linux-redhat';
}
}

return [$autoOs, $autoOsVariant];
}
}
19 changes: 19 additions & 0 deletions src/Downloads/Resolution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace phpweb\Downloads;

final class Resolution
{
/**
* @param array<string, mixed> $options resolved download options for rendering
* @param ?string $redirectQuery query string to redirect to (without leading
* '?'), or null when the request should render
*/
public function __construct(
public readonly array $options,
public readonly ?string $redirectQuery = null,
) {
}
}
Loading