Compare commits

..

22 Commits

Author SHA1 Message Date
cc3c0cab45 organic: Add external BioC api 2025-07-16 23:28:46 +02:00
a44f886086 organic: Add exernal BioQS api 2025-07-15 18:11:16 +02:00
3a39cb6635 organic: Add pdf signature check 2025-07-14 20:03:44 +02:00
40093957a3 organic: Add external LKV api 2025-07-14 12:00:26 +02:00
f108f026b9 organic/pdf: Add pdf upload feature 2025-07-13 22:04:06 +02:00
fffc450e87 organic: Fix Content-Disposition 2025-07-13 21:29:34 +02:00
a3c401ead2 organic: Add pdf endpoint 2025-07-13 20:15:33 +02:00
8757940a3c Add /organic API 2025-07-11 23:45:28 +02:00
25805f0475 clients: Move /clients to sync.elwig.at 2025-05-03 16:22:30 +02:00
223c959cd9 access: Move /access to access.elwig.at 2025-05-03 15:45:59 +02:00
4962218fe3 access: Fix error handling in JS 2025-05-03 11:53:19 +02:00
7cf3e21efc access: Add token auth support for access-multiple.js 2025-04-30 00:03:06 +02:00
a93afcdf97 www: Improve error handling for file uploads 2025-02-20 15:23:47 +01:00
0ce071c256 index: Add query parameter v to style.css 2025-01-12 16:53:52 +01:00
99d0356b58 index: Add #changelog to /changelog redirect 2025-01-08 11:22:57 +01:00
a6072fa465 access: Minor JS improvements 2025-01-05 14:44:52 +01:00
c69d9c11ec www: Change de-AT to de 2025-01-05 12:52:57 +01:00
297f575d4b index: Add /de/ and /en/ versions 2025-01-05 01:09:46 +01:00
5836c63afc index: Rename Großinzersdorf to Groß-Inzersdorf 2025-01-01 23:29:39 +01:00
bc47f8f463 files: Add debian package repository 2024-12-30 01:03:05 +01:00
a7921f6d5b access: Fix 404 when invalid client was specified 2024-12-29 12:12:24 +01:00
34e5c44115 access: Fix today check 2024-12-29 12:11:43 +01:00
28 changed files with 1320 additions and 1279 deletions

@@ -1,2 +1,4 @@
# Elwig REST API # Elwig website
https://elwig.at/

@@ -19,15 +19,3 @@ function authenticate(): void {
http_401_unauthorized(); http_401_unauthorized();
} }
} }
function authenticate_client(string $client): void {
global $CLIENT_CREDENTIALS;
$credentials = $CLIENT_CREDENTIALS[$client];
if (!array_key_exists('PHP_AUTH_USER', $_SERVER) ||
!array_key_exists('PHP_AUTH_PW', $_SERVER) ||
!array_key_exists($_SERVER['PHP_AUTH_USER'], $credentials) ||
$_SERVER['PHP_AUTH_PW'] !== $credentials[$_SERVER['PHP_AUTH_USER']])
{
http_401_unauthorized();
}
}

@@ -1,32 +1,9 @@
<?php <?php
global $GITEA_TOKEN; global $GITEA_TOKEN;
global $CREDENTIALS; global $CREDENTIALS;
global $CLIENT_CREDENTIALS;
global $CLIENT_ACCESS;
global $COMBINED_ACCESS;
$GITEA_TOKEN = 'token'; $GITEA_TOKEN = 'token';
$CREDENTIALS = [ $CREDENTIALS = [
'username' => 'password', 'username' => 'password',
]; ];
$CLIENT_CREDENTIALS = [
'name' => [
'username' => 'password',
],
];
$CLIENT_ACCESS = [
'WGX' => [
'name' => 'Winzergenossenschaft',
'api' => 'https://example.com/elwig/api/v1',
],
];
$COMBINED_ACCESS = [
'HOLDING' => [
'name' => 'Name',
'clients' => ['WGX'],
],
];

@@ -1,100 +0,0 @@
<?php
require ".php/credentials.inc";
global $CLIENT_ACCESS;
global $COMBINED_ACCESS;
$info = explode('/', $_SERVER['PATH_INFO']);
$client = null;
foreach ($CLIENT_ACCESS as $name => $data) {
if ($name === $info[1] && (sizeof($info) === 2 || $info[2] === '')) {
$client = $data;
$client['id'] = $name;
}
}
foreach ($COMBINED_ACCESS as $name => $data) {
if ($name === $info[1] && (sizeof($info) === 2 || $info[2] === '')) {
$client = $data;
$client['id'] = $name;
}
}
if ($_SERVER['PATH_INFO'] !== "/$client[id]/") {
header('Status: 308');
header("Location: /access/$client[id]/");
exit();
} else if ($client === null) {
header('Status: 404');
header('Content-Length: 0');
exit();
}
if ($client['api']) {
$data = "window.ELWIG_API = \"$client[api]\";";
} else {
$data = "window.CLIENTS = {";
$first = true;
foreach ($client['clients'] as $id) {
$c = $CLIENT_ACCESS[$id];
if (!$first) $data .= ", ";
$data .= "\"$id\": {\"name\": \"$c[name]\", \"short\": \"$c[short]\", \"api\": \"$c[api]\"}";
$first = false;
}
$data .= "};";
}
header('Content-Type: application/xhtml+xml; charset=UTF-8');
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; ?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="de-AT">
<head>
<meta charset="UTF-8"/>
<title><?php echo $client['name']; ?> - Elwig</title>
<link rel="icon" href="/favicon.ico" sizes="16x16 20x20 24x24 30x30 32x32 36x36 40x40 48x48 60x60 64x64 72x72 80x80 96x96 128x128 256x256"/>
<link rel="stylesheet" href="/res/style.css"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<script>window.CLIENT = "<?php echo $info[1]; ?>"; <?php echo $data; ?></script>
<script src="/res/access.js" type="application/javascript"/>
<script src="<?php echo $client['api'] ? "/res/access-single.js" : "/res/access-multiple.js"; ?>" type="application/javascript"/>
</head>
<body class="header-footer">
<header>
<nav>
<div>
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
</div>
<ul>
<?php if (isset($client['clients'])) {
foreach ($client['clients'] as $id) {
$c = $CLIENT_ACCESS[$id];
echo " <li><a href=\"#/$id\">$c[short]</a></li>\n";
}
} else { ?>
<li><a href="#/">Übersicht</a></li>
<li><a href="#/mitglied">Mitglied</a></li>
<li><a href="#/lieferungen">Lieferungen</a></li>
<li><a href="#/anmeldungen">Anmeldungen</a></li>
<?php } ?>
</ul>
<div>
<?php if (!isset($client['clients'])) { ?>
<a href="#/mitglied" id="user">
<div id="usertext"></div>
<img src="/res/avatar.png" alt="Avatar" style="border-radius: 50%; height: 2.25em; margin: 0.5em;"/>
</a>
<?php } ?>
</div>
</nav>
</header>
<main id="access"/>
<footer>
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig"/></a>
<p>
<strong>Impressum</strong><br/>
Lorenz Stechauner, Thomas Hilscher<br/>
Österreich (Austria)<br/>
</p>
<p>
<strong>Kontakt</strong><br/>
E-Mail: <a href="mailto:contact@necronda.net">contact@necronda.net</a><br/>
</p>
</footer>
</body>
</html>

