init
commit
174e809054
|
@ -0,0 +1 @@
|
|||
htdocs/vendor/*
|
|
@ -0,0 +1,2 @@
|
|||
htdocs/cache/*
|
||||
htdocs/vendor/*
|
|
@ -0,0 +1,3 @@
|
|||
htdocs/cache/*
|
||||
htdocs/vendor/*
|
||||
idea/*
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"require": {
|
||||
"johngrogg/ics-parser": "^2.1"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
$date = strtotime($_REQUEST["date"]);
|
||||
$format = isset($_REQUEST["format"]) ? $_REQUEST["format"] : "U";
|
||||
echo date($format, $date);
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue