master
Dirk Heilig 2023-04-20 12:37:33 +02:00
commit 174e809054
12 changed files with 584 additions and 0 deletions

1
.dockerignore 100644
View File

@ -0,0 +1 @@
htdocs/vendor/*

2
.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
htdocs/cache/*
htdocs/vendor/*

3
.prettierignore 100644
View File

@ -0,0 +1,3 @@
htdocs/cache/*
htdocs/vendor/*
idea/*

8
Dockerfile 100644
View File

@ -0,0 +1,8 @@
FROM php:8-apache
RUN apt-get update && apt-get upgrade -y && apt-get install -y curl git unzip && rm -rf /var/lib/apt/lists/*
RUN rm /var/www/html/* -rf
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
COPY htdocs/ /var/www/html
WORKDIR /var/www/html
RUN composer install
RUN chown www-data:www-data /var/www/html

16
Makefile 100644
View File

@ -0,0 +1,16 @@
.PHONY: install prettier
prettier:
prettier -w .
install: htdocs/vendor
htdocs/vendor: htdocs/composer.json htdocs/composer.lock
cd htdocs ; composer install --no-dev --optimize-autoloader
touch htdocs/vendor
htdocs/composer.lock: htdocs/composer.json
cd htdocs ; composer install --no-dev --optimize-autoloader
touch htdocs/composer.lock

View File

@ -0,0 +1,198 @@
<?php
header("Access-Control-Allow-Origin: *");
use ICal\Event;
use ICal\ICal;
define("CACHE_DIR", "/tmp/app_data/cache");
define("CACHE_TTL", 60 * 60);
define("MAX_FILE_SIZE", 1024 * 1024 * 10);
if (!isset($_REQUEST["url"])) {
header("HTTP/1.1 400 Bad Request");
echo "No url given";
exit();
}
$url = $_REQUEST["url"];
$parsedUrl = parse_url($url);
$host = $parsedUrl["host"];
$hostIp = gethostbyname($host);
if (
!filter_var(
$hostIp,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
)
) {
header("HTTP/1.1 400 Bad Request");
exit();
}
if (0 !== strpos($url, "http")) {
header("HTTP/1.1 400 Bad Request");
exit();
}
$start = isset($_REQUEST["start"]) ? $_REQUEST["start"] : "today";
$end = isset($_REQUEST["end"]) ? $_REQUEST["end"] : "tomorrow";
$maxItems = isset($_REQUEST["maxitems"]) ? intval($_REQUEST["maxitems"]) : 10;
$completeCacheFile =
CACHE_DIR .
"/complete_" .
md5(json_encode([$url, $start, $end, $maxItems]));
$start = strtotime($start);
$end = strtotime($end);
if (false === $start) {
header("HTTP/1.1 400 Bad Request");
echo "Start value is not a valid strtotime() parameter";
exit();
}
if (false === $end) {
header("HTTP/1.1 400 Bad Request");
echo "End value is not a valid strtotime() parameter";
exit();
}
if ($start > $end) {
header("HTTP/1.1 400 Bad Request");
echo "Start value is after end value";
exit();
}
if ($end - $start > 60 * 60 * 24 * 90) {
header("HTTP/1.1 400 Bad Request");
echo "Maximum range is 90 days";
exit();
}
if ($maxItems > 100) {
header("HTTP/1.1 400 Bad Request");
echo "Maximum number of items is 100";
exit();
}
header("X-Debug-used-url: $url");
header("X-Debug-start: $start (" . date("Y-m-d", $start) . ")");
header("X-Debug-end: $end (" . date("Y-m-d", $end) . ")");
header("X-Debug-max-items: $maxItems");
require_once __DIR__ . "/../vendor/autoload.php";
if (!is_dir(CACHE_DIR)) {
mkdir(CACHE_DIR, 0777, true);
}
$urlCacheFile = CACHE_DIR . "/url_" . md5($url);
header(
"X-Debug-Cache-File-data: " .
json_encode([$url, $start, $end, $maxItems], JSON_UNESCAPED_SLASHES)
);
if (
file_exists($completeCacheFile) &&
filemtime($completeCacheFile) > time() - CACHE_TTL
) {
header("Content-Type: application/json");
header("X-Debug-Cache-Hit: complete");
readfile($completeCacheFile);
//use fast response time to allow cache cleaning for 500ms
$cacheCleanEnd = microtime(true) + 0.5;
$cacheFiles = glob(CACHE_DIR . "/*");
do {
$cacheFile = array_shift($cacheFiles);
if (filemtime($cacheFile) < time() - CACHE_TTL) {
unlink($cacheFile);
}
} while (microtime(true) <= $cacheCleanEnd && count($cacheFiles) > 0);
exit();
}
if (
!file_exists($urlCacheFile) ||
filemtime($urlCacheFile) < time() - CACHE_TTL
) {
$dlStart = microtime(true);
$size = 0;
$fp_in = fopen($url, "r");
$fp_out = fopen($urlCacheFile, "w");
while ($fp_in && !feof($fp_in)) {
$size += fwrite($fp_out, fread($fp_in, 1024 * 1024));
if ($size > MAX_FILE_SIZE) {
header("HTTP/1.1 400 Bad Request");
echo "File is too big";
fclose($fp_in);
fclose($fp_out);
unlink($urlCacheFile);
exit();
}
}
fclose($fp_in);
fclose($fp_out);
$dlEnd = microtime(true);
$dlTime = round($dlEnd - $dlStart, 3);
header("X-Debug-Download-Time: $dlTime s");
$prefixes = ["B", "KB", "MB", "GB", "TB", "PB"];
$prefix = array_shift($prefixes);
while ($size > 1500) {
$prefix = array_shift($prefixes);
$size = $size / 1024;
}
$hsize = round($size, 3) . " " . $prefix;
header("X-Debug-Download-Size: $hsize");
} else {
header("X-Debug-Cache-Hit: url");
}
$errors = false;
try {
$parseStart = microtime(true);
$ical = new ICal($urlCacheFile, [
"defaultTimeZone" => "UTC",
]);
$events = $ical->eventsFromRange(
date("Y-m-d", strtotime("today")),
date("Y-m-d", strtotime("today + 90 days"))
);
$parseEnd = microtime(true);
$parseTime = round($parseEnd - $parseStart, 3);
header("X-Debug-Parse-Time: $parseTime s");
while (count($events) > $maxItems) {
array_pop($events);
}
$events = array_map(function (Event $event) {
$r = [];
@$r["summary"] = $event->summary;
@$r["description"] = $event->description;
@$r["location"] = $event->location;
@$r["start"] = $event->dtstart_array[2];
@$r["end"] = $event->dtend_array[2];
@$r["duration"] = $event->duration;
@$r["url"] = $event->url;
@$r["status"] = $event->status;
return $r;
}, $events);
usort($events, function ($a, $b) {
return $a["start"] - $b["start"];
});
} catch (\Exception $e) {
header("HTTP/1.1 500 Internal Server Error");
echo "Error parsing ical file";
exit();
}
$data = json_encode(
$events,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
);
file_put_contents($completeCacheFile, $data);
header("Content-Type: application/json");
echo $data;
foreach (glob(CACHE_DIR . "/*") as $file) {
if (filemtime($file) < time() - CACHE_TTL) {
unlink($file);
}
}

86
htdocs/app.js 100644
View File

@ -0,0 +1,86 @@
let previewData = [];
let getData = function () {
let data = {};
data.url = document.getElementById("url").value;
data.start = document.getElementById("start").value;
data.end = document.getElementById("end").value;
data.maxitems = document.getElementById("maxitems").value;
for (let key in data) {
if (data[key] == "") {
delete data[key];
}
}
return data;
};
document.getElementById("form").onsubmit = function () {
// Get data from form
let data = getData();
// Send data to server as url form encoded
let xhr = new XMLHttpRequest();
let outputEl = document.getElementById("preview");
outputEl.innerHTML = "Loading...";
outputEl.classList.add("loading");
outputEl.classList.remove("error");
let url = "/api/?" + new URLSearchParams(data).toString();
xhr.open("get", url);
xhr.onload = function () {
if (xhr.status == 200) {
outputEl.classList.remove("loading");
outputEl.classList.remove("error");
} else {
outputEl.classList.remove("loading");
outputEl.classList.add("error");
}
document.getElementById("preview").innerHTML = xhr.responseText;
previewData = JSON.parse(xhr.responseText);
};
xhr.send();
document.getElementById("previewUrl").value =
window.location.protocol + "//" + window.location.host + url;
return false;
};
let start = document.getElementById("start");
start.onchange = function () {
let val = this.value;
if (val == "") {
val = start.getAttribute("placeholder");
}
let xhr = new XMLHttpRequest();
xhr.open(
"get",
"/strtotime/?" +
new URLSearchParams({ date: val, format: "Y-m-d" }).toString()
);
xhr.onload = function () {
if (xhr.status == 200) {
document.querySelector(".previewStart").textContent = xhr.responseText;
}
};
xhr.send();
};
start.onkeyup = start.onchange;
start.onchange();
let end = document.getElementById("end");
end.onchange = function () {
let val = this.value;
if (val == "") {
val = end.getAttribute("placeholder");
}
let xhr = new XMLHttpRequest();
xhr.open(
"get",
"/strtotime/?" +
new URLSearchParams({ date: val, format: "Y-m-d" }).toString()
);
xhr.onload = function () {
if (xhr.status == 200) {
document.querySelector(".previewEnd").textContent = xhr.responseText;
}
};
xhr.send();
};
end.onkeyup = end.onchange;
end.onchange();

View File

@ -0,0 +1,5 @@
{
"require": {
"johngrogg/ics-parser": "^2.1"
}
}

155
htdocs/composer.lock generated 100644
View File

@ -0,0 +1,155 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "10482d28879f2a0ab5cd26452cfd53d5",
"packages": [
{
"name": "johngrogg/ics-parser",
"version": "v2.2.2",
"source": {
"type": "git",
"url": "https://github.com/u01jmg3/ics-parser.git",
"reference": "69c80471a0a99142ebc72b21c2bc084e81a7c4f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/u01jmg3/ics-parser/zipball/69c80471a0a99142ebc72b21c2bc084e81a7c4f4",
"reference": "69c80471a0a99142ebc72b21c2bc084e81a7c4f4",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.3.9"
},
"require-dev": {
"phpunit/phpunit": "^4",
"squizlabs/php_codesniffer": "~2.9.1"
},
"type": "library",
"autoload": {
"psr-0": {
"ICal": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": ["MIT"],
"authors": [
{
"name": "Jonathan Goode",
"role": "Developer/Owner"
},
{
"name": "John Grogg",
"email": "john.grogg@gmail.com",
"role": "Developer/Prior Owner"
}
],
"description": "ICS Parser",
"homepage": "https://github.com/u01jmg3/ics-parser",
"keywords": [
"iCalendar",
"ical",
"ical-parser",
"ics",
"ics-parser",
"ifb"
],
"support": {
"issues": "https://github.com/u01jmg3/ics-parser/issues",
"source": "https://github.com/u01jmg3/ics-parser/tree/v2.2.2"
},
"funding": [
{
"url": "https://github.com/sponsors/u01jmg3",
"type": "github"
}
],
"time": "2020-11-02T10:28:33+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": ["bootstrap.php"],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": ["MIT"],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": ["compatibility", "mbstring", "polyfill", "portable", "shim"],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

50
htdocs/index.html 100644
View File

@ -0,0 +1,50 @@
<html>
<head>
<title>ICAL2JSON helper</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>URL-Builder</h1>
<form id="form">
<label><input type="url" name="url" id="url" />ICAL-URL</label>
<label
><input type="text" name="start" id="start" placeholder="today" />Start
Date in a strtotime() compatible format (<span
class="previewStart"
></span
>)</label
>
<label
><input
type="text"
name="end"
id="end"
placeholder="last day of next month"
/>End Date in a strtotime() compatible format (<span
class="previewEnd"
></span
>)</label
>
<label
><input
type="number"
name="maxitems"
id="maxitems"
min="1"
max="100"
step="1"
placeholder="100"
/>maximal number of items shown</label
>
<label><input type="submit" value="Preview" /></label>
</form>
<h1>Preview data</h1>
<label
><input type="text" aria-disabled="true" id="previewUrl" value="" /> Your
json url</label
>
<div id="preview">No data</div>
<script src="app.js"></script>
</body>
</html>

View File

@ -0,0 +1,4 @@
<?php
$date = strtotime($_REQUEST["date"]);
$format = isset($_REQUEST["format"]) ? $_REQUEST["format"] : "U";
echo date($format, $date);

56
htdocs/styles.css 100644
View File

@ -0,0 +1,56 @@
label {
display: block;
}
label > input {
margin: 1em;
height: 3em;
width: 50em;
}
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 1em;
color: #333;
background: #fff;
}
#preview {
font-family: monospace;
white-space: pre;
border: 0.3em inset #4c4;
min-height: 3em;
}
#preview.error {
border-color: #c44;
}
#preview.loading {
border-color: #cc4;
}
textarea {
display: block;
margin: 1em;
width: calc(100% - 2em);
}
.grow-wrap {
display: grid;
}
.grow-wrap::after {
content: attr(data-replicated-value) " ";
white-space: pre-wrap;
visibility: hidden;
}
.grow-wrap > textarea {
resize: none;
overflow: hidden;
}
.grow-wrap > textarea,
.grow-wrap::after {
border: 1px solid black;
padding: 0.5rem;
font: inherit;
grid-area: 1 / 1 / 2 / 2;
}