libreqr/index.php
2026-03-23 22:46:17 +05:30

390 lines
No EOL
13 KiB
PHP
Executable file

<?php declare(strict_types=1);
// This file is part of LibreQR, which is distributed under the GNU AGPLv3+ license
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile;
require 'config.inc.php';
require 'vendor/autoload.php';
const CONTRAST_THRESHOLD = 64;
const LibreQR_VERSION = '2.0.1+dev';
// Defines the locale to be used
$locale = DEFAULT_LOCALE;
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$clientLocales = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
$clientLocales = preg_replace('#[A-Z0-9]|q=|;|-|\.#', '', $clientLocales);
$clientLocales = explode(',', $clientLocales);
foreach (array_diff(scandir('locales'), ['.', '..']) as $key => $localeFile) {
$availableLocales[$key] = basename($localeFile, '.php');
}
foreach ($clientLocales as $clientLocale) {
if (in_array($clientLocale, $availableLocales, true)) {
$locale = $clientLocale;
break;
}
}
}
header('Content-Language: ' . $locale);
require 'locales/' . $locale . '.php';
$chosen_loc = $loc;
if ($locale !== DEFAULT_LOCALE) {
require 'locales/' . DEFAULT_LOCALE . '.php';
$default_loc = $loc;
} else {
$default_loc = $chosen_loc;
}
require 'locales/en.php';
$english_loc = $loc;
// Function to get a specific string from the locale file, fall back on default then english if missing
function getIntlString(
string $string_label,
bool $raw = false,
): string {
global $chosen_loc, $default_loc, $english_loc, $locale;
if (array_key_exists($string_label, $chosen_loc) && $chosen_loc[$string_label] !== '') {
return $chosen_loc[$string_label];
}
if ($locale !== DEFAULT_LOCALE && array_key_exists($string_label, $default_loc) && $default_loc[$string_label] !== '') {
if ($raw) {
return $default_loc[$string_label];
}
return '<span lang="' . DEFAULT_LOCALE . '">' . $default_loc[$string_label] . '</span>';
}
if ($raw) {
return $english_loc[$string_label];
}
return '<span lang="en">' . $english_loc[$string_label] . '</span>';
}
function getIntlStringInterpolated(
string $string_label,
array $vars = [],
bool $raw = false,
): string {
return vsprintf(getIntlString($string_label, $raw), $vars);
}
preg_match('#.*/(?<page>.*)$#', $_SERVER['REQUEST_URI'], $matches);
define('PAGE', match ($matches['page']) {
'wifi' => 'wifi',
'' => 'home',
default => 'unknown',
});
define('ICONS_DIR', 'themes/' . THEME . '/icons');
$icon_files = array_diff(scandir(ICONS_DIR), ['.', '..']);
if (PAGE === 'unknown') {
$allowed_filenames['LICENSE.html'] = 'text/html';
$allowed_filenames['synclog.txt'] = 'text/html';
$allowed_filenames['style.css'] = 'text/css';
$allowed_filenames['themes/' . THEME . '/theme.css'] = 'text/css';
$allowed_filenames['themes/' . THEME . '/logo-dark.png'] = 'image/png';
$allowed_filenames['themes/' . THEME . '/logo-light.png'] = 'image/png';
foreach ($icon_files as $icon_file) {
$allowed_filenames[ICONS_DIR . '/' . $icon_file] = 'image/png';
}
foreach ($allowed_filenames as $filename => $type) {
if (str_ends_with($_SERVER['REQUEST_URI'], $filename)) {
header('Content-Type: ' . $type);
echo file_get_contents($filename);
exit;
}
}
http_response_code(404);
}
$_POST = [
'wifi' => [
'ssid' => (string) ($_POST['wifi']['ssid'] ?? ''),
'password' => (string) ($_POST['wifi']['password'] ?? ''),
],
'main' => [
'txt' => (string) ($_POST['main']['txt'] ?? ''),
'redundancy' => $_POST['main']['redundancy'] ?? (string) DEFAULT_REDUNDANCY,
'margin' => $_POST['main']['margin'] ?? (string) DEFAULT_MARGIN,
'size' => $_POST['main']['size'] ?? (string) DEFAULT_SIZE,
'bgColor' => $_POST['main']['bgColor'] ?? '#' . (string) DEFAULT_BGCOLOR,
'fgColor' => $_POST['main']['fgColor'] ?? '#' . (string) DEFAULT_FGCOLOR,
],
];
if ($_POST['wifi']['ssid'] !== '') {
if (!(strlen($_POST['wifi']['ssid']) >= 1 && strlen($_POST['wifi']['ssid']) <= 4096)) {
http_response_code(400);
exit('Wrong value for ssid');
}
$escaped_ssid = preg_replace('/([\\\;\,"\:])/', '\\\$1', $_POST['wifi']['ssid']);
if ($_POST['wifi']['password'] === '') {
$_POST['main']['txt'] = "WIFI:T:nopass;S:{$escaped_ssid};;";
} else {
if (strlen($_POST['wifi']['password']) > 4096) {
http_response_code(400);
exit('Wrong value for password');
}
$escaped_password = preg_replace('/([\\\;\,"\:])/', '\\\$1', $_POST['wifi']['password']);
$_POST['main']['txt'] = "WIFI:T:WPA;S:{$escaped_ssid};P:{$escaped_password};;";
}
}
$qrCodeAvailable = null;
if ($_POST['main']['txt'] !== '') {
$qrCodeAvailable = true;
if (!(strlen($_POST['main']['txt']) >= 1 && strlen($_POST['main']['txt']) <= 4096)) {
http_response_code(400);
exit('Wrong value for txt');
}
if (!in_array($_POST['main']['redundancy'], ['low', 'medium', 'quartile', 'high'], strict: true)) {
http_response_code(400);
exit('Wrong value for redundancy');
}
if (!(is_numeric($_POST['main']['margin']) && $_POST['main']['margin'] >= 0 && $_POST['main']['margin'] <= 1024)) {
http_response_code(400);
exit('Wrong value for margin');
}
if (!(is_numeric($_POST['main']['size']) && $_POST['main']['size'] >= 21 && $_POST['main']['size'] <= 4096)) {
http_response_code(400);
exit('Wrong value for size');
}
if (preg_match('/^#[abcdefABCDEF0-9]{6}$/', $_POST['main']['bgColor']) === false) {
http_response_code(400);
exit('Wrong value for bgColor');
}
if (preg_match('/^#[abcdefABCDEF0-9]{6}$/', $_POST['main']['fgColor']) === false) {
http_response_code(400);
exit('Wrong value for fgColor');
}
$rgbBgColor = [
'r' => hexdec(substr($_POST['main']['bgColor'], 1, 2)),
'g' => hexdec(substr($_POST['main']['bgColor'], 3, 2)),
'b' => hexdec(substr($_POST['main']['bgColor'], 5, 2)),
];
$qrCode = Builder::create()
->data($_POST['main']['txt'])
->margin((int) $_POST['main']['margin'])
->size((int) $_POST['main']['size'])
->errorCorrectionLevel(match ($_POST['main']['redundancy']) {
'low' => new ErrorCorrectionLevelLow(),
'medium' => new ErrorCorrectionLevelMedium(),
'quartile' => new ErrorCorrectionLevelQuartile(),
'high' => new ErrorCorrectionLevelHigh(),
})
->backgroundColor(new Color(
(int) $rgbBgColor['r'],
(int) $rgbBgColor['g'],
(int) $rgbBgColor['b'],
))
->foregroundColor(new Color(
(int) hexdec(substr($_POST['main']['fgColor'], 1, 2)),
(int) hexdec(substr($_POST['main']['fgColor'], 3, 2)),
(int) hexdec(substr($_POST['main']['fgColor'], 5, 2)),
))
;
try {
$result = $qrCode->build();
} catch (Exception $ex) {
http_response_code(500);
$qrCodeAvailable = false;
error_log('FbIN QR encountered an error while generating a QR code: ' . $ex);
}
}
?>
<!DOCTYPE html>
<html lang="<?= $locale ?>">
<head>
<meta charset="utf-8">
<title><?= match (PAGE) {
'home' => 'FbIN QR · ' . getIntlString('subtitle'),
'wifi' => getIntlString('tab_wifi_title') . ' · FbIN QR',
'unknown' => getIntlString('error_404') . ' · FbIN QR',
} ?></title>
<meta name="description" content="<?= getIntlString('description', raw: true) ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<meta name="application-name" content="Libre QR">
<meta name="referrer" content="no-referrer">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' data:; style-src 'self'; form-action 'self';">
<!-- Avoids leaking the "prefers-color-scheme" media query to the server -->
<link rel="preload" as="image" href="themes/<?= THEME ?>/logo-dark.png">
<link rel="preload" as="image" href="themes/<?= THEME ?>/logo-light.png">
<link rel="stylesheet" media="screen" href="style.css">
<link rel="stylesheet" media="screen" href="themes/<?= THEME ?>/theme.css">
<?php
natsort($icon_files);
foreach ($icon_files as $icon_file) {
if (ctype_digit($icon_size = substr($icon_file, 0, -4))) {
echo ' <link rel="icon" type="image/png" href="' . ICONS_DIR . '/' . $icon_size . '.png" sizes="' . $icon_size . 'x' . $icon_size . '">' . "\n";
}
}
?>
</head>
<body>
<header>
<a id="linkTitles" href="./">
<hgroup id="titles">
<h1>FbIN QR</h1>
<p><?= getIntlString('subtitle') ?></p>
</hgroup>
</a>
</header>
<nav>
<h2 class="sr-only">Type de code QR</h2>
<ul>
<li<?php if (PAGE === 'home') {
echo ' class="tab-selected"';
} ?>><a href="./"><div><?= getIntlString('tab_text') ?></div></a></li>
<li<?php if (PAGE === 'wifi') {
echo ' class="tab-selected"';
} ?>><a href="./wifi"><div><?= getIntlString('tab_wifi') ?></div></a></li>
</ul>
</nav>
<?php if (PAGE === 'wifi') { ?>
<form method="post" action="./wifi#output" id="form">
<div class="param textboxParam">
<label for="ssid"><?= getIntlString('label_wifi_ssid') ?></label>
<input type="text" id="ssid" placeholder="<?= getIntlString('placeholder_wifi_ssid', raw: true) ?>" name="wifi[ssid]" required maxlength="4096" value="<?= htmlspecialchars($_POST['wifi']['ssid']) ?>">
</div>
<div class="param textboxParam">
<details>
<summary><label for="password"><?= getIntlString('label_wifi_password') ?></label></summary>
<div class="helpText">
<?= getIntlString('help_wifi_password') ?>
</div>
</details>
<input type="text" id="password" placeholder="<?= getIntlString('placeholder_wifi_password', raw: true) ?>" name="wifi[password]" maxlength="4096" value="<?= htmlspecialchars($_POST['wifi']['password']) ?>">
</div>
<?php require 'common.php' ?>
</form>
<?php } elseif (PAGE === 'home') { ?>
<form method="post" action="./#output" id="form">
<div class="param textboxParam" id="txtParam">
<details>
<summary><label for="txt"><?= getIntlString('label_content') ?></label></summary>
<div class="helpText">
<?= getIntlString('help_content') ?>
</div>
</details>
<textarea rows="3" id="txt" placeholder="<?= getIntlString('placeholder', raw: true) ?>" name="main[txt]"><?= htmlspecialchars($_POST['main']['txt']) ?></textarea>
</div>
<?php require 'common.php' ?>
</form>
<?php } else { ?>
<p>
<?= getIntlString('error_404') ?>
</p>
<?php } ?>
<?php
if ($qrCodeAvailable) {
$dataUri = $result->getDataUri();
$qrSize = (int) ($_POST['main']['size']) + 2 * (int) ($_POST['main']['margin']);
?>
<section id="output">
<div class="centered" id="downloadQR">
<output form="form"><a href="<?= $dataUri ?>" class="button" download="<?= htmlspecialchars($_POST['main']['txt']); ?>.png"><?= getIntlString('button_download') ?></a></output>
</div>
<div class="centered" id="showOnlyQR">
<output form="form"><a title="<?= getIntlString('title_showOnlyQR', raw: true) ?>" href="<?= $dataUri ?>"><img width="<?= $qrSize ?>" height="<?= $qrSize ?>" alt='<?= getIntlStringInterpolated('alt_QR', [htmlspecialchars($_POST['main']['txt'])], raw: true) ?>' id="qrCode"<?php
// Compute the difference between the QR code and theme background colors to determine whether a CSS corner is needed to let the user see the margin of the QR code
preg_match(
'/prefers-color-scheme: light.+--bg: (?<bg_light>#[A-Fa-f0-9]{6});.+prefers-color-scheme: dark.+--bg: (?<bg_dark>#[A-Fa-f0-9]{6});.+/s',
file_get_contents('themes/' . THEME . '/theme.css'),
$css,
);
if (
abs($rgbBgColor['r'] - hexdec(substr($css['bg_light'], -6, 2)))
+ abs($rgbBgColor['g'] - hexdec(substr($css['bg_light'], -4, 2)))
+ abs($rgbBgColor['b'] - hexdec(substr($css['bg_light'], -2, 2)))
< CONTRAST_THRESHOLD
) {
echo " class='needLightContrast'";
}
if (
abs($rgbBgColor['r'] - hexdec(substr($css['bg_dark'], -6, 2)))
+ abs($rgbBgColor['g'] - hexdec(substr($css['bg_dark'], -4, 2)))
+ abs($rgbBgColor['b'] - hexdec(substr($css['bg_dark'], -2, 2)))
< CONTRAST_THRESHOLD
) {
echo " class='needDarkContrast'";
}
?> src="<?= $dataUri ?>"></a></output>
</div>
<?php if (PAGE === 'wifi') { ?>
<p>
<?= getIntlStringInterpolated('wifi_raw_content', ['<code>' . htmlspecialchars($_POST['main']['txt']) . '</code>']) ?>
</p>
<form method="POST" action="./">
<?php foreach ($_POST['main'] as $name => $value) { ?>
<input type="hidden" name="main[<?= htmlspecialchars($name) ?>]" value="<?= htmlspecialchars($value) ?>">
<?php } ?>
<input type="submit" class="button" value="<?= getIntlString('button_edit', raw: true) ?>">
</form>
<?php } ?>
</section>
<?php
} elseif ($qrCodeAvailable === false) {
echo ' <p><strong>' . getIntlString('error_generation') . '</strong></p></body></html>';
}
?>
<footer>
<section id="info" class="metaText">
<?= getIntlString('metaText_qr') ?>
</section>
<?php if (CUSTOM_TEXT_ENABLED) { ?>
<section class="metaText">
<?= CUSTOM_TEXT ?>
</section>
<?php } ?>
<section class="metaText">
<small><?= getIntlStringInterpolated('metaText_legal', [LibreQR_VERSION]) ?></small>
</section>
</footer>
</body>
</html>