@@ -1,267 +0,0 @@
<?php
require ".php/format.inc";
require ".php/auth.inc";
require ".php/credentials.inc";
global $CLIENT_CREDENTIALS;
$clients = array_keys($CLIENT_CREDENTIALS);
$format = get_fmt();
function get_zip_meta($path): array {
$meta = 'null';
$fp = fopen($path, "rb");
$zipHdr1 = fread($fp, 30);
if (strlen($zipHdr1) !== 30 || !str_starts_with($zipHdr1, "PK\x03\x04") ||
$zipHdr1[8] !== "\x00" || $zipHdr1[9] !== "\x00" || $zipHdr1[26] !== "\x07" || $zipHdr1[27] !== "\x00")
{
fclose($fp);
return array($meta, null, null);
}
$extraFieldLen1 = unpack("v", substr($zipHdr1, 28, 2))[1];
$name1 = fread($fp, 7);
fseek($fp, $extraFieldLen1, SEEK_CUR);
$data1 = fread($fp, unpack("V", substr($zipHdr1, 18, 4))[1]);
if ($name1 !== "version" || !str_starts_with($data1, "elwig:")) {
fclose($fp);
return array($meta, null, null);
}
$version = (int)substr($data1, 6);
$zipHdr2 = fread($fp, 30);
if (strlen($zipHdr2) !== 30 || !str_starts_with($zipHdr2, "PK\x03\x04") ||
$zipHdr2[8] !== "\x00" || $zipHdr2[9] !== "\x00" || $zipHdr2[26] !== "\x09" || $zipHdr2[27] !== "\x00")
{
fclose($fp);
return array($meta, $version, null);
}
$extraFieldLen2 = unpack("v", substr($zipHdr2, 28, 2))[1];
$name2 = fread($fp, 9);
fseek($fp, $extraFieldLen2, SEEK_CUR);
if ($name2 !== "meta.json") {
fclose($fp);
return array($meta, $version, null);
}
$meta = fread($fp, unpack("V", substr($zipHdr2, 18, 4))[1]);
$files = "{";
$first = true;
while (!feof($fp)) {
$zipHdr3 = fread($fp, 30);
if (strlen($zipHdr3) !== 30 || !str_starts_with($zipHdr3, "PK\x03\x04"))
continue;
$compSize = unpack("V", substr($zipHdr3, 18, 4))[1];
$uncompSize = unpack("V", substr($zipHdr3, 22, 4))[1];
$name = fread($fp, unpack("v", substr($zipHdr3, 26, 2))[1]);
$crc = unpack("V", substr($zipHdr3, 14, 4))[1];
fseek($fp, $compSize, SEEK_CUR);
$hex = substr("00000000" . dechex($crc), -8);
if (!$first) $files .= ", ";
$files .= "\"$name\": {\"compressed_size\": $compSize, \"uncompressed_size\": $uncompSize, \"crc32\": \"$hex\"}";
$first = false;
}
$files .= "}";
fclose($fp);
return array($meta, $version, $files);
}
$path = $_SERVER['PATH_INFO'];
if ($path == '') {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
header('Status: 405');
header('Allow: GET');
if ($format === 'text') {
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 23');
echo "405 Method Not Allowed\n";
} else if ($format === 'json') {
header('Content-Type: application/json; charset=UTF-8');
header('Content-Length: 48');
echo "{\"errors\": [{\"message\": \"Method not allowed\"}]}\n";
} else {
header('Content-Type: text/html; charset=UTF-8');
header('Content-Length: 0');
}
exit();
}
if ($format === 'text') {
header('Content-Type: text/plain; charset=UTF-8');
foreach ($clients as $c)
echo "$c\n";
} else if ($format === 'json') {
header('Content-Type: application/json; charset=UTF-8');
echo "{\"data\": [";
$first = true;
foreach ($clients as $c) {
if (!$first) echo ",";
echo "\n {\"name\": \"$c\"}";
$first = false;
}
echo "\n]}\n";
} else if ($format === 'html') {
header('Content-Type: application/xhtml+xml; charset=UTF-8');
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="de-AT">
<head>
<meta charset="UTF-8"/>
<title>Mandanten - Elwig</title>
<meta name="description" content="Elektronische Winzergenossenschaftsverwaltung"/>
<link rel="icon" href="/favicon.ico" sizes="16x16 20x20 24x24 30x30 32x32 36x36 40x40 48x48 60x60 64x64 72x72 80x80 96x96 128x128 256x256"/>
<link rel="stylesheet" href="/res/style.css"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
</head>
<body>
<main>
<section>
<h3>Mandanten</h3>
<p class="center"><a href="/">Startseite</a></p>
<table>
<thead><tr><th>Name</th></tr></thead>
<tbody>
<?php foreach ($clients as $c) {
echo " <tr><td><a href='clients/$c'>$c</a></td></tr>\n";
} ?>
</tbody>
</table>
<p class="center"><a href="?format=json">JSON-Format</a></p>
</section>
</main>
</body>
</html>
<?php }
exit();
}
foreach ($clients as $c) {
if ($path !== "/$c" && !str_starts_with($path, "/$c/"))
continue;
header('Content-Type: text/plain; charset=UTF-8');
authenticate_client($c);
if ($path === "/$c") {
header('Status: 308');
header("Location: $c/");
header('Content-Length: 23');
exit("308 Permanent Redirect\n");
} elseif ($path === "/$c/") {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
header("Status: 405");
header("Allow: GET");
header('Content-Length: 23');
exit("405 Method Not Allowed\n");
}
header('Content-Type: application/json; charset=UTF-8');
echo "{\"data\": [\n";
$first = true;
foreach (scandir(".data/clients/$c/") as $file) {
if (str_starts_with($file, ".") || str_ends_with($file, ".php") || str_ends_with($file, ".inc")) continue;
if (!$first) echo ",\n";
$path = ".data/clients/$c/$file";
$size = filesize($path);
$url = "https://elwig.at/clients/$c/$file";
$mod = date(DATE_ATOM, filemtime($path));
$cre = date(DATE_ATOM, filectime($path));
$datetime = "null";
$zwstid = "null";
if (str_ends_with($file, ".elwig.zip") && substr_count($file, "_") === 2) {
$parts = explode("_", substr($file, 0, -10));
$time = str_replace("-", ":", $parts[1]);
$dt = DateTime::createFromFormat("Y-m-d H:i:s", "$parts[0] $time");
$datetime = '"' . $dt->format(DateTimeInterface::RFC3339) . '"';
$zwstid = "\"$parts[2]\"";
}
list($meta, $version, $files) = get_zip_meta($path);
$files ??= "null";
$version ??= "null";
echo " {\"name\": \"$file\", \"timestamp\": $datetime, \"zwstid\": $zwstid, \"meta\": $meta, \"files\": $files, " .
"\"version\": $version, \"url\": \"$url\", \"size\": $size, \"created\": \"$cre\", \"modified\": \"$mod\"}";
$first = false;
}
echo "\n]}\n";
exit();
}
$file = substr($path, strlen("/$c/"));
$path = ".data/clients/$c/$file";
if (!preg_match_all('/[A-Za-z0-9_.-]+/', $file) && !($file === '*' && $_SERVER['REQUEST_METHOD'] === 'DELETE')) {
header('Status: 400');
header('Content-Length: 16');
exit("400 Bad Request\n");
} elseif ($_SERVER['REQUEST_METHOD'] === 'GET') {
$size = filesize($path);
if ($size === false) {
header('Status: 404');
header('Content-Length: 14');
exit("404 Not Found\n");
}
$type = mime_content_type($path);
header("Content-Type: $type");
header("Content-Disposition: attachment; filename=\"$file\"");
header("Content-Length: $size");
readfile($path);
} elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$putdata = fopen('php://input', 'r');
$fp = fopen($path, 'wb');
if ($fp === false) {
header("Status: 500");
header("Content-Length: 26");
exit("500 Internal Server Error\n");
}
while ($data = fread($putdata, 4096))
fwrite($fp, $data);
fclose($fp);
fclose($putdata);
header("Status: 201");
header('Content-Length: 12');
exit("201 Created\n");
} elseif ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
if ($file === '*') {
foreach (scandir(".data/clients/$c/") as $f) {
if (str_starts_with($f, ".") || str_ends_with($f, ".php") || str_ends_with($f, ".inc")) continue;
if (unlink(".data/clients/$c/$f") === false) {
header("Status: 500");
exit("500 Internal Server Error\n");
}
echo "Deleted $f\n";
}
} else if (!is_file($path)) {
header("Status: 404");
header("Content-Length: 14");
exit("404 Not Found\n");
} else if (unlink($path) === false) {
header("Status: 500");
exit("500 Internal Server Error\n");
}
exit("200 OK\n");
} else {
header("Status: 405");
header("Allow: GET, PUT, DELETE");
header("Content-Length: 23");
exit("405 Method Not Allowed\n");
}
exit();
}
header("Status: 404");
if ($format === 'text') {
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 14');
echo "404 Not Found\n";
} else if ($format === 'json') {
header('Content-Type: application/json; charset=UTF-8');
header('Content-Length: 39');
echo "{\"errors\": [{\"message\": \"Not found\"}]}\n";
} else {
header('Content-Type: text/html; charset=UTF-8');
header('Content-Length: 0');
}
exit();

@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="de-AT" prefix="og: https://ogp.me/ns#"> <html xmlns="http://www.w3.org/1999/xhtml" lang="de" prefix="og: https://ogp.me/ns#">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<title>Elwig</title> <title>Elwig - Elektronische Winzergenossenschaftsverwaltung</title>
<meta name="description" content="Elektronische Winzergenossenschaftsverwaltung"/> <meta name="description" content="Elektronische Winzergenossenschaftsverwaltung"/>
<link rel="icon" href="/favicon.ico" sizes="16x16 20x20 24x24 30x30 32x32 36x36 40x40 48x48 60x60 64x64 72x72 80x80 96x96 128x128 256x256"/> <link rel="icon" href="/favicon.ico" sizes="16x16 20x20 24x24 30x30 32x32 36x36 40x40 48x48 60x60 64x64 72x72 80x80 96x96 128x128 256x256"/>
<link rel="stylesheet" href="/res/style.css"/> <link rel="stylesheet" href="/res/style.css?v=2025-01-12"/>
<link rel="alternate" href="/en/" hreflang="en"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/> <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<meta name="theme-color" content="#A040C0"/> <meta name="theme-color" content="#A040C0"/>
<meta name="mobile-web-app-capable" content="yes"/> <meta name="mobile-web-app-capable" content="yes"/>
@@ -20,7 +21,7 @@
<meta property="og:image:type" content="image/jpeg"/> <meta property="og:image:type" content="image/jpeg"/>
<meta property="og:image:width" content="1200"/> <meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/> <meta property="og:image:height" content="630"/>
<meta property="og:locale" content="de_AT"/> <meta property="og:locale" content="de"/>
<meta property="og:ttl" content="60"/> <meta property="og:ttl" content="60"/>
<meta name="twitter:card" content="summary_large_image"/> <meta name="twitter:card" content="summary_large_image"/>
</head> </head>
@@ -28,16 +29,18 @@
<header> <header>
<nav class="index"> <nav class="index">
<div> <div>
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig Logo"/></a> <a href="https://elwig.at/de/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
</div> </div>
<ul> <ul>
<li><a href="/#">Start</a></li> <li><a href="#">Start</a></li>
<li><a href="/#about">Über</a></li> <li><a href="#about">Über</a></li>
<li><a href="/#clients">Genossenschaften</a></li> <li><a href="#clients">Genossenschaften</a></li>
<li><a href="/files/">Downloads</a></li> <li><a href="/files/">Downloads</a></li>
<li><a href="https://git.necronda.net/winzer/">Quellcode</a></li> <li><a href="https://git.necronda.net/winzer/">Quellcode</a></li>
</ul> </ul>
<div/> <div>
<a href="/en/" class="flag"><div/></a>
</div>
</nav> </nav>
</header> </header>
@@ -105,9 +108,9 @@
<div class="filling"/> <div class="filling"/>
<div> <div>
<h4>WG Weinland</h4> <h4>WG Weinland</h4>
<h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Großinzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5> <h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Groß-Inzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Zweigstellen:</h6> <h6>Zweigstellen:</h6>
<div class="branches">Großinzersdorf</div> <div class="branches">Groß-Inzersdorf</div>
<p class="link"></p> <p class="link"></p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/> <div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div> </div>

149
www/en/index.xhtml Normal file

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" prefix="og: https://ogp.me/ns#">
<head>
<meta charset="UTF-8"/>
<title>Elwig - Electronic Management for Vintners' Cooperatives</title>
<meta name="description" content="Electronic Management for Vintners' Cooperatives"/>
<link rel="icon" href="/favicon.ico" sizes="16x16 20x20 24x24 30x30 32x32 36x36 40x40 48x48 60x60 64x64 72x72 80x80 96x96 128x128 256x256"/>
<link rel="stylesheet" href="/res/style.css?v=2025-01-12"/>
<link rel="alternate" href="/de/" hreflang="de"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<meta name="theme-color" content="#A040C0"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<meta property="og:site_name" content="Elwig"/>
<meta property="og:title" content="Elwig"/>
<meta property="og:description" content="Electronic Management for Vintners' Cooperatives"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://elwig.at/"/>
<meta property="og:image" content="https://elwig.at/res/images/preview.jpg"/>
<meta property="og:image:type" content="image/jpeg"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<meta property="og:locale" content="de"/>
<meta property="og:ttl" content="60"/>
<meta name="twitter:card" content="summary_large_image"/>
</head>
<body class="header-footer" style="background-color: var(--bg-color);">
<header>
<nav class="index">
<div>
<a href="https://elwig.at/en/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
</div>
<ul>
<li><a href="#">Start</a></li>
<li><a href="#about">About</a></li>
<li><a href="#clients">Cooperatives</a></li>
<li><a href="/files/">Downloads</a></li>
<li><a href="https://git.necronda.net/winzer/">Source&#160;Code</a></li>
</ul>
<div>
<a href="/de/" class="flag"><div/></a>
</div>
</nav>
</header>
<main>
<section class="home background">
<a href="#about">
<div>
<img src="/res/elwig.png" alt="Elwig"/>
<div style="max-width: 16em; margin-bottom: 0.5em;">
<h1>Elwig</h1>
<h2>Electronic Management for Vintners' Cooperatives</h2>
</div>
</div>
</a>
<div class="blur bottom"/>
</section>
<section class="about">
<span id="about"/>
<h3>Elwig</h3>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore etdolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore etdolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
</section>
<section class="picture-1 background">
<div class="blur top"/>
<div class="blur bottom"/>
</section>
<section class="clients">
<span id="clients"/>
<h3>The Cooperatives</h3>
<div class="container background">
<div>
<h4>WG Matzen</h4>
<h5><span>Winzergenossenschaft</span> <span>für Matzen und Umgebung</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Branches:</h6>
<div class="branches">Matzen</div>
<p class="link">
<a href="https://winzermatzen.at/">winzermatzen.at</a>
</p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>
<div class="filling"/>
<div>
<h4>Winzerkeller im Weinviertel</h4>
<h5><span>Winzerkeller im Weinviertel</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Branches:</h6>
<div class="branches">Wolkersdorf, Haugsdorf, Sitzendorf</div>
<p class="link">
<a href="http://www.winzerkeller.eu/">winzerkeller.eu</a>
</p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>
<div class="filling"/>
<div>
<h4>WG Weinland</h4>
<h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Groß-Inzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Branches:</h6>
<div class="branches">Groß-Inzersdorf</div>
<p class="link"></p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>
<div class="filling"/>
<div>
<h4>WG Baden</h4>
<h5><span>Winzergenossenschaft</span> <span>Baden - Bad Vöslau</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Zweigstellen:</h6>
<div class="branches">Baden</div>
<p class="link">
<a href="http://www.wg-baden.at/">wg-baden.at</a>
</p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>
</div>
</section>
<section class="picture-2 background">
<div class="blur top"/>
</section>
</main>
<footer>
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig"/></a>
<p>
<strong>Imprint</strong><br/>
Lorenz Stechauner, Thomas Hilscher<br/>
Austria<br/>
</p>
<p>
<strong>Contact</strong><br/>
E-Mail: <a href="mailto:contact@necronda.net">contact@necronda.net</a><br/>
</p>
</footer>
</body>
</html>

