summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorMinteck <contact@minteck.org>2022-08-31 22:03:07 +0200
committerMinteck <contact@minteck.org>2022-08-31 22:03:07 +0200
commitb5f589c323f415bb42ea7069cb4d1a8a2233dd69 (patch)
treec3b80234ab7f463a2e7b8b672ceff57422b3496b /includes
parent09bd0164ebc020a54b944b7326dcba496fb5d82c (diff)
downloadpluralconnect-b5f589c323f415bb42ea7069cb4d1a8a2233dd69.tar.gz
pluralconnect-b5f589c323f415bb42ea7069cb4d1a8a2233dd69.tar.bz2
pluralconnect-b5f589c323f415bb42ea7069cb4d1a8a2233dd69.zip
Update I guess - Stuffie
Diffstat (limited to 'includes')
-rw-r--r--includes/backup.php81
-rw-r--r--includes/banner.php24
-rw-r--r--includes/bitset.php4
-rw-r--r--includes/emergency.php2
-rw-r--r--includes/footer.php7
-rw-r--r--includes/functions.php100
-rw-r--r--includes/header.php73
-rw-r--r--includes/ical/LICENSE24
-rw-r--r--includes/ical/bin/timezones.php20
-rw-r--r--includes/ical/main.php7
-rw-r--r--includes/ical/src/EventsList.php54
-rw-r--r--includes/ical/src/Freq.php633
-rw-r--r--includes/ical/src/IcalParser.php466
-rw-r--r--includes/ical/src/Recurrence.php234
-rw-r--r--includes/ical/src/WindowsTimezones.php214
-rw-r--r--includes/keywords.php100
-rw-r--r--includes/member.php2
-rw-r--r--includes/planner.php11
-rw-r--r--includes/pleasure.php2
-rw-r--r--includes/random.php15
-rw-r--r--includes/refresh.php16
-rw-r--r--includes/restore.php130
-rw-r--r--includes/session.php6
-rw-r--r--includes/sysbanner.php2
-rw-r--r--includes/system/compare.php38
25 files changed, 2224 insertions, 41 deletions
diff --git a/includes/backup.php b/includes/backup.php
new file mode 100644
index 0000000..18ed6a4
--- /dev/null
+++ b/includes/backup.php
@@ -0,0 +1,81 @@
+<?php
+
+$root = array_filter(scandir("data"), function ($i) {
+ return !str_starts_with($i, ".");
+});
+$files = [];
+$data = [
+ "date" => date('c'),
+ "files" => []
+];
+
+foreach ($root as $file) {
+ if ($file === "backup.poniesbackup" || $file === "backup.ponieskey" || $file === "encrypted" || str_ends_with($file, ".poniesbackup")) continue;
+
+ if (is_dir("data/$file")) {
+ foreach (array_filter(scandir("data/$file"), function ($i) {
+ return !str_starts_with($i, ".");
+ }) as $dirfile) {
+ if ($dirfile === "backup.poniesbackup" || $dirfile === "backup.ponieskey" || $dirfile === "encrypted" || str_ends_with($dirfile, ".poniesbackup")) continue;
+
+ $files[] = [
+ "dir" => $file,
+ "file" => $dirfile
+ ];
+ }
+ } else {
+ $files[] = [
+ "dir" => "",
+ "file" => $file
+ ];
+ }
+}
+
+foreach ($files as $file) {
+ $file["mime"] = mime_content_type("data/$file[dir]/$file[file]");
+ $file["checksum"] = [
+ sha1_file("data/$file[dir]/$file[file]"),
+ md5_file("data/$file[dir]/$file[file]")
+ ];
+ $file["content"] = base64_encode(file_get_contents("data/$file[dir]/$file[file]"));
+
+ $data["files"][] = $file;
+}
+
+function pkcs7_pad($data, $size) {
+ $length = $size - strlen($data) % $size;
+ return $data . str_repeat(chr($length), $length);
+}
+
+if (!file_exists("./data/backup.ponieskey")) {
+ $key = openssl_random_pseudo_bytes(512);
+ $iv = openssl_random_pseudo_bytes(16);
+ file_put_contents("./data/backup.ponieskey", base64_encode(json_encode([
+ "iv" => bin2hex($iv),
+ "key" => bin2hex($key)
+ ])));
+} else {
+ $key_raw = json_decode(base64_decode(file_get_contents("./data/backup.ponieskey")), true);
+ $key = hex2bin($key_raw["key"]);
+ $iv = hex2bin($key_raw["iv"]);
+}
+
+$payload = json_encode($data);
+$encrypted = openssl_encrypt(pkcs7_pad($payload, 16), 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
+
+file_put_contents("./data/backup.poniesbackup", $encrypted);
+@mkdir("./data/encrypted");
+
+$id = str_replace(":", "-", date('c'));
+copy("./data/backup.poniesbackup", "./data/encrypted/" . $id . ".poniesbackup");
+
+exec("scp ./data/encrypted/" . $id . ".poniesbackup fedora@bridlewood.equestria.dev:/opt/ponies");
+exec('ssh fedora@bridlewood.equestria.dev bash -c "cd /opt/ponies; ls -tp | grep -v \'/$\' | tail -n +20 | xargs -I {} rm -- {}"');
+
+exec("scp ./data/encrypted/" . $id . ".poniesbackup root@canterlot.equestria.dev:/opt/ponies");
+exec('ssh root@canterlot.equestria.dev bash -c "cd /opt/ponies; ls -tp | grep -v \'/$\' | tail -n +20 | xargs -I {} rm -- {}"');
+
+copy("./data/encrypted/" . $id . ".poniesbackup", "/opt/ponies/" . $id . ".poniesbackup");
+exec('bash -c "cd /opt/ponies; ls -tp | grep -v \'/$\' | tail -n +20 | xargs -I {} rm -- {}"');
+
+unlink("./data/encrypted/" . $id . ".poniesbackup"); \ No newline at end of file
diff --git a/includes/banner.php b/includes/banner.php
index 6305857..2582b63 100644
--- a/includes/banner.php
+++ b/includes/banner.php
@@ -322,14 +322,14 @@ function getMemberBannerData(string $id, string $system, bool $french = false) {
if ($metadata["host"] ?? false) {
if (!$travelling[$member['id']]["travelling"]) {
$badges[] = [
- "id" => "host",
+ "id" => "mcf",
"color" => "primary",
"html" => (
$french
?
- '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Hôte</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Hôte</span>'
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fronteuse la plus présente</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Fronteuse la plus présente</span>'
:
- '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Host</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Host</span>'
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Most common fronter</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Most common fronter</span>'
)
];
}
@@ -338,7 +338,7 @@ function getMemberBannerData(string $id, string $system, bool $french = false) {
if (($metadata["age_spells"] ?? false) && !$french) {
$badges[] = [
"id" => "age_spells",
- "color" => "primary",
+ "color" => "#6f42c1",
"html" => '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Affected by age spells</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' can feel younger than ' . getMemberPronouns($member['pronouns'])["subjective"] . ' actually ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' when somepony else casts an age spell on ' . getMemberPronouns($member['pronouns'])["object"] . '." class="badge rounded-pill" style="background-color: #6f42c1;">Affected by age spells</span>'
];
}
@@ -357,6 +357,22 @@ function getMemberBannerData(string $id, string $system, bool $french = false) {
];
}
+ if ($metadata["less_frequent"] ?? false) {
+ $badges[] = [
+ "id" => "nonverbal",
+ "color" => "#fd7e14",
+ "html" => '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fronts less often</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "front" : "fronts") . ' less often than ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' headmates, this can due to various reasons." class="badge rounded-pill" style="background-color:#fd7e14;">Fronts less often</span>'
+ ];
+ }
+
+ if ($metadata["nonverbal"] ?? false) {
+ $badges[] = [
+ "id" => "nonverbal",
+ "color" => "#20c997",
+ "html" => '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Non verbal IRL</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' non verbal in real life, although text communication is still possible." class="badge rounded-pill" style="background-color:#20c997;">Non verbal IRL</span>'
+ ];
+ }
+
if ($member["name"] === "fusion") {
$badges[] = [
"id" => "fusion",
diff --git a/includes/bitset.php b/includes/bitset.php
index 6483669..2e5a645 100644
--- a/includes/bitset.php
+++ b/includes/bitset.php
@@ -7,6 +7,8 @@ function parseBitset ($bitset) {
$median = substr($bin, 10 + 16, 1) !== "0";
$little = bindec(substr($bin, 11 + 16, 2));
$food = bindec(substr($bin, 16, 2));
+ $nonverbal = substr($bin, 15, 1) !== "0";
+ $lessFrequent = substr($bin, 14, 1) !== "0";
$magic = bindec(substr($bin, 2 + 16, 3));
$sensitivity = bindec(substr($bin, 5 + 16, 3));
$protector = substr($bin, 13 + 16, 1) !== "0";
@@ -63,6 +65,8 @@ function parseBitset ($bitset) {
'sensitivity' => $sensitivity,
'food' => $food,
'plush' => $plush,
+ 'nonverbal' => $nonverbal,
+ 'less_frequent' => $lessFrequent,
'age_spells' => $age,
'species' => array_filter([
$species1,
diff --git a/includes/emergency.php b/includes/emergency.php
index a86e090..5490985 100644
--- a/includes/emergency.php
+++ b/includes/emergency.php
@@ -1,4 +1,4 @@
-<h2>Emergency Alert
+<h2>Emergency alert
<details style="display: inline-block;font-size:12px;">
<summary class="text-muted" style="opacity:.5;"></summary>
<label><input id="test-mode" type="checkbox"> Test Mode</label> · <label><input id="fake-requests" type="checkbox"> Fake Requests</label>
diff --git a/includes/footer.php b/includes/footer.php
index 4bf7877..4592b8e 100644
--- a/includes/footer.php
+++ b/includes/footer.php
@@ -35,8 +35,13 @@ if (!function_exists("timeAgo")) {
<hr>
<div class="container text-muted">
+ <?php
+
+ $refresh = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/refresh.json"), true);
+
+ ?>
© <?= date("Y") ?> <a href="https://equestria.horse" target="_blank" class="text-muted">Equestria.dev Developers</a> · <a href="https://git.equestria.dev/equestria.dev/ponies.equestria.horse" target="_blank" class="text-muted">Source Code</a><br>
- PluralKit data updated <?= trim(timeAgo(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/refresh.json"), true)["timestamp"])) ?>, next update in <?php $t = 5 - round((time() - json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/refresh.json"), true)["timestamp"]) / 60); ?><?= $t > 1 ? $t . " minutes" : ($t > 0 ? "a minute" : "a few seconds") ?>
+ <a href="/-/debug" class="text-muted" style="text-decoration: none;">Data updated <?= trim(timeAgo($refresh["timestamp"])) ?> (<?= date('D j M, G:i:s T', $refresh["timestamp"]) ?>; took <?= round($refresh["duration"] * 1000) ?> ms, <?= count($refresh["restored"]) > 0 ? (count($refresh["restored"]) > 1 ? "reported " . count($refresh["restored"]) . " failures" : "reported 1 failure") : "no failures reported" ?>)</a>
<br><br><br>
</div>
diff --git a/includes/functions.php b/includes/functions.php
index 51317ca..db4a8df 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -34,6 +34,26 @@ if (!function_exists("getSystemMember")) {
}
}
+if (!function_exists("getMemberWithoutSystem")) {
+ function getMemberWithoutSystem(string $id) {
+ $member = null;
+
+ $members1 = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc-members.json"), true);
+ foreach ($members1 as $m) {
+ $m["_system"] = "ynmuc";
+ if ($m["id"] === $id) $member = $m;
+ }
+
+ $members2 = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd-members.json"), true);
+ foreach ($members2 as $m) {
+ $m["_system"] = "gdapd";
+ if ($m["id"] === $id) $member = $m;
+ }
+
+ return $member;
+ }
+}
+
if (!function_exists("showMembersFromList")) {
function showMembersFromList(array $list) {
foreach ($list as $member) { if ($member['name'] !== "unknown" && $member['name'] !== "fusion") {
@@ -167,6 +187,86 @@ if (!function_exists("timeAgo")) {
}
}
+if (!function_exists("timeIn")) {
+ function timeIn($time): string {
+ if (!is_numeric($time)) {
+ $time = strtotime($time);
+ }
+
+ $periods = ["second", "minute", "hour", "day", "week", "month", "year", "age"];
+ $lengths = array("60", "60", "24", "7", "4.35", "12", "100");
+
+ $now = time();
+
+ $difference = $time - $now;
+ if ($difference <= 10 && $difference >= 0) {
+ return $tense = "now";
+ } elseif ($difference > 0) {
+ $tense = "in";
+ } else {
+ $tense = "ago";
+ }
+
+ for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
+ $difference /= $lengths[$j];
+ }
+
+ $difference = round($difference);
+
+ $period = $periods[$j] . ($difference >1 ? "s" :'');
+ return "{$tense} {$difference} {$period}";
+ }
+}
+
+if (!function_exists("duration")) {
+ function duration($seconds) {
+ if ($seconds >= 60) {
+ if (floor($seconds / 60) >= 60) {
+ if (floor($seconds / 3600) >= 24) {
+ $days = floor($seconds / 86400);
+ return $days . " day" . ($days > 1 ? "s" : "");
+ } else {
+ $hours = floor($seconds / 3600);
+ return $hours . " hour" . ($hours > 1 ? "s" : "");
+ }
+ } else {
+ $minutes = floor($seconds / 60);
+ return $minutes . " minute" . ($minutes > 1 ? "s" : "");
+ }
+ } else {
+ return $seconds . " seconds";
+ }
+ }
+}
+
+if (!function_exists("relativeDate")) {
+ function relativeDate($date, $showTime = true) {
+ if (!is_numeric($date)) $date = strtotime($date);
+
+ if (!$showTime) {
+ if (date('Y-m-d', $date) === date('Y-m-d')) {
+ return "today";
+ } elseif (date('Y-m-d', $date) === date('Y-m-d', time() + 86400)) {
+ return "tomorrow";
+ } elseif ($date < time() + 518400) {
+ return date('l', $date);
+ } else {
+ return date('D j M', $date);
+ }
+ } else {
+ if (date('Y-m-d', $date) === date('Y-m-d')) {
+ return "today, <span class='time-adjust'>" . date('H:i', $date) . "</span>";
+ } elseif (date('Y-m-d', $date) === date('Y-m-d', time() + 86400)) {
+ return "tomorrow, <span class='time-adjust'>" . date('H:i', $date) . "</span>";
+ } elseif ($date < time() + 518400) {
+ return date('l', $date) . ", <span class='time-adjust'>" . date('H:i', $date) . "</span>";
+ } else {
+ return date('D j M', $date) . ", <span class='time-adjust'>" . date('H:i', $date) . "</span>";
+ }
+ }
+ }
+}
+
if (!function_exists("getMemberSystem")) {
function getMemberSystem(string $id) {
$list = scoreOrderGlobal();
diff --git a/includes/header.php b/includes/header.php
index 8ce8389..1f71aa5 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -29,6 +29,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLogg
require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/banner.php";
require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/rainbow.php";
require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/main.php";
?>
<!doctype html>
@@ -230,12 +231,33 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php";
grid-template-columns: 1fr !important;
text-align: left;
}
+
+ #member-icon-mobile {
+ display: inline-block !important;
+ }
+
+ #system-info {
+ grid-template-columns: 1fr !important;
+ }
+
+ #member-icon, #member-icon-outer {
+ display: none !important;
+ }
+
+ #member-relations {
+ grid-template-columns: 1fr !important;
+ text-align: left;
+ }
}
#page-content a {
color: #afd0ff;
}
+ #page-content .btn-outline-light:hover {
+ color: black !important;
+ }
+
#page-content a:hover {
opacity: .75;
}
@@ -616,7 +638,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php";
}
.navbar-collapse.show {
- z-index: 999;
+ z-index: 99999;
}
@media (max-width: 991px) {
@@ -637,6 +659,25 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php";
text-align: left;
}
}
+
+ .linked-card {
+ opacity: 1 !important;
+ color: white !important;
+ text-decoration: none !important;
+ }
+
+ .linked-card:hover {
+ opacity: .75 !important;
+ }
+
+ .linked-card:active {
+ opacity: .5 !important;
+ }
+
+ .navbar-brand {
+ position: relative;
+ z-index: 9999;
+ }
</style>
</head>
<body>
@@ -712,28 +753,48 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php";
<li><hr class="dropdown-divider"></li>
<li><h5 class="dropdown-header">Applications</h5></li>
+ <li><a class="dropdown-item" href="/-/dashboard">
+ <img src="/assets/icons/dashboard.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
+ <span style="vertical-align: middle;">Dashboard</span>
+ </a></li>
<li><a class="dropdown-item" href="/-/fronting">
<img src="/assets/icons/fronting.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
<span style="vertical-align: middle;">Front planner</span>
</a></li>
+ <!--<li><a class="dropdown-item" href="/-/actions">
+ <img src="/assets/icons/actions.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
+ <span style="vertical-align: middle;">Actions database</span>
+ </a></li>
+ <li><a class="dropdown-item" href="/-/rules">
+ <img src="/assets/icons/rules.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
+ <span style="vertical-align: middle;">Systems rules</span>
+ </a></li>
+ <li><a class="dropdown-item" href="/-/nicknames">
+ <img src="/assets/icons/nicknames.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
+ <span style="vertical-align: middle;">Relationships nicknames</span>
+ </a></li>
<li><a class="dropdown-item" href="/-/together">
<img src="/assets/icons/together.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
<span style="vertical-align: middle;">Watch Together</span>
- </a></li>
+ </a></li>-->
<li><a class="dropdown-item" href="/-/travelling">
<img src="/assets/icons/travel.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
<span style="vertical-align: middle;">System travels manager</span>
</a></li>
- <li><hr class="dropdown-divider"></li>
-
- <li><h5 class="dropdown-header">Utilities</h5></li>
- <li><a class="dropdown-item" href="/-/splitting">
+ <!--<li><a class="dropdown-item" href="/-/splitting">
<img src="/assets/icons/form.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
<span style="vertical-align: middle;">Members by splitting date</span>
</a></li>
<li><a class="dropdown-item" href="/-/bitset">
<img src="/assets/icons/bitset.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
<span style="vertical-align: middle;">Bitset calculator</span>
+ </a></li>-->
+ <li><hr class="dropdown-divider"></li>
+
+ <li><h5 class="dropdown-header">Debugging</h5></li>
+ <li><a class="dropdown-item" href="/-/debug">
+ <img src="/assets/icons/debug.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
+ <span style="vertical-align: middle;">Data updater debugging</span>
</a></li>
<li><a class="dropdown-item" href="/-/score">
<img src="/assets/icons/score.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;">
diff --git a/includes/ical/LICENSE b/includes/ical/LICENSE
new file mode 100644
index 0000000..ce7d41d
--- /dev/null
+++ b/includes/ical/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2014-2022, Roman Ožana
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the <organization> nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/includes/ical/bin/timezones.php b/includes/ical/bin/timezones.php
new file mode 100644
index 0000000..3bf3708
--- /dev/null
+++ b/includes/ical/bin/timezones.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * This file generates a map from windows timezones to tz database timezones
+ *
+ * @author Clement Wong <cw@clement.hk>
+ * @license http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+$windows_timezones = [];
+$windowstimezonexml = new DOMDocument();
+$windowstimezonexml->load('https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml');
+$zones = $windowstimezonexml->getElementsByTagName('mapZone');
+foreach ($zones as $zone) {
+ if ($zone->getAttribute('territory') === '001') {
+ $windows_timezones[$zone->getAttribute('other')] = $zone->getAttribute('type');
+ }
+}
+
+file_put_contents(__DIR__ . '/../src/WindowsTimezones.php', "<?php\n\$windows_timezones = " . var_export($windows_timezones, true) . ';');
+
diff --git a/includes/ical/main.php b/includes/ical/main.php
new file mode 100644
index 0000000..56d00af
--- /dev/null
+++ b/includes/ical/main.php
@@ -0,0 +1,7 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/EventsList.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/Freq.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/IcalParser.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/Recurrence.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/WindowsTimezones.php"; \ No newline at end of file
diff --git a/includes/ical/src/EventsList.php b/includes/ical/src/EventsList.php
new file mode 100644
index 0000000..3325368
--- /dev/null
+++ b/includes/ical/src/EventsList.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace om;
+
+/**
+ * Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
+ *
+ * @license BSD-3-Clause
+ * @author Roman Ožana <roman@ozana.cz>
+ */
+class EventsList extends \ArrayObject {
+
+ /**
+ * Return array of Events
+ *
+ * @return array
+ */
+ public function getArrayCopy(): array {
+ return array_values(parent::getArrayCopy());
+ }
+
+ /**
+ * Return sorted EventList (the newest dates are first)
+ *
+ * @return $this
+ */
+ public function sorted(): EventsList {
+ $this->uasort(static function ($a, $b): int {
+ if ($a['DTSTART'] === $b['DTSTART']) {
+ return 0;
+ }
+ return ($a['DTSTART'] < $b['DTSTART']) ? -1 : 1;
+ });
+
+ return $this;
+ }
+
+ /**
+ * Return reversed sorted EventList (the oldest dates are first)
+ *
+ * @return $this
+ */
+ public function reversed(): EventsList {
+ $this->uasort(static function ($a, $b): int {
+ if ($a['DTSTART'] === $b['DTSTART']) {
+ return 0;
+ }
+ return ($a['DTSTART'] > $b['DTSTART']) ? -1 : 1;
+ });
+
+ return $this;
+ }
+
+} \ No newline at end of file
diff --git a/includes/ical/src/Freq.php b/includes/ical/src/Freq.php
new file mode 100644
index 0000000..8557683
--- /dev/null
+++ b/includes/ical/src/Freq.php
@@ -0,0 +1,633 @@
+<?php
+
+namespace om;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+
+/**
+ * Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Freq.php)
+ *
+ * @author PC Drew <pc@schoolblocks.com>
+ */
+
+/**
+ * A class to store Frequency-rules in. Will allow a easy way to find the
+ * last and next occurrence of the rule.
+ *
+ * No - this is so not pretty. But.. ehh.. You do it better, and I will
+ * gladly accept patches.
+ *
+ * Created by trail-and-error on the examples given in the RFC.
+ *
+ * TODO: Update to a better way of doing calculating the different options.
+ * Instead of only keeping track of the best of the current dates found
+ * it should instead keep a array of all the calculated dates within the
+ * period.
+ * This should fix the issues with multi-rule + multi-rule interference,
+ * and make it possible to implement the SETPOS rule.
+ * By pushing the next period onto the stack as the last option will
+ * (hopefully) remove the need for the awful simpleMode
+ *
+ * @author Morten Fangel (C) 2008
+ * @author Michael Kahn (C) 2013
+ * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK
+ */
+class Freq {
+
+ protected array $weekdays = [
+ 'MO' => 'monday',
+ 'TU' => 'tuesday',
+ 'WE' => 'wednesday',
+ 'TH' => 'thursday',
+ 'FR' => 'friday',
+ 'SA' => 'saturday',
+ 'SU' => 'sunday',
+ ];
+ protected array $knownRules = [
+ 'month',
+ 'weekno',
+ 'day',
+ 'monthday',
+ 'yearday',
+ 'hour',
+ 'minute',
+ ]; //others : 'setpos', 'second'
+
+ protected array $ruleModifiers = ['wkst'];
+ protected bool $simpleMode = true;
+
+ protected array $rules = ['freq' => 'yearly', 'interval' => 1];
+ protected int $start = 0;
+ protected string $freq = '';
+
+ protected array $excluded; //EXDATE
+ protected array $added; //RDATE
+
+ protected $cache; // getAllOccurrences()
+
+ /**
+ * Constructs a new Frequency-rule
+ *
+ * @param string|array $rule
+ * @param int $start Unix-timestamp (important : Need to be the start of Event)
+ * @param array $excluded of int (timestamps), see EXDATE documentation
+ * @param array $added of int (timestamps), see RDATE documentation
+ * @throws Exception
+ */
+ public function __construct($rule, int $start, array $excluded = [], array $added = []) {
+ $this->start = $start;
+ $this->excluded = [];
+
+ $rules = [];
+ foreach ($rule as $k => $v) {
+ $this->rules[strtolower($k)] = $v;
+ }
+
+ if (isset($this->rules['until']) && is_string($this->rules['until'])) {
+ $this->rules['until'] = strtotime($this->rules['until']);
+ } elseif ($this->rules['until'] instanceof DateTime) {
+ $this->rules['until'] = $this->rules['until']->getTimestamp();
+ }
+ $this->freq = strtolower($this->rules['freq']);
+
+ foreach ($this->knownRules as $rule) {
+ if (isset($this->rules['by' . $rule])) {
+ if ($this->isPrerule($rule, $this->freq)) {
+ $this->simpleMode = false;
+ }
+ }
+ }
+
+ if (!$this->simpleMode) {
+ if (!(isset($this->rules['byday']) || isset($this->rules['bymonthday']) || isset($this->rules['byyearday']))) {
+ $this->rules['bymonthday'] = date('d', $this->start);
+ }
+ }
+
+ //set until, and cache
+ if (isset($this->rules['count'])) {
+
+ $cache[$ts] = $ts = $this->start;
+ for ($n = 1; $n < $this->rules['count']; $n++) {
+ $ts = $this->findNext($ts);
+ $cache[$ts] = $ts;
+ }
+ $this->rules['until'] = $ts;
+
+ //EXDATE
+ if (!empty($excluded)) {
+ foreach ($excluded as $ts) {
+ unset($cache[$ts]);
+ }
+ }
+ //RDATE
+ if (!empty($added)) {
+ $cache = array_unique(array_merge(array_values($cache), $added));
+ asort($cache);
+ }
+
+ $this->cache = array_values($cache);
+ }
+
+ $this->excluded = $excluded;
+ $this->added = $added;
+ }
+
+ private function isPrerule(string $rule, string $freq): bool {
+ if ($rule === 'year') {
+ return false;
+ }
+ if ($rule === 'month' && $freq === 'yearly') {
+ return true;
+ }
+ if ($rule === 'monthday' && in_array($freq, ['yearly', 'monthly']) && !isset($this->rules['byday'])) {
+ return true;
+ }
+ // TODO: is it faster to do monthday first, and ignore day if monthday exists? - prolly by a factor of 4..
+ if ($rule === 'yearday' && $freq === 'yearly') {
+ return true;
+ }
+ if ($rule === 'weekno' && $freq === 'yearly') {
+ return true;
+ }
+ if ($rule === 'day' && in_array($freq, ['yearly', 'monthly', 'weekly'])) {
+ return true;
+ }
+ if ($rule === 'hour' && in_array($freq, ['yearly', 'monthly', 'weekly', 'daily'])) {
+ return true;
+ }
+ if ($rule === 'minute') {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Calculates the next time after the given offset that the rule
+ * will apply.
+ *
+ * The approach to finding the next is as follows:
+ * First we establish a timeframe to find timestamps in. This is
+ * between $offset and the end of the period that $offset is in.
+ *
+ * We then loop though all the rules (that is a Prerule in the
+ * current freq.), and finds the smallest timestamp inside the
+ * timeframe.
+ *
+ * If we find something, we check if the date is a valid recurrence
+ * (with validDate). If it is, we return it. Otherwise we try to
+ * find a new date inside the same timeframe (but using the new-
+ * found date as offset)
+ *
+ * If no new timestamps were found in the period, we try in the
+ * next period
+ *
+ * @param int $offset
+ * @return int|bool
+ * @throws Exception
+ */
+ public function findNext(int $offset) {
+ if (!empty($this->cache)) {
+ foreach ($this->cache as $ts) {
+ if ($ts > $offset) {
+ return $ts;
+ }
+ }
+ }
+
+ $debug = false;
+
+ //make sure the offset is valid
+ if ($offset === false || (isset($this->rules['until']) && $offset > $this->rules['until'])) {
+ if ($debug) printf("STOP: %s\n", date('r', $offset));
+ return false;
+ }
+
+ $found = true;
+
+ //set the timestamp of the offset (ignoring hours and minutes unless we want them to be
+ //part of the calculations.
+ if ($debug) printf("O: %s\n", date('r', $offset));
+ $hour = (in_array($this->freq, ['hourly', 'minutely']) && $offset > $this->start) ? date('H', $offset) : date(
+ 'H', $this->start
+ );
+ $minute = (($this->freq === 'minutely' || isset($this->rules['byminute'])) && $offset > $this->start) ? date(
+ 'i', $offset
+ ) : date('i', $this->start);
+ $t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y', $offset));
+ if ($debug) printf("START: %s\n", date('r', $t));
+
+ if ($this->simpleMode) {
+ if ($offset < $t) {
+ $ts = $t;
+ if ($ts && in_array($ts, $this->excluded, true)) {
+ $ts = $this->findNext($ts);
+ }
+ } else {
+ $ts = $this->findStartingPoint($t, $this->rules['interval'], false);
+ if (!$this->validDate($ts)) {
+ $ts = $this->findNext($ts);
+ }
+ }
+
+ return $ts;
+ }
+
+ //EOP needs to have the same TIME as START ($t)
+ $tO = new DateTime('@' . $t, new DateTimeZone('UTC'));
+
+ $eop = $this->findEndOfPeriod($offset);
+ $eopO = new DateTime('@' . $eop, new DateTimeZone('UTC'));
+ $eopO->setTime($tO->format('H'), $tO->format('i'), $tO->format('s'));
+ $eop = $eopO->getTimestamp();
+ unset($eopO, $tO);
+
+ if ($debug) {
+ echo 'EOP: ' . date('r', $eop) . "\n";
+ }
+ foreach ($this->knownRules as $rule) {
+ if ($found && isset($this->rules['by' . $rule])) {
+ if ($this->isPrerule($rule, $this->freq)) {
+ $subRules = explode(',', $this->rules['by' . $rule]);
+ $_t = null;
+ foreach ($subRules as $subRule) {
+ $imm = call_user_func_array([$this, "ruleBy$rule"], [$subRule, $t]);
+ if ($imm === false) {
+ break;
+ }
+ if ($debug) {
+ printf("%s: %s A: %d\n", strtoupper($rule), date('r', $imm), intval($imm > $offset && $imm < $eop));
+ }
+ if ($imm > $offset && $imm <= $eop && ($_t == null || $imm < $_t)) {
+ $_t = $imm;
+ }
+ }
+ if ($_t !== null) {
+ $t = $_t;
+ } else {
+ $found = $this->validDate($t);
+ }
+ }
+ }
+ }
+
+ if ($offset < $this->start && $this->start < $t) {
+ $ts = $this->start;
+ } elseif ($found && ($t != $offset)) {
+ if ($this->validDate($t)) {
+ if ($debug) echo 'OK' . "\n";
+ $ts = $t;
+ } else {
+ if ($debug) echo 'Invalid' . "\n";
+ $ts = $this->findNext($t);
+ }
+ } else {
+ if ($debug) echo 'Not found' . "\n";
+ $ts = $this->findNext($this->findStartingPoint($offset, $this->rules['interval']));
+ }
+ if ($ts && in_array($ts, $this->excluded, true)) {
+ return $this->findNext($ts);
+ }
+
+ return $ts;
+ }
+
+ /**
+ * Finds the starting point for the next rule. It goes $interval
+ * 'freq' forward in time since the given offset
+ *
+ * @param int $offset
+ * @param int $interval
+ * @param boolean $truncate
+ * @return int
+ */
+ private function findStartingPoint(int $offset, int $interval, $truncate = true): int {
+ $_freq = ($this->freq === 'daily') ? 'day__' : $this->freq;
+ $t = '+' . $interval . ' ' . substr($_freq, 0, -2) . 's';
+ if ($_freq === 'monthly' && $truncate) {
+ if ($interval > 1) {
+ $offset = strtotime('+' . ($interval - 1) . ' months ', $offset); // FIXME return type int|false
+ }
+ $t = '+' . (date('t', $offset) - date('d', $offset) + 1) . ' days';
+ }
+
+ $sp = strtotime($t, $offset);
+
+ if ($truncate) {
+ $sp = $this->truncateToPeriod($sp, $this->freq);
+ }
+
+ return $sp;
+ }
+
+ /**
+ * Resets the timestamp to the beginning of the
+ * period specified by freq
+ *
+ * Yes - the fall-through is on purpose!
+ *
+ * @param int $time
+ * @param string $freq
+ * @return int
+ */
+ private function truncateToPeriod(int $time, string $freq): int {
+ $date = getdate($time);
+ switch ($freq) {
+ case 'yearly':
+ $date['mon'] = 1;
+ case 'monthly':
+ $date['mday'] = 1;
+ case 'daily':
+ $date['hours'] = 0;
+ case 'hourly':
+ $date['minutes'] = 0;
+ case 'minutely':
+ $date['seconds'] = 0;
+ break;
+ case 'weekly':
+ if (date('N', $time) == 1) { // FIXME wrong compare, date return string|false
+ $date['hours'] = 0;
+ $date['minutes'] = 0;
+ $date['seconds'] = 0;
+ } else {
+ $date = getdate(strtotime('last monday 0:00', $time));
+ }
+ break;
+ }
+ return mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']);
+ }
+
+ private function validDate($t): bool {
+ if (isset($this->rules['until']) && $t > $this->rules['until']) {
+ return false;
+ }
+
+ if (in_array($t, $this->excluded, true)) {
+ return false;
+ }
+
+ if (isset($this->rules['bymonth'])) {
+ $months = explode(',', $this->rules['bymonth']);
+ if (!in_array(date('m', $t), $months, true)) {
+ return false;
+ }
+ }
+ if (isset($this->rules['byday'])) {
+ $days = explode(',', $this->rules['byday']);
+ foreach ($days as $i => $k) {
+ $days[$i] = $this->weekdays[preg_replace('/[^A-Z]/', '', $k)];
+ }
+ if (!in_array(strtolower(date('l', $t)), $days, true)) {
+ return false;
+ }
+ }
+ if (isset($this->rules['byweekno'])) {
+ $weeks = explode(',', $this->rules['byweekno']);
+ if (!in_array(date('W', $t), $weeks, true)) {
+ return false;
+ }
+ }
+ if (isset($this->rules['bymonthday'])) {
+ $weekdays = explode(',', $this->rules['bymonthday']);
+ foreach ($weekdays as $i => $k) {
+ if ($k < 0) {
+ $weekdays[$i] = date('t', $t) + $k + 1;
+ }
+ }
+ if (!in_array(date('d', $t), $weekdays, true)) {
+ return false;
+ }
+ }
+ if (isset($this->rules['byhour'])) {
+ $hours = explode(',', $this->rules['byhour']);
+ if (!in_array(date('H', $t), $hours, true)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Finds the earliest timestamp possible outside this period.
+ *
+ * @param int $offset
+ * @return int
+ */
+ public function findEndOfPeriod($offset = 0) {
+ return $this->findStartingPoint($offset, 1, false);
+ }
+
+ /**
+ * Returns the previous (most recent) occurrence of the rule from the
+ * given offset
+ *
+ * @param int $offset
+ * @return int
+ * @throws Exception
+ */
+ public function previousOccurrence(int $offset) {
+ if (!empty($this->cache)) {
+ $t2 = $this->start;
+ foreach ($this->cache as $ts) {
+ if ($ts >= $offset) {
+ return $t2;
+ }
+ $t2 = $ts;
+ }
+ } else {
+ $ts = $this->start;
+ while (($t2 = $this->findNext($ts)) < $offset) {
+ if ($t2 == false) {
+ break;
+ }
+ $ts = $t2;
+ }
+ }
+
+ return $ts;
+ }
+
+ /**
+ * Returns the next occurrence of this rule after the given offset
+ *
+ * @param int $offset
+ * @return int
+ * @throws Exception
+ */
+ public function nextOccurrence(int $offset) {
+ if ($offset < $this->start) {
+ return $this->firstOccurrence();
+ }
+ return $this->findNext($offset);
+ }
+
+ /**
+ * Finds the first occurrence of the rule.
+ *
+ * @return int timestamp
+ * @throws Exception
+ */
+ public function firstOccurrence() {
+ $t = $this->start;
+ if (in_array($t, $this->excluded)) {
+ $t = $this->findNext($t);
+ }
+
+ return $t;
+ }
+
+ /**
+ * Finds the absolute last occurrence of the rule from the given offset.
+ * Builds also the cache, if not set before...
+ *
+ * @return int timestamp
+ * @throws Exception
+ */
+ public function lastOccurrence() {
+ //build cache if not done
+ $this->getAllOccurrences();
+ //return last timestamp in cache
+ return end($this->cache);
+ }
+
+ /**
+ * Returns all timestamps array(), build the cache if not made before
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function getAllOccurrences() {
+ if (empty($this->cache)) {
+ $cache = [];
+
+ //build cache
+ $next = $this->firstOccurrence();
+ while ($next) {
+ $cache[] = $next;
+ $next = $this->findNext($next);
+ }
+ if (!empty($this->added)) {
+ $cache = array_unique(array_merge($cache, $this->added));
+ asort($cache);
+ }
+ $this->cache = $cache;
+ }
+
+ return $this->cache;
+ }
+
+ /**
+ * Applies the BYDAY rule to the given timestamp
+ *
+ * @param string $rule
+ * @param int $t
+ * @return int
+ */
+ private function ruleByDay(string $rule, int $t): int {
+ $dir = ($rule[0] === '-') ? -1 : 1;
+ $dir_t = ($dir === 1) ? 'next' : 'last';
+
+ $d = $this->weekdays[substr($rule, -2)];
+ $s = $dir_t . ' ' . $d . ' ' . date('H:i:s', $t);
+
+ if ($rule == substr($rule, -2)) {
+ if (date('l', $t) == ucfirst($d)) {
+ $s = 'today ' . date('H:i:s', $t);
+ }
+
+ $_t = strtotime($s, $t);
+
+ if ($_t == $t && in_array($this->freq, ['weekly', 'monthly', 'yearly'])) {
+ // Yes. This is not a great idea.. but hey, it works.. for now
+ $s = 'next ' . $d . ' ' . date('H:i:s', $t);
+ $_t = strtotime($s, $_t);
+ }
+
+ return $_t;
+ } else {
+ $_f = $this->freq;
+ if (isset($this->rules['bymonth']) && $this->freq === 'yearly') {
+ $this->freq = 'monthly';
+ }
+ if ($dir === -1) {
+ $_t = $this->findEndOfPeriod($t);
+ } else {
+ $_t = $this->truncateToPeriod($t, $this->freq);
+ }
+ $this->freq = $_f;
+
+ $c = preg_replace('/[^0-9]/', '', $rule);
+ $c = ($c == '') ? 1 : $c;
+
+ $n = $_t;
+ while ($c > 0) {
+ if ($dir === 1 && $c == 1 && date('l', $t) == ucfirst($d)) {
+ $s = 'today ' . date('H:i:s', $t);
+ }
+ $n = strtotime($s, $n);
+ $c--;
+ }
+
+ return $n;
+ }
+ }
+
+ private function ruleByMonth($rule, int $t) {
+ $_t = mktime(date('H', $t), date('i', $t), date('s', $t), $rule, date('d', $t), date('Y', $t));
+ if ($t == $_t && isset($this->rules['byday'])) {
+ // TODO: this should check if one of the by*day's exists, and have a multi-day value
+ return false;
+ } else {
+ return $_t;
+ }
+ }
+
+ private function ruleByMonthday($rule, int $t) {
+ if ($rule < 0) {
+ $rule = date('t', $t) + $rule + 1;
+ }
+
+ return mktime(date('H', $t), date('i', $t), date('s', $t), date('m', $t), $rule, date('Y', $t));
+ }
+
+ private function ruleByYearday($rule, int $t) {
+ if ($rule < 0) {
+ $_t = $this->findEndOfPeriod();
+ $d = '-';
+ } else {
+ $_t = $this->truncateToPeriod($t, $this->freq);
+ $d = '+';
+ }
+ $s = $d . abs($rule - 1) . ' days ' . date('H:i:s', $t);
+
+ return strtotime($s, $_t);
+ }
+
+ private function ruleByWeekno($rule, int $t) {
+ if ($rule < 0) {
+ $_t = $this->findEndOfPeriod();
+ $d = '-';
+ } else {
+ $_t = $this->truncateToPeriod($t, $this->freq);
+ $d = '+';
+ }
+
+ $sub = (date('W', $_t) == 1) ? 2 : 1;
+ $s = $d . abs($rule - $sub) . ' weeks ' . date('H:i:s', $t);
+ $_t = strtotime($s, $_t);
+
+ return $_t;
+ }
+
+ private function ruleByHour($rule, int $t) {
+ return mktime($rule, date('i', $t), date('s', $t), date('m', $t), date('d', $t), date('Y', $t));
+ }
+
+ private function ruleByMinute($rule, int $t) {
+ return mktime(date('h', $t), $rule, date('s', $t), date('m', $t), date('d', $t), date('Y', $t));
+ }
+}
diff --git a/includes/ical/src/IcalParser.php b/includes/ical/src/IcalParser.php
new file mode 100644
index 0000000..b4996e4
--- /dev/null
+++ b/includes/ical/src/IcalParser.php
@@ -0,0 +1,466 @@
+<?php
+
+namespace om;
+
+use DateInterval;
+use DateTime;
+use DateTimeZone;
+use Exception;
+use InvalidArgumentException;
+use RuntimeException;
+
+/**
+ * Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
+ *
+ * @license BSD-3-Clause
+ * @author Roman Ožana <roman@ozana.cz>
+ */
+class IcalParser {
+
+ /** @var ?DateTimeZone */
+ public ?DateTimeZone $timezone = null;
+
+ /** @var array|null */
+ public ?array $data = null;
+
+ /** @var array */
+ protected array $counters = [];
+
+ /** @var array */
+ private $windowsTimezones;
+
+ public function __construct() {
+ $this->windowsTimezones = require __DIR__ . '/WindowsTimezones.php'; // load Windows timezones from separate file
+ }
+
+ /**
+ * @param string $file
+ * @param callable|null $callback
+ * @return array|null
+ * @throws Exception
+ */
+ public function parseFile(string $file, callable $callback = null): array {
+ if (!$handle = fopen($file, 'rb')) {
+ throw new RuntimeException('Can\'t open file' . $file . ' for reading');
+ }
+ fclose($handle);
+
+ return $this->parseString(file_get_contents($file), $callback);
+ }
+
+ /**
+ * @param string $string
+ * @param callable|null $callback
+ * @param boolean $add if true the parsed string is added to existing data
+ * @return array|null
+ * @throws Exception
+ */
+ public function parseString(string $string, callable $callback = null, bool $add = false): ?array {
+ if ($add === false) {
+ // delete old data
+ $this->data = [];
+ $this->counters = [];
+ }
+
+ if (!preg_match('/BEGIN:VCALENDAR/', $string)) {
+ throw new InvalidArgumentException('Invalid ICAL data format');
+ }
+
+ $section = 'VCALENDAR';
+
+ // Replace \r\n with \n
+ $string = str_replace("\r\n", "\n", $string);
+
+ // Unfold multi-line strings
+ $string = str_replace("\n ", '', $string);
+
+ foreach (explode("\n", $string) as $row) {
+
+ switch ($row) {
+ case 'BEGIN:DAYLIGHT':
+ case 'BEGIN:VALARM':
+ case 'BEGIN:VTIMEZONE':
+ case 'BEGIN:VFREEBUSY':
+ case 'BEGIN:VJOURNAL':
+ case 'BEGIN:STANDARD':
+ case 'BEGIN:VTODO':
+ case 'BEGIN:VEVENT':
+ $section = substr($row, 6);
+ $this->counters[$section] = isset($this->counters[$section]) ? $this->counters[$section] + 1 : 0;
+ continue 2; // while
+ case 'END:VEVENT':
+ $section = substr($row, 4);
+ $currCounter = $this->counters[$section];
+ $event = $this->data[$section][$currCounter];
+ if (!empty($event['RECURRENCE-ID'])) {
+ $this->data['_RECURRENCE_IDS'][$event['RECURRENCE-ID']] = $event;
+ }
+
+ continue 2; // while
+ case 'END:DAYLIGHT':
+ case 'END:VALARM':
+ case 'END:VTIMEZONE':
+ case 'END:VFREEBUSY':
+ case 'END:VJOURNAL':
+ case 'END:STANDARD':
+ case 'END:VTODO':
+ continue 2; // while
+
+ case 'END:VCALENDAR':
+ $veventSection = 'VEVENT';
+ if (!empty($this->data[$veventSection])) {
+ foreach ($this->data[$veventSection] as $currCounter => $event) {
+ if (!empty($event['RRULE']) || !empty($event['RDATE'])) {
+ $recurrences = $this->parseRecurrences($event);
+ if (!empty($recurrences)) {
+ $this->data[$veventSection][$currCounter]['RECURRENCES'] = $recurrences;
+ }
+
+ if (!empty($event['UID'])) {
+ $this->data["_RECURRENCE_COUNTERS_BY_UID"][$event['UID']] = $currCounter;
+ }
+ }
+ }
+ }
+ continue 2; // while
+ }
+
+ [$key, $middle, $value] = $this->parseRow($row);
+
+ if ($callback) {
+ // call user function for processing line
+ call_user_func($callback, $row, $key, $middle, $value, $section, $this->counters[$section]);
+ } else {
+ if ($section === 'VCALENDAR') {
+ $this->data[$key] = $value;
+ } else {
+
+ // use an array since there can be multiple entries for this key. This does not
+ // break the current implementation--it leaves the original key alone and adds
+ // a new one specifically for the array of values.
+
+ if ($newKey = $this->isMultipleKey($key)) {
+ $this->data[$section][$this->counters[$section]][$newKey][] = $value;
+ }
+
+ // CATEGORIES can be multiple also but there is special case that there are comma separated categories
+
+ if ($this->isMultipleKeyWithCommaSeparation($key)) {
+
+ if (strpos($value, ',') !== false) {
+ $values = array_map('trim', preg_split('/(?<![^\\\\]\\\\),/', $value));
+ } else {
+ $values = [$value];
+ }
+
+ foreach ($values as $value) {
+ $this->data[$section][$this->counters[$section]][$key][] = $value;
+ }
+
+ } else {
+ $this->data[$section][$this->counters[$section]][$key] = $value;
+ }
+
+ }
+
+ }
+ }
+
+ return ($callback) ? null : $this->data;
+ }
+
+ /**
+ * @param $event
+ * @return array
+ * @throws Exception
+ */
+ public function parseRecurrences($event): array {
+ $recurring = new Recurrence($event['RRULE']);
+ $exclusions = [];
+ $additions = [];
+
+ if (!empty($event['EXDATES'])) {
+ foreach ($event['EXDATES'] as $exDate) {
+ if (is_array($exDate)) {
+ foreach ($exDate as $singleExDate) {
+ $exclusions[] = $singleExDate->getTimestamp();
+ }
+ } else {
+ $exclusions[] = $exDate->getTimestamp();
+ }
+ }
+ }
+
+ if (!empty($event['RDATES'])) {
+ foreach ($event['RDATES'] as $rDate) {
+ if (is_array($rDate)) {
+ foreach ($rDate as $singleRDate) {
+ $additions[] = $singleRDate->getTimestamp();
+ }
+ } else {
+ $additions[] = $rDate->getTimestamp();
+ }
+ }
+ }
+
+ $until = $recurring->getUntil();
+ if ($until === false) {
+ //forever... limit to 3 years from now
+ $end = new DateTime('now');
+ $end->add(new DateInterval('P3Y')); // + 3 years
+ $recurring->setUntil($end);
+ $until = $recurring->getUntil();
+ }
+
+ date_default_timezone_set($event['DTSTART']->getTimezone()->getName());
+ $frequency = new Freq($recurring->rrule, $event['DTSTART']->getTimestamp(), $exclusions, $additions);
+ $recurrenceTimestamps = $frequency->getAllOccurrences();
+ $recurrences = [];
+ foreach ($recurrenceTimestamps as $recurrenceTimestamp) {
+ $tmp = new DateTime('now', $event['DTSTART']->getTimezone());
+ $tmp->setTimestamp($recurrenceTimestamp);
+
+ $recurrenceIDDate = $tmp->format('Ymd');
+ $recurrenceIDDateTime = $tmp->format('Ymd\THis');
+ if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDate]) &&
+ empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTime])) {
+ $gmtCheck = new DateTime('now', new DateTimeZone('UTC'));
+ $gmtCheck->setTimestamp($recurrenceTimestamp);
+ $recurrenceIDDateTimeZ = $gmtCheck->format('Ymd\THis\Z');
+ if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTimeZ])) {
+ $recurrences[] = $tmp;
+ }
+ }
+ }
+
+ return $recurrences;
+ }
+
+ private function parseRow($row): array {
+ preg_match('#^([\w-]+);?([\w-]+="[^"]*"|.*?):(.*)$#i', $row, $matches);
+
+ $key = false;
+ $middle = null;
+ $value = null;
+
+ if ($matches) {
+ $key = $matches[1];
+ $middle = $matches[2];
+ $value = $matches[3];
+ $timezone = null;
+
+ if ($key === 'X-WR-TIMEZONE' || $key === 'TZID') {
+ if (preg_match('#(\w+/\w+)$#i', $value, $matches)) {
+ $value = $matches[1];
+ }
+ $value = $this->toTimezone($value);
+ $this->timezone = new DateTimeZone($value);
+ }
+
+ // have some middle part ?
+ if ($middle && preg_match_all('#(?<key>[^=;]+)=(?<value>[^;]+)#', $middle, $matches, PREG_SET_ORDER)) {
+ $middle = [];
+ foreach ($matches as $match) {
+ if ($match['key'] === 'TZID') {
+ $match['value'] = trim($match['value'], "'\"");
+ $match['value'] = $this->toTimezone($match['value']);
+ try {
+ $middle[$match['key']] = $timezone = new DateTimeZone($match['value']);
+ } catch (Exception $e) {
+ $middle[$match['key']] = $match['value'];
+ }
+ } elseif ($match['key'] === 'ENCODING') {
+ if ($match['value'] === 'QUOTED-PRINTABLE') {
+ $value = quoted_printable_decode($value);
+ }
+ }
+ }
+ }
+ }
+
+ // process simple dates with timezone
+ if (in_array($key, ['DTSTAMP', 'LAST-MODIFIED', 'CREATED', 'DTSTART', 'DTEND'], true)) {
+ try {
+ $value = new DateTime($value, ($timezone ?: $this->timezone));
+ } catch (Exception $e) {
+ $value = null;
+ }
+ } elseif (in_array($key, ['EXDATE', 'RDATE'])) {
+ $values = [];
+ foreach (explode(',', $value) as $singleValue) {
+ try {
+ $values[] = new DateTime($singleValue, ($timezone ?: $this->timezone));
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+ if (count($values) === 1) {
+ $value = $values[0];
+ } else {
+ $value = $values;
+ }
+ }
+
+ if ($key === 'RRULE' && preg_match_all('#(?<key>[^=;]+)=(?<value>[^;]+)#', $value, $matches, PREG_SET_ORDER)) {
+ $middle = null;
+ $value = [];
+ foreach ($matches as $match) {
+ if (in_array($match['key'], ['UNTIL'])) {
+ try {
+ $value[$match['key']] = new DateTime($match['value'], ($timezone ?: $this->timezone));
+ } catch (Exception $e) {
+ $value[$match['key']] = $match['value'];
+ }
+ } else {
+ $value[$match['key']] = $match['value'];
+ }
+ }
+ }
+
+ //implement 4.3.11 Text ESCAPED-CHAR
+ $text_properties = [
+ 'CALSCALE', 'METHOD', 'PRODID', 'VERSION', 'CATEGORIES', 'CLASS', 'COMMENT', 'DESCRIPTION',
+ 'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'TRANSP', 'TZID', 'TZNAME', 'CONTACT',
+ 'RELATED-TO', 'UID', 'ACTION', 'REQUEST-STATUS', 'URL',
+ ];
+ if (in_array($key, $text_properties, true) || strpos($key, 'X-') === 0) {
+ if (is_array($value)) {
+ foreach ($value as &$var) {
+ $var = strtr($var, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']);
+ }
+ } else {
+ $value = strtr($value, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']);
+ }
+ }
+
+ return [$key, $middle, $value];
+ }
+
+ /**
+ * Process timezone and return correct one...
+ *
+ * @param string $zone
+ * @return mixed|null
+ */
+ private function toTimezone(string $zone) {
+ return $this->windowsTimezones[$zone] ?? $zone;
+ }
+
+ public function isMultipleKey(string $key): ?string {
+ return (['ATTACH' => 'ATTACHMENTS', 'EXDATE' => 'EXDATES', 'RDATE' => 'RDATES'])[$key] ?? null;
+ }
+
+ /**
+ * @param $key
+ * @return string|null
+ */
+ public function isMultipleKeyWithCommaSeparation($key): ?string {
+ return (['X-CATEGORIES' => 'X-CATEGORIES', 'CATEGORIES' => 'CATEGORIES'])[$key] ?? null;
+ }
+
+ public function getAlarms(): array {
+ return $this->data['VALARM'] ?? [];
+ }
+
+ public function getTimezone(): array {
+ return $this->getTimezones();
+ }
+
+ public function getTimezones(): array {
+ return $this->data['VTIMEZONE'] ?? [];
+ }
+
+ /**
+ * Return sorted event list as ArrayObject
+ *
+ * @deprecated use IcalParser::getEvents()->sorted() instead
+ */
+ public function getSortedEvents(): \ArrayObject {
+ return $this->getEvents()->sorted();
+ }
+
+ public function getEvents(): EventsList {
+ $events = new EventsList();
+ if (isset($this->data['VEVENT'])) {
+ foreach ($this->data['VEVENT'] as $iValue) {
+ $event = $iValue;
+
+ if (empty($event['RECURRENCES'])) {
+ if (!empty($event['RECURRENCE-ID']) && !empty($event['UID']) && isset($event['SEQUENCE'])) {
+ $modifiedEventUID = $event['UID'];
+ $modifiedEventRecurID = $event['RECURRENCE-ID'];
+ $modifiedEventSeq = (int)$event['SEQUENCE'];
+
+ if (isset($this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID])) {
+ $counter = $this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID];
+
+ $originalEvent = $this->data['VEVENT'][$counter];
+ if (isset($originalEvent['SEQUENCE'])) {
+ $originalEventSeq = (int)$originalEvent['SEQUENCE'];
+ $originalEventFormattedStartDate = $originalEvent['DTSTART']->format('Ymd\THis');
+ if ($modifiedEventRecurID === $originalEventFormattedStartDate && $modifiedEventSeq > $originalEventSeq) {
+ // this modifies the original event
+ $modifiedEvent = array_replace_recursive($originalEvent, $event);
+ $this->data['VEVENT'][$counter] = $modifiedEvent;
+ foreach ($events as $z => $event) {
+ if ($events[$z]['UID'] === $originalEvent['UID'] &&
+ $events[$z]['SEQUENCE'] === $originalEvent['SEQUENCE']) {
+ // replace the original event with the modified event
+ $events[$z] = $modifiedEvent;
+ break;
+ }
+ }
+ $event = null; // don't add this to the $events[] array again
+ } elseif (!empty($originalEvent['RECURRENCES'])) {
+ for ($j = 0; $j < count($originalEvent['RECURRENCES']); $j++) {
+ $recurDate = $originalEvent['RECURRENCES'][$j];
+ $formattedStartDate = $recurDate->format('Ymd\THis');
+ if ($formattedStartDate === $modifiedEventRecurID) {
+ unset($this->data['VEVENT'][$counter]['RECURRENCES'][$j]);
+ $this->data['VEVENT'][$counter]['RECURRENCES'] = array_values($this->data['VEVENT'][$counter]['RECURRENCES']);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!empty($event)) {
+ $events->append($event);
+ }
+ } else {
+ $recurrences = $event['RECURRENCES'];
+ $event['RECURRING'] = true;
+ $event['DTEND'] = !empty($event['DTEND']) ? $event['DTEND'] : $event['DTSTART'];
+ $eventInterval = $event['DTSTART']->diff($event['DTEND']);
+
+ $firstEvent = true;
+ foreach ($recurrences as $j => $recurDate) {
+ $newEvent = $event;
+ if (!$firstEvent) {
+ unset($newEvent['RECURRENCES']);
+ $newEvent['DTSTART'] = $recurDate;
+ $newEvent['DTEND'] = clone($recurDate);
+ $newEvent['DTEND']->add($eventInterval);
+ }
+
+ $newEvent['RECURRENCE_INSTANCE'] = $j;
+ $events->append($newEvent);
+ $firstEvent = false;
+ }
+ }
+ }
+ }
+ return $events;
+ }
+
+ /**
+ * @return \ArrayObject
+ * @deprecated use IcalParser::getEvents->reversed();
+ */
+ public function getReverseSortedEvents(): \ArrayObject {
+ return $this->getEvents()->reversed();
+ }
+
+}
diff --git a/includes/ical/src/Recurrence.php b/includes/ical/src/Recurrence.php
new file mode 100644
index 0000000..15f39cd
--- /dev/null
+++ b/includes/ical/src/Recurrence.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace om;
+
+use DateTime;
+use Exception;
+
+/**
+ * Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Recurrence.php)
+ *
+ * A wrapper for recurrence rules in iCalendar. Parses the given line and puts the
+ * recurrence rules in the correct field of this object.
+ *
+ * See http://tools.ietf.org/html/rfc2445 for more information. Page 39 and onward contains more
+ * information on the recurrence rules themselves. Page 116 and onward contains
+ * some great examples which were often used for testing.
+ *
+ * @author Steven Oxley
+ * @author Michael Kahn (C) 2013
+ * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK
+ */
+class Recurrence {
+
+ public $rrule;
+ protected $freq;
+ protected $until;
+ protected $count;
+ protected $interval;
+ protected $bysecond;
+ protected $byminute;
+ protected $byhour;
+ protected $byday;
+ protected $bymonthday;
+ protected $byyearday;
+ protected $byweekno;
+ protected $bymonth;
+ protected $bysetpos;
+ protected $wkst;
+ /**
+ * A list of the properties that can have comma-separated lists for values.
+ *
+ * @var array
+ */
+ protected array $listProperties = [
+ 'bysecond', 'byminute', 'byhour', 'byday', 'bymonthday',
+ 'byyearday', 'byweekno', 'bymonth', 'bysetpos',
+ ];
+
+ /**
+ * Creates an recurrence object with a passed in line. Parses the line.
+ *
+ * @param array $rrule an om\icalparser row array which will be parsed to get the
+ * desired information.
+ */
+ public function __construct(array $rrule) {
+ $this->parseRrule($rrule);
+ }
+
+ /**
+ * Parses an 'RRULE' array and sets the member variables of this object.
+ * Expects a string that looks like this: 'FREQ=WEEKLY;INTERVAL=2;BYDAY=SU,TU,WE'
+ *
+ * @param $rrule
+ */
+ protected function parseRrule($rrule): void {
+ $this->rrule = $rrule;
+ //loop through the properties in the line and set their associated
+ //member variables
+ foreach ($this->rrule as $propertyName => $propertyValue) {
+ //need the lower-case name for setting the member variable
+ $propertyName = strtolower($propertyName);
+ //split up the list of values into an array (if it's a list)
+ if (in_array($propertyName, $this->listProperties, true)) {
+ $propertyValue = explode(',', $propertyValue);
+ }
+ $this->$propertyName = $propertyValue;
+ }
+ }
+
+ /**
+ * Returns the frequency - corresponds to FREQ in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getFreq() {
+ return $this->getMember('freq');
+ }
+
+ /**
+ * Retrieves the desired member variable and returns it (if it's set)
+ *
+ * @param string $member name of the member variable
+ * @return mixed the variable value (if set), false otherwise
+ */
+ protected function getMember(string $member) {
+ return $this->$member ?? false;
+ }
+
+ /**
+ * Returns when the event will go until - corresponds to UNTIL in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getUntil() {
+ return $this->getMember('until');
+ }
+
+ /**
+ * Set the $until member
+ *
+ * @param mixed $ts timestamp (int) / Valid DateTime format (string)
+ * @throws Exception
+ */
+ public function setUntil($ts): void {
+ if ($ts instanceof DateTime) {
+ $dt = $ts;
+ } elseif (is_int($ts)) {
+ $dt = new DateTime('@' . $ts);
+ } else {
+ $dt = new DateTime($ts);
+ }
+ $this->until = $dt->format('Ymd\THisO');
+ $this->rrule['until'] = $this->until;
+ }
+
+ /**
+ * Returns the count of the times the event will occur (should only appear if 'until'
+ * does not appear) - corresponds to COUNT in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getCount() {
+ return $this->getMember('count');
+ }
+
+ /**
+ * Returns the interval - corresponds to INTERVAL in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getInterval() {
+ return $this->getMember('interval');
+ }
+
+ /**
+ * Returns the bysecond part of the event - corresponds to BYSECOND in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getBySecond() {
+ return $this->getMember('bysecond');
+ }
+
+ /**
+ * Returns the byminute information for the event - corresponds to BYMINUTE in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getByMinute() {
+ return $this->getMember('byminute');
+ }
+
+ /**
+ * Corresponds to BYHOUR in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getByHour() {
+ return $this->getMember('byhour');
+ }
+
+ /**
+ *Corresponds to BYDAY in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getByDay() {
+ return $this->getMember('byday');
+ }
+
+ /**
+ * Corresponds to BYMONTHDAY in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getByMonthDay() {
+ return $this->getMember('bymonthday');
+ }
+
+ /**
+ * Corresponds to BYYEARDAY in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getByYearDay() {
+ return $this->getMember('byyearday');
+ }
+
+ /**
+ * Corresponds to BYWEEKNO in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getByWeekNo() {
+ return $this->getMember('byweekno');
+ }
+
+ /**
+ * Corresponds to BYMONTH in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getByMonth() {
+ return $this->getMember('bymonth');
+ }
+
+ /**
+ * Corresponds to BYSETPOS in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getBySetPos() {
+ return $this->getMember('bysetpos');
+ }
+
+ /**
+ * Corresponds to WKST in RFC 2445.
+ *
+ * @return mixed string if the member has been set, false otherwise
+ */
+ public function getWkst() {
+ return $this->getMember('wkst');
+ }
+}
diff --git a/includes/ical/src/WindowsTimezones.php b/includes/ical/src/WindowsTimezones.php
new file mode 100644
index 0000000..9bfa391
--- /dev/null
+++ b/includes/ical/src/WindowsTimezones.php
@@ -0,0 +1,214 @@
+<?php
+
+/**
+ * List of Windows Timezones
+ */
+return [
+ 'Dateline Standard Time' => 'Etc/GMT+12',
+ '(UTC-12:00) International Date Line West' => 'Etc/GMT+12',
+ 'UTC-11' => 'Etc/GMT+11',
+ '(UTC-11:00) Coordinated Universal Time -11' => 'Etc/GMT+11',
+ 'Hawaiian Standard Time' => 'Pacific/Honolulu',
+ '(UTC-10:00) Hawaii' => 'Pacific/Honolulu',
+ 'Alaskan Standard Time' => 'America/Anchorage',
+ '(UTC-09:00) Alaska' => 'America/Anchorage',
+ 'Pacific Standard Time (Mexico)' => 'America/Santa_Isabel',
+ '(UTC-08:00) Baja California' => 'America/Santa_Isabel',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Pacific Time' => 'America/Los_Angeles',
+ '(UTC-08:00) Pacific Time (US and Canada)' => 'America/Los_Angeles',
+ '(UTC-08:00) Pacific Time (US & Canada)' => 'America/Los_Angeles',
+ 'US Mountain Standard Time' => 'America/Phoenix',
+ '(UTC-07:00) Arizona' => 'America/Phoenix',
+ 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
+ '(UTC-07:00) Chihuahua, La Paz, Mazatlan' => 'America/Chihuahua',
+ 'Mountain Standard Time' => 'America/Denver',
+ 'Mountain Time' => 'America/Denver',
+ '(UTC-07:00) Mountain Time (US and Canada)' => 'America/Denver',
+ '(UTC-07:00) Mountain Time (US & Canada)' => 'America/Denver',
+ 'Central America Standard Time' => 'America/Guatemala',
+ '(UTC-06:00) Central America' => 'America/Guatemala',
+ 'Central Standard Time' => 'America/Chicago',
+ 'Central Time' => 'America/Chicago',
+ '(UTC-06:00) Central Time (US and Canada)' => 'America/Chicago',
+ '(UTC-06:00) Central Time (US & Canada)' => 'America/Chicago',
+ 'Central Standard Time (Mexico)' => 'America/Mexico_City',
+ '(UTC-06:00) Guadalajara, Mexico City, Monterrey' => 'America/Mexico_City',
+ 'Canada Central Standard Time' => 'America/Regina',
+ '(UTC-06:00) Saskatchewan' => 'America/Regina',
+ 'SA Pacific Standard Time' => 'America/Bogota',
+ '(UTC-05:00) Bogota, Lima, Quito' => 'America/Bogota',
+ 'Eastern Standard Time' => 'America/New_York',
+ 'Eastern Time' => 'America/New_York',
+ '(UTC-05:00) Eastern Time (US and Canada)' => 'America/New_York',
+ '(UTC-05:00) Eastern Time (US & Canada)' => 'America/New_York',
+ 'US Eastern Standard Time' => 'America/Indianapolis',
+ '(UTC-05:00) Indiana (East)' => 'America/Indianapolis',
+ 'Venezuela Standard Time' => 'America/Caracas',
+ '(UTC-04:30) Caracas' => 'America/Caracas',
+ 'Paraguay Standard Time' => 'America/Asuncion',
+ '(UTC-04:00) Asuncion' => 'America/Asuncion',
+ 'Atlantic Standard Time' => 'America/Halifax',
+ '(UTC-04:00) Atlantic Time (Canada)' => 'America/Halifax',
+ 'Central Brazilian Standard Time' => 'America/Cuiaba',
+ '(UTC-04:00) Cuiaba' => 'America/Cuiaba',
+ 'SA Western Standard Time' => 'America/La_Paz',
+ '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan' => 'America/La_Paz',
+ 'Pacific SA Standard Time' => 'America/Santiago',
+ '(UTC-04:00) Santiago' => 'America/Santiago',
+ 'Newfoundland Standard Time' => 'America/St_Johns',
+ '(UTC-03:30) Newfoundland' => 'America/St_Johns',
+ 'E. South America Standard Time' => 'America/Sao_Paulo',
+ '(UTC-03:00) Brasilia' => 'America/Sao_Paulo',
+ 'Argentina Standard Time' => 'America/Buenos_Aires',
+ '(UTC-03:00) Buenos Aires' => 'America/Buenos_Aires',
+ 'SA Eastern Standard Time' => 'America/Cayenne',
+ '(UTC-03:00) Cayenne, Fortaleza' => 'America/Cayenne',
+ 'Greenland Standard Time' => 'America/Godthab',
+ '(UTC-03:00) Greenland' => 'America/Godthab',
+ 'Montevideo Standard Time' => 'America/Montevideo',
+ '(UTC-03:00) Montevideo' => 'America/Montevideo',
+ 'Bahia Standard Time' => 'America/Bahia',
+ 'UTC-02' => 'Etc/GMT+2',
+ '(UTC-02:00) Coordinated Universal Time -02' => 'Etc/GMT+2',
+ 'Azores Standard Time' => 'Atlantic/Azores',
+ '(UTC-01:00) Azores' => 'Atlantic/Azores',
+ 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
+ '(UTC-01:00) Cabo Verde Is.' => 'Atlantic/Cape_Verde',
+ 'Morocco Standard Time' => 'Africa/Casablanca',
+ '(UTC) Casablanca' => 'Africa/Casablanca',
+ 'UTC' => 'Etc/GMT',
+ 'Microsoft/Utc' => 'Etc/GMT',
+ 'GMT Standard Time' => 'Europe/London',
+ '(UTC) Dublin, Edinburgh, Lisbon, London' => 'Europe/London',
+ 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
+ '(UTC) Monrovia, Reykjavik' => 'Atlantic/Reykjavik',
+ 'W. Europe Standard Time' => 'Europe/Berlin',
+ '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
+ '(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rome\, Stockholm\, Vienna' => 'Europe/Berlin',
+ '(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rom\, Stockholm\, Wien' => 'Europe/Berlin',
+ 'Central Europe Standard Time' => 'Europe/Budapest',
+ '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest',
+ 'Romance Standard Time' => 'Europe/Paris',
+ '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
+ 'Central European Standard Time' => 'Europe/Warsaw',
+ '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb' => 'Europe/Warsaw',
+ 'W. Central Africa Standard Time' => 'Africa/Lagos',
+ '(UTC+01:00) West Central Africa' => 'Africa/Lagos',
+ 'Namibia Standard Time' => 'Africa/Windhoek',
+ '(UTC+01:00) Windhoek' => 'Africa/Windhoek',
+ 'GTB Standard Time' => 'Europe/Bucharest',
+ '(UTC+02:00) Athens, Bucharest' => 'Europe/Bucharest',
+ 'Middle East Standard Time' => 'Asia/Beirut',
+ '(UTC+02:00) Beirut' => 'Asia/Beirut',
+ 'Egypt Standard Time' => 'Africa/Cairo',
+ '(UTC+02:00) Cairo' => 'Africa/Cairo',
+ 'Syria Standard Time' => 'Asia/Damascus',
+ '(UTC+02:00) Damascus' => 'Asia/Damascus',
+ 'South Africa Standard Time' => 'Africa/Johannesburg',
+ '(UTC+02:00) Harare, Pretoria' => 'Africa/Johannesburg',
+ 'FLE Standard Time' => 'Europe/Kiev',
+ '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' => 'Europe/Kiev',
+ 'Turkey Standard Time' => 'Europe/Istanbul',
+ '(UTC+02:00) Istanbul' => 'Europe/Istanbul',
+ 'Israel Standard Time' => 'Asia/Jerusalem',
+ '(UTC+02:00) Jerusalem' => 'Asia/Jerusalem',
+ 'Libya Standard Time' => 'Africa/Tripoli',
+ 'Jordan Standard Time' => 'Asia/Amman',
+ '(UTC+02:00) Amman' => 'Asia/Amman',
+ 'Arabic Standard Time' => 'Asia/Baghdad',
+ '(UTC+03:00) Baghdad' => 'Asia/Baghdad',
+ 'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
+ '(UTC+03:00) Kaliningrad' => 'Europe/Kaliningrad',
+ 'Arab Standard Time' => 'Asia/Riyadh',
+ '(UTC+03:00) Kuwait, Riyadh' => 'Asia/Riyadh',
+ 'E. Africa Standard Time' => 'Africa/Nairobi',
+ '(UTC+03:00) Nairobi' => 'Africa/Nairobi',
+ 'Iran Standard Time' => 'Asia/Tehran',
+ '(UTC+03:30) Tehran' => 'Asia/Tehran',
+ 'Arabian Standard Time' => 'Asia/Dubai',
+ '(UTC+04:00) Abu Dhabi, Muscat' => 'Asia/Dubai',
+ 'Azerbaijan Standard Time' => 'Asia/Baku',
+ '(UTC+04:00) Baku' => 'Asia/Baku',
+ 'Russian Standard Time' => 'Europe/Moscow',
+ '(UTC+04:00) Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
+ 'Mauritius Standard Time' => 'Indian/Mauritius',
+ '(UTC+04:00) Port Louis' => 'Indian/Mauritius',
+ 'Georgian Standard Time' => 'Asia/Tbilisi',
+ '(UTC+04:00) Tbilisi' => 'Asia/Tbilisi',
+ 'Caucasus Standard Time' => 'Asia/Yerevan',
+ '(UTC+04:00) Yerevan' => 'Asia/Yerevan',
+ 'Afghanistan Standard Time' => 'Asia/Kabul',
+ '(UTC+04:30) Kabul' => 'Asia/Kabul',
+ 'West Asia Standard Time' => 'Asia/Tashkent',
+ '(UTC+05:00) Tashkent' => 'Asia/Tashkent',
+ 'Pakistan Standard Time' => 'Asia/Karachi',
+ '(UTC+05:00) Islamabad, Karachi' => 'Asia/Karachi',
+ 'India Standard Time' => 'Asia/Calcutta',
+ '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi' => 'Asia/Calcutta',
+ 'Sri Lanka Standard Time' => 'Asia/Colombo',
+ '(UTC+05:30) Sri Jayawardenepura' => 'Asia/Colombo',
+ 'Nepal Standard Time' => 'Asia/Katmandu',
+ '(UTC+05:45) Kathmandu' => 'Asia/Katmandu',
+ 'Central Asia Standard Time' => 'Asia/Almaty',
+ '(UTC+06:00) Astana' => 'Asia/Almaty',
+ 'Bangladesh Standard Time' => 'Asia/Dhaka',
+ '(UTC+06:00) Dhaka' => 'Asia/Dhaka',
+ 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+ '(UTC+06:00) Ekaterinburg' => 'Asia/Yekaterinburg',
+ 'Myanmar Standard Time' => 'Asia/Rangoon',
+ '(UTC+06:30) Yangon (Rangoon)' => 'Asia/Rangoon',
+ 'SE Asia Standard Time' => 'Asia/Bangkok',
+ '(UTC+07:00) Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
+ 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
+ '(UTC+07:00) Novosibirsk' => 'Asia/Novosibirsk',
+ 'China Standard Time' => 'Asia/Shanghai',
+ '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi' => 'Asia/Shanghai',
+ 'North Asia Standard Time' => 'Asia/Krasnoyarsk',
+ '(UTC+08:00) Krasnoyarsk' => 'Asia/Krasnoyarsk',
+ 'Singapore Standard Time' => 'Asia/Singapore',
+ '(UTC+08:00) Kuala Lumpur, Singapore' => 'Asia/Singapore',
+ 'W. Australia Standard Time' => 'Australia/Perth',
+ '(UTC+08:00) Perth' => 'Australia/Perth',
+ 'Taipei Standard Time' => 'Asia/Taipei',
+ '(UTC+08:00) Taipei' => 'Asia/Taipei',
+ 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
+ '(UTC+08:00) Ulaanbaatar' => 'Asia/Ulaanbaatar',
+ 'North Asia East Standard Time' => 'Asia/Irkutsk',
+ '(UTC+09:00) Irkutsk' => 'Asia/Irkutsk',
+ 'Tokyo Standard Time' => 'Asia/Tokyo',
+ '(UTC+09:00) Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
+ 'Korea Standard Time' => 'Asia/Seoul',
+ '(UTC+09:00) Seoul' => 'Asia/Seoul',
+ 'Cen. Australia Standard Time' => 'Australia/Adelaide',
+ '(UTC+09:30) Adelaide' => 'Australia/Adelaide',
+ 'AUS Central Standard Time' => 'Australia/Darwin',
+ '(UTC+09:30) Darwin' => 'Australia/Darwin',
+ 'E. Australia Standard Time' => 'Australia/Brisbane',
+ '(UTC+10:00) Brisbane' => 'Australia/Brisbane',
+ 'AUS Eastern Standard Time' => 'Australia/Sydney',
+ '(UTC+10:00) Canberra, Melbourne, Sydney' => 'Australia/Sydney',
+ 'West Pacific Standard Time' => 'Pacific/Port_Moresby',
+ '(UTC+10:00) Guam, Port Moresby' => 'Pacific/Port_Moresby',
+ 'Tasmania Standard Time' => 'Australia/Hobart',
+ '(UTC+10:00) Hobart' => 'Australia/Hobart',
+ 'Yakutsk Standard Time' => 'Asia/Yakutsk',
+ '(UTC+10:00) Yakutsk' => 'Asia/Yakutsk',
+ 'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
+ '(UTC+11:00) Solomon Is., New Caledonia' => 'Pacific/Guadalcanal',
+ 'Vladivostok Standard Time' => 'Asia/Vladivostok',
+ '(UTC+11:00) Vladivostok' => 'Asia/Vladivostok',
+ 'New Zealand Standard Time' => 'Pacific/Auckland',
+ '(UTC+12:00) Auckland, Wellington' => 'Pacific/Auckland',
+ 'UTC+12' => 'Etc/GMT-12',
+ '(UTC+12:00) Coordinated Universal Time +12' => 'Etc/GMT-12',
+ 'Fiji Standard Time' => 'Pacific/Fiji',
+ '(UTC+12:00) Fiji' => 'Pacific/Fiji',
+ 'Magadan Standard Time' => 'Asia/Magadan',
+ '(UTC+12:00) Magadan' => 'Asia/Magadan',
+ 'Tonga Standard Time' => 'Pacific/Tongatapu',
+ '(UTC+13:00) Nuku\'alofa' => 'Pacific/Tongatapu',
+ 'Samoa Standard Time' => 'Pacific/Apia',
+ '(UTC-11:00)Samoa' => 'Pacific/Apia',
+ 'W. Europe Standard Time 1' => 'Europe/Berlin',
+];
diff --git a/includes/keywords.php b/includes/keywords.php
new file mode 100644
index 0000000..4d6afaf
--- /dev/null
+++ b/includes/keywords.php
@@ -0,0 +1,100 @@
+<?php
+
+function getKeyWords() {
+ $actions = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true);
+ $toys = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json"), true);
+ $pages = [];
+
+ foreach ($actions as $action) {
+ $base = strtolower($action["name"]);
+ $addKeywords = $action["keywords"];
+ $keywords = [
+ $base,
+ ucfirst($base),
+ ucwords($base)
+ ];
+
+ for ($i = 0; $i < strlen($base); $i++) {
+ $keywords[] = substr($base, 0, $i) . strtoupper(substr($base, $i, 1)) . substr($base, $i + 1, strlen($base) - $i - 1);
+
+ for ($j = 0; $j < strlen($base); $j++) {
+ $keywords[] = substr($base, 0, $i) . strtoupper(substr($base, $i,$j)) . substr($base, $i + $j, strlen($base) - $i - $j);
+ }
+ }
+
+ foreach ($addKeywords as $keyword) {
+ $keywords[] = $keyword;
+ $keywords[] = ucfirst($keyword);
+ $keywords[] = ucwords($keyword);
+
+ for ($i = 0; $i < strlen($keyword); $i++) {
+ $keywords[] = substr($keyword, 0, $i) . strtoupper(substr($keyword, $i, 1)) . substr($keyword, $i + 1, strlen($keyword) - $i - 1);
+
+ for ($j = 0; $j < strlen($keyword); $j++) {
+ $keywords[] = substr($keyword, 0, $i) . strtoupper(substr($keyword, $i, $j)) . substr($keyword, $i + $j, strlen($keyword) - $i - $j);
+ }
+ }
+ }
+
+ $pages[$action["id"]] = [
+ "keywords" => array_unique($keywords),
+ "link" => "/-/actions/$action[id]"
+ ];
+ }
+
+ foreach ($toys as $toy) {
+ $base = strtolower($toy["name"]);
+ $addKeywords = $toy["keywords"];
+ $keywords = [
+ $base,
+ ucfirst($base),
+ ucwords($base)
+ ];
+
+ for ($i = 0; $i < strlen($base); $i++) {
+ $keywords[] = substr($base, 0, $i) . strtoupper(substr($base, $i, 1)) . substr($base, $i + 1, strlen($base) - $i - 1);
+
+ for ($j = 0; $j < strlen($base); $j++) {
+ $keywords[] = substr($base, 0, $i) . strtoupper(substr($base, $i, $j)) . substr($base, $i + $j, strlen($base) - $i - $j);
+ }
+ }
+
+ foreach ($addKeywords as $keyword) {
+ $keywords[] = $keyword;
+ $keywords[] = ucfirst($keyword);
+ $keywords[] = ucwords($keyword);
+
+ for ($i = 0; $i < strlen($keyword); $i++) {
+ $keywords[] = substr($keyword, 0, $i) . strtoupper(substr($keyword, $i, 1)) . substr($keyword, $i + 1, strlen($keyword) - $i - 1);
+
+ for ($j = 0; $j < strlen($keyword); $j++) {
+ $keywords[] = substr($keyword, 0, $i) . strtoupper(substr($keyword, $i, $j)) . substr($keyword, $i + $j, strlen($keyword) - $i - $j);
+ }
+ }
+ }
+
+ $pages[$toy["id"]] = [
+ "keywords" => array_unique($keywords),
+ "link" => "/-/toys/$toy[id]"
+ ];
+ }
+
+ $keywords = [];
+ foreach ($pages as $page) {
+ foreach ($page["keywords"] as $keyword) {
+ $keywords[$keyword] = $page["link"];
+ }
+ }
+
+ return $keywords;
+}
+
+function replaceKeyWords(string $input): string {
+ $keywords = getKeyWords();
+
+ foreach ($keywords as $keyword => $url) {
+ $input = str_replace($keyword, "<a href='$url'>$keyword</a>", $input);
+ }
+
+ return $input;
+} \ No newline at end of file
diff --git a/includes/member.php b/includes/member.php
index d027fa7..f8050b0 100644
--- a/includes/member.php
+++ b/includes/member.php
@@ -165,7 +165,7 @@ if ($memberData["name"] === "fusion") {
<li>
<b>Score breakdown:</b> <code><?= $score["total"] ?></code>
<ul>
- <li><b>Host score:</b> <code><?= $score["host"] ?></code></li>
+ <li><b>Most common fronter score:</b> <code><?= $score["host"] ?></code></li>
<li><b>Relationships score:</b> <code><?= $score["relations"] ?></code></li>
<li><b>Fictive score:</b> <code><?= $score["fictive"] ?></code></li>
<li><b>Median score:</b> <code><?= $score["median"] ?></code></li>
diff --git a/includes/planner.php b/includes/planner.php
index 72575cd..64d87e4 100644
--- a/includes/planner.php
+++ b/includes/planner.php
@@ -1,11 +1,20 @@
<?php
+require_once $_SERVER["DOCUMENT_ROOT"] . "/includes/travelling.php"; global $travelling;
+require_once $_SERVER["DOCUMENT_ROOT"] . "/includes/score.php";
+require_once $_SERVER["DOCUMENT_ROOT"] . "/includes/pronouns.php";
+require_once $_SERVER["DOCUMENT_ROOT"] . "/includes/bitset.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; global $isUserLoggedIn;
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/banner.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/rainbow.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php";
+
$cloudburst = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc-planner.json"), true);
$raindrops = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd-planner.json"), true);
?>
-<h2>Front Planner</h2>
+<h2>Front planner</h2>
<table id="planner">
<tbody>
<tr class="planner-day">
diff --git a/includes/pleasure.php b/includes/pleasure.php
index 1bbfcea..8b4835b 100644
--- a/includes/pleasure.php
+++ b/includes/pleasure.php
@@ -1,4 +1,4 @@
-<h2>Pleasure Alert
+<h2>Pleasure alert
<details style="display: inline-block;font-size:12px;">
<summary class="text-muted" style="opacity:.5;"></summary>
<label><input id="test-mode" type="checkbox"> Test Mode</label> · <label><input id="fake-requests" type="checkbox"> Fake Requests</label>
diff --git a/includes/random.php b/includes/random.php
new file mode 100644
index 0000000..b6e7905
--- /dev/null
+++ b/includes/random.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @throws Exception
+ */
+function random($length = 13): string {
+ if (function_exists("random_bytes")) {
+ $bytes = random_bytes(ceil($length / 2));
+ } elseif (function_exists("openssl_random_pseudo_bytes")) {
+ $bytes = openssl_random_pseudo_bytes(ceil($length / 2));
+ } else {
+ throw new Exception("No cryptographically secure random function available");
+ }
+ return substr(bin2hex($bytes), 0, $length);
+} \ No newline at end of file
diff --git a/includes/refresh.php b/includes/refresh.php
index 79e3b7f..320af54 100644
--- a/includes/refresh.php
+++ b/includes/refresh.php
@@ -1,5 +1,6 @@
<?php
+$app = json_decode(file_get_contents("./app.json"), true);
$start = microtime(true);
@mkdir("./data");
@@ -47,10 +48,14 @@ function getSystem(string $id) {
getSystem("gdapd"); // Raindrops
getSystem("ynmuc"); // Cloudburst
+echo("Calendar\n");
+$currentOpStart = microtime(true);
+file_put_contents("./data/calendar.ics", file_get_contents($app["calendar"]));
+$times["calendar"] = microtime(true) - $currentOpStart;
+
echo("Downloading images.\n");
if (!file_exists("./data/images")) mkdir("./data/images");
-$currentOpStart = microtime(true);
foreach (json_decode(file_get_contents("./data/gdapd-members.json"), true) as $member) {
$currentOpStart2 = microtime(true);
echo(" " . $member['id'] . "\n");
@@ -68,9 +73,7 @@ foreach (json_decode(file_get_contents("./data/gdapd-members.json"), true) as $m
}
$times["images-gdapd-" . $member['id']] = microtime(true) - $currentOpStart2;
}
-$times["images-gdapd"] = microtime(true) - $currentOpStart;
-$currentOpStart = microtime(true);
foreach (json_decode(file_get_contents("./data/ynmuc-members.json"), true) as $member) {
$currentOpStart2 = microtime(true);
echo(" " . $member['id'] . "\n");
@@ -88,7 +91,6 @@ foreach (json_decode(file_get_contents("./data/ynmuc-members.json"), true) as $m
}
$times["images-ynmuc-" . $member['id']] = microtime(true) - $currentOpStart2;
}
-$times["images-ynmuc"] = microtime(true) - $currentOpStart;
$currentOpStart = microtime(true);
function isJson($string): bool {
@@ -104,11 +106,13 @@ foreach ($files as $file) {
}
$times["restore"] = microtime(true) - $currentOpStart;
-$time = (microtime(true) - $start);
+require_once "./backup.php";
+
+$time = array_sum($times);
echo("Completed in " . $time . " seconds.\n");
file_put_contents("./data/refresh.json", json_encode([
- "timestamp" => time(),
+ "timestamp" => microtime(true),
"duration" => $time,
"restored" => $restored,
"times" => $times
diff --git a/includes/restore.php b/includes/restore.php
new file mode 100644
index 0000000..72748ab
--- /dev/null
+++ b/includes/restore.php
@@ -0,0 +1,130 @@
+<?php
+
+function isJson($string): bool {
+ json_decode($string);
+ return (json_last_error() == JSON_ERROR_NONE);
+}
+
+function pkcs7_unpad($data) {
+ return substr($data, 0, -ord($data[strlen($data) - 1]));
+}
+
+function timeAgo($time): string {
+ if (!is_numeric($time)) {
+ $time = strtotime($time);
+ }
+
+ $periods = ["second", "minute", "hour", "day", "week", "month", "year", "age"];
+ $lengths = array("60", "60", "24", "7", "4.35", "12", "100");
+
+ $now = time();
+
+ $difference = $now - $time;
+ if ($difference <= 10 && $difference >= 0) {
+ return $tense = "now";
+ } elseif ($difference > 0) {
+ $tense = "ago";
+ } else {
+ $tense = "later";
+ }
+
+ for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
+ $difference /= $lengths[$j];
+ }
+
+ $difference = round($difference);
+
+ $period = $periods[$j] . ($difference >1 ? "s" :'');
+ return "{$difference} {$period} {$tense}";
+}
+
+if (!isset($_SERVER['argv'][1]) || !isset($_SERVER['argv'][2])) {
+ echo("Usage: php " . $_SERVER['argv'][0] . " <file> <key>\n");
+ die();
+} else {
+ $file = @file_get_contents($_SERVER['argv'][1]);
+ $raw = @file_get_contents($_SERVER['argv'][2]);
+
+ if ($file === false) {
+ echo("Unable to open backup file\n");
+ die();
+ }
+
+ if ($raw === false) {
+ echo("Unable to open key file\n");
+ die();
+ }
+
+ $raw2 = base64_decode($raw);
+
+ if (!isJson($raw2)) {
+ echo("Key file is corrupt\n");
+ die();
+ }
+
+ $keydata = json_decode($raw2, true);
+
+ if (!is_array($keydata) || !isset($keydata["iv"]) || !isset($keydata["key"])) {
+ echo("Key file is invalid\n");
+ die();
+ }
+
+ $iv = hex2bin($keydata["iv"]);
+ $key = hex2bin($keydata["key"]);
+
+ $decrypted = openssl_decrypt($file, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
+
+ if ($decrypted === false) {
+ echo("Unable to decrypt backup\n");
+ die();
+ }
+
+ $unpadded = pkcs7_unpad($decrypted);
+
+ if (!is_string($unpadded)) {
+ echo("Unable to decrypt backup\n");
+ die();
+ }
+
+ if (!isJson($unpadded)) {
+ echo("Backup is corrupt\n");
+ die();
+ }
+
+ $data = json_decode($unpadded, true);
+
+ if (!is_array($data) || !isset($data["date"]) || !isset($data["files"])) {
+ echo("Backup is invalid\n");
+ die();
+ }
+
+ echo(realpath($_SERVER['argv'][1]) . "\n Key: " . $_SERVER['argv'][2] . "\n Date: " . date('r', strtotime($data["date"])) . " (" . timeAgo($data["date"]) . ")" . "\n Contents: " . count($data["files"]) . " files\n");
+
+ @mkdir("./_restored");
+
+ $index = 0;
+ foreach ($data["files"] as $file) {
+ if ($file["dir"] === "") {
+ print("[$index] /" . $file["file"] . "\n");
+ } else {
+ print("[$index] /" . $file["dir"] . "/" . $file["file"] . "\n");
+ }
+
+ $content = base64_decode($file["content"]);
+ if (sha1($content) !== $file["checksum"][0]) {
+ print(" Backed up file is corrupted (SHA1 mismatch)\n Expected: " . $file["checksum"][0] . "\n Got: " . sha1($content) . "\n");
+ die("Backup aborted.\n");
+ }
+ if (md5($content) !== $file["checksum"][1]) {
+ print(" Backed up file is corrupted (MD5 mismatch)\n Expected: " . $file["checksum"][1] . "\n Got: " . md5($content) . "\n");
+ die("Backup aborted.\n");
+ }
+
+ @mkdir("./_restored/" . $file["dir"], 0777, true);
+ file_put_contents("./_restored/" . $file["dir"] . "/" . $file["file"], $content);
+
+ $index++;
+ }
+
+ print("Restored backup to ./_restored; review files before restoring to production\n");
+} \ No newline at end of file
diff --git a/includes/session.php b/includes/session.php
index 2157f5f..b440c9c 100644
--- a/includes/session.php
+++ b/includes/session.php
@@ -11,6 +11,12 @@ if (isset($_COOKIE['PEH2_SESSION_TOKEN'])) {
if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens/" . str_replace(".", "", str_replace("/", "", $_COOKIE['PEH2_SESSION_TOKEN'])))) {
$_PROFILE = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens/" . str_replace(".", "", str_replace("/", "", $_COOKIE['PEH2_SESSION_TOKEN']))), true);
+
+ if (isset($_GET['invert'])) {
+ $_PROFILE["login"] = $_PROFILE["login"] === "raindrops" ? "cloudburst" : "raindrops";
+ $_PROFILE["name"] = $_PROFILE["name"] === "Raindrops System" ? "Cloudburst System" : "Raindrops System";
+ }
+
$isLoggedIn = true;
} else {
$isLoggedIn = false;
diff --git a/includes/sysbanner.php b/includes/sysbanner.php
index 5ffe000..3a2b8c1 100644
--- a/includes/sysbanner.php
+++ b/includes/sysbanner.php
@@ -18,7 +18,7 @@ $travelling = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includ
<h3 style="height:max-content;"><?= $systemCommonName ?></h3>
<div style="height:max-content;display:grid;grid-template-columns: repeat(5, 1fr);" id="member-card">
<span>
- <b>Host: </b><?php
+ <b><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr>: </b><?php
$foundHost = false;
$hostGoneTravelling = false;
diff --git a/includes/system/compare.php b/includes/system/compare.php
index f597cf3..9714488 100644
--- a/includes/system/compare.php
+++ b/includes/system/compare.php
@@ -27,55 +27,55 @@ function getMember(string $id) {
<span class="comparison-header-l2">Member</span>
<span class="comparison-header-l3">Member</span>
<span class="comparison-header-l4">Member</span>
- <span class="comparison-header-l5">Mmbr.</span>
+ <span class="comparison-header-l5"><abbr title="Member" data-bs-toggle="tooltip">Mmbr.</abbr></span>
</span>
<span class="comparison-header comparison-item">
<span class="comparison-header-l0">Species</span>
<span class="comparison-header-l1">Species</span>
<span class="comparison-header-l2">Species</span>
<span class="comparison-header-l3">Species</span>
- <span class="comparison-header-l4">Spec.</span>
- <span class="comparison-header-l5">Spec.</span>
+ <span class="comparison-header-l4"><abbr title="Species" data-bs-toggle="tooltip">Spec.</abbr></span>
+ <span class="comparison-header-l5"><abbr title="Species" data-bs-toggle="tooltip">Spec.</abbr></span>
</span>
<span class="comparison-header comparison-item">
<span class="comparison-header-l0">Relations</span>
<span class="comparison-header-l1">Relations</span>
<span class="comparison-header-l2">Relations</span>
<span class="comparison-header-l3">Relations</span>
- <span class="comparison-header-l4">Relt.</span>
- <span class="comparison-header-l5">Relt.</span>
+ <span class="comparison-header-l4"><abbr title="Relations" data-bs-toggle="tooltip">Relt.</abbr></span>
+ <span class="comparison-header-l5"><abbr title="Relations" data-bs-toggle="tooltip">Relt.</abbr></span>
</span>
<span class="comparison-header comparison-item">
- <span class="comparison-header-l0">Host</span>
- <span class="comparison-header-l1">Host</span>
- <span class="comparison-header-l2">Host</span>
- <span class="comparison-header-l3">Host</span>
- <span class="comparison-header-l4">Hst.</span>
- <span class="comparison-header-l5">Hst.</span>
+ <span class="comparison-header-l0"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span>
+ <span class="comparison-header-l1"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span>
+ <span class="comparison-header-l2"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span>
+ <span class="comparison-header-l3"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span>
+ <span class="comparison-header-l4"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span>
+ <span class="comparison-header-l5"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span>
</span>
<span class="comparison-header comparison-item">
<span class="comparison-header-l0">Fictive</span>
<span class="comparison-header-l1">Fictive</span>
<span class="comparison-header-l2">Fictive</span>
<span class="comparison-header-l3">Fictive</span>
- <span class="comparison-header-l4">Fic.</span>
- <span class="comparison-header-l5">Fic.</span>
+ <span class="comparison-header-l4"><abbr title="Fictive" data-bs-toggle="tooltip">Fic.</abbr></span>
+ <span class="comparison-header-l5"><abbr title="Fictive" data-bs-toggle="tooltip">Fic.</abbr></span>
</span>
<span class="comparison-header comparison-item">
<span class="comparison-header-l0">Little</span>
<span class="comparison-header-l1">Little</span>
<span class="comparison-header-l2">Little</span>
<span class="comparison-header-l3">Little</span>
- <span class="comparison-header-l4">Ltl.</span>
- <span class="comparison-header-l5">Ltl.</span>
+ <span class="comparison-header-l4"><abbr title="Little" data-bs-toggle="tooltip">Ltl.</abbr></span>
+ <span class="comparison-header-l5"><abbr title="Little" data-bs-toggle="tooltip">Ltl.</abbr></span>
</span>
<span class="comparison-header comparison-item">
<span class="comparison-header-l0">Protector</span>
<span class="comparison-header-l1">Protector</span>
- <span class="comparison-header-l2">Protect.</span>
- <span class="comparison-header-l3">Protect.</span>
- <span class="comparison-header-l4">Prt.</span>
- <span class="comparison-header-l5">Prt.</span>
+ <span class="comparison-header-l2"><abbr title="Protector" data-bs-toggle="tooltip">Protect.</abbr></span>
+ <span class="comparison-header-l3"><abbr title="Protector" data-bs-toggle="tooltip">Protect.</abbr></span>
+ <span class="comparison-header-l4"><abbr title="Protector" data-bs-toggle="tooltip">Prt.</abbr></span>
+ <span class="comparison-header-l5"><abbr title="Protector" data-bs-toggle="tooltip">Prt.</abbr></span>
</span>
<?php foreach (scoreOrder($members, $systemID) as $member):