Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.idea
|
||||
credentials.*
|
||||
!*.sample.*
|
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
# Elwig synchronization backend
|
||||
|
||||
https://sync.elwig.at/
|
0
www/.data/.gitkeep
Normal file
0
www/.data/.gitkeep
Normal file
22
www/.php/auth.inc
Normal file
22
www/.php/auth.inc
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
require "credentials.inc";
|
||||
|
||||
function http_401_unauthorized(): void {
|
||||
header('Status: 401');
|
||||
header('WWW-Authenticate: Basic realm="Elwig"');
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
header('Content-Length: 17');
|
||||
exit("401 Unauthorized\n");
|
||||
}
|
||||
|
||||
function authenticate(string $client): void {
|
||||
global $CREDENTIALS;
|
||||
$credentials = $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();
|
||||
}
|
||||
}
|
8
www/.php/credentials.sample.inc
Normal file
8
www/.php/credentials.sample.inc
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
global $CREDENTIALS;
|
||||
|
||||
$CREDENTIALS = [
|
||||
'name' => [
|
||||
'username' => 'password',
|
||||
],
|
||||
];
|
50
www/.php/format.inc
Normal file
50
www/.php/format.inc
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
date_default_timezone_set('Europe/Vienna');
|
||||
|
||||
function get_fmt(): string {
|
||||
$fmt = _get_fmt();
|
||||
if ($fmt === 'ascii') {
|
||||
header('Status: 303');
|
||||
header('Location: ?format=text');
|
||||
header('Content-Length: 14');
|
||||
exit("303 See Other\n");
|
||||
} else if ($fmt !== 'json' && $fmt !== 'html' && $fmt !== 'text') {
|
||||
header('Status: 300');
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header('Content-Length: 162');
|
||||
echo "<!DOCTYPE html><html><head></head><body>\n<a href='?format=html'>HTML</a><br/>\n<a href='?format=json'>JSON</a><br/>\n<a href='?format=text'>Text</a>\n</body></html>\n";
|
||||
exit();
|
||||
}
|
||||
return $fmt;
|
||||
}
|
||||
|
||||
function _get_fmt(): string {
|
||||
if (!empty($_GET['format'])) return $_GET['format'];
|
||||
|
||||
$fmts = [];
|
||||
foreach (explode(',', $_SERVER['HTTP_ACCEPT']) as $acc) {
|
||||
$acc = explode(';', trim($acc));
|
||||
$q = 1;
|
||||
if (sizeof($acc) > 1) {
|
||||
$qv = trim($acc[1]);
|
||||
if (str_starts_with($qv, 'q=')) {
|
||||
$q = (double)substr($qv, 2);
|
||||
}
|
||||
}
|
||||
$fmts[trim($acc[0])] = $q;
|
||||
}
|
||||
arsort($fmts, SORT_NUMERIC);
|
||||
array_filter($fmts, function($k) {
|
||||
return str_contains($k, '/json') || $k === 'text/html' || $k === 'text/plain' || str_contains($k, 'text/*');
|
||||
});
|
||||
|
||||
$type = sizeof($fmts) > 0 ? array_key_first($fmts) : null;
|
||||
if (str_contains($type, '/json')) {
|
||||
return 'json';
|
||||
} else if ($type === 'text/plain') {
|
||||
return 'text';
|
||||
} else {
|
||||
return 'html';
|
||||
}
|
||||
}
|
BIN
www/favicon.ico
Normal file
BIN
www/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 229 KiB |
276
www/index.php
Normal file
276
www/index.php
Normal file
@ -0,0 +1,276 @@
|
||||
<?php
|
||||
require ".php/format.inc";
|
||||
require ".php/auth.inc";
|
||||
require ".php/credentials.inc";
|
||||
global $CREDENTIALS;
|
||||
|
||||
$clients = array_keys($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">
|
||||
<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="https://elwig.at/">Homepage</a></p>
|
||||
<table>
|
||||
<thead><tr><th>Name</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($clients as $c) {
|
||||
echo " <tr><td><a href='$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($c);
|
||||
if ($path === "/$c") {
|
||||
header('Status: 308');
|
||||
header("Location: $c/");
|
||||
header('Content-Length: 23');
|
||||
exit("308 Permanent Redirect\n");
|
||||
} else if ($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/$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/$c/$file";
|
||||
$size = filesize($path);
|
||||
$url = "https://sync.elwig.at/$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/$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");
|
||||
} else if ($_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);
|
||||
} else if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
|
||||
$upload = fopen("php://input", "r");
|
||||
$fp = fopen("/tmp/upload-$file", "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);
|
||||
fclose($fp);
|
||||
fclose($upload);
|
||||
|
||||
if (!rename("/tmp/upload-$file", $path)) {
|
||||
header('Status: 500');
|
||||
header('Content-Length: 26');
|
||||
exit("500 Internal Server Error\n");
|
||||
}
|
||||
|
||||
header("Status: 201");
|
||||
header('Content-Length: 12');
|
||||
exit("201 Created\n");
|
||||
} else if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||
if ($file === '*') {
|
||||
foreach (scandir(".data/$c/") as $f) {
|
||||
if (str_starts_with($f, ".") || str_ends_with($f, ".php") || str_ends_with($f, ".inc")) continue;
|
||||
if (unlink(".data/$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();
|
161
www/res/style.css
Normal file
161
www/res/style.css
Normal file
@ -0,0 +1,161 @@
|
||||
|
||||
/**** Shared ****/
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Arial', sans-serif;
|
||||
scroll-behavior: smooth;
|
||||
--main-color: #A040C0;
|
||||
--light-color: #D098E0;
|
||||
--accent-color: #C0F080;
|
||||
--bg-color: #EBEFE7;
|
||||
--blur-color: #60804020;
|
||||
--blur: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.header-footer {
|
||||
min-height: calc(100vh + 8em);
|
||||
padding: 3em 0 8em 0;
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 3em;
|
||||
box-shadow: 0 0 0.5em #00000060;
|
||||
background-color: #FFFFFF;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
header img {
|
||||
height: 2.5em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
nav {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
nav > *:first-child,
|
||||
nav > *:last-child {
|
||||
flex: 100px 1 1;
|
||||
}
|
||||
|
||||
nav > *:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
nav li {
|
||||
flex: 100px 1 1;
|
||||
}
|
||||
|
||||
nav li a {
|
||||
text-decoration: none;
|
||||
padding: 1em 2em;
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
nav li a:hover,
|
||||
nav li.active a{
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
nav a.flag {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav a.flag div {
|
||||
display: inline-block;
|
||||
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,
|
||||
table a {
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
main section h3 {
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main section p,
|
||||
main section table {
|
||||
max-width: 1000px;
|
||||
margin: 1em auto;
|
||||
}
|
3
www/robots.txt
Normal file
3
www/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# robots.txt for sync.elwig.at
|
||||
User-Agent: *
|
||||
Disallow: /
|
Reference in New Issue
Block a user