28
www/files/debian/.update.sh Executable file

@@ -0,0 +1,28 @@
#!/bin/bash
set -x
dpkg-scanpackages --arch all pool/ > dists/stable/main/binary-all/Packages
cat dists/stable/main/binary-all/Packages | gzip -9 > dists/stable/main/binary-all/Packages.gz
do_hash() {
HASH_NAME=$1
HASH_CMD=$2
echo "${HASH_NAME}:"
for f in $(find dists/stable -type f -not -name '.*' -not -name 'Release'); do
echo " $(${HASH_CMD} $f | cut -d" " -f1) $(wc -c $f | sed 's|dists/stable/||')"
done
}
cat > dists/stable/Release << EOF
Origin: Elwig
Suite: stable
Codename: stable
Version: 1.0
Architectures: all
Components: main
Date: $(date -Ru)
EOF
do_hash "MD5Sum" "md5sum" >> dists/stable/Release
do_hash "SHA1" "sha1sum" >> dists/stable/Release
do_hash "SHA256" "sha256sum" >> dists/stable/Release

@@ -5,22 +5,40 @@ require "../.php/auth.inc";
if ($_SERVER['REQUEST_METHOD'] === 'PUT') { if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
authenticate(); authenticate();
header('Content-Type: text/plain; charset=UTF-8');
$name = substr($_SERVER['PATH_INFO'], 1); $name = substr($_SERVER['PATH_INFO'], 1);
if (str_contains($name, "..") || str_contains($name, "/")) { if (str_contains($name, "..") || str_contains($name, "/")) {
header('Status: 403'); header('Status: 403');
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 14'); header('Content-Length: 14');
exit("403 Forbidden\n"); exit("403 Forbidden\n");
} else if (!isset($_SERVER['HTTP_CONTENT_LENGTH'])) {
header('Status: 411');
header('Content-Length: 20');
exit("411 Length Required\n");
} }
$upload = fopen("php://input", "r"); $upload = fopen("php://input", "r");
$fp = fopen($name, "wb+"); $fp = fopen("/tmp/upload-$name", "wb+");
if (!$upload || !$fp) {
fclose($fp);
fclose($upload);
header('Status: 500');
header('Content-Length: 26');
exit("500 Internal Server Error\n");
}
while ($data = fread($upload, 4096)) fwrite($fp, $data); while ($data = fread($upload, 4096)) fwrite($fp, $data);
fclose($fp); fclose($fp);
fclose($upload); fclose($upload);
if (!rename("/tmp/upload-$name", $name)) {
header('Status: 500');
header('Content-Length: 26');
exit("500 Internal Server Error\n");
}
header('Status: 201'); header('Status: 201');
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 12'); header('Content-Length: 12');
exit("201 Created\n"); exit("201 Created\n");
} else if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') { } else if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
@@ -120,7 +138,7 @@ if ($format === 'json') {
header('Content-Type: application/xhtml+xml; charset=UTF-8'); header('Content-Type: application/xhtml+xml; charset=UTF-8');
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
?> ?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="de-AT" prefix="og: https://ogp.me/ns#"> <html xmlns="http://www.w3.org/1999/xhtml" lang="de" prefix="og: https://ogp.me/ns#">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<title>Downloads - Elwig</title> <title>Downloads - Elwig</title>
@@ -141,7 +159,7 @@ if ($format === 'json') {
<meta property="og:image:type" content="image/jpeg"/> <meta property="og:image:type" content="image/jpeg"/>
<meta property="og:image:width" content="1200"/> <meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/> <meta property="og:image:height" content="630"/>
<meta property="og:locale" content="de_AT"/> <meta property="og:locale" content="de"/>
<meta property="og:ttl" content="60"/> <meta property="og:ttl" content="60"/>
<meta name="twitter:card" content="summary_large_image"/> <meta name="twitter:card" content="summary_large_image"/>
</head> </head>

@@ -1,9 +1,26 @@
<?php <?php
header("Content-Length: 0"); header("Content-Length: 0");
switch ($_SERVER['PATH_INFO']) { switch ($_SERVER['PATH_INFO']) {
case '':
case '/':
$lang = 'de'; // prefer german
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $pref) {
$l = substr($pref, 0, 2);
if ($l === 'de') {
$lang = 'de';
break; // force german
} else if ($l === 'en') {
$lang = 'en'; // use english only, if user specifically asks for it
}
}
}
header("Status: 303");
header("Location: /$lang/");
break;
case '/changelog': case '/changelog':
header("Status: 303"); header("Status: 303");
header("Location: https://git.necronda.net/winzer/elwig/src/branch/main/CHANGELOG.md"); header("Location: https://git.necronda.net/winzer/elwig/src/branch/main/CHANGELOG.md#changelog");
break; break;
case '/vcs': case '/vcs':
case '/git': case '/git':
@@ -13,6 +30,22 @@ switch ($_SERVER['PATH_INFO']) {
header("Status: 303"); header("Status: 303");
header("Location: https://git.necronda.net/winzer/"); header("Location: https://git.necronda.net/winzer/");
break; break;
case '/access':
header("Status: 303");
header("Location: https://access.elwig.at/");
break;
case '/clients':
header("Status: 303");
header("Location: https://sync.elwig.at/");
break;
default: default:
header("Status: 404"); if (str_starts_with($_SERVER['PATH_INFO'], '/access/')) {
header("Status: 308");
header("Location: https://access.elwig.at/" . substr($_SERVER['PATH_INFO'], 8));
} else if (str_starts_with($_SERVER['PATH_INFO'], '/clients/')) {
header("Status: 308");
header("Location: https://sync.elwig.at/" . substr($_SERVER['PATH_INFO'], 9));
} else {
header("Status: 404");
}
} }

99
www/organic/external/bioc/operators.php vendored Normal file

@@ -0,0 +1,99 @@
<?php
header('Access-Control-Allow-Origin: *');
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
header('Status: 405');
header('Content-Length: 0');
header('Allow: GET, HEAD');
exit;
}
function jenc($data): string {
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
$info = $_SERVER['PATH_INFO'];
if ($info !== '') {
$id = substr($info, 1);
header('Status: 404');
header('Content-Length: 0');
exit;
}
$source_groups = [
[ '',
'0892fdd7-6649-cb3b-75ef-a60649cfebfd', 'fde4ccef-0f36-4db9-9f2f-9bb6009c872a',
'8e6d20d3-6bbe-c7ae-7a77-fb7fa80ee291', '830b3bd7-f1da-e20e-300d-e561bfb8205b',
'ed025d59-056f-ffd0-79e0-05348fe88d59', '95d536c8-c869-9f72-1bce-4256a2a4ce49'
], [ '',
'bef6b49a-3186-ba7f-fffb-281bfb4ca2d2', 'fc7a0cc3-c4b4-a4fb-5b7e-5cf9b68ed198',
'9aecd829-8f3c-6829-449f-9cb511e0c1ff', 'e7a7089b-0fd7-60b9-48af-c0988a90ec37',
'63d570d8-0ab2-b793-fe11-62e0777e4236'
],
];
$country = [
'AT' => 'a1e51f85-27b5-a1e3-65f7-561569884d38',
'DE' => '1be4d468-ba47-08f1-f048-f45869af856f',
][$_GET['country'] ?? null] ?? null;
$postalCode = $_GET['postalCode'] ?? null;
$name = $_GET['name'] ?? null;
$idNr = $_GET['idNr'] ?? null;
if ($country === null) {
header('Status: 400');
header('Content-Length: 0');
exit;
}
$data = [];
$url = "https://www.bioc.info/search/producerSearchQuery?search[name]=" . urlencode($name) . "&search[citycode]=" . urlencode($postalCode) . "&search[operatorId]=" . urlencode($idNr) . "&search[country]=$country";
if ($_GET['allDBs'] === 'true') {
$s = curl_init($url . implode('&sources[]=', $source_groups[0]));
curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
if (($json = curl_exec($s)) === false) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
$res = json_decode($json, true)['results'];
foreach ($res as $r) {
$data[$r['id']] = $r['row'];
}
}
$s = curl_init($url . implode('&sources[]=', $source_groups[1]));
curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
if (($json = curl_exec($s)) === false) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
$res = json_decode($json, true)['results'];
foreach ($res as $r) {
$data[$r['id']] = $r['row'];
}
header('Content-Type: application/json; charset=UTF-8');
$first = true;
echo "{\"data\":[\n";
foreach ($data as $id => $row) {
if (!$first) echo ",\n";
$row = explode("</", $row);
$auth = explode(' ', substr($row[3], strrpos($row[3], '>') + 1), 2);
echo " ";
echo jenc([
'id' => $id,
'name' => substr($row[0], strrpos($row[0], '>') + 1),
'postalCode' => substr($row[1], strrpos($row[1], '>') + 1),
'city' => substr($row[2], strrpos($row[2], '>') + 1),
'authorityCode' => $auth[0],
'authorityName' => $auth[1],
]);
$first = false;
}
echo "\n]}\n";

54
www/organic/external/bioqs/.attachment.py vendored Executable file

@@ -0,0 +1,54 @@
#!/bin/env python3
import re
import argparse
import requests
import sys
BASE_URL = 'https://www.bioqs.at'
URL = f'{BASE_URL}/ACM/faces/form/cms/portal/index.jsp'
ACTION_RE = re.compile(r'action="([^"]*)"')
HIDDEN_RE = re.compile(r'<input type="hidden" name="([^"]*)" .*?value="([^"]*)"')
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument('cert_nr', type=str)
args = parser.parse_args()
s = requests.Session()
r = s.get(f'{URL}?menu_sid=5002')
uri = ACTION_RE.findall(r.text)[0]
hidden = {m[1]: m[2] for m in HIDDEN_RE.finditer(r.text)}
r = s.post(f'{BASE_URL}{uri}', data={
'PartnerCertSearchForm:pcs_seqidall': args.cert_nr,
'PartnerCertSearchForm:button_search': 'Suche starten...',
'PartnerCertSearchForm_SUBMIT': '1',
'javax.faces.ViewState': hidden['javax.faces.ViewState'],
})
p1 = r.text.find(f'>{args.cert_nr}<')
p2 = r.text.find('id="', p1)
p3 = r.text.find('"', p2 + 4)
if p1 == -1 or p2 == -1 or p3 == -1:
exit(1)
id = r.text[p2 + 4:p3]
r = s.post(f'{BASE_URL}{uri}', data={
'PartnerCertSearchForm:_idcl': id,
'PartnerCertSearchForm_SUBMIT': '1',
'javax.faces.ViewState': hidden['javax.faces.ViewState'],
})
if 'Content-Disposition' in r.headers:
dispo = r.headers['Content-Disposition']
if 'filename="' in dispo:
filename = dispo[dispo.find('filename="') + 10:dispo.rfind('"')]
print(filename, file=sys.stderr)
sys.stdout.buffer.write(r.content)
if __name__ == '__main__':
main()

92
www/organic/external/bioqs/.operators.py vendored Executable file

@@ -0,0 +1,92 @@
#!/bin/env python3
import re
import argparse
import requests
import html
import json
import urllib.parse
BASE_URL = 'https://www.bioqs.at'
URL = f'{BASE_URL}/ACM/faces/form/cms/portal/index.jsp'
ACTION_RE = re.compile(r'action="([^"]*)"')
HIDDEN_RE = re.compile(r'<input type="hidden" name="([^"]*)" .*?value="([^"]*)"')
ROW_RE = re.compile(r'<tr[^>]*>\s*(.*?)\s*</tr>', re.DOTALL)
UNCOLLAPSED_ROW_RE = re.compile(r'<tr style="">(\s*<td>\s*(.*?)\s*</td>\s*){7}</tr>', re.DOTALL)
COLLAPSED_ROW_RE = re.compile(r'<table width=[^>]*>\s*(.*?)\s*</table>', re.DOTALL)
TD_RE = re.compile(r'<td[^>]*>\s*(.*?)\s*</td>', re.DOTALL)
TAG_RE = re.compile(r'<[^>]*>')
SPACE_RE = re.compile(r'\s+')
ATTACHMENT_RE = re.compile(r"\[\['cert_attachment_sid','([^']*)'\]\]")
def remove_tags(text: str) -> str:
return SPACE_RE.sub(' ', html.unescape(TAG_RE.sub(' ', text))).strip()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument('query', type=str)
args = parser.parse_args()
query = {'PartnerCertSearchForm:pcs_' + q.split('=', 1)[0]: urllib.parse.unquote(q.split('=', 1)[-1]) for q in args.query.split('&')}
s = requests.Session()
r = s.get(f'{URL}?menu_sid=5002')
uri = ACTION_RE.findall(r.text)[0]
hidden = {m[1]: m[2] for m in HIDDEN_RE.finditer(r.text)}
r = s.post(f'{BASE_URL}{uri}', data={
**query,
'PartnerCertSearchForm:button_search': 'Suche starten...',
'PartnerCertSearchForm_SUBMIT': '1',
'javax.faces.ViewState': hidden['javax.faces.ViewState'],
})
result_table = r.text[r.text.find('<table'):r.text.rfind('</table>') + 8]
uncollapsed_rows = [tuple(remove_tags(m[1])
for m in TD_RE.finditer(row[0]))
for row in UNCOLLAPSED_ROW_RE.finditer(result_table)]
collapsed_rows = [[tuple(remove_tags((ATTACHMENT_RE.search(m[1]) or m)[1]) for m in TD_RE.finditer(row[1]))
for row in ROW_RE.finditer(tbl[0])]
for tbl in COLLAPSED_ROW_RE.finditer(result_table)]
print('[')
first = True
for row, tbl in zip(uncollapsed_rows, collapsed_rows):
meta = {}
certificates = []
for srow in tbl:
if len(srow) == 1:
[k,v] = srow[0].split(':', 1)
meta[k.strip()] = v.strip()
continue
if len(srow) == 0:
continue
certificates.append({
'nr': srow[0],
'validFrom': '-'.join(reversed(srow[1].split('-'))),
'validTo': '-'.join(reversed(srow[2].split('-'))),
'type': srow[3],
'attachmentSid': srow[4],
'url': f'https://elwig.at/organic/external/bioqs/attachments/{urllib.parse.quote(srow[0])}',
})
if not first:
print(',', flush=True)
print(' ', json.dumps({
'idNr': row[0],
'lfbisNr': row[1] or None,
'name': row[2],
'postalCode': row[3],
'city': row[4],
'address': row[5],
'autorityName': meta['Kontrollstelle'],
'productGroups': meta['Bereiche'],
'certificates': certificates,
}, ensure_ascii=False), end='')
first = False
print('\n]')
if __name__ == '__main__':
main()

@@ -0,0 +1,36 @@
<?php
header('Access-Control-Allow-Origin: *');
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
header('Status: 405');
header('Content-Length: 0');
header('Allow: GET, HEAD');
exit;
}
$info = $_SERVER['PATH_INFO'];
if ($info === '') {
header('Status: 404');
header('Content-Length: 0');
exit;
}
$certId = substr($info, 1);
$file = tmpfile();
if (!$file) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
$filename = stream_get_meta_data($file)['uri'];
if (($pdfName = exec("python3 .attachment.py " . escapeshellarg($certId) . " 2>&1 > $filename ")) === false) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
header('Content-Type: application/pdf');
header('Content-Length: ' . filesize($filename));
header('Content-Disposition: inline; filename="' . $pdfName . '"');
readfile($filename);

@@ -0,0 +1,43 @@
<?php
header('Access-Control-Allow-Origin: *');
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
header('Status: 405');
header('Content-Length: 0');
header('Allow: GET, HEAD');
exit;
}
$info = $_SERVER['PATH_INFO'];
if ($info !== '') {
header('Status: 404');
header('Content-Length: 0');
exit;
}
$query = [];
if (isset($_GET['country'])) {
$query[] = 'country=' . [
'AT' => '1',
'DE' => '2',
][$_GET['country']] ?? '';
}
if (isset($_GET['postalCode'])) {
$query[] = 'zipcode=' . urlencode($_GET['postalCode']);
}
if (isset($_GET['name'])) {
$query[] = 'aname=' . urlencode($_GET['name']);
}
if (isset($_GET['idNr'])) {
$query[] = 'clientcode=' . urlencode($_GET['idNr']);
}
if (isset($_GET['lfbisNr'])) {
$query[] = 'lfbis=' . urlencode($_GET['lfbisNr']);
}
header('Content-Type: application/json; charset=UTF-8');
echo "{\"data\":";
passthru("python3 .operators.py " . escapeshellarg(implode('&', $query)));
echo "}\n";

@@ -0,0 +1,278 @@
<?php
header('Access-Control-Allow-Origin: *');
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
header('Status: 405');
header('Content-Length: 0');
header('Allow: GET, HEAD');
exit;
}
function jenc($data): string {
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
$info = $_SERVER['PATH_INFO'];
if ($info !== '') {
$parts = explode(':', substr($info, 1), 2);
if (sizeof($parts) !== 2) {
$url = "https://elwig.at/organic/external/easy-cert/operators?idNr=" . substr($info, 1);
$s = curl_init($url);
curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
if (($json = curl_exec($s)) === false) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
$json = json_decode($json);
if ($json->{'totalCount'} !== 1) {
header('Status: 404');
header('Content-Length: 0');
exit;
}
header('Status: 303');
header('Location: ./' . $json->{'data'}[0]->{'db'} . ':' . $json->{'data'}[0]->{'id'});
exit;
}
$refDate = $_GET['referenceDate'] ?? null;
if ($refDate !== null) {
$refDate = explode('-', $refDate);
$refDate = $refDate[2] . '.' . $refDate[1] . '.' . $refDate[0];
}
$url = "https://www.easy-cert.com/htm/suchresultat-detail.htm?sprache=de&db=" . urlencode($parts[0]) . "&id=" . urlencode(str_replace('-', '_', $parts[1])) . "&historyDate=" . urlencode($refDate);
$s = curl_init($url);
curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
if (($html = curl_exec($s)) === false) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
$html = preg_replace('/<!--.*?-->/s', ' ', $html);
preg_match_all('@<tr>\s*<th[^>]*>([^<]*)</th>\s*<td[^>]*>([^<]*)</td>\s*</tr>@', $html, $matches, PREG_SET_ORDER);
$data = [];
foreach ($matches as $m) {
$data[$m[1]] = trim(html_entity_decode($m[2]));
if ($data[$m[1]] === '') {
$data[$m[1]] = null;
}
}
$plzOrt = $data['PLZ / Ort'] ?? null;
if ($plzOrt === null) {
$postalCode = null;
$city = null;
} else {
$p = explode(' ', $plzOrt, 2);
$postalCode = trim($p[0]);
$city = trim($p[1]);
}
if ($data['Name'] === null) {
header('Status: 404');
header('Content-Length: 0');
exit;
}
if (preg_match('@name="historydate_input" value="([^"]*)"@', $html, $matches) === 1) {
$refDate = $matches[1];
$refDate = explode('.', $refDate);
$refDate = $refDate[2] . '-' . $refDate[1] . '-' . $refDate[0];
} else {
$refDate = null;
}
$certs = [];
preg_match_all('@<tr>\s*<td nowrap>([^<]*)</td>\s*<td[^>]*>([^<]*)</td>\s*<td[^>]*>\s*([^<]*)\s*.*?</td>\s*<td [^\']*\'(https?://[^\']*)\'[^>]*>[^<]*</td>\s*</tr>@', $html, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$certs[] = [
'nr' => $m[1],
'validUntil' => implode('-', array_reverse(explode('.', $m[2]))),
'type' => $m[3] === '' ? null : $m[3],
'pdfUrl' => str_replace('http://', 'https://', $m[4]),
];
}
$labels = [];
preg_match_all('@<tr>\s*<td>([^<]*)</td>\s*<td[^>]*>([^<]*)</td>\s*<td[^>]*>\s*([^<]*)\s*.*?</td>\s*<td [^\']*\'(https?://[^\']*)\'[^>]*>[^<]*</td>\s*</tr>@', $html, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$labels[] = [
'nr' => $m[1],
'validUntil' => implode('-', array_reverse(explode('.', $m[2]))),
'type' => $m[3] === '' ? null : $m[3],
'pdfUrl' => str_replace('http://', 'https://', $m[4]),
];
}
header("Content-Type: application/json; charset=UTF-8");
echo '{"db":' . jenc($parts[0]) .
',"id":' . jenc($parts[1]) .
",\n \"idNr\":" . jenc($data['ID-Nummer'] ?? null) .
',"name":' . jenc($data['Name']) .
',"address":' . jenc($data['Strasse'] ?? null) .
',"postalCode":' . jenc($postalCode) .
',"city":' . jenc($city) .
',"countryCode":' . jenc($data['Land'] ?? null) .
",\n \"referenceDate\":" . jenc($refDate) .
",\n \"certificates\":" . jenc($certs) .
",\n \"privateStandardApprovals\":" . jenc($labels) .
'}';
exit;
}
$search_url = null;
$url = null;
$query_id = null;
$timestamp = null;
$limit = $_GET['limit'] ?? null;
$offset = intval($_GET['offset'] ?? "0");
if ($limit === '') $limit = null;
if ($limit !== null) $limit = intval($limit);
if (isset($_GET['queryId'])) {
$query_id = $_GET['queryId'];
if ($cache = fopen('.cache.csv', 'r')) {
while (($line = fgets($cache)) !== false) {
$parts = explode(';', trim($line), 4);
$timestamp = intval($parts[0]);
if ($parts[1] !== $query_id) continue;
$url = $parts[2];
$search_url = $parts[3];
break;
}
fclose($cache);
}
} else {
$country = $_GET['country'] ?? null;
$postalCode = $_GET['postalCode'] ?? null;
$name = $_GET['name'] ?? null;
$idNr = $_GET['idNr'] ?? null;
$renew = ($_GET['renew'] ?? 'false') === 'true';
$search_url = "https://www.easy-cert.com/htm/suchergebnis.htm?suchtyp=einfach&CountryCode=$country&PostalCode=$postalCode&Name=$name&CustomerNumber=$idNr";
if (!$renew && $cache = fopen('.cache.csv', 'r')) {
while (($line = fgets($cache)) !== false) {
$parts = explode(';', trim($line), 4);
$timestamp = intval($parts[0]);
if (time() - $timestamp > 60 * 30) continue;
if ($parts[3] !== $search_url) continue;
$url = $parts[2];
$query_id = $parts[1];
break;
}
fclose($cache);
}
}
if ($url === null) {
$timestamp = time();
$s = curl_init($search_url);
curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
$html = curl_exec($s);
preg_match_all('/id="control_([^"]*)" value="([^"]*)"/', $html, $matches, PREG_SET_ORDER);
$control = [];
foreach ($matches as $m) {
$control[$m[1]] = $m[2];
}
$query_id = $control['iniqueid'];
$server_count = $control['countserver'];
$lines = [];
for ($i = 0; $i < 30; $i++) {
sleep(1);
$s = curl_init("https://www.easy-cert.com/_includes/search/result_items.php");
curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
curl_setopt($s, CURLOPT_HTTPHEADER, [
'Referer: https://www.easy-cert.com/htm/suchergebnis.htm',
'X-Requested-With: XMLHttpRequest',
'X-USE: 1dfd3nsdv234njsdf923masddj123n12l31lg28gsdf2k34',
]);
curl_setopt($s, CURLOPT_POSTFIELDS, "uniqueid=$query_id&countServer=$server_count");
$json = json_decode(curl_exec($s));
if ($json->{'count'} >= $server_count) {
$url = "https://www.easy-cert.com/" . str_replace('../', '', $json->{'filename'});
break;
}
}
if ($url !== null)
file_put_contents('.cache.csv', "$timestamp;$query_id;$url;$search_url\n", FILE_APPEND);
}
if ($url === null) {
header("Status: 500");
header("Content-Length: 0");
exit;
}
$sed = [
's/^.*"searchresults":\[//',
's/],.*$//',
's/},{/},\n {/g',
'/^$/d',
's/^/ /',
's/}$/},/',
];
$replace = [
'"DB":' => '"db":',
'"Name":' => '"name":',
'"PostalCode":' => '"postalCode":',
'"Town":' => '"city":',
'"CustomerNumber":' => '"idNr":',
'"ID":' => '"id":',
'"CountryCode":' => '"countryCode":',
'"xx"' => 'null',
'"XX"' => 'null',
':""' => ':null',
];
$replaceSed = array_map(fn($v, $k): string => "s/$k/$v/", $replace, array_keys($replace));
header("Content-Type: application/json; charset=UTF-8");
echo '{"searchUrl":' . jenc($search_url) .
',"queryId":' . jenc($query_id) .
',"rawFileUrl":' . jenc($url) .
',"timestamp":' . jenc(gmdate('Y-m-d\TH:i:s\Z', $timestamp)) .
',"limit":' . jenc($limit) .
',"offset":' . jenc($offset) . ",\"data\":[\n";
$fd_spec = [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
2 => ["pipe", "w"], // stderr
3 => ["pipe", "w"], // line count
];
$offset++;
$process = proc_open(
['bash', '-c',
"curl -s -N " . escapeshellarg($url) . " | " . // fetch data (silent and unbuffered)
"xxd -p | " . // convert to hex stream
"sed 's/7d2c7b/7d2c0a7b/g' | " . // roughly break up into lines (s/},{/},\n{/g)
"xxd -r -p | " . // convert to text stream
"sed '" . implode(';', $sed) . "' | " . // apply sed commands
"tee >(wc -l 1>&3) | " . // copy stdout into wc and write result into fd 3
"tail -n +$offset | " . // apply offset
($limit !== null ? " head -n $limit | " : "") . // optionally apply limit
"sed '" . implode(';', $replaceSed) . "' | " . // replace strings in json
"sed '\$s/.$//'"], // remove last comma of last line
$fd_spec,
$pipes
);
fclose($pipes[0]);
fpassthru($pipes[1]);
fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
$count = intval(trim(stream_get_contents($pipes[3])));
fclose($pipes[3]);
$return_value = proc_close($process);
echo '],"totalCount":' . jenc($count) . "}\n";

33
www/organic/external/lkv/operators.php vendored Normal file

@@ -0,0 +1,33 @@
<?php
header('Access-Control-Allow-Origin: *');
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
header('Status: 405');
header('Content-Length: 0');
header('Allow: GET, HEAD');
exit;
}
function jenc($data): string {
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
$info = $_SERVER['PATH_INFO'];
if ($info !== '') {
header('Status: 404');
header('Content-Length: 0');
exit;
}
header('Content-Type: application/json; charset=UTF-8');
echo "{\"data\":[\n";
passthru(<<<EOF
curl -s 'https://lkv.at/at/zertifizierung/themen/BIO/zertifizierte-BIO-Betriebe.php' | grep '<tr' -A 3 \
| sed 's@<tr>@ {@;s@</tr>@ },@;s@<td><a .*href="\([^"]*\)".*>\(.*\)</a></td>@ "certUrl":"\\1","certNr":"\\2",@;s@<td>\(.*\)<br/>\(.*\)<br/>\([0-9]*\) \(.*\)</td>@ "name":"\\1","address":"\\2","postalCode":"\\3","city":"\\4"@;s@<td>\(.*\)<br/>\(.*\)</td>@ "name":"\\1","type":"\\2"@;\$s/.$//' \
| sed 's@"certUrl":"\(/[^"]*\)"@"certUrl":"https://lkv.at\\1"@' \
| sed 's@\s*",@",@g;s@:"\s*@:"@g'
EOF);
echo "]}\n";

86
www/organic/index.php Normal file

@@ -0,0 +1,86 @@
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
header('Status: 405');
header('Content-Length: 0');
header('Allow: GET, HEAD');
exit;
}
$info = $_SERVER['PATH_INFO'];
if ($info !== '/' && str_ends_with($info, '/')) {
header('Status: 303');
header('Content-Length: 0');
header('Location: /organic/' . substr($info, 0, -1));
exit;
}
$parts = explode('/', $info);
if (str_starts_with($info, '/certificates/')) {
$id = $parts[2];
if (str_ends_with($id, '.txt')) {
$id = substr($id, 0, -4);
}
if (str_contains($id, '/') || !file_exists("certificates/$id.pdf")) {
header('Content-Length: 0');
header('Status: 404');
exit;
}
if (str_ends_with($parts[2], '.txt')) {
$mode = '-layout';
if (isset($_GET['raw']) && strtolower($_GET['raw']) === 'true') {
$mode = '-raw';
}
header('Content-Type: text/plain; charset=UTF-8');
system("pdftotext $mode 'certificates/$id.pdf' -");
exit;
}
if (str_ends_with($id, '.appendix')) {
header('Status: 303');
header('Location: ' . substr($id, 0, -9));;
exit;
}
$cert = shell_exec("pdftotext -raw 'certificates/$id.pdf' -");
$appendix = shell_exec("pdftotext -raw 'certificates/$id.appendix.pdf' -");
$p1 = strpos($cert, "\nI.3 ");
$p2 = strpos($cert, "\nI.4 ");
echo substr($cert, $p1 + 5, $p2 - $p1 - 5);
exit;
} else if ($info === '/authorities') {
header('Content-Type: application/json; charset=UTF-8');
echo <<<EOF
{"data":[
{"id":"AT-BIO-301","countryCode":"AT","handle":"ABG","name":"Austria Bio Garantie GmbH","website":"https://www.bio-garantie.at/","apis":["easy-cert"]},
{"id":"AT-BIO-302","countryCode":"AT","handle":"ABG-LW","name":"Austria Bio Garantie Landwirtschaft GmbH","website":"https://www.bio-garantie.at/","apis":["easy-cert"]},
{"id":"AT-BIO-401","countryCode":"AT","handle":"BIOS","name":"BIOS Biokontrollservice Österreich GmbH","website":"https://www.bios-kontrolle.at/","apis":["bioqs"]},
{"id":"AT-BIO-402","countryCode":"AT","handle":"LACON","name":"LACON GmbH ","website":"https://www.lacon-institut.com/","apis":["easy-cert"]},
{"id":"AT-BIO-501","countryCode":"AT","handle":"SLK","name":"SLK GesmbH","website":"https://slk.at/","apis":["bioc"]},
{"id":"AT-BIO-901","countryCode":"AT","handle":"LVA","name":"LVA GmbH","website":"https://www.lva.at/","apis":[]},
{"id":"AT-BIO-902","countryCode":"AT","handle":"SGS","name":"SGS Austria Controll-Co. Ges.m.b.H.","website":"https://www.sgs.com/de-at ","apis":["bioc"]},
{"id":"AT-BIO-903","countryCode":"AT","handle":"LKV","name":"LKV Austria Gemeinnützige GmbH","website":"https://www.lkv.at/","apis":["bioc","lkv"]}
]}
EOF;
exit;
} else if (str_starts_with($info, '/authorities/')) {
$code = $parts[2];
header('Content-Type: text/plain; charset=UTF-8');
echo "Control Authority Code: $code\n";
exit;
} else if ($info === '/operators') {
header('Status: 501');
exit;
} else if (str_starts_with($info, '/operators/')) {
$ooid = $parts[2];
header('Content-Type: text/plain; charset=UTF-8');
echo "Organic Operator Id: $ooid\n";
exit;
}
header('Content-Length: 0');
header('Status: 404');

269
www/organic/pdf.php Normal file

@@ -0,0 +1,269 @@
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
header('Status: 405');
header('Content-Length: 0');
header('Allow: GET, POST, HEAD');
exit;
}
$info = $_SERVER['PATH_INFO'];
if ($info !== '') {
header('Status: 404');
header('Content-Length: 0');
exit;
}
$url = isset($_GET['url']) ? str_replace(' ', '+', $_GET['url']) : null;
$format = $_GET['format'] ?? 'json';
function get_address($array, $from, $to): array {
$address = [];
$postalCode = [];
$city = [];
for ($i = $to; $i >= $from; $i--) {
$el = $array[$i];
if (sizeof($postalCode) > 0) {
if (sizeof($address) === 0) $el = rtrim($el, ", \n\r\t\v\0");
if (strlen($el) === 0) continue;
array_unshift($address, $el);
} else if (preg_match("/^[A-Z0-9.\-]{3,},?$/", $el)) {
array_unshift($postalCode, trim($el, ", \n\r\t\v\0"));
} else {
array_unshift($city, $el);
}
}
return [implode(' ', $address), implode(' ', $postalCode), implode(' ', $city)];
}
function jenc($data): string {
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
$file = tmpfile();
$headerfile = tmpfile();
if (!$file || !$headerfile) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
$filename = stream_get_meta_data($file)['uri'];
$headerfilename = stream_get_meta_data($headerfile)['uri'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stdin = fopen("php://input", "rb");
while (!feof($stdin)) {
if (($data = fread($stdin, 8192)) === false)
break;
fwrite($file, $data);
}
fclose($stdin);
} else {
if (exec("curl -s -D " . escapeshellarg($headerfilename) . " -o " . escapeshellarg($filename) . " " . escapeshellarg($url)) === false) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
}
if ($format === 'text') {
header('Content-Type: text/plain; charset=UTF-8');
passthru("pdftotext -raw " . escapeshellarg($filename) . " -");
} else if ($format === 'sig') {
header('Content-Type: text/plain; charset=UTF-8');
passthru("pdfsig " . escapeshellarg($filename));
} else if ($format === 'json') {
header('Content-Type: application/json; charset=UTF-8');
if (exec("pdftotext -raw " . escapeshellarg($filename) . " -", $text) === false) {
header('Status: 500');
header('Content-Length: 0');
exit;
}
$text = implode("\n", $text);
exec("pdfsig " . escapeshellarg($filename), $sig);
$sig = implode("\n", $sig);
$r = preg_match('@([a-z]{2}) (https://webgate\.ec\.europa\.eu/tracesnt/directory/publication/organic-operator/(.*?)\.pdf) (\d+) / (\d+)@', $text, $matches);
if ($r === 1) {
// TRACES certificate
$data = [];
$parts = preg_split('@\n(I+\.\d+) ([^\n]*)@', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$status = str_replace("\n", '', $parts[0]);
for ($i = 3; $i < sizeof($parts); $i += 3) {
$data[$parts[$i - 2]] = trim($parts[$i]);
}
$lang = $matches[1];
$splitAddr = [
'de' => 'Adresse',
'en' => 'Address',
][$lang];
$splitCountry = [
'de' => 'Land',
'en' => 'Country',
][$lang];
$statusMap = [
'de' => [
'AUSGESTELLT' => 'issued',
],
'en' => [
'ISSUED' => 'issued',
]
][$lang];
$activityMap = [
'de' => [
'Aufbereitung' => 'preparation',
'Ausfuhr' => 'export',
'Einfuhr' => 'import',
'Lagerung' => 'storing',
'Produktion' => 'production',
'Vertrieb' => 'distribution',
'Vertrieb/Inverkehrbringen' => 'distribution_placing_on_the_market',
],
'en' => [
'Distribution' => 'distribution',
'Distribution/Placing on the market' => 'distribution_placing_on_the_market',
'Export' => 'export',
'Import' => 'import',
'Preparation' => 'preparation',
'Production' => 'production',
'Storing' => 'storing',
],
][$lang];
$certUrl = $matches[2];
$certId = $matches[3];
$authorityId = explode('.', $certId)[0];
$operatorId = explode('.', $certId)[1];
$operator = preg_split('@\s+@', trim($data['I.3']));
$p1 = array_search($splitAddr, $operator);
$p2 = array_search($splitCountry, $operator);
$operatorName = trim(implode(' ', array_filter($operator, fn($k,$i) => $i > 0 && $i < $p1, ARRAY_FILTER_USE_BOTH)), ', ');
[$opAddr, $opPostal, $opCity] = get_address($operator, $p1 + 1, $p2 - 1);
$authority = preg_split('@\s+@', trim($data['I.4']));
$until = array_search("($authorityId)", $authority);
$p1 = array_search($splitAddr, $authority);
$p2 = array_search($splitCountry, $authority);
$authorityName = implode(' ', array_filter($authority, fn($k,$i) => $i > 0 && $i < $p1 - 1 && ($i !== $p1 - 2 || !str_starts_with($k, '(')), ARRAY_FILTER_USE_BOTH));
[$aAddr, $aPostal, $aCity] = get_address($authority, $p1 + 1, $p2 - 1);
$activities = [];
foreach (explode("\n", $data['I.5']) as $a) {
$activities[] = $activityMap[trim($a, '• ')];
}
preg_match_all('/\([a-g]\)/', $data['I.6'], $matches, PREG_SET_ORDER);
$products = [];
foreach ($matches as $m) {
$products[] = $m[0];
}
preg_match_all('@\d+/\d+/\d+@', $data['I.8'], $matches, PREG_SET_ORDER);
$valid1 = implode('-', array_reverse(explode('/', $matches[0][0])));
$valid2 = implode('-', array_reverse(explode('/', $matches[1][0])));
$sigs = [];
foreach (array_slice(explode("\nSignature #", $sig), 1) as $s) {
$sData = [];
$sData2 = [];
preg_match_all('/\n {2}- (([^:\n]*): )?([^\n]*)/', $s, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
if (strlen($m[2]) === 0) {
$sData2[] = $m[3];
} else {
$sData[$m[2]] = $m[3];
}
}
$sigs[] = [
'signerCommonName' => $sData['Signer Certificate Common Name'],
'valid' => $sData['Signature Validation'] === 'Signature is Valid.',
'trusted' => $sData['Certificate Validation'] === 'Certificate is Trusted.',
'totalDocument' => in_array('Total document signed', $sData2),
'timestamp' => gmdate('Y-m-d\TH:i:s\Z', strtotime($sData['Signing Time'])),
'type' => $sData['Signature Type'],
'hashAlgorithm' => $sData['Signing Hash Algorithm'],
'signerDistinguishedName' => $sData['Signer full Distinguished Name'],
'fieldName' => $sData['Signature Field Name'],
];
}
echo "{\"type\":\"traces\",\"lang\":\"$lang\",\"id\":\"$certId\",\"status\":\"$statusMap[$status]\"";
echo ",\n \"operator\":{\"id\":" . jenc($operatorId).
',"groupOfOperators":' . jenc(!str_starts_with($data['I.2'], '☑')) .
',"name":' . jenc($operatorName) .
',"address":' . jenc($opAddr) .
',"postalCode":' . jenc($opPostal) .
',"city":' . jenc($opCity) .
',"countryCode":' . jenc($operator[sizeof($operator) - 1]) .
"},\n \"authority\":{\"id\":" . jenc($authorityId) .
',"name":' . jenc($authorityName) .
',"address":' . jenc($aAddr) .
',"postalCode":' . jenc($aPostal) .
',"city":' . jenc($aCity) .
',"countryCode":' . jenc($authority[sizeof($authority) - 1]) .
"},\n \"activities\":" . jenc($activities) .
",\n \"productCategories\":" . jenc($products) .
",\n \"validFrom\":" . jenc($valid1) .
',"validTo":' . jenc($valid2) .
",\n \"url\":\"$certUrl\"" .
",\n \"digitalSignatures\":" . jenc($sigs) .
"\n}\n";
exit;
}
if (preg_match('/AT-BIO-[0-9]{3}/', $text, $matches) === 1) {
$authorityId = $matches[0];
$certId = null;
$certNr = null;
if (preg_match("/$authorityId\.040-[0-9]{7}\.[0-9]{4}\.[0-9]{3}/", $text, $matches) === 1)
$certId = $matches[0];
if (preg_match_all("/\b[0-9]+([._-])[0-9]+\g{-1}[0-9]+\b/", $text, $matches, PREG_SET_ORDER) !== false) {
foreach ($matches as $m) {
if (strlen($m[0]) > 10 && !str_ends_with($certId, $m[0]))
$certNr = $m[0];
}
}
echo "{\"type\":\"$authorityId\",\"lang\":\"de\",\"id\":" . jenc($certId) . ",\"nr\":" . jenc($certNr);
echo ",\n \"operator\":{},\n \"authority\":{\"id\":" . jenc($authorityId) . "}}\n";
exit;
}
echo "{\"type\":\"unknown\"}\n";
} else {
$headers = [];
$status_code = null;
foreach (explode("\n", file_get_contents($headerfilename)) as $line) {
if (trim($line) === '') break;
if ($status_code === null) {
$status_code = intval(explode(' ', $line)[1]);
if ($status_code !== 200)
break;
continue;
}
[$k,$v] = explode(':', $line, 2);
$k = strtolower(trim($k));
$v = trim($v);
$headers[$k] = $v;
}
if (str_starts_with($headers['content-type'], "application/pdf")) {
header('Content-Type: application/pdf');
$content_length = null;
if (isset($headers['content-length'])) {
$content_length = intval($headers['content-length']);
header('Content-Length: ' . $headers['content-length']);
}
$parts = explode('/', $url);
$realFilename = $parts[sizeof($parts) - 1];
if (isset($headers['content-disposition'])) {
preg_match('@filename="(.*?)"@', $headers['content-disposition'], $matches);
$realFilename = $matches[1];
}
header('Content-Disposition: inline; filename="' . $realFilename . '"');
fpassthru($file);
} else {
header('Status: 500');
header('Content-Length: 0');
}
}

@@ -1,206 +0,0 @@
"use strict";
function getCredentialsUsername(client) {
return window.localStorage.getItem(`${CLIENT}/${client}/username`);
}
function getCredentialsPassword(client) {
return window.localStorage.getItem(`${CLIENT}/${client}/password`);
}
function getBasicAuth(client) {
return {
'Authorization': 'Basic ' + btoa(getCredentialsUsername(client) + ':' + getCredentialsPassword(client)),
}
}
async function _get(client, path) {
const res = await fetch(`${CLIENTS[client]['api']}${path}`, {
method: 'GET',
headers: {...getBasicAuth(client)},
});
const json = await res.json();
if (!res.ok) throw new ApiError(res.status, json['message']);
return json;
}
async function get(client, path) {
return (await _get(client, path))['data'];
}
async function getWineVarieties(client) {
return Object.fromEntries((await get(client, '/wine/varieties')).map(item => [item['sortid'], item]));
}
async function getWineQualityLevels(client) {
return Object.fromEntries((await get(client, '/wine/quality_levels')).map(item => [item['qualid'], item]));
}
async function getDeliverySchedules(client, filters, limit, offset) {
const query = [];
if (!!filters) query.push(`filters=${filters.join(',')}`);
if (!!limit) query.push(`limit=${limit}`);
if (!!offset) query.push(`offset=${offset}`);
return await _get(client, `/delivery_schedules${!!query ? '?' : ''}${query.join('&')}`);
}
async function load(client) {
const main = document.getElementById("access");
const form = main.getElementsByTagName("form")[0];
if (form) {
const elements = form.getElementsByClassName('error');
for (const e of elements) form.removeChild(e);
}
try {
window.WINE_VARIETIES = await getWineVarieties(client);
window.WINE_QUALITY_LEVELS = await getWineQualityLevels(client);
return true;
} catch (e) {
if (form) {
window.localStorage.removeItem(`${CLIENT}/${client}/password`);
const error = document.createElement('div');
error.className = 'error';
error.innerText = e.localizedMessage ?? ERROR_MESSAGES[e.message] ?? 'Unbekannter Fehler';
form.insertBefore(error, form.lastChild.previousSibling);
} else {
window.location.hash = `#/${client}/login`;
}
return false;
}
}
async function init() {
//await load();
render();
}
async function updateOverview(client) {
const [schedules] = await Promise.all([getDeliverySchedules(client, [`year=${getCurrentLastSeason()}`])]);
const rows = [];
const days = groupBy(schedules.data, 'date');
const now = new Date();
for (const [dateString, day] of Object.entries(days)) {
const date = new Date(dateString);
const row = document.createElement('div');
row.className = 'day';
if (now.getDate() === date.getDate()) row.classList.add('today');
row.innerHTML = `<div><span style="font-size: 0.75em; display: block">${fmtDateWeekday(date)}</span>${fmtDate(date)}</div>`;
const container = document.createElement('div');
container.className = 'schedule-container';
for (const schedule of day) {
const from = schedule.announcement_from !== null ? new Date(schedule.announcement_from) : null;
const to = schedule.announcement_to !== null ? new Date(schedule.announcement_to) : null;
const status = from === null && to === null ? 'Anmeldung offen' : from > now ? `Anmeldung ab ${fmtDateTime(from)}` : to > now ? `Anmeldung bis ${fmtDateTime(new Date(to - 1))}` : 'Anmeldefrist vorbei';
const link = document.createElement('a');
if (schedule.is_cancelled) link.className = 'cancelled';
link.innerHTML += `<div><span style="font-size: 0.75em; display: block;">${escapeHTML(schedule.branch.name)}</span><span>${escapeHTML(schedule.description)}</span><span style="font-size: 0.75em; display: block;">${status}</span></div>`;
if (schedule.delivered_weight > 0) {
link.innerHTML += `
<span>
<span><strong>${fmtInt(schedule.delivered_weight)} kg</strong></span> /
<span class="min-kg">${fmtInt(schedule.announced_weight)} kg</span>
(<span class="min-percent">${fmtInt(Math.round(schedule.delivered_weight / schedule.announced_weight * 100))}%</span>)
</span>`;
} else if (schedule.max_weight !== null) {
link.innerHTML += `
<span>
<span>${fmtInt(schedule.announced_weight)} kg</span> /
<span class="min-kg">${fmtInt(schedule.max_weight)} kg</span>
(<span class="min-percent">${fmtInt(Math.round(schedule.announced_weight / schedule.max_weight * 100))}%</span>)
</span>`;
} else {
link.innerHTML += `<span><span>${fmtInt(schedule.announced_weight)} kg</span></span>`;
}
container.append(link);
}
row.appendChild(container);
rows.push(row);
}
const main = document.getElementsByTagName('main')[0];
main.replaceChildren(main.firstElementChild, ...rows);
}
function render() {
const hash = window.location.hash;
const main = document.getElementById("access");
const nav = document.getElementsByTagName("nav")[0].getElementsByTagName("ul")[0];
for (const li of nav.children) li.className = '';
let client = null;
for (const id in CLIENTS) {
if (hash.startsWith(`#/${id}/`) || hash === `#/${id}`) {
client = id;
}
}
if (client === null) {
window.location.hash = `#/${Object.keys(CLIENTS)[0]}`;
return;
}
nav.children[Object.keys(CLIENTS).indexOf(client)].className = 'active';
if ((!getCredentialsUsername(client) || !getCredentialsPassword(client)) && window.location.hash !== `#/${client}/login`) {
window.location.hash = `#/${client}/login`;
return;
}
if (hash === `#/${client}/login`) {
main.className = 'login';
main.innerHTML = `
<form onsubmit="return actionLogin(this);">
<h1>Anmelden</h1>
<input type="text" name="username" placeholder="Benutzername" value="${getCredentialsUsername(client) ?? ''}"/>
<input type="password" name="password" placeholder="Kennwort"/>
<input type="hidden" name="client" value="${client}"/>
<button type="submit">Anmelden</button>
</form>`;
} else if (hash === `#/${client}`) {
main.className = 'overview';
main.innerHTML = `
<h1>${CLIENTS[client].name}</h1>`;
updateOverview(client).then();
} else {
window.location.hash = `#/${client}`;
}
}
function update() {
const hash = window.location.hash;
let client = null;
for (const id in CLIENTS) {
if (hash.startsWith(`#/${id}/`) || hash === `#/${id}`) {
client = id;
}
}
if (document.hidden) {
// do nothing
} else {
if (hash === `#/${client}`) {
updateOverview(client).then();
}
}
}
document.addEventListener('DOMContentLoaded', async () => {
await init();
setInterval(update, 60_000);
});
window.addEventListener('hashchange', () => {
render();
});
window.addEventListener('pageshow', update)
document.addEventListener('visibilitychange', update);
function actionLogin(form) {
window.localStorage.setItem(`${CLIENT}/${form.client.value}/username`, form.username.value);
window.localStorage.setItem(`${CLIENT}/${form.client.value}/password`, form.password.value);
load(form.client.value).then(success => {
if (success) window.location.hash = `#/${form.client.value}`;
});
return false;
}

@@ -1,331 +0,0 @@
"use strict";
function getCredentialsUsername() {
return window.localStorage.getItem(`${CLIENT}/username`);
}
function getCredentialsPassword() {
return window.localStorage.getItem(`${CLIENT}/password`);
}
function getBasicAuth() {
return {
'Authorization': 'Basic ' + btoa(getCredentialsUsername() + ':' + getCredentialsPassword()),
}
}
async function _get(path) {
const res = await fetch(`${ELWIG_API}${path}`, {
method: 'GET',
headers: {...getBasicAuth()},
});
const json = await res.json();
if (!res.ok) throw new ApiError(res.status, json['message']);
return json;
}
async function get(path) {
return (await _get(path))['data'];
}
async function getMember(mgnr) {
return await get(`/members/${mgnr}`);
}
async function getWineVarieties() {
return Object.fromEntries((await get('/wine/varieties')).map(item => [item['sortid'], item]));
}
async function getWineQualityLevels() {
return Object.fromEntries((await get('/wine/quality_levels')).map(item => [item['qualid'], item]));
}
async function getWineAttributes() {
return Object.fromEntries((await get('/wine/attributes')).map(item => [item['attrid'], item]));
}
async function getWineCultivations() {
return Object.fromEntries((await get('/wine/cultivations')).map(item => [item['cultid'], item]));
}
async function getModifiers() {
const list = await get('/modifiers');
const dict = {};
for (const item of list) {
if (!dict[item['year']]) dict[item['year']] = {};
dict[item['year']][item['modid']] = item;
}
return dict;
}
async function getDeliveries(filters, limit, offset) {
const query = ['sort=reverse'];
if (!!filters) query.push(`filters=${filters.join(',')}`);
if (!!limit) query.push(`limit=${limit}`);
if (!!offset) query.push(`offset=${offset}`);
return await _get(`/deliveries${!!query ? '?' : ''}${query.join('&')}`);
}
async function getDeliveryStats(filters, detail) {
const query = [];
if (!!filters) query.push(`filters=${filters.join(',')}`);
if (!!detail) query.push(`detail=${detail}`);
return await _get(`/deliveries/stat${!!query ? '?' : ''}${query.join('&')}`);
}
async function getDeliverySchedules(filters, limit, offset) {
const query = [];
if (!!filters) query.push(`filters=${filters.join(',')}`);
if (!!limit) query.push(`limit=${limit}`);
if (!!offset) query.push(`offset=${offset}`);
return await _get(`/delivery_schedules${!!query ? '?' : ''}${query.join('&')}`);
}
async function getDeliveryAnnouncements(filters, limit, offset) {
const query = [];
if (!!filters) query.push(`filters=${filters.join(',')}`);
if (!!limit) query.push(`limit=${limit}`);
if (!!offset) query.push(`offset=${offset}`);
return await _get(`/delivery_announcements${!!query ? '?' : ''}${query.join('&')}`);
}
async function load() {
const main = document.getElementById("access");
const form = main.getElementsByTagName("form")[0];
if (form) {
const elements = form.getElementsByClassName('error');
for (const e of elements) form.removeChild(e);
}
try {
window.MEMBER = await getMember(getCredentialsUsername());
const txt = document.getElementById('usertext');
txt.innerHTML = `${MEMBER.prefix ?? ''} ${MEMBER.given_name ?? ''} ${MEMBER.middle_names ?? ''} ${MEMBER.name ?? ''} ${MEMBER.suffix ?? ''}<br/><div>MgNr. ${MEMBER.mgnr}</div>`;
window.WINE_VARIETIES = await getWineVarieties();
window.WINE_QUALITY_LEVELS = await getWineQualityLevels();
window.WINE_ATTRIBUTES = await getWineAttributes();
window.WINE_CULTIVATIONS = await getWineCultivations();
window.MODIFIERS = await getModifiers();
return true;
} catch (e) {
if (form) {
window.localStorage.removeItem(`${CLIENT}/password`);
const error = document.createElement('div');
error.className = 'error';
error.innerText = e.localizedMessage ?? ERROR_MESSAGES[e.message] ?? 'Unbekannter Fehler';
form.insertBefore(error, form.lastChild.previousSibling);
} else {
window.location.hash = '#/login';
}
return false;
}
}
async function init() {
if (!getCredentialsUsername() || !getCredentialsPassword()) {
window.location.hash = '#/login';
render();
return;
}
await load();
render();
}
async function updateOverview() {
const [schedules] = await Promise.all([getDeliverySchedules([`year=${getCurrentLastSeason()}`])]);
const main = document.getElementsByTagName('main')[0];
const days = groupBy(schedules.data, 'date');
for (const [dateString, day] of Object.entries(days)) {
const row = document.createElement('div');
row.className = 'day';
const date = new Date(dateString);
row.innerHTML = `<div><span style="font-size: 0.75em; display: block">${fmtDateWeekday(date)}</span>${fmtDate(date)}</div>`;
const container = document.createElement('div');
container.className = 'schedule-container';
for (const schedule of day) {
const now = new Date();
const from = schedule.announcement_from !== null ? new Date(schedule.announcement_from) : null;
const to = schedule.announcement_to !== null ? new Date(schedule.announcement_to) : null;
const status = from === null && to === null ? 'Anmeldung offen' : from > now ? `Anmeldung ab ${fmtDateTime(from)}` : to > now ? `Anmeldung bis ${fmtDateTime(new Date(to - 1))}` : 'Anmeldefrist vorbei';
const link = document.createElement('a');
link.href = `#/anmelden/${schedule.year}/${schedule.dsnr}`
link.innerHTML += `<div><span>${schedule.description}</span><span style="font-size: 0.75em; display: block;">${status}</span></div>`;
if (schedule.delivered_weight > 0) {
link.innerHTML += `
<span>
<span><strong>${fmtInt(schedule.delivered_weight)} kg</strong></span> /
<span class="min-kg">${fmtInt(schedule.announced_weight)} kg</span>
(<span class="min-percent">${fmtInt(Math.round(schedule.delivered_weight / schedule.announced_weight * 100))}%</span>)
</span>`;
} else {
link.innerHTML += `
<span>
<span>${fmtInt(schedule.announced_weight)} kg</span> /
<span class="min-kg">${fmtInt(schedule.max_weight)} kg</span>
(<span class="min-percent">${fmtInt(Math.round(schedule.announced_weight / schedule.max_weight * 100))}%</span>)
</span>`;
}
container.append(link);
}
row.appendChild(container);
main.appendChild(row);
}
}
async function updateDeliveries(year) {
const filters = [`mgnr=${MEMBER.mgnr}`, `year=${year}`];
const [deliveries, stat] = await Promise.all([getDeliveries(filters), getDeliveryStats(filters)]);
const tbody = document.getElementById('delivery-list');
tbody.innerHTML = '';
for (const delivery of deliveries.data) {
const tr = document.createElement('tr');
tr.style.background = '#C0C0C0';
tr.innerHTML = `
<th colspan="2">${delivery.lsnr}</th>
<td>${fmtDate(new Date(delivery.date))}</td>
<td>${delivery.time.substring(0, 5)}</td>
<td colspan="2">${delivery.branch.name}</td>
<td colspan="7">${delivery.comment ?? ''}</td>`;
tbody.appendChild(tr);
for (const part of delivery.parts) {
const tr = document.createElement('tr');
const defaultQualityLevel = getDefaultQualityLevel(part.gradation.kmw);
tr.innerHTML = `
<th>${part.dpnr}</th>
<td colspan="2">${WINE_VARIETIES[part.variety.sortid]?.name ?? ''}</td>
<td colspan="2" style="font-weight: bold;">${WINE_CULTIVATIONS[part.cultivation?.cultid]?.name ?? ''}</td>
<td colspan="2" style="font-weight: bold;">${WINE_ATTRIBUTES[part.attribute?.attrid]?.name ?? ''}</td>
<td class="${defaultQualityLevel['qualid'] != part.quality_level.qualid ? 'abgewertet' : ''}">${WINE_QUALITY_LEVELS[part.quality_level.qualid]?.name ?? ''}</td>
<td class="center">${fmtOe(part.gradation.oe)}</td>
<td class="center">${fmtKmw(part.gradation.kmw)}</td>
<td class="number">${fmtInt(part.weight)}</td>
<td>${part.modifiers.map(m => MODIFIERS[delivery.year][m.modid]).sort(m => m.ordering).map(m => m.name).join(' / ')}</td>
<td>${part.comment ?? ''}</td>`;
tbody.appendChild(tr);
}
}
const element = document.getElementById('delivery-stat');
element.innerText = `(Teil-)Lieferungen: ${fmtInt(stat.data.total.count)} (${fmtInt(stat.data.total.parts)}), Gewicht: ${fmtInt(stat.data.total.weight.sum)} kg`;
}
function render() {
const hash = window.location.hash;
const main = document.getElementById("access");
const nav = document.getElementsByTagName("nav")[0].getElementsByTagName("ul")[0];
for (const li of nav.children) li.className = '';
if (hash === '#/login') {
main.className = 'login';
main.innerHTML = `
<form onsubmit="return actionLogin(this);">
<h1>Anmelden</h1>
<input type="text" name="username" placeholder="Mitgliedsnummer" value="${getCredentialsUsername() ?? ''}"/>
<input type="password" name="password" placeholder="Kennwort"/>
<button type="submit">Anmelden</button>
</form>`;
} else if (hash === '#/') {
nav.children[0].className = 'active';
main.className = 'overview';
main.innerHTML = `
<h1>Übersicht</h1>`;
updateOverview().then();
} else if (hash === '#/mitglied') {
nav.children[1].className = 'active';
main.className = 'member';
main.innerHTML = `
<h1>Mitglied</h1>
<button onclick="actionLogout()">Abmelden</button>
<pre>${JSON.stringify(MEMBER, null, 2)}</pre>`;
} else if (hash === '#/lieferungen') {
nav.children[2].className = 'active';
main.className = 'deliveries';
main.innerHTML = `
<h1>Lieferungen</h1>
<form>
<div>
<label for="season">Saison:</label>
<input name="season" type="number" min="1900" max="9999"
value="${getCurrentLastSeason()}" onchange="updateDeliveries(this.value).then()"/>
</div>
<div id="delivery-stat"/>
</form>
<table style="width: 100%;">
<colgroup>
<col style="width: 50px;"/>
<col style="width: 70px;"/>
<col style="width: 100px;"/>
<col style="width: 60px;"/>
<col style="width: 50px;"/>
<col style="width: 70px;"/>
<col style="width: 50px;"/>
<col style="width: 120px;"/>
<col style="width: 50px;"/>
<col style="width: 50px;"/>
<col style="width: 60px;"/>
<col style="min-width: 80px;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2"></th>
<th rowspan="2" colspan="2">Sorte</th>
<th rowspan="2" colspan="2">Bewirt.</th>
<th rowspan="2" colspan="2">Attribut</th>
<th rowspan="2">Qualitätsstufe</th>
<th colspan="2" class="center">Gradation</th>
<th class="center">Gewicht</th>
<th rowspan="2">Zu-/Abschläge</th>
<th></th>
</tr>
<tr>
<th class="unit">[°Oe]</th>
<th class="unit">[°KMW]</th>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody id="delivery-list"></tbody>
</table>`;
updateDeliveries(getCurrentLastSeason()).then();
} else if (hash === '#/anmeldungen') {
nav.children[3].className = 'active';
main.className = 'announcements';
main.innerHTML = '<h1>Anmeldungen</h1>';
} else if (hash.startsWith('#/anmelden/')) {
nav.children[3].className = 'active';
main.className = 'announce';
main.innerHTML = '<h1>Anmelden</h1>';
} else {
window.location.hash = `#/`;
}
}
document.addEventListener('DOMContentLoaded', async () => {
await init();
});
window.addEventListener('hashchange', () => {
if ((!getCredentialsUsername() || !getCredentialsPassword()) && window.location.hash !== '#/login') {
window.location.hash = '#/login';
return;
}
render();
});
window.addEventListener('pageshow', update)
document.addEventListener('visibilitychange', update);
function actionLogin(form) {
window.localStorage.setItem(`${CLIENT}/username`, form.username.value);
window.localStorage.setItem(`${CLIENT}/password`, form.password.value);
load().then(success => {
if (success) window.location.hash = '#/';
});
return false;
}
function actionLogout() {
window.localStorage.removeItem(`${CLIENT}/username`);
window.localStorage.removeItem(`${CLIENT}/password`);
window.location.reload();
}

@@ -1,79 +0,0 @@
"use strict";
const ERROR_MESSAGES = {
401: 'Ungültige Anmeldedaten',
'NetworkError when attempting to fetch resource.': 'Netzwerkfehler',
};
class ApiError extends Error {
constructor(statusCode, message) {
super(statusCode + ' - ' + message);
this.name = 'ApiError';
this.localizedMessage = ERROR_MESSAGES[statusCode];
}
}
function escapeHTML(str) {
const p = document.createElement("p");
p.appendChild(document.createTextNode(str));
return p.innerHTML;
}
function fmtDate(date) {
return date.toLocaleDateString('de-AT', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
}
function fmtDateTime(date) {
return date.toLocaleDateString('de-AT', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function fmtDateWeekday(date) {
return date.toLocaleDateString('de-AT', {
weekday: 'long',
});
}
function fmtOe(oe) {
return oe.toLocaleString('de-AT', {minimumFractionDigits: 0, maximumFractionDigits: 0});
}
function fmtKmw(kmw) {
return kmw.toLocaleString('de-AT', {minimumFractionDigits: 1, maximumFractionDigits: 1});
}
function fmtInt(num) {
return num.toLocaleString('de-AT', {minimumFractionDigits: 0, maximumFractionDigits: 0});
}
function groupBy(list, key) {
return list.reduce((groups, value) => {
(groups[value[key]] = groups[value[key]] ?? []).push(value);
return groups;
}, {});
}
function getCurrentLastSeason() {
const date = new Date();
return date.getFullYear() - ((date.getMonth() + 1) <= 6 ? 1 : 0);
}
function getDefaultQualityLevel(kmw) {
const list = Object.values(WINE_QUALITY_LEVELS).filter(q => !q['is_predicate']).sort((a, b) => a['min_kmw'] - b['min_kmw']);
let last = list[0];
for (const q of list) {
if (q['min_kmw'] > kmw) {
return last;
}
last = q;
}
return last;
}

@@ -81,11 +81,63 @@ nav li.active a{
color: var(--main-color); color: var(--main-color);
} }
/**** Index ****/ nav a.flag {
text-decoration: none;
}
main span[id] { nav a.flag div {
position: relative; display: inline-block;
top: -8em; font-size: 1.25em;
width: 1em;
margin: 0.75rem 1rem;
}
nav a.flag[href='/de/'] div::before,
nav a.flag[href='/en/']:hover div::before,
nav a.flag[href='/en/']:focus div::before {
content: '\1F1EC\1F1E7'; /* GB */
}
nav a.flag[href='/en/'] div::before,
nav a.flag[href='/de/']:hover div::before,
nav a.flag[href='/de/']:focus div::before {
content: '\1F1E6\1F1F9'; /* AT */
}
footer {
height: 8em;
padding: 1em;
width: 100%;
box-shadow: 0 0 0.5rem #00000060;
background-color: #404040;
color: #FFFFFF;
display: flex;
justify-content: center;
gap: 2em;
position: absolute;
bottom: 0;
}
footer img {
height: 4em;
width: 4em;
}
footer a {
align-self: center;
}
@media screen and (max-width: 800px) {
footer {
flex-direction: column;
height: unset;
align-items: center;
gap: 1em;
padding-top: 3em;
}
footer > *:not(:first-child) {
width: 300px;
}
} }
p a, p a,
@@ -93,6 +145,22 @@ table a {
color: var(--main-color); color: var(--main-color);
} }
main section h3 {
text-align: center;
font-size: 2em;
}
.center {
text-align: center;
}
/**** Index ****/
main span[id] {
position: relative;
top: -8em;
}
main .background { main .background {
background-image: var(--img); background-image: var(--img);
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -204,15 +272,6 @@ main section p {
text-align: justify; text-align: justify;
} }
.center {
text-align: center;
}
main section h3 {
text-align: center;
font-size: 2em;
}
main .about { main .about {
padding: 0 0 1em 0; padding: 0 0 1em 0;
} }
@@ -331,29 +390,6 @@ table .unit {
padding-top: 0; padding-top: 0;
} }
footer {
height: 8em;
padding: 1em;
width: 100%;
box-shadow: 0 0 0.5rem #00000060;
background-color: #404040;
color: #FFFFFF;
display: flex;
justify-content: center;
gap: 2em;
position: absolute;
bottom: 0;
}
footer img {
height: 4em;
width: 4em;
}
footer a {
align-self: center;
}
@media screen and (min-width: 1921px) { @media screen and (min-width: 1921px) {
main .picture-1 { main .picture-1 {
height: calc(36em + 4rem); height: calc(36em + 4rem);
@@ -374,16 +410,6 @@ footer a {
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
footer {
flex-direction: column;
height: unset;
align-items: center;
gap: 1em;
padding-top: 3em;
}
footer > *:not(:first-child) {
width: 300px;
}
body.header-footer { body.header-footer {
min-height: calc(100vh + 20em); min-height: calc(100vh + 20em);
padding-bottom: 20em; padding-bottom: 20em;
@@ -422,197 +448,3 @@ footer a {
font-size: 0.5em; font-size: 0.5em;
} }
} }
/**** Access ****/
main#access {
width: calc(100% - 2em);
max-width: 1200px;
margin: 4em auto 4em auto;
border-radius: 4px;
box-shadow: 0 0 0.5em #00000060;
padding: 1em 2em;
}
main#access h1 {
text-align: center;
}
main#access.login {
max-width: 500px;
}
main#access.login h1 {
margin: 1rem 0 1rem 0;
}
main#access.login form {
text-align: center;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 1em;
margin: 1em 0;
}
main#access.login form input,
main#access.login form button {
font-size: 1em;
border: 1px solid #C0C0C0;
border-radius: 4px;
padding: 0.25em 0.5em;
}
main#access.login form .error {
font-size: 1em;
color: #C00000;
border: 1px solid #C00000;
border-radius: 4px;
padding: 0.25em 0.5em;
text-align: center;
font-weight: bold;
margin: 0;
}
#user {
display: flex;
justify-content: end;
align-items: center;
text-decoration: none;
color: inherit;
position: absolute;
right: 0;
height: 100%;
}
#user:hover {
color: var(--main-color);
}
#usertext {
font-size: 0.875em;
}
#usertext > div {
font-size: 0.75em;
opacity: 0.5;
}
main .number {
text-align: right;
}
main.deliveries form {
margin: 1em;
display: flex;
align-items: center;
align-content: center;
gap: 1em;
}
.abgewertet {
color: #C00000;
font-weight: bold;
}
main.overview .day {
border-radius: 4px;
padding-left: 1.5em;
background: #E0E0E0;
margin: 1em 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 2em;
height: 10em;
}
main.overview .day.today {
background-color: var(--light-color);
}
main.overview .schedule-container {
background: #00000020;
border-radius: 4px;
display: flex;
flex-direction: column;
flex-grow: 1;
height: 100%;
justify-content: center;
}
main.overview .schedule-container a {
border-radius: 4px;
color: unset;
text-decoration: none;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1em;
transition: background-color 0.125s;
}
main.overview .schedule-container a.cancelled > div {
color: #A00000;
text-decoration: line-through;
}
main.overview .schedule-container a.cancelled > span {
opacity: 0.25;
}
main.overview .schedule-container a:hover {
background-color: #00000020;
}
.min-percent {
min-width: 2.625em;
display: inline-block;
text-align: right;
}
.min-kg {
min-width: 5em;
display: inline-block;
text-align: right;
}
@media screen and (max-width: 1000px) {
main#access {
padding: 1em 0.5em;
margin-left: 0;
margin-right: 0;
width: 100%;
}
main.overview .day {
flex-direction: column;
height: unset;
padding-left: 0;
gap: 0.5em;
padding-top: 0.5em;
}
main.overview .day > div:first-child {
text-align: center;
}
main.overview .schedule-container {
height: unset;
width: 100%;
}
main.overview .schedule-container a {
padding: 1em;
}
}
@media screen and (max-width: 650px) {
main.overview .schedule-container a {
flex-direction: column;
}
main.overview .schedule-container a > *:first-child {
width: 100%;
}
main.overview .schedule-container a > *:last-child {
width: 100%;
text-align: right;
}
}

@@ -1,4 +1,3 @@
# robots.txt for elwig.at # robots.txt for elwig.at
User-Agent: * User-Agent: *
Disallow: /clients/
Sitemap: https://elwig.at/sitemap.xml Sitemap: https://elwig.at/sitemap.xml

@@ -1,10 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
<url> <url>
<loc>https://elwig.at/</loc> <loc>https://elwig.at/de/</loc>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>1.0</priority> <priority>1.0</priority>
</url> </url>
<url>
<loc>https://elwig.at/en/</loc>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url> <url>
<loc>https://elwig.at/files/</loc> <loc>https://elwig.at/files/</loc>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>