first commit

main
Jonas Lührig 2 years ago
commit 74e1f0054a

@ -0,0 +1,3 @@
## ICS Parser and Renderer ##
The index.php takes a public ICS via GET parameter and renders it either as a feed view with n-amount of future events or as week/month overview

@ -0,0 +1,58 @@
<?php require_once ('php/calendar-month.php'); ?>
<html>
<head>
<link href="css/style.css" rel="stylesheet">
<link href="css/dialog.css" rel="stylesheet">
<link href="css/calendar.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<!-- Content area of the page to avoid overflows -->
<div class="page-content">
<div class="calendar-parent">
<div class="calendar-header">
<div class="calendar-header-value first">
<!--<a href="?month=9" id="monthBck">&#x1F81C;</a>-->
<span id="monthName"><?php echoMonthName(); ?></span>
<!--<a href="?month=11" id="monthFwd">&#x1F81E;</a>-->
</div>
<div class="calendar-header-value">Montag</div>
<div class="calendar-header-value">Dienstag</div>
<div class="calendar-header-value">Mittwoch</div>
<div class="calendar-header-value">Donnerstag</div>
<div class="calendar-header-value">Freitag</div>
<div class="calendar-header-value">Samstag</div>
<div class="calendar-header-value">Sonntag</div>
</div>
<div class="calendar-body">
<?php echoCalendarEntries(); ?>
</div>
</div>
</div>
<!-- Dialog to ask user for ICS URL if not specified through GET -->
<div class="dialog-parent <?php hideMissingIcsDialog(); ?>">
<div class="dialog">
<div class="dialog-content">
Es wurde keine ICS URL als GET Parameter &uuml;bergeben.<br>
<br>
<label>Anzuzeigende ICS URL:</label>
<input type="text" form="dialog-form" name="ics_url" />
</div>
<div class="dialog-buttons">
<form id="dialog-form" method="get"></form>
<input type="submit" form="dialog-form" value="OK" />
</div>
</div>
</div>
<!-- Renders PHP errors nicely as HTML dialog -->
<?php insertErrorsHtml(); ?>
</body>
<script src="script/dialog.js"></script>
<script src="script/calendar.js"></script>
<script src="script/autoscroll.js"></script>
</html>

@ -0,0 +1,108 @@
<html>
<head>
<link href="css/style.css" rel="stylesheet">
<link href="css/dialog.css" rel="stylesheet">
<link href="css/calendar.css" rel="stylesheet">
<link href="css/calendar-week.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<!-- Content area of the page to avoid overflows -->
<div class="page-content">
<div class="calendar-parent">
<div class="calendar-header">
<div class="calendar-header-value first">
<!--<a href="?month=9" id="monthBck">&#x1F81C;</a>-->
<span id="monthName">Woche 1</span>
<!--<a href="?month=11" id="monthFwd">&#x1F81E;</a>-->
</div>
<div class="calendar-header-value">Montag</div>
<div class="calendar-header-value">Dienstag</div>
<div class="calendar-header-value">Mittwoch</div>
<div class="calendar-header-value">Donnerstag</div>
<div class="calendar-header-value">Freitag</div>
<div class="calendar-header-value">Samstag</div>
<div class="calendar-header-value">Sonntag</div>
</div>
<div class="calendar-body week-view">
<div class="calendar-header hours">
<div class="calendar-header-value">00:00</div>
<div class="calendar-header-value">01:00</div>
<div class="calendar-header-value">02:00</div>
<div class="calendar-header-value">03:00</div>
<div class="calendar-header-value">04:00</div>
<div class="calendar-header-value">05:00</div>
<div class="calendar-header-value">06:00</div>
<div class="calendar-header-value">07:00</div>
<div class="calendar-header-value">08:00</div>
<div class="calendar-header-value">09:00</div>
<div class="calendar-header-value">10:00</div>
<div class="calendar-header-value">11:00</div>
<div class="calendar-header-value">12:00</div>
<div class="calendar-header-value">13:00</div>
<div class="calendar-header-value">14:00</div>
<div class="calendar-header-value">15:00</div>
<div class="calendar-header-value">16:00</div>
<div class="calendar-header-value">17:00</div>
<div class="calendar-header-value">18:00</div>
<div class="calendar-header-value">19:00</div>
<div class="calendar-header-value">20:00</div>
<div class="calendar-header-value">21:00</div>
<div class="calendar-header-value">23:00</div>
</div>
<div class="calendar-weekday-column">
<div class="test-entry">a</div>
<div class="test-entry two">b</div>
<div class="test-entry three">b</div>
</div>
<div class="calendar-weekday-column"></div>
<div class="calendar-weekday-column"></div>
<div class="calendar-weekday-column"></div>
<div class="calendar-weekday-column"></div>
<div class="calendar-weekday-column"></div>
<div class="calendar-weekday-column"></div>
</div>
</div>
</div>
<!-- Dialog to ask user for ICS URL if not specified through GET -->
<div class="dialog-parent hidden">
<div class="dialog">
<div class="dialog-content">
Es wurde keine ICS URL als GET Parameter &uuml;bergeben.<br>
<br>
<label>Anzuzeigende ICS URL:</label>
<input type="text" form="dialog-form" name="icsUrl" />
</div>
<div class="dialog-buttons">
<form id="dialog-form" method="get"></form>
<input type="submit" form="dialog-form" value="OK" />
</div>
</div>
</div>
</body>
<script src="script/dialog.js"></script>
<script src="script/calendar.js"></script>
<script src="script/autoscroll.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
return;
let cbody = document.getElementsByClassName ("calendar-body")[0];
let hour = 0;
for (let i = 0; i < 8 * 24; i++) {
let hourPadded = String(hour).padStart(2, '0');
if (i % 8 == 0) {
cbody.innerHTML += `<div class="calendar-header-value">${hourPadded}:00</div>\n`;
hour++;
} else {
cbody.innerHTML += '<div class="calendar-entry">-</div>\n';
}
}
});
</script>
</html>

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

83
composer.lock generated

@ -0,0 +1,83 @@
{
"_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": "f1be773dbf3c5fddbb5187c8c187ceb4",
"packages": [
{
"name": "johngrogg/ics-parser",
"version": "v3.3.1",
"source": {
"type": "git",
"url": "https://github.com/u01jmg3/ics-parser.git",
"reference": "eeb51c4c0c06e6df3266f85ea774ca314536aba4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/u01jmg3/ics-parser/zipball/eeb51c4c0c06e6df3266f85ea774ca314536aba4",
"reference": "eeb51c4c0c06e6df3266f85ea774ca314536aba4",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.6.40"
},
"require-dev": {
"phpunit/phpunit": "^5|^9|^10"
},
"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/v3.3.1"
},
"funding": [
{
"url": "https://github.com/sponsors/u01jmg3",
"type": "github"
}
],
"time": "2023-10-10T09:58:49+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

@ -0,0 +1,27 @@
.calendar-header.hours {
grid-template-columns: 120px;
}
.calendar-weekday-column {
background: white;
position: relative;
gap: 1px;
display: grid;
}
.test-entry {
position: initial;
background: yellow;
padding: 0px 5px;
top: calc(100% / 23 * 11 + 1px);
height: calc(100% / 23 * 3 - 1px);
}
.test-entry.two {
background: red;
top: calc(100% / 23 * 1.1 + 1px);
height: calc(100% / 23 * 3 - 1px);
}

@ -0,0 +1,188 @@
.calendar-parent {
height: 100%;
display: flex;
flex-direction: column;
gap: 1px;
background: #555;
border: 1px solid #555;
}
/* Grid setup */
.calendar-header,
.calendar-body {
display: grid;
grid-template-columns: 120px repeat(7, 1fr);
grid-auto-rows: 1fr;
width: 100%;
gap: 1px;
}
/* Body of the calendar */
.calendar-body {
height: 100%;
min-height: 0;
}
/* Common layout of all grid elements */
.calendar-header-value,
.calendar-entry {
padding: 5px;
text-align: center;
}
/* Header elements within grid */
.calendar-header-value {
display: flex;
justify-content: center;
align-items: center;
background: #000;
color: white;
font-weight: bold;
}
/* First header, which shows the current month */
.calendar-header-value.first {
display: flex;
/*justify-content: space-between;*/
justify-content: center;
align-items: center;
font-size: larger;
-webkit-user-select: none; /* Safari */
user-select: none;
}
/* Links in first header to change month */
.calendar-header-value > a {
margin: 0px 5px;
text-decoration: none;
color: #7a7ac7;
}
/* Month day entry */
.calendar-entry {
display: inline-flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
/* Day entry for previous or next month */
.calendar-entry.other-month {
background: #222 !important;
color: #bbb;
}
.calendar-entry.other-month .day-event-entry {
background: #444;
border-color: #333;
}
/* Two different backgrounds for odd and even rows*/
.calendar-entry.even-row {
background: #ccc;
}
.calendar-entry.odd-row {
background: #aaa;
}
/* Yet again different backgrounds for weekend days */
.calendar-entry.weekend {
background: #777;
}
/* Header of month day entry */
.day-header {
display: inline-block;
width: 100%;
text-align: center;
font-weight: bold;
padding-bottom: 4px;
}
/* Modification of day-header that highlights today */
.day-header.today > span {
padding: 0px 5px;
border-radius: 3px;
background: black;
color: white;
}
/* Scrollbox containing the day events element */
.day-events-scrollbox {
flex: 1;
overflow: hidden;
position: relative;
border-radius: 3px;
}
/* Element containing all event elements for a day */
.day-events {
display: flex;
height: 100%;
flex-direction: column;
overflow: scroll;
scroll-behavior: smooth;
gap: 2px;
scrollbar-width: none;
}
/* Hide day-events scrollbars */
.day-events::-webkit-scrollbar {
display: none;
}
/* Shadow for the scrollbox */
.day-events-shadow {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
pointer-events: none;
}
/* Top, bottom and top-bottom shadows for day-events-scrollbox */
.box-shadow-bottom {
box-shadow: inset 0px -11px 8px -10px #000;
}
.box-shadow-top {
box-shadow: inset 0px 11px 8px -10px #000;
}
.box-shadow-topbottom {
box-shadow:
inset 0px -11px 8px -10px #000,
inset 0px 11px 8px -10px #000;
}
/* Event entry for a day */
.day-event-entry {
background: yellow;
border: 1px solid darkorange;
border-radius: 3px;
padding: 2px 4px;
margin: 0px 2px;
display: inline-flex;
}
/* Event timespan */
.day-event-entry > .time {
font-weight: bold;
margin-right: 3px;
white-space: nowrap;
}
/* Event Description */
.day-event-entry > .description {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

@ -0,0 +1,54 @@
.dialog-parent {
display: flex;
position: absolute;
top: 0px;
left: 0px;
width: 100vw;
height: 100vh;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.5);
}
.dialog {
display: inline-flex;
position: relative;
min-width: 320px;
min-height: 100px;
max-width: 90vw;
max-height: 90vh;
background: white;
box-shadow: 0px 0px 5px black;
}
.dialog-content {
display: inline-block;
margin-bottom: 35px;
padding: 10px;
overflow: scroll;
}
.dialog-buttons {
display: flex;
position: absolute;
left: 0px;
bottom: 0px;
height: 35px;
width: 100%;
justify-content: center;
align-items: center;
column-gap: 15px;
}
.dialog-content > input[type=text] {
width: 100%;
}
.dialog-content > label {
font-size: smaller;
}

@ -0,0 +1,81 @@
body {
font-size: 20px;
}
.page-content {
box-shadow: none;
}
.flow-parent {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
}
.flow-header {
width: 100%;
}
.flow-header-value {
margin: 0px 18px;
font-weight: bold;
font-size: 125%;
}
.flow-element, .flow-header {
display: flex;
margin: 10px;
width: 100%;
/*max-width: 400px;*/
}
.flow-day-name,
.flow-header-spacer {
font-weight: bold;
font-size: 175%;
margin: 0px 10px;
text-align: center;
min-width: 50px;
}
.flow-day-name .weekday {
font-size: 50%;
}
.flow-day-entries {
display: grid;
margin: 10px;
gap: 5px;
flex: 1;
}
.flow-day-entry {
background: yellow;
border: 1px solid darkorange;
border-radius: 5px;
padding: 4px 6px;
margin: 0px 2px;
min-width: 0;
display: inline-flex;
flex-direction: column;
}
.flow-day-entry > .time {
font-size: smaller;
font-weight: bold;
white-space: nowrap;
}
.flow-day-entry > .description {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

@ -0,0 +1,36 @@
body {
display: flex;
flex-direction: column;
margin: 0px;
width: 100vw;
height: 100vh;
font-family: sans-serif;
font-size: 14px;
}
.page-title {
font-size: larger;
font-weight: bold;
padding: 10px;
padding-bottom: 0px;
}
.page-content {
flex: 1;
min-height: 0;
margin: 10px;
box-shadow: 5px 7px #777;
}
.hidden {
display: none !important;
}
/* Disabled/empty link */
a[href="#"] {
color: #555 !important;
cursor: not-allowed;
pointer-events: none;
}

@ -0,0 +1,41 @@
<?php require_once ('php/flow.php'); ?>
<html>
<head>
<link href="css/style.css" rel="stylesheet">
<link href="css/dialog.css" rel="stylesheet">
<link href="css/flow.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body class="autoscroll">
<!-- Content area of the page to avoid overflows -->
<div class="page-content">
<div class="flow-parent">
<?php echoFlowBody(); ?>
<div class="flow-element">
</div>
</div>
</div>
<!-- Dialog to ask user for ICS URL if not specified through GET -->
<div class="dialog-parent <?php hideMissingIcsDialog(); ?>">
<div class="dialog">
<div class="dialog-content">
Es wurde keine ICS URL als GET Parameter &uuml;bergeben.<br>
<br>
<label>Anzuzeigende ICS URL:</label>
<input type="text" form="dialog-form" name="icsUrl" />
</div>
<div class="dialog-buttons">
<form id="dialog-form" method="get"></form>
<input type="submit" form="dialog-form" value="OK" />
</div>
</div>
</div>
</body>
<script src="script/dialog.js"></script>
<script src="script/autoscroll.js"></script>
</html>

@ -0,0 +1,217 @@
<?php
require_once ('ics.php');
$ical = null;
if (isset ($_GET["ics_url"])) {
$ical = getCalendar($_GET["ics_url"]);
}
/**
* Sets and returns the global $requested_month variable
* to either the GET requested month, or the current month
*
* @return int Month number
*/
function getRequestedMonth () {
global $_GET, $requested_month;
if (isset ($requested_month)) return $requested_month;
if (
! isset ($_GET["month"]) ||
! ($requested_month = intval ($_GET["month"])) ||
$requested_month > 12 ||
$requested_month < 1
) {
$requested_month = idate("m");
}
return $requested_month;
}
/**
* Echoes the requested months' name
*/
function echoMonthName () {
echo getLocalizedMonthName (getRequestedMonth ());
}
/**
* Returns HTML for a days' events list (CSS class day-events-scrollbox)
*
* @param ICal $ical ICal object
* @param int $day Day of month
* @param int $month Month (defaults to current month)
* @param int $year Year (defaults to current year)
*
* @return string HTML containing DIV of class day-events-scrollbox
* @return false False on failure
*/
function generateDayEventsList ($ical, $day, $month = null, $year = null) {
if (! $ical) return false;
$html = '
<div class="day-events-scrollbox">
<div class="day-events autoscroll">
%s
</div>
<div class="day-events-shadow"></div>
</div>
';
foreach (getEventsForDay ($ical, $day, $month, $year) as $event) {
$dtstart = $ical -> iCalDateToDateTime($event -> dtstart_array[3]);
$dtend = $ical -> iCalDateToDateTime($event -> dtend_array[3]);
$today_start = new DateTime(
date('Y-m-d H:i:s', mktime(0, 0, 0, $month, $day, $year)),
$dtstart -> getTimezone()
);
$today_end = clone $today_start; $today_end -> modify("+1 day");
$time_separator = "-";
$time_prefix = "";
$time_start = $dtstart -> format('H:i');
$time_end = $dtend -> format('H:i');
// When start date is in past, don't display start time
if ($dtstart < $today_start) {
$time_start = "";
$time_separator = "Bis ";
}
// When end date is in future, don't display end time
if ($dtend > $today_end) {
$time_end = "";
$time_separator = "";
$time_prefix = "Ab ";
}
// Assemble time display string
$time_str = "{$time_prefix}{$time_start}{$time_separator}{$time_end}";
// Check if event is all day
if ($time_start == "00:00" && $time_end == "00:00") {
//$time_str = "Ganztägig";
$time_str = "";
}
// Check if event is multi day
if ($time_start == "" && $time_end == "") {
$time_str = "Mehrtägig";
}
// Check if multi day event without start times
if ($time_start . $time_end == "00:00") {
$time_str = "Mehrtägig";
}
$html = sprintf (
$html,
sprintf(
'<span class="day-event-entry">
<span class="time">%s</span>
<span class="description">%s</span>
</span>
%%s',
$time_str,
$event -> summary
),
);
}
$html = sprintf ($html, "<!-- {$day} -->");
return $html;
}
/**
* Returns HTML with all elements going in the DIV with CSS class calendar-body
*
* @param ICal $Ical ICal object
* @param int $month Month
*
* @return string HTML containing elements for calendar body
* @return false False on failure
*/
function generateCalendarBodyItems ($ical, $month) {
if ($month > 12 || $month < 1) return false;
$html = '%s';
$odd_row = false;
// Number of days in month
$month_max_days = idate ("t", mktime (0, 0, 0, $month));
// First of month unix timestamp
$first_of_month = new DateTime (
date('Y-m-d H:i:s', mktime (0, 0, 0, $month, 1))
);
// Days needed to pad the calendar at the start
$pad_days = idate ("N", $first_of_month -> getTimestamp()) - 1;
// First of the month, minus the pad days
$day = $first_of_month -> modify ("-{$pad_days} days");
// Number of total entries in calendar
$num_rows = ceil (($pad_days + $month_max_days) / 7);
$num_entries = ($num_rows * 8);
// Loop over all entry slots and generate entries
for ($item_index = 0; $item_index < $num_entries; $item_index ++) {
// If item_index is beginning of row, add week number entry
if ($item_index % 8 == 0) {
$week = $day -> format ("W");
$html = sprintf (
$html,
"<div class=\"calendar-header-value\">Woche {$week}</div>
%s",
);
// Swap odd and even rows
$odd_row = ! $odd_row;
continue;
}
$calendar_entry_html = '
<div class="calendar-entry %s %s %s">
<div class="day-header %s"><span>%d.</span></div>
%s
</div>
%s
';
$html = sprintf (
$html,
sprintf(
$calendar_entry_html,
$odd_row? "odd-row" : "even-row",
(isDateTimeWithinMonth ($day, $month)) ? "" : "other-month",
(isDateTimeDuringWeekend ($day)) ? "weekend" : "",
(isDateTimeToday ($day))? "today" : "",
$day -> format ("d"),
generateDayEventsList(
$ical,
intval ($day -> format ("d")),
intval ($day -> format ("m")),
),
"%s"
)
);
$day -> modify ("+1 day");
}
return sprintf($html, "");
}
/**
* Echoes HTML to insert all calendar day entries
*/
function echoCalendarEntries() {
global $ical;
echo generateCalendarBodyItems($ical, getRequestedMonth());
}

@ -0,0 +1,207 @@
<?php
require_once ('ics.php');
$ical = null;
if (isset ($_GET["ics_url"])) {
$ical = getCalendar($_GET["ics_url"]);
}
/**
* Fetches the user requested start date for the flow
*/
function getRequestedStartDate () {
global $_GET, $requested_start_date;
if (isset ($requested_start_date)) return $requested_start_date;
if (
! isset ($_GET["start_date"]) ||
! ($requested_start_date = new DateTime ($_GET["start_date"]))
) {
$requested_start_date = new DateTime ();
}
return $requested_start_date;
}
/**
* Fetches the user requested amount of days to display
*/
function getRequestedDisplayedDays () {
global $_GET, $requested_display_days;
if (isset ($requested_display_days)) return $requested_display_days;
if (
! isset ($_GET["days"]) ||
! ($requested_display_days = intval($_GET["days"]))
) {
$requested_display_days = 7;
}
return $requested_display_days;
}
/**
* Return HTML for a flow day entry
*
* @param ICal $ical ICal object
* @param DateTime $date DateTime object
*
* @return string Returns HTML with flow day entry
* @return false False on failure
*/
function generateFlowEntryForDay ($ical, $date) {
if ($ical == null || $date == null) return false;
$html = sprintf (
'
<div class="flow-element">
<div class="flow-day-name">
<div class="day">%s</div>
<div class="weekday">%s</div>
</div>
<div class="flow-day-entries">
%%s
</div>
</div>
',
$date -> format ("d"),
getLocalizedShortDayName (
intval ($date -> format ("N"))
)
);
// Fetch events for the specified date
$events = getEventsForDate ($ical, $date);
// Skip empty days
if (count ($events) == 0) return "";
// Iterate over each event and add an event entry to the HTML
foreach ($events as $event) {
$dtstart = $ical -> iCalDateToDateTime($event -> dtstart_array[3]);
$dtend = $ical -> iCalDateToDateTime($event -> dtend_array[3]);
$today_end = clone $date; $today_end -> modify("+1 day");
$time_separator = " - ";
$time_prefix = "";
$time_start = $dtstart -> format('H:i');
$time_end = $dtend -> format('H:i');
// When start date is in past, don't display start time
if ($dtstart < $date) {
$time_start = "";
$time_separator = "Bis ";
}
// When end date is in future, don't display end time
if ($dtend > $today_end) {
$time_end = "";
$time_separator = "";
$time_prefix = "Ab ";
}
// Assemble time display string
$time_str = "{$time_prefix}{$time_start}{$time_separator}{$time_end}";
// Check if event is all day
if ($time_start == "00:00" && $time_end == "00:00") {
$time_str = "Ganztägig";
}
// Check if event is multi day
if ($time_start == "" && $time_end == "") {
$time_str = "Mehrtägig";
}
// Check if multi day event without start times
if ($time_start . $time_end == "00:00") {
$time_str = "Mehrtägig";
}
$html = sprintf (
$html,
sprintf (
'<div class="flow-day-entry">
<span class="description">%s</span>
<span class="time">%s</span>
</div>
%%s',
$event -> summary,
$time_str
),
);
}
$html = sprintf ($html, '');
return $html;
}
/**
* Return HTML with flow header for specific date
*
* @param DateTime $date DateTime object
*
* @return string Returns HTML with flow header
* @return false False on failure
*/
function generateFlowHeader ($date) {
if ($date == null) return false;
return sprintf (
'<div class="flow-header">
<div class="flow-header-spacer"></div>
<div class="flow-header-value">%s</div>
</div>',
getLocalizedMonthName (intval ($date -> format ("m")))
);
}
/**
* Return HTML for the flow body
*
* @param ICal $ical ICal object
* @param DateTime $date DateTime object
* @param int $days Days to display
*
* @return string Returns HTML with the entire flow data
* @return false False on failure
*/
function generateFlowData ($ical, $date, $days) {
if ($ical == null || $date == null) return false;
$html = generateFlowHeader ($date) . "\n%s";
$end_date = clone $date; $end_date -> modify ("+{$days} days");
while ($date <= $end_date) {
if ($date -> format ("d") == "01") $html = sprintf (
$html,
generateFlowHeader ($date) . "\n%s"
);
$html = sprintf (
$html,
generateFlowEntryForDay($ical, $date) . "%s"
);
$date -> modify ("+1 day");
}
$html = sprintf ($html, "");
return $html;
}
/**
* Echoes the entire HTML for the flow body
*/
function echoFlowBody () {
global $ical;
echo generateFlowData (
$ical,
getRequestedStartDate(),
getRequestedDisplayedDays()
);
}

@ -0,0 +1,73 @@
<?php
$_ERRORS = [];
/**
* Adds an error to the $_ERROR array to be displayed at the end
* of a PHP script as HTML dialogs
*
* @param string $message Error message
*/
function showError ($message) {
if ($message == "") return;
$message = str_replace("\n", "<br>", $message);
$message = str_replace(" ", "&nbsp;", $message);
global $_ERRORS;
$_ERRORS[] = $message;
}
/**
* Write text into stderr
*
* @param string $text Text to log
*/
function debug ($text) {
error_log("\n> $text" . PHP_EOL);
}
/**
* Echoes HTML code to display all collected error messages
*/
function insertErrorsHtml() {
global $_ERRORS;
if (count ($_ERRORS) == 0) return;
// Dialog frame HTML
$base_html = '
<div class="dialog-parent">
<div class="dialog">
<div class="dialog-content">
<b>Fehler beim Laden der Seite:</b><br>
<br>
%s
</div>
<div class="dialog-buttons">
<button onclick="closeDialog(this)">OK</button>
</div>
</div>
</div>
';
$errors_html = "";
foreach ($_ERRORS as $error) {
$errors_html .= "{$error}<br>";
}
printf ($base_html, $errors_html);
}
/**
* Hides the dialog box asking for an ICS URL if one
* was already specified by the user
*/
function hideMissingIcsDialog () {
global $_GET;
if (isset ($_GET["ics_url"]) && $_GET["ics_url"] != "") {
echo "hidden";
}
}

@ -0,0 +1,206 @@
<?php
require_once ('html-components.php');
require_once ('vendor/autoload.php');
use ICal\ICal;
date_default_timezone_set("Europe/Berlin");
/**
* Returns the name of a month
*
* @param int $month Month index, 1 to 12
*
* @return string Month name
* @return string Blank string if invalid month was given
*/
function getLocalizedMonthName ($month) {
if (! $month || $month > 12 || $month < 1) return "";
return [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember"
][$month - 1];
}
/**
* Returns the short name of a day of week
*
* @param int $day_of_week DoW index, 1 to 7
*
* @return string Day of week name
* @return string Blank string if invalid DoW was given
*/
function getLocalizedShortDayName ($day_of_week) {
if ($day_of_week < 1 || $day_of_week > 7) return "";
return [
"Mo.",
"Di.",
"Mi.",
"Do.",
"Fr.",
"Sa.",
"So."
][$day_of_week - 1];
}
/**
* Tests if a given date is today
*
* @param DateTime $date DateTime object to compare against
*
* @return true when the DateTime object is today
* @return false when the DateTime object is not today
*/
function isDateTimeToday ($date) {
return $date -> format ("Y-m-d") == date ("Y-m-d");
}
/**
* Tests if a given date is within the current month
*
* @param DateTime $date DateTime object to compare against
* @param int $month Month to check against (defaults to current month)
*
* @return true when the DateTime object is within the current month
* @return false when the DateTime object is not within the current month
*/
function isDateTimeWithinMonth ($date, $month = null) {
$compareDate = new DateTime ();
if ($month != null) $compareDate = new DateTime ("this year $month/01");
return $date -> format ("Y-m") == $compareDate -> format ("Y-m");
}
/**
* Tests if a given date is during the weekend
*
* @param DateTime $date DateTime object to test
*
* @return true when the DateTime object is a weekend day
* @return false when the DateTime object is a week day
*/
function isDateTimeDuringWeekend ($date) {
return intval ($date -> format ("N")) > 5;
}
/**
* Fetches an ICal object from an ICS URL
*
* @param string $url ICS URL to fetch from
*
* @return ICal ICal object
*/
function getCalendar ($url) {
try {
$ical = new ICal (false, array(
'defaultSpan' => 2, // Default value
'defaultTimeZone' => 'Europe/Berlin',
'defaultWeekStart' => 'MO', // Default value
'disableCharacterReplacement' => false, // Default value
'filterDaysAfter' => null, // Default value
'filterDaysBefore' => null
));
$ical -> initUrl(
$url,
$username = null,
$password = null,
$userAgent = null
);
} catch (Exception $e) {
showError( "ICal error: " . $e -> getMessage() );
return $ical;
}
return $ical;
}
/**
* Returns all events from an iCal object
*
* @param ICal $ical ICal object
*
* @return array Array of event objects
* @return false False on failure
*/
function getEvents ($ical) {
if (! $ical) return false;
return $ical -> sortEventsWithOrder (
$ical -> events ()
);
}
/**
* Returns all events from an ICal object for a specific day
*
* @param ICal $ical ICal object
* @param int $day Day
* @param int $month Month (defaults to current month)
* @param int $year Year (default to current year)
*
* @return array Array of event objects
* @return false False on failure
*/
function getEventsForDay ($ical, $day, $month = null, $year = null) {
if (! $ical) return false;
$date_format = "Y-m-d h:i:s";
return $ical -> sortEventsWithOrder (
$ical -> eventsFromRange (
date ($date_format, mktime (0, 0, 0, $month, $day, $year)),
date ($date_format, mktime (23, 59, 59, $month, $day, $year)),
)
);
}
/**
* Wrapper for getEventsForDay.
* Returns all events from an ICal object for a specific date
*
* @param ICal $ical ICal object
* @param DateTime $date DateTime object
*
* @return array Array of event objects
* @return false False on failure
*/
function getEventsForDate ($ical, $date) {
return getEventsForDay (
$ical,
intval($date -> format ("d")),
intval($date -> format ("m")),
intval($date -> format ("Y"))
);
}
/**
* Get events from ICal object limited by a start date
* and amount to days forward from the start date
*
* @param ICal $ical ICal object
* @param DateTime $startDate Date to start from
* @param int $days Amount of days to get
*/
function getEventsFromRange ($ical, $startDate, $days) {
if (! $ical) return false;
return $ical -> sortEventsWithOrder (
$ical -> getEventsFromRange (
$startDate -> format ("Y-m-d H:i:s"),
$startDate -> modify ("+ {$days} days") -> format ("Y-m-d H:i:s")
)
);
}

@ -0,0 +1,44 @@
// Defines the framerate of autoscroller
setInterval(autoScroll, 50);
/**
* Auto scrolls all elements with class autoscroll
*/
function autoScroll () {
Array.from (document.getElementsByClassName("autoscroll")).forEach((objDiv) => {
// Skip non-scrollable
if (objDiv.scrollHeight - objDiv.clientHeight == 0) return;
// Wait timeout until scroll direction change
let scrollpause = parseInt(objDiv.dataset.scrollpause);
if (scrollpause != NaN && scrollpause > 0) {
scrollpause --;
objDiv.dataset.scrollpause = scrollpause;
return;
}
// Step either forward or backwards by one
let direction = 1;
if (objDiv.dataset.scrolldirection == "up") direction = -1;
objDiv.scrollTop += direction;
// If this is the first scroll step after, remove the scrollpause tag
if (scrollpause != NaN && scrollpause == 0) {
delete objDiv.dataset.scrollpause;
return;
}
// Reached the bottom, turn around
if (objDiv.scrollTop == objDiv.scrollHeight - objDiv.clientHeight) {
objDiv.dataset.scrolldirection = "up";
objDiv.dataset.scrollpause = 50;
}
// Reached the top, turn around
if (objDiv.scrollTop == 0) {
delete objDiv.dataset.scrolldirection;
objDiv.dataset.scrollpause = 50;
}
});
}

@ -0,0 +1,100 @@
/**
* Global variables
*/
var requestedMonth;
/**
* Checks the scroll status of an element on a scroll
* event and adds box shadows either top, bottom or both
* depening on the position of the scroll viewport
*
* @param {Event} e Scroll event object
*/
function onscrollTestScrollShadow(e) {
let shadowTarget = e.target.parentElement.children[1];
if (e.target.clientHeight == e.target.scrollHeight) {
// Target has no overflowing content and is not scrollable
shadowTarget.classList.remove("box-shadow-top");
shadowTarget.classList.remove("box-shadow-bottom");
shadowTarget.classList.remove("box-shadow-topbottom");
return;
}
if (e.target.scrollTop == e.target.scrollHeight - e.target.clientHeight) {
// Scroll is at bottom
shadowTarget.classList.add("box-shadow-top");
shadowTarget.classList.remove("box-shadow-bottom");
shadowTarget.classList.remove("box-shadow-topbottom");
} else if (e.target.scrollTop == 0) {
// Scroll is at top
shadowTarget.classList.remove("box-shadow-top");
shadowTarget.classList.add("box-shadow-bottom");
shadowTarget.classList.remove("box-shadow-topbottom");
} else {
// Scroll is in middle somewhere
shadowTarget.classList.remove("box-shadow-top");
shadowTarget.classList.remove("box-shadow-bottom");
shadowTarget.classList.add("box-shadow-topbottom");
}
}
/**
* Prepares an element with class day-events for scrolling, resetting
* the scroll position to top, initializing the onscrollTestScrollShadow
* function and adding the same function as scroll event handler
*
* @param {Element} parent Parent element with class calendar-element
*/
function setupScroll (parent) {
if (! parent.classList.contains("calendar-entry")) return;
if (parent.children.length <= 1) return;
if (parent.children[1].childElementCount == 0) return;
let target = parent.children[1].children[0];
if (! target.classList.contains("day-events")) return;
target.scrollTop = 0;
onscrollTestScrollShadow ( {target: target} );
target.addEventListener("scroll", onscrollTestScrollShadow);
}
/**
* Populates the month forward and backward buttons with proper links
* and displays the correct month name in the top left corner
*/
function setupMonthDisplayAndButtons () {
fetchRequestedMonth();
let monthNameElement = document.getElementById("monthName");
let monthFwdElement = document.getElementById("monthFwd");
let monthBckElement = document.getElementById("monthBck");
let urlSearchParams = new URLSearchParams(window.location.search);
monthNameElement.innerText = getMonthName(requestedMonth);
if (requestedMonth + 1 > 12) {
monthFwdElement.href = "#";
} else {
urlSearchParams.set("month", requestedMonth + 1);
monthFwdElement.href = "?" + urlSearchParams.toString();
}
if (requestedMonth - 1 < 1) {
monthBckElement.href = "#";
} else {
urlSearchParams.set("month", requestedMonth - 1);
monthBckElement.href = "?" + urlSearchParams.toString();
}
}
// Add shadow indicators to calendar event lists when scrolling is available
window.addEventListener("DOMContentLoaded", () => {
let calendarElement = document.getElementsByClassName("calendar-body")[0];
Array.from(calendarElement.children).forEach( setupScroll );
//setupMonthDisplayAndButtons();
fetchRequestedMonth();
});

@ -0,0 +1,11 @@
/**
* Closes a dialog box containing the element calling this function
*
* @param {Element} sender Sender element
*/
function closeDialog (sender) {
sender.
parentElement.
parentElement.
parentElement.remove();
}

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitf1be773dbf3c5fddbb5187c8c187ceb4::getLoader();

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

@ -0,0 +1,19 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'ICal' => array($vendorDir . '/johngrogg/ics-parser/src'),
);

@ -0,0 +1,9 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitf1be773dbf3c5fddbb5187c8c187ceb4
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitf1be773dbf3c5fddbb5187c8c187ceb4', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitf1be773dbf3c5fddbb5187c8c187ceb4', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitf1be773dbf3c5fddbb5187c8c187ceb4::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

@ -0,0 +1,31 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitf1be773dbf3c5fddbb5187c8c187ceb4
{
public static $prefixesPsr0 = array (
'I' =>
array (
'ICal' =>
array (
0 => __DIR__ . '/..' . '/johngrogg/ics-parser/src',
),
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixesPsr0 = ComposerStaticInitf1be773dbf3c5fddbb5187c8c187ceb4::$prefixesPsr0;
$loader->classMap = ComposerStaticInitf1be773dbf3c5fddbb5187c8c187ceb4::$classMap;
}, null, ClassLoader::class);
}
}

@ -0,0 +1,73 @@
{
"packages": [
{
"name": "johngrogg/ics-parser",
"version": "v3.3.1",
"version_normalized": "3.3.1.0",
"source": {
"type": "git",
"url": "https://github.com/u01jmg3/ics-parser.git",
"reference": "eeb51c4c0c06e6df3266f85ea774ca314536aba4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/u01jmg3/ics-parser/zipball/eeb51c4c0c06e6df3266f85ea774ca314536aba4",
"reference": "eeb51c4c0c06e6df3266f85ea774ca314536aba4",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.6.40"
},
"require-dev": {
"phpunit/phpunit": "^5|^9|^10"
},
"time": "2023-10-10T09:58:49+00:00",
"type": "library",
"installation-source": "dist",
"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/v3.3.1"
},
"funding": [
{
"url": "https://github.com/sponsors/u01jmg3",
"type": "github"
}
],
"install-path": "../johngrogg/ics-parser"
}
],
"dev": true,
"dev-package-names": []
}

@ -0,0 +1,32 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => NULL,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => NULL,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'johngrogg/ics-parser' => array(
'pretty_version' => 'v3.3.1',
'version' => '3.3.1.0',
'reference' => 'eeb51c4c0c06e6df3266f85ea774ca314536aba4',
'type' => 'library',
'install_path' => __DIR__ . '/../johngrogg/ics-parser',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 50640)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 5.6.40". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

@ -0,0 +1,11 @@
# https://editorconfig.org/
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

@ -0,0 +1,53 @@
name: Bug Report
description: "Report something that's broken."
labels: ["bug-normal"]
body:
- type: markdown
attributes:
value: "Before raising an issue, please check the issue has not already been fixed in `dev-master`. You can also search through our [closed issues](../issues?q=is%3Aissue+is%3Aclosed+)."
- type: input
id: php-version
attributes:
label: PHP Version
description: Provide the PHP version that you are using.
placeholder: 8.1.4
validations:
required: true
- type: input
id: php-date-timezone
attributes:
label: PHP date.timezone
description: Provide the PHP date.timezone that you are using.
placeholder: "[Country] / [City]"
validations:
required: true
- type: input
id: ics-parser-version
attributes:
label: ICS Parser Version
description: Provide the `ics-parser` library version that you are using.
placeholder: 3.2.1
validations:
required: true
- type: input
id: operating-system
attributes:
label: Operating System
description: Provide the operating system that you are using.
placeholder: "Windows / Mac / Linux"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Provide a detailed description of the issue that you are facing.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Provide detailed steps to reproduce your issue. It is **essential** that you supply a copy of the iCal file that is causing the parser to behave incorrectly to allow us to investigate. Prior to uploading the iCal file, please remove any personal or identifying information.
validations:
required: true

@ -0,0 +1,12 @@
> :information_source:
> - File a bug on our [issue tracker](https://github.com/u01jmg3/ics-parser/issues) (if there isn't one already).
> - If your patch is going to be large it might be a good idea to get the discussion started early. We are happy to discuss it in a new issue beforehand.
> - Please follow the coding standards already adhered to in the file you're editing before committing
> - This includes the use of *4 spaces* over tabs for indentation
> - Trim all trailing whitespace
> - Using single quotes (`'`) where possible
> - Use `PHP_EOL` where possible or default to `\n`
> - Using the [1TBS](https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS_.28OTBS.29) indent style
> - If a function is added or changed, please remember to update the [API documentation in the README](https://github.com/u01jmg3/ics-parser/blob/master/README.md#api)
> - Please include unit tests to verify any new functionality
> - Also check that existing tests still pass: `composer test`

@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
reviewers:
- u01jmg3
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly

@ -0,0 +1,9 @@
# Release Checklist
- [ ] Update docblock in `src/ICal/ICal.php`
- [ ] Ensure the documentation is up to date
- [ ] Push the code changes to GitHub (`git push`)
- [ ] Tag the release (`git tag v1.2.3`)
- [ ] Push the tag (`git push --tag`)
- [ ] Check [Packagist](https://packagist.org/packages/johngrogg/ics-parser) is updated
- [ ] Notify anyone who opened [an issue or PR](https://github.com/u01jmg3/ics-parser/issues?q=is%3Aopen) of the fix

@ -0,0 +1,67 @@
name: Coding Standards
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
Scan:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [5.6, 7.4, '8.0', 8.1, 8.2]
name: PHP ${{ matrix.php }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:2.2
coverage: none
- name: Install dependencies for PHP 5.6
run: composer update --quiet --no-scripts
if: matrix.php == 5.6
- name: Install dependencies for PHP 7.4+
run: composer install --quiet --no-scripts
if: matrix.php >= 7.4
- name: Execute tests
run: vendor/bin/phpunit --verbose
- name: Install additional dependencies
run: |
composer config allow-plugins.bamarni/composer-bin-plugin true --no-plugins
composer require bamarni/composer-bin-plugin rector/rector squizlabs/php_codesniffer --dev --quiet --no-scripts
composer bin easy-coding-standard config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer bin easy-coding-standard require symplify/easy-coding-standard slevomat/coding-standard --dev --quiet --no-scripts
if: matrix.php == 8.2
- name: Execute PHPCodeSniffer
run: vendor/bin/phpcs -n -s --standard=psr12 src
if: matrix.php == 8.2
- name: Execute Rector
run: vendor/bin/rector process src --dry-run
if: matrix.php == 8.2
- name: Execute ECS
run: vendor/bin/ecs check .
if: matrix.php == 8.2
- name: Execute PHPStan
run: vendor/bin/phpstan analyse src
if: matrix.php == 8.2

@ -0,0 +1,59 @@
###################
# Compiled Source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
############
# Packages #
############
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
######################
# Logs and Databases #
######################
*.log
*.sqlite
######################
# OS Generated Files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
.phpunit.result.cache
ehthumbs.db
Thumbs.db
workbench
####################
# Package Managers #
####################
auth.json
node_modules
vendor
##########
# Custom #
##########
*.git
*-report.*
########
# IDEs #
########
.idea
*.iml

@ -0,0 +1,18 @@
## Contributing
ICS Parser is an open source project. It is licensed under the [MIT license](https://opensource.org/licenses/MIT).
We appreciate pull requests, here are our guidelines:
1. Firstly, check if your issue is present within the latest version (`dev-master`) as the problem may already have been fixed.
1. Log a bug in our [issue tracker](https://github.com/u01jmg3/ics-parser/issues) (if there isn't one already).
- If your patch is going to be large it might be a good idea to get the discussion started early.
- We are happy to discuss it in an issue beforehand.
- If you could provide an iCal snippet causing the parser to behave incorrectly it is extremely useful for debugging
- Please remove all irrelevant events
1. Please follow the coding standard already present in the file you are editing _before_ committing
- Adhere to the [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standard
- Use *4 spaces* instead of tabs for indentation
- Trim all trailing whitespace and blank lines
- Use single quotes (`'`) where possible instead of double
- Use `PHP_EOL` where possible or default to `\n`
- Abide by the [1TBS](https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS_.28OTBS.29) indentation style

@ -0,0 +1 @@
github: u01jmg3

@ -0,0 +1,15 @@
The MIT License (MIT)
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,255 @@
# PHP ICS Parser
[![Latest Stable Release](https://poser.pugx.org/johngrogg/ics-parser/v/stable.png "Latest Stable Release")](https://packagist.org/packages/johngrogg/ics-parser)
[![Total Downloads](https://poser.pugx.org/johngrogg/ics-parser/downloads.png "Total Downloads")](https://packagist.org/packages/johngrogg/ics-parser)
---
## Installation
### Requirements
- PHP 5 (≥ 5.6.40)
- [Valid ICS](https://icalendar.org/validator.html) (`.ics`, `.ical`, `.ifb`) file
- [IANA](https://www.iana.org/time-zones), [Unicode CLDR](http://cldr.unicode.org/translation/timezones) or [Windows](https://support.microsoft.com/en-ca/help/973627/microsoft-time-zone-index-values) Time Zones
### Setup
- Install [Composer](https://getcomposer.org/)
- Add the following dependency to `composer.json`
- :warning: **Note with Composer the owner is `johngrogg` and not `u01jmg3`**
- To access the latest stable branch (`v3`) use the following
- To access new features you can require [`dev-master`](https://getcomposer.org/doc/articles/aliases.md#branch-alias)
```yaml
{
"require": {
"johngrogg/ics-parser": "^3"
}
}
```
## Running tests
```sh
composer test
```
## How to use
### How to instantiate the Parser
- Using the example script as a guide, [refer to this code](https://github.com/u01jmg3/ics-parser/blob/master/examples/index.php#L1-L22)
#### What will the parser return?
- Each key/value pair from the iCal file will be parsed creating an associative array for both the calendar and every event it contains.
- Also injected will be content under `dtstart_tz` and `dtend_tz` for accessing start and end dates with time zone data applied.
- Where possible [`DateTime`](https://secure.php.net/manual/en/class.datetime.php) objects are used and returned.
- :information_source: **Note the parser is limited to [relative date formats](https://www.php.net/manual/en/datetime.formats.relative.php) which can inhibit how complex recurrence rule parts are processed (e.g. `BYDAY` combined with `BYSETPOS`)**
```php
// Dump the whole calendar
var_dump($ical->cal);
// Dump every event
var_dump($ical->events());
```
- Also included are special `{property}_array` arrays which further resolve the contents of a key/value pair.
```php
// Dump a parsed event's start date
var_dump($event->dtstart_array);
// array (size=4)
// 0 =>
// array (size=1)
// 'TZID' => string 'America/Detroit' (length=15)
// 1 => string '20160409T090000' (length=15)
// 2 => int 1460192400
// 3 => string 'TZID=America/Detroit:20160409T090000' (length=36)
```
### Are you using Outlook?
Outlook has a quirk where it requires the User Agent string to be set in your request headers.
We have done this for you by injecting a default User Agent string, if one has not been specified.
If you wish to provide your own User agent string you can do so by using the `httpUserAgent` argument when creating your ICal object.
```php
$ical = new ICal($url, array('httpUserAgent' => 'A Different User Agent'));
```
---
## When Parsing an iCal Feed
Parsing [iCal/iCalendar/ICS](https://en.wikipedia.org/wiki/ICalendar) resources can pose several challenges. One challenge is that
the specification is a moving target; the original RFC has only been updated four times in ten years. The other challenge is that vendors
were both liberal (read: creative) in interpreting the specification and productive implementing proprietary extensions.
However, what impedes efficient parsing most directly are recurrence rules for events. This library parses the original
calendar into an easy to work with memory model. This requires that each recurring event is expanded or exploded. Hence,
a single event that occurs daily will generate a new event instance for each day as this parser processes the
calendar ([`$defaultSpan`](#variables) limits this). To get an idea how this is done take a look at the
[call graph](https://user-images.githubusercontent.com/624195/45904641-f3cd0a80-bded-11e8-925f-7bcee04b8575.png).
As a consequence the _entire_ calendar is parsed line-by-line, and thus loaded into memory, first. As you can imagine
large calendars tend to get huge when exploded i.e. with all their recurrence rules evaluated. This is exacerbated when
old calendars do not remove past events as they get fatter and fatter every year.
This limitation is particularly painful if you only need a window into the original calendar. It seems wasteful to parse
the entire fully exploded calendar into memory if you later are going to call the
[`eventsFromInterval()` or `eventsFromRange()`](#methods) on it.
In late 2018 [#190](https://github.com/u01jmg3/ics-parser/pull/190) added the option to drop all events outside a given
range very early in the parsing process at the cost of some precision (time zone calculations are not calculated at that point). This
massively reduces the total time for parsing a calendar. The same goes for memory consumption. The precondition is that
you know upfront that you don't care about events outside a given range.
Let's say you are only interested in events from yesterday, today and tomorrow. To compensate for the fact that the
tricky time zone transformations and calculations have not been executed yet by the time the parser has to decide whether
to keep or drop an event you can set it to filter for **+-2d** instead of +-1d. Once it is done you would then call
`eventsFromRange()` with +-1d to get precisely the events in the window you are interested in. That is what the variables
[`$filterDaysBefore` and `$filterDaysAfter`](#variables) are for.
In Q1 2019 [#213](https://github.com/u01jmg3/ics-parser/pull/213) further improved the performance by immediately
dropping _non-recurring_ events once parsed if they are outside that fuzzy window. This greatly reduces the maximum
memory consumption for large calendars. PHP by default does not allocate more than 128MB heap and would otherwise crash
with `Fatal error: Allowed memory size of 134217728 bytes exhausted`. It goes without saying that recurring events first
need to be evaluated before non-fitting events can be dropped.
---
## API
### `ICal` API
#### Variables
| Name | Configurable | Default Value | Description |
|--------------------------------|:------------------------:|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `$alarmCount` | :heavy_multiplication_x: | N/A | Tracks the number of alarms in the current iCal feed |
| `$cal` | :heavy_multiplication_x: | N/A | The parsed calendar |
| `$defaultSpan` | :ballot_box_with_check: | `2` | The value in years to use for indefinite, recurring events |
| `$defaultTimeZone` | :ballot_box_with_check: | [System default](https://secure.php.net/manual/en/function.date-default-timezone-get.php) | Enables customisation of the default time zone |
| `$defaultWeekStart` | :ballot_box_with_check: | `MO` | The two letter representation of the first day of the week |
| `$disableCharacterReplacement` | :ballot_box_with_check: | `false` | Toggles whether to disable all character replacement. Will replace curly quotes and other special characters with their standard equivalents if `false`. Can be a costly operation! |
| `$eventCount` | :heavy_multiplication_x: | N/A | Tracks the number of events in the current iCal feed |
| `$filterDaysAfter` | :ballot_box_with_check: | `null` | When set the parser will ignore all events more than roughly this many days _after_ now. To be on the safe side it is advised that you make the filter window `+/- 1` day larger than necessary. For performance reasons this filter is applied before any date and time zone calculations are done. Hence, depending the time zone settings of the parser and the calendar the cut-off date is not "calibrated". You can then use `$ical->eventsFromRange()` to precisely shrink the window. |
| `$filterDaysBefore` | :ballot_box_with_check: | `null` | When set the parser will ignore all events more than roughly this many days _before_ now. See `$filterDaysAfter` above for more details. |
| `$freeBusyCount` | :heavy_multiplication_x: | N/A | Tracks the free/busy count in the current iCal feed |
| `$httpBasicAuth` | :heavy_multiplication_x: | `array()` | Holds the username and password for HTTP basic authentication |
| `$httpUserAgent` | :ballot_box_with_check: | `null` | Holds the custom User Agent string header |
| `$httpAcceptLanguage` | :heavy_multiplication_x: | `null` | Holds the custom Accept Language request header, e.g. "en" or "de" |
| `$httpProtocolVersion` | :heavy_multiplication_x: | `null` | Holds the custom HTTP Protocol version, e.g. "1.0" or "1.1" |
| `$shouldFilterByWindow` | :heavy_multiplication_x: | `false` | `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set |
| `$skipRecurrence` | :ballot_box_with_check: | `false` | Toggles whether to skip the parsing of recurrence rules |
| `$todoCount` | :heavy_multiplication_x: | N/A | Tracks the number of todos in the current iCal feed |
| `$windowMaxTimestamp` | :heavy_multiplication_x: | `null` | If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined by this field and `$windowMinTimestamp` |
| `$windowMinTimestamp` | :heavy_multiplication_x: | `null` | If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined by this field and `$windowMaxTimestamp` |
#### Methods
| Method | Parameter(s) | Visibility | Description |
|-------------------------------------------------|-----------------------------------------------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `__construct` | `$files = false`, `$options = array()` | `public` | Creates the ICal object |
| `initFile` | `$file` | `protected` | Initialises lines from a file |
| `initLines` | `$lines` | `protected` | Initialises the parser using an array containing each line of iCal content |
| `initString` | `$string` | `protected` | Initialises lines from a string |
| `initUrl` | `$url`, `$username = null`, `$password = null`, `$userAgent = null`, `$acceptLanguage = null` | `protected` | Initialises lines from a URL. Accepts a username/password combination for HTTP basic authentication, a custom User Agent string and the accepted client language |
| `addCalendarComponentWithKeyAndValue` | `$component`, `$keyword`, `$value` | `protected` | Add one key and value pair to the `$this->cal` array |
| `calendarDescription` | - | `public` | Returns the calendar description |
| `calendarName` | - | `public` | Returns the calendar name |
| `calendarTimeZone` | `$ignoreUtc` | `public` | Returns the calendar time zone |
| `cleanCharacters` | `$data` | `protected` | Replaces curly quotes and other special characters with their standard equivalents |
| `eventsFromInterval` | `$interval` | `public` | Returns a sorted array of events following a given string |
| `eventsFromRange` | `$rangeStart = false`, `$rangeEnd = false` | `public` | Returns a sorted array of events in a given range, or an empty array if no events exist in the range |
| `events` | - | `public` | Returns an array of Events |
| `fileOrUrl` | `$filename` | `protected` | Reads an entire file or URL into an array |
| `filterValuesUsingBySetPosRRule` | `$bysetpos`, `$valueslist` | `protected` | Filters a provided values-list by applying a BYSETPOS RRule |
| `freeBusyEvents` | - | `public` | Returns an array of arrays with all free/busy events |
| `getDaysOfMonthMatchingByDayRRule` | `$bydays`, `$initialDateTime` | `protected` | Find all days of a month that match the BYDAY stanza of an RRULE |
| `getDaysOfMonthMatchingByMonthDayRRule` | `$byMonthDays`, `$initialDateTime` | `protected` | Find all days of a month that match the BYMONTHDAY stanza of an RRULE |
| `getDaysOfYearMatchingByDayRRule` | `$byDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYDAY stanza of an RRULE |
| `getDaysOfYearMatchingByMonthDayRRule` | `$byMonthDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYMONTHDAY stanza of an RRULE |
| `getDaysOfYearMatchingByWeekNoRRule` | `$byWeekNums`, `$initialDateTime` | `protected` | Find all days of a year that match the BYWEEKNO stanza of an RRULE |
| `getDaysOfYearMatchingByYearDayRRule` | `$byYearDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYYEARDAY stanza of an RRULE |
| `hasEvents` | - | `public` | Returns a boolean value whether the current calendar has events or not |
| `iCalDateToDateTime` | `$icalDate` | `public` | Returns a `DateTime` object from an iCal date time format |
| `iCalDateToUnixTimestamp` | `$icalDate` | `public` | Returns a Unix timestamp from an iCal date time format |
| `iCalDateWithTimeZone` | `$event`, `$key`, `$format = DATE_TIME_FORMAT` | `public` | Returns a date adapted to the calendar time zone depending on the event `TZID` |
| `doesEventStartOutsideWindow` | `$event` | `protected` | Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp` |
| `isFileOrUrl` | `$filename` | `protected` | Checks if a filename exists as a file or URL |
| `isOutOfRange` | `$calendarDate`, `$minTimestamp`, `$maxTimestamp` | `protected` | Determines whether a valid iCalendar date is within a given range |
| `isValidCldrTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid CLDR time zone |
| `isValidDate` | `$value` | `public` | Checks if a date string is a valid date |
| `isValidIanaTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid IANA time zone |
| `isValidWindowsTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a recognised Windows (non-CLDR) time zone |
| `isValidTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is valid (IANA, CLDR, or Windows) |
| `keyValueFromString` | `$text` | `public` | Gets the key value pair from an iCal string |
| `parseLine` | `$line` | `protected` | Parses a line from an iCal file into an array of tokens |
| `mb_chr` | `$code` | `protected` | Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()` |
| `escapeParamText` | `$candidateText` | `protected` | Places double-quotes around texts that have characters not permitted in parameter-texts, but are permitted in quoted-texts. |
| `parseDuration` | `$date`, `$duration`, `$format = 'U'` | `protected` | Parses a duration and applies it to a date |
| `parseExdates` | `$event` | `public` | Parses a list of excluded dates to be applied to an Event |
| `processDateConversions` | - | `protected` | Processes date conversions using the time zone |
| `processEvents` | - | `protected` | Performs admin tasks on all events as read from the iCal file |
| `processRecurrences` | - | `protected` | Processes recurrence rules |
| `reduceEventsToMinMaxRange` | | `protected` | Reduces the number of events to the defined minimum and maximum range |
| `removeLastEventIfOutsideWindowAndNonRecurring` | | `protected` | Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by `$windowMinTimestamp` / `$windowMaxTimestamp` |
| `removeUnprintableChars` | `$data` | `protected` | Removes unprintable ASCII and UTF-8 characters |
| `resolveIndicesOfRange` | `$indexes`, `$limit` | `protected` | Resolves values from indices of the range 1 -> `$limit` |
| `sortEventsWithOrder` | `$events`, `$sortOrder = SORT_ASC` | `public` | Sorts events based on a given sort order |
| `timeZoneStringToDateTimeZone` | `$timeZoneString` | `public` | Returns a `DateTimeZone` object based on a string containing a time zone name. |
| `unfold` | `$lines` | `protected` | Unfolds an iCal file in preparation for parsing |
#### Constants
| Name | Description |
|---------------------------|-----------------------------------------------|
| `DATE_TIME_FORMAT_PRETTY` | Default pretty date time format to use |
| `DATE_TIME_FORMAT` | Default date time format to use |
| `ICAL_DATE_TIME_TEMPLATE` | String template to generate an iCal date time |
| `ISO_8601_WEEK_START` | First day of the week, as defined by ISO-8601 |
| `RECURRENCE_EVENT` | Used to isolate generated recurrence events |
| `SECONDS_IN_A_WEEK` | The number of seconds in a week |
| `TIME_FORMAT` | Default time format to use |
| `TIME_ZONE_UTC` | UTC time zone string |
| `UNIX_FORMAT` | Unix timestamp date format |
| `UNIX_MIN_YEAR` | The year Unix time began |
---
### `Event` API (extends `ICal` API)
#### Methods
| Method | Parameter(s) | Visibility | Description |
|---------------|---------------------------------------------|-------------|---------------------------------------------------------------------|
| `__construct` | `$data = array()` | `public` | Creates the Event object |
| `prepareData` | `$value` | `protected` | Prepares the data for output |
| `printData` | `$html = HTML_TEMPLATE` | `public` | Returns Event data excluding anything blank within an HTML template |
| `snakeCase` | `$input`, `$glue = '_'`, `$separator = '-'` | `protected` | Converts the given input to snake_case |
#### Constants
| Name | Description |
|-----------------|-----------------------------------------------------|
| `HTML_TEMPLATE` | String template to use when pretty printing content |
---
## Credits
- [Jonathan Goode](https://github.com/u01jmg3) (programming, bug fixing, codebase enhancement, coding standard adoption)
- [s0600204](https://github.com/s0600204) (major enhancements to RRULE support, many bug fixes and other contributions)
---
## Tools for Testing
- [iCal Validator](https://icalendar.org/validator.html)
- [Recurrence Rule Tester](https://jakubroztocil.github.io/rrule/)
- [Unix Timestamp Converter](https://www.unixtimestamp.com)

@ -0,0 +1,49 @@
{
"name": "johngrogg/ics-parser",
"description": "ICS Parser",
"homepage": "https://github.com/u01jmg3/ics-parser",
"keywords": [
"ical",
"ical-parser",
"icalendar",
"ics",
"ics-parser",
"ifb"
],
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Jonathan Goode",
"role": "Developer/Owner"
},
{
"name": "John Grogg",
"email": "john.grogg@gmail.com",
"role": "Developer/Prior Owner"
}
],
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/u01jmg3"
}
],
"require": {
"php": ">=5.6.40",
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "^5|^9|^10"
},
"autoload": {
"psr-0": {
"ICal": "src/"
}
},
"scripts": {
"test": [
"phpunit --colors=always"
]
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
use PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\SpaceAfterNotSniff;
use PHP_CodeSniffer\Standards\Squiz\Sniffs\Classes\SelfMemberReferenceSniff;
use PhpCsFixer\Fixer\Alias\NoAliasFunctionsFixer;
use PhpCsFixer\Fixer\Alias\NoMixedEchoPrintFixer;
use PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer;
use PhpCsFixer\Fixer\ArrayNotation\NoMultilineWhitespaceAroundDoubleArrowFixer;
use PhpCsFixer\Fixer\ArrayNotation\NormalizeIndexBraceFixer;
use PhpCsFixer\Fixer\ArrayNotation\TrimArraySpacesFixer;
use PhpCsFixer\Fixer\Basic\EncodingFixer;
use PhpCsFixer\Fixer\Basic\NoTrailingCommaInSinglelineFixer;
use PhpCsFixer\Fixer\Casing\ConstantCaseFixer;
use PhpCsFixer\Fixer\Casing\LowercaseKeywordsFixer;
use PhpCsFixer\Fixer\Casing\LowercaseStaticReferenceFixer;
use PhpCsFixer\Fixer\Casing\MagicConstantCasingFixer;
use PhpCsFixer\Fixer\Casing\MagicMethodCasingFixer;
use PhpCsFixer\Fixer\Casing\NativeFunctionCasingFixer;
use PhpCsFixer\Fixer\Casing\NativeFunctionTypeDeclarationCasingFixer;
use PhpCsFixer\Fixer\CastNotation\CastSpacesFixer;
use PhpCsFixer\Fixer\CastNotation\NoShortBoolCastFixer;
use PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer;
use PhpCsFixer\Fixer\ClassNotation\SingleClassElementPerStatementFixer;
use PhpCsFixer\Fixer\Comment\NoTrailingWhitespaceInCommentFixer;
use PhpCsFixer\Fixer\Comment\SingleLineCommentStyleFixer;
use PhpCsFixer\Fixer\ControlStructure\ElseifFixer;
use PhpCsFixer\Fixer\ControlStructure\IncludeFixer;
use PhpCsFixer\Fixer\ControlStructure\NoUnneededControlParenthesesFixer;
use PhpCsFixer\Fixer\ControlStructure\NoUnneededCurlyBracesFixer;
use PhpCsFixer\Fixer\ControlStructure\SwitchCaseSemicolonToColonFixer;
use PhpCsFixer\Fixer\ControlStructure\SwitchCaseSpaceFixer;
use PhpCsFixer\Fixer\ControlStructure\TrailingCommaInMultilineFixer;
use PhpCsFixer\Fixer\ControlStructure\YodaStyleFixer;
use PhpCsFixer\Fixer\FunctionNotation\FunctionDeclarationFixer;
use PhpCsFixer\Fixer\FunctionNotation\LambdaNotUsedImportFixer;
use PhpCsFixer\Fixer\FunctionNotation\MethodArgumentSpaceFixer;
use PhpCsFixer\Fixer\FunctionNotation\NoSpacesAfterFunctionNameFixer;
use PhpCsFixer\Fixer\FunctionNotation\NoUnreachableDefaultArgumentValueFixer;
use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer;
use PhpCsFixer\Fixer\Import\SingleImportPerStatementFixer;
use PhpCsFixer\Fixer\Import\SingleLineAfterImportsFixer;
use PhpCsFixer\Fixer\ListNotation\ListSyntaxFixer;
use PhpCsFixer\Fixer\NamespaceNotation\BlankLinesBeforeNamespaceFixer;
use PhpCsFixer\Fixer\NamespaceNotation\NoLeadingNamespaceWhitespaceFixer;
use PhpCsFixer\Fixer\Operator\ObjectOperatorWithoutWhitespaceFixer;
use PhpCsFixer\Fixer\Operator\StandardizeNotEqualsFixer;
use PhpCsFixer\Fixer\Phpdoc\NoEmptyPhpdocFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocIndentFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocInlineTagNormalizerFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocNoAccessFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocNoPackageFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocNoUselessInheritdocFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocParamOrderFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocSingleLineVarSpacingFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocTrimFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocTypesFixer;
use PhpCsFixer\Fixer\PhpTag\FullOpeningTagFixer;
use PhpCsFixer\Fixer\PhpTag\NoClosingTagFixer;
use PhpCsFixer\Fixer\ReturnNotation\NoUselessReturnFixer;
use PhpCsFixer\Fixer\Semicolon\MultilineWhitespaceBeforeSemicolonsFixer;
use PhpCsFixer\Fixer\Semicolon\NoEmptyStatementFixer;
use PhpCsFixer\Fixer\Semicolon\SpaceAfterSemicolonFixer;
use PhpCsFixer\Fixer\StringNotation\HeredocToNowdocFixer;
use PhpCsFixer\Fixer\StringNotation\SingleQuoteFixer;
use PhpCsFixer\Fixer\Whitespace\BlankLineBeforeStatementFixer;
use PhpCsFixer\Fixer\Whitespace\CompactNullableTypehintFixer;
use PhpCsFixer\Fixer\Whitespace\LineEndingFixer;
use PhpCsFixer\Fixer\Whitespace\NoExtraBlankLinesFixer;
use PhpCsFixer\Fixer\Whitespace\NoSpacesInsideParenthesisFixer;
use PhpCsFixer\Fixer\Whitespace\NoWhitespaceInBlankLineFixer;
use PhpCsFixer\Fixer\Whitespace\SingleBlankLineAtEofFixer;
use PhpCsFixer\Fixer\Whitespace\TypeDeclarationSpacesFixer;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Variables\UnusedVariableSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
// ecs check --fix .
return static function (ECSConfig $ecsConfig): void {
$ecsConfig->disableParallel();
// https://github.com/easy-coding-standard/easy-coding-standard/blob/main/config/set/psr12.php
$ecsConfig->import(SetList::PSR_12);
$ecsConfig->lineEnding("\n");
$ecsConfig->skip(array(
// Fixers
'PhpCsFixer\Fixer\Whitespace\StatementIndentationFixer' => array('examples/index.php'),
'PhpCsFixer\Fixer\Basic\BracesFixer' => null,
'PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer' => null,
'PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer' => null,
'PhpCsFixer\Fixer\Phpdoc\PhpdocScalarFixer' => null,
'PhpCsFixer\Fixer\Phpdoc\PhpdocSummaryFixer' => null,
'PhpCsFixer\Fixer\Phpdoc\PhpdocVarWithoutNameFixer' => null,
'PhpCsFixer\Fixer\ReturnNotation\SimplifiedNullReturnFixer' => null,
// Requires PHP 7.1 and above
'PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer' => null,
));
$ecsConfig->ruleWithConfiguration(SpaceAfterNotSniff::class, array('spacing' => 0));
$ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, array('syntax' => 'long'));
$ecsConfig->ruleWithConfiguration(
YodaStyleFixer::class,
array(
'equal' => false,
'identical' => false,
'less_and_greater' => false,
)
);
$ecsConfig->ruleWithConfiguration(ListSyntaxFixer::class, array('syntax' => 'long')); // PHP 5.6
$ecsConfig->ruleWithConfiguration(
BlankLineBeforeStatementFixer::class,
array(
'statements' => array(
'continue',
'declare',
'return',
'throw',
'try',
),
)
);
$ecsConfig->rules(
array(
AlphabeticallySortedUsesSniff::class,
UnusedVariableSniff::class,
SelfMemberReferenceSniff::class,
BlankLinesBeforeNamespaceFixer::class,
CastSpacesFixer::class,
ClassDefinitionFixer::class,
CompactNullableTypehintFixer::class,
ConstantCaseFixer::class,
ElseifFixer::class,
EncodingFixer::class,
FullOpeningTagFixer::class,
FunctionDeclarationFixer::class,
HeredocToNowdocFixer::class,
IncludeFixer::class,
LambdaNotUsedImportFixer::class,
LineEndingFixer::class,
LowercaseKeywordsFixer::class,
LowercaseStaticReferenceFixer::class,
MagicConstantCasingFixer::class,
MagicMethodCasingFixer::class,
MethodArgumentSpaceFixer::class,
MultilineWhitespaceBeforeSemicolonsFixer::class,
NativeFunctionCasingFixer::class,
NativeFunctionTypeDeclarationCasingFixer::class,
NoAliasFunctionsFixer::class,
NoClosingTagFixer::class,
NoEmptyPhpdocFixer::class,
NoEmptyStatementFixer::class,
NoExtraBlankLinesFixer::class,
NoLeadingNamespaceWhitespaceFixer::class,
NoMixedEchoPrintFixer::class,
NoMultilineWhitespaceAroundDoubleArrowFixer::class,
NoShortBoolCastFixer::class,
NoSpacesAfterFunctionNameFixer::class,
NoSpacesInsideParenthesisFixer::class,
NoTrailingCommaInSinglelineFixer::class,
NoTrailingWhitespaceInCommentFixer::class,
NoUnneededControlParenthesesFixer::class,
NoUnneededCurlyBracesFixer::class,
NoUnreachableDefaultArgumentValueFixer::class,
NoUnusedImportsFixer::class,
NoUselessReturnFixer::class,
NoWhitespaceInBlankLineFixer::class,
NormalizeIndexBraceFixer::class,
ObjectOperatorWithoutWhitespaceFixer::class,
PhpdocIndentFixer::class,
PhpdocInlineTagNormalizerFixer::class,
PhpdocNoAccessFixer::class,
PhpdocNoPackageFixer::class,
PhpdocNoUselessInheritdocFixer::class,
PhpdocParamOrderFixer::class,
PhpdocSingleLineVarSpacingFixer::class,
PhpdocToCommentFixer::class,
PhpdocTrimFixer::class,
PhpdocTypesFixer::class,
SingleBlankLineAtEofFixer::class,
SingleClassElementPerStatementFixer::class,
SingleImportPerStatementFixer::class,
SingleLineAfterImportsFixer::class,
SingleLineCommentStyleFixer::class,
SingleQuoteFixer::class,
SpaceAfterSemicolonFixer::class,
StandardizeNotEqualsFixer::class,
SwitchCaseSemicolonToColonFixer::class,
SwitchCaseSpaceFixer::class,
TrailingCommaInMultilineFixer::class,
TrimArraySpacesFixer::class,
TypeDeclarationSpacesFixer::class,
)
);
};

@ -0,0 +1,338 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Testkalender
X-WR-TIMEZONE:UTC
X-WR-CALDESC:Nur zum testen vom Google Kalender
BEGIN:VFREEBUSY
UID:f06ff6b3564b2f696bf42d393f8dea59
ORGANIZER:MAILTO:jane_smith@host1.com
DTSTAMP:20170316T204607Z
DTSTART:20170213T204607Z
DTEND:20180517T204607Z
URL:https://www.host.com/calendar/busytime/jsmith.ifb
FREEBUSY;FBTYPE=BUSY:20170623T070000Z/20170223T110000Z
FREEBUSY;FBTYPE=BUSY:20170624T131500Z/20170316T151500Z
FREEBUSY;FBTYPE=BUSY:20170715T131500Z/20170416T150000Z
FREEBUSY;FBTYPE=BUSY:20170716T131500Z/20170516T100500Z
END:VFREEBUSY
BEGIN:VEVENT
DTSTART:20171032T000000
DTEND:20171101T2300
DESCRIPTION:Invalid date - parser will skip the event
SUMMARY:Invalid date - parser will skip the event
DTSTAMP:20170406T063924
LOCATION:
UID:f81b0b41a2e138ae0903daee0a966e1e
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE;TZID=America/Los_Angeles:19410512
DTEND;VALUE=DATE;TZID=America/Los_Angeles:19410512
DTSTAMP;TZID=America/Los_Angeles:19410512T195741Z
UID:dh3fki5du0opa7cs5n5s87ca02@google.com
CREATED:20380101T141901Z
DESCRIPTION;LANGUAGE=en-gb:
LAST-MODIFIED:20380101T141901Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:Before 1970-Test: Konrad Zuse invents the Z3, the "first
digital Computer"
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20380201
DTEND;VALUE=DATE:20380202
DTSTAMP;TZID="GMT Standard Time":20380101T195741Z
UID:dh3fki5du0opa7cs5n5s87ca01@google.com
CREATED:20380101T141901Z
DESCRIPTION;LANGUAGE=en-gb:
LAST-MODIFIED:20380101T141901Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:Year 2038 problem test
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART:20160105T090000Z
DTEND:20160107T173000Z
DTSTAMP;TZID="Greenwich Mean Time:Dublin; Edinburgh; Lisbon; London":20110121T195741Z
UID:15lc1nvupht8dtfiptenljoiv4@google.com
CREATED:20110121T195616Z
DESCRIPTION;LANGUAGE=en-gb:This is a short description\nwith a new line. Some "special" 's
igns' may be interesting\, too.
&nbsp; And a non-breaking space.
LAST-MODIFIED:20150409T150000Z
LOCATION:Kansas
SEQUENCE:2
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:My Holidays
TRANSP:TRANSPARENT
ORGANIZER;CN="My Name":mailto:my.name@mydomain.com
END:VEVENT
BEGIN:VEVENT
ATTENDEE;CN="Page, Larry <l.page@google.com> (l.page@google.com)";ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:l.page@google.com
ATTENDEE;CN="Brin, Sergey <s.brin@google.com> (s.brin@google.com)";ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:s.brin@google.com
DTSTART;VALUE=DATE:20160112
DTEND;VALUE=DATE:20160116
DTSTAMP;TZID="GMT Standard Time":20110121T195741Z
UID:1koigufm110c5hnq6ln57murd4@google.com
CREATED:20110119T142901Z
DESCRIPTION;LANGUAGE=en-gb:Project xyz Review Meeting Minutes\n
Agenda\n1. Review of project version 1.0 requirements.\n2.
Definition
of project processes.\n3. Review of project schedule.\n
Participants: John Smith, Jane Doe, Jim Dandy\n-It was
decided that the requirements need to be signed off by
product marketing.\n-Project processes were accepted.\n
-Project schedule needs to account for scheduled holidays
and employee vacation time. Check with HR for specific
dates.\n-New schedule will be distributed by Friday.\n-
Next weeks meeting is cancelled. No meeting until 3/23.
LAST-MODIFIED:20150409T150000Z
LOCATION:
SEQUENCE:2
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:Test 2
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20160119
DTEND;VALUE=DATE:20160120
DTSTAMP;TZID="GMT Standard Time":20110121T195741Z
UID:rq8jng4jgq0m1lvpj8486fttu0@google.com
CREATED:20110119T141904Z
DESCRIPTION;LANGUAGE=en-gb:
LAST-MODIFIED:20150409T150000Z
LOCATION:
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:DST Change
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20160119
DTEND;VALUE=DATE:20160120
DTSTAMP;TZID="GMT Standard Time":20110121T195741Z
UID:dh3fki5du0opa7cs5n5s87ca00@google.com
CREATED:20110119T141901Z
DESCRIPTION;LANGUAGE=en-gb:
LAST-MODIFIED:20150409T150000Z
LOCATION:
RRULE:FREQ=WEEKLY;COUNT=5;INTERVAL=2;BYDAY=TU
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:Test 1
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
SUMMARY:Duration Test
DTSTART:20160425T150000Z
DTSTAMP:20160424T150000Z
DURATION:PT1H15M5S
RRULE:FREQ=DAILY;COUNT=2
UID:calendar-62-e7c39bf02382917349672271dd781c89
END:VEVENT
BEGIN:VEVENT
SUMMARY:BYMONTHDAY Test
DTSTART:20160922T130000Z
DTEND:20160922T150000Z
DTSTAMP:20160921T130000Z
RRULE:FREQ=MONTHLY;UNTIL=20170923T000000Z;INTERVAL=1;BYMONTHDAY=23
UID:33844fe8df15fbfc13c97fc41c0c4b00392c6870@google.com
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=Europe/Paris:20160921T080000
DTEND;TZID=Europe/Paris:20160921T090000
RRULE:FREQ=WEEKLY;BYDAY=WE
DTSTAMP:20161117T165045Z
UID:884bc8350185031337d9ec49d2e7e101dd5ae5fb@google.com
CREATED:20160920T133918Z
DESCRIPTION:
LAST-MODIFIED:20160920T133923Z
LOCATION:
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Paris Timezone Test
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
DTSTART:20160215T080000Z
DTEND:20160515T090000Z
DTSTAMP:20161121T113027Z
CREATED:20161121T113027Z
UID:65323c541a30dd1f180e2bbfa2724995
DESCRIPTION:
LAST-MODIFIED:20161121T113027Z
LOCATION:
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Long event covering the range from example with special chars:
ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÕÖÒÓÔØÙÚÛÜÝÞß
àáâãäåæçèéêėëìíîïðñòóôõöøùúûüūýþÿž
“ ” „ ‟ — …
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
CLASS:PUBLIC
CREATED:20160706T161104Z
DTEND;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160409T110000
DTSTAMP:20160706T150005Z
DTSTART;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160409T090000
EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)":
20160528T090000,
20160625T090000
LAST-MODIFIED:20160707T182011Z
EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160709T090000
EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160723T090000
LOCATION:Sanctuary
PRIORITY:5
RRULE:FREQ=WEEKLY;COUNT=15;BYDAY=SA
SEQUENCE:0
SUMMARY:Microsoft Unicode CLDR EXDATE Test
TRANSP:OPAQUE
UID:040000008200E00074C5B7101A82E0080000000020F6512D0B48CF0100000000000000001000000058BFB8CBB85D504CB99FBA637BCFD6BF
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
X-MICROSOFT-CDO-IMPORTANCE:1
X-MICROSOFT-DISALLOW-COUNTER:FALSE
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20170118
DTEND;VALUE=DATE:20170118
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=MONTHLY;BYSETPOS=3;BYDAY=WE;COUNT=5
UID:4dnsuc3nknin15kv25cn7ridss@google.com
CREATED:20170119T142059Z
DESCRIPTION;LANGUAGE=en-gb:BYDAY Test 1
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYDAY Test 1
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20190101
DTEND;VALUE=DATE:20190101
DTSTAMP;TZID="GMT Standard Time":20190101T195741Z
RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1
UID:4dnsuc3nknin15kv25cn7ridssy@google.com
CREATED:20190101T142059Z
DESCRIPTION;LANGUAGE=en-gb:BYSETPOS First weekday of every month
LAST-MODIFIED:20190101T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYSETPOS First weekday of every month
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20190131
DTEND;VALUE=DATE:20190131
DTSTAMP;TZID="GMT Standard Time":20190121T195741Z
RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1
UID:4dnsuc3nknin15kv25cn7ridssx@google.com
CREATED:20190119T142059Z
DESCRIPTION;LANGUAGE=en-gb:BYSETPOS Last day of every month
LAST-MODIFIED:20190409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYSETPOS Last day of every month
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20170301
DTEND;VALUE=DATE:20170301
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=WE
UID:h6f7sdjbpt47v3dkral8lnsgcc@google.com
CREATED:20170119T142040Z
DESCRIPTION;LANGUAGE=en-gb:BYDAY Test 2
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYDAY Test 2
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20170111
DTEND;VALUE=DATE:20170111
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=5;BYMONTH=1,2,3
UID:f50e8b89a4a3b0070e0b687d03@google.com
CREATED:20170119T142040Z
DESCRIPTION;LANGUAGE=en-gb:BYMONTH Multiple Test 1
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYMONTH Multiple Test 1
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20170405
DTEND;VALUE=DATE:20170405
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=YEARLY;BYMONTH=4,5,6;BYDAY=WE;COUNT=5
UID:675f06aa795665ae50904ebf0e@google.com
CREATED:20170119T142040Z
DESCRIPTION;LANGUAGE=en-gb:BYMONTH Multiple Test 2
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYMONTH Multiple Test 2
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT30M
ACTION:DISPLAY
DESCRIPTION:Buzz buzz
END:VALARM
DTSTART;VALUE=DATE;TZID=Germany/Berlin:20170123
DTEND;VALUE=DATE;TZID=Germany/Berlin:20170123
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=MONTHLY;BYDAY=-2MO;COUNT=5
EXDATE;VALUE=DATE:20171020
UID:d287b7ec808fcf084983f10837@google.com
CREATED:20170119T142040Z
DESCRIPTION;LANGUAGE=en-gb:Negative BYDAY
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:Negative BYDAY
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=Australia/Sydney:20170813T190000
DTEND;TZID=Australia/Sydney:20170813T213000
RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=2SU;COUNT=2
DTSTAMP:20170809T114431Z
UID:testuid@google.com
CREATED:20170802T135539Z
DESCRIPTION:
LAST-MODIFIED:20170802T135935Z
LOCATION:
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Parent Recurrence Event
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=Australia/Sydney:20170813T190000
DTEND;TZID=Australia/Sydney:20170813T213000
DTSTAMP:20170809T114431Z
UID:testuid@google.com
RECURRENCE-ID;TZID=Australia/Sydney:20170813T190000
CREATED:20170802T135539Z
DESCRIPTION:
LAST-MODIFIED:20170809T105604Z
LOCATION:Melbourne VIC\, Australia
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Override Parent Recurrence Event
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR

@ -0,0 +1,175 @@
<?php
// phpcs:disable Generic.Arrays.DisallowLongArraySyntax
require_once '../vendor/autoload.php';
use ICal\ICal;
try {
$ical = new ICal('ICal.ics', array(
'defaultSpan' => 2, // Default value
'defaultTimeZone' => 'UTC',
'defaultWeekStart' => 'MO', // Default value
'disableCharacterReplacement' => false, // Default value
'filterDaysAfter' => null, // Default value
'filterDaysBefore' => null, // Default value
'httpUserAgent' => null, // Default value
'skipRecurrence' => false, // Default value
));
// $ical->initFile('ICal.ics');
// $ical->initUrl('https://raw.githubusercontent.com/u01jmg3/ics-parser/master/examples/ICal.ics', $username = null, $password = null, $userAgent = null);
} catch (\Exception $e) {
die($e);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<title>PHP ICS Parser example</title>
<style>body { background-color: #eee }</style>
</head>
<body>
<div class="container-fluid">
<h4 class="mt-3 mb-2">PHP ICS Parser example</h3>
<ul class="list-group">
<li class="list-group-item">
The number of events
<span class="badge rounded-pill bg-secondary float-end"><?php echo $ical->eventCount ?></span>
</li>
<li class="list-group-item">
The number of free/busy time slots
<span class="badge rounded-pill bg-secondary float-end"><?php echo $ical->freeBusyCount ?></span>
</li>
<li class="list-group-item">
The number of todos
<span class="badge rounded-pill bg-secondary float-end"><?php echo $ical->todoCount ?></span>
</li>
<li class="list-group-item">
The number of alarms
<span class="badge rounded-pill bg-secondary float-end"><?php echo $ical->alarmCount ?></span>
</li>
</ul>
<?php
$showExample = array(
'interval' => true,
'range' => true,
'all' => true,
);
?>
<?php
if ($showExample['interval']) {
$events = $ical->eventsFromInterval('1 week');
if ($events) {
echo '<h4 class="mt-3 mb-2">Events in the next 7 days:</h4>';
}
$count = 1;
?>
<div class="row">
<?php
foreach ($events as $event) : ?>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-body">
<h4 class="mt-3 mb-2"><?php
$dtstart = $ical->iCalDateToDateTime($event->dtstart_array[3]);
echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
?></h3>
<?php echo $event->printData() ?>
</div>
</div>
</div>
<?php
if ($count > 1 && $count % 3 === 0) {
echo '</div><div class="row">';
}
$count++;
?>
<?php
endforeach
?>
</div>
<?php } ?>
<?php
if ($showExample['range']) {
$events = $ical->eventsFromRange('2017-03-01 12:00:00', '2017-04-31 17:00:00');
if ($events) {
echo '<h4 class="mt-3 mb-2">Events March through April:</h4>';
}
$count = 1;
?>
<div class="row">
<?php
foreach ($events as $event) : ?>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-body">
<h4 class="mt-3 mb-2"><?php
$dtstart = $ical->iCalDateToDateTime($event->dtstart_array[3]);
echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
?></h3>
<?php echo $event->printData() ?>
</div>
</div>
</div>
<?php
if ($count > 1 && $count % 3 === 0) {
echo '</div><div class="row">';
}
$count++;
?>
<?php
endforeach
?>
</div>
<?php } ?>
<?php
if ($showExample['all']) {
$events = $ical->sortEventsWithOrder($ical->events());
if ($events) {
echo '<h4 class="mt-3 mb-2">All Events:</h4>';
}
?>
<div class="row">
<?php
$count = 1;
foreach ($events as $event) : ?>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-body">
<h4 class="mt-3 mb-2"><?php
$dtstart = $ical->iCalDateToDateTime($event->dtstart_array[3]);
echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
?></h3>
<?php echo $event->printData() ?>
</div>
</div>
</div>
<?php
if ($count > 1 && $count % 3 === 0) {
echo '</div><div class="row">';
}
$count++;
?>
<?php
endforeach
?>
</div>
<?php } ?>
</div>
</body>
</html>

@ -0,0 +1,7 @@
parameters:
paths:
- src
level: 6
checkMissingIterableValueType: false

@ -0,0 +1,7 @@
<phpunit>
<testsuites>
<testsuite name="ics-parser">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersion;
use Rector\Php53\Rector\Ternary\TernaryToElvisRector;
use Rector\Set\ValueObject\SetList;
// rector process src
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->disableParallel();
$rectorConfig->importShortClasses(false);
$rectorConfig->phpVersion(PhpVersion::PHP_56);
$rectorConfig->skip(
array(
Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector::class,
Rector\CodeQuality\Rector\Concat\JoinStringConcatRector::class,
Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector::class,
Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector::class,
Rector\CodeQuality\Rector\FuncCall\InlineIsAInstanceOfRector::class,
Rector\CodeQuality\Rector\FuncCall\IntvalToTypeCastRector::class,
Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector::class,
Rector\CodeQuality\Rector\Identical\BooleanNotIdenticalToNotIdenticalRector::class,
Rector\CodeQuality\Rector\Identical\SimplifyBoolIdenticalTrueRector::class,
Rector\CodeQuality\Rector\If_\CombineIfRector::class,
Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector::class,
Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector::class,
Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector::class,
Rector\CodeQuality\Rector\Isset_\IssetOnPropertyObjectToPropertyExistsRector::class,
Rector\CodingStyle\Rector\ClassMethod\UnSpreadOperatorRector::class,
Rector\CodingStyle\Rector\Closure\StaticClosureRector::class,
Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector::class,
Rector\CodingStyle\Rector\PostInc\PostIncDecToPreIncDecRector::class,
Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector::class,
Rector\CodingStyle\Rector\String_\SymplifyQuoteEscapeRector::class,
Rector\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector::class,
Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector::class,
Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector::class,
Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector::class,
Rector\DeadCode\Rector\StaticCall\RemoveParentCallWithoutParentRector::class,
Rector\Php70\Rector\MethodCall\ThisCallOnStaticMethodToStaticCallRector::class,
Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector::class,
Rector\Php71\Rector\FuncCall\CountOnNullRector::class,
Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector::class,
Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector::class,
Rector\Transform\Rector\String_\StringToClassConstantRector::class,
// PHP 5.6 incompatible
Rector\CodeQuality\Rector\Ternary\ArrayKeyExistsTernaryThenValueToCoalescingRector::class, // PHP 7
Rector\Php70\Rector\If_\IfToSpaceshipRector::class,
Rector\Php70\Rector\Ternary\TernaryToSpaceshipRector::class,
Rector\Php71\Rector\BooleanOr\IsIterableRector::class,
Rector\Php71\Rector\List_\ListToArrayDestructRector::class,
Rector\Php71\Rector\TryCatch\MultiExceptionCatchRector::class,
Rector\Php73\Rector\FuncCall\ArrayKeyFirstLastRector::class,
Rector\Php73\Rector\BooleanOr\IsCountableRector::class,
Rector\Php74\Rector\Assign\NullCoalescingOperatorRector::class,
Rector\Php74\Rector\FuncCall\ArraySpreadInsteadOfArrayMergeRector::class,
Rector\Php74\Rector\LNumber\AddLiteralSeparatorToNumberRector::class,
Rector\Php74\Rector\StaticCall\ExportToReflectionFunctionRector::class,
Rector\CodingStyle\Rector\ClassConst\RemoveFinalFromConstRector::class, // PHP 8
)
);
$rectorConfig->sets(
array(
SetList::CODE_QUALITY,
SetList::CODING_STYLE,
SetList::DEAD_CODE,
SetList::PHP_70,
SetList::PHP_71,
SetList::PHP_72,
SetList::PHP_73,
SetList::PHP_74,
SetList::PHP_80,
SetList::PHP_81,
SetList::PHP_82,
)
);
$rectorConfig->rule(TernaryToElvisRector::class);
};

@ -0,0 +1,259 @@
<?php
namespace ICal;
class Event
{
// phpcs:disable Generic.Arrays.DisallowLongArraySyntax
const HTML_TEMPLATE = '<p>%s: %s</p>';
/**
* https://www.kanzaki.com/docs/ical/summary.html
*
* @var string
*/
public $summary;
/**
* https://www.kanzaki.com/docs/ical/dtstart.html
*
* @var string
*/
public $dtstart;
/**
* https://www.kanzaki.com/docs/ical/dtend.html
*
* @var string
*/
public $dtend;
/**
* https://www.kanzaki.com/docs/ical/duration.html
*
* @var string
*/
public $duration;
/**
* https://www.kanzaki.com/docs/ical/dtstamp.html
*
* @var string
*/
public $dtstamp;
/**
* When the event starts, represented as a timezone-adjusted string
*
* @var string
*/
public $dtstart_tz;
/**
* When the event ends, represented as a timezone-adjusted string
*
* @var string
*/
public $dtend_tz;
/**
* https://www.kanzaki.com/docs/ical/uid.html
*
* @var string
*/
public $uid;
/**
* https://www.kanzaki.com/docs/ical/created.html
*
* @var string
*/
public $created;
/**
* https://www.kanzaki.com/docs/ical/lastModified.html
*
* @var string
*/
public $last_modified;
/**
* https://www.kanzaki.com/docs/ical/description.html
*
* @var string
*/
public $description;
/**
* https://www.kanzaki.com/docs/ical/location.html
*
* @var string
*/
public $location;
/**
* https://www.kanzaki.com/docs/ical/sequence.html
*
* @var string
*/
public $sequence;
/**
* https://www.kanzaki.com/docs/ical/status.html
*
* @var string
*/
public $status;
/**
* https://www.kanzaki.com/docs/ical/transp.html
*
* @var string
*/
public $transp;
/**
* https://www.kanzaki.com/docs/ical/organizer.html
*
* @var string
*/
public $organizer;
/**
* https://www.kanzaki.com/docs/ical/attendee.html
*
* @var string
*/
public $attendee;
/**
* Manage additional properties
*
* @var array<string, mixed>
*/
private $additionalProperties = array();
/**
* Creates the Event object
*
* @param array $data
* @return void
*/
public function __construct(array $data = array())
{
foreach ($data as $key => $value) {
$variable = self::snakeCase($key);
if (property_exists($this, $variable)) {
$this->{$variable} = $this->prepareData($value);
} else {
$this->additionalProperties[$variable] = $this->prepareData($value);
}
}
}
/**
* Magic getter method
*
* @param string $additionalPropertyName
* @return mixed
*/
public function __get($additionalPropertyName)
{
if (array_key_exists($additionalPropertyName, $this->additionalProperties)) {
return $this->additionalProperties[$additionalPropertyName];
}
return null;
}
/**
* Magic isset method
*
* @param string $name
* @return boolean
*/
public function __isset($name)
{
return is_null($this->$name) === false;
}
/**
* Prepares the data for output
*
* @param mixed $value
* @return mixed
*/
protected function prepareData($value)
{
if (is_string($value)) {
return stripslashes(trim(str_replace('\n', "\n", $value)));
}
if (is_array($value)) {
return array_map(function ($value) {
return $this->prepareData($value);
}, $value);
}
return $value;
}
/**
* Returns Event data excluding anything blank
* within an HTML template
*
* @param string $html HTML template to use
* @return string
*/
public function printData($html = self::HTML_TEMPLATE)
{
$data = array(
'SUMMARY' => $this->summary,
'DTSTART' => $this->dtstart,
'DTEND' => $this->dtend,
'DTSTART_TZ' => $this->dtstart_tz,
'DTEND_TZ' => $this->dtend_tz,
'DURATION' => $this->duration,
'DTSTAMP' => $this->dtstamp,
'UID' => $this->uid,
'CREATED' => $this->created,
'LAST-MODIFIED' => $this->last_modified,
'DESCRIPTION' => $this->description,
'LOCATION' => $this->location,
'SEQUENCE' => $this->sequence,
'STATUS' => $this->status,
'TRANSP' => $this->transp,
'ORGANISER' => $this->organizer,
'ATTENDEE(S)' => $this->attendee,
);
// Remove any blank values
$data = array_filter($data);
$output = '';
foreach ($data as $key => $value) {
$output .= sprintf($html, $key, $value);
}
return $output;
}
/**
* Converts the given input to snake_case
*
* @param string $input
* @param string $glue
* @param string $separator
* @return string
*/
protected static function snakeCase($input, $glue = '_', $separator = '-')
{
$input = preg_split('/(?<=[a-z])(?=[A-Z])/x', $input);
$input = implode($glue, $input);
$input = str_replace($separator, $glue, $input);
return strtolower($input);
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,32 @@
<?php
use ICal\ICal;
use PHPUnit\Framework\TestCase;
class CleanCharacterTest extends TestCase
{
// phpcs:disable Generic.Arrays.DisallowLongArraySyntax
// phpcs:disable Squiz.Commenting.FunctionComment
protected static function getMethod($name)
{
$class = new ReflectionClass(ICal::class);
$method = $class->getMethod($name);
// < PHP 8.1.0
$method->setAccessible(true);
return $method;
}
public function testCleanCharacters()
{
$ical = new ICal();
$input = 'Test with emoji 🔴👍🏻';
self::assertSame(
self::getMethod('cleanCharacters')->invokeArgs($ical, array($input)),
$input
);
}
}

@ -0,0 +1,24 @@
<?php
use ICal\ICal;
use PHPUnit\Framework\TestCase;
class DynamicPropertiesTest extends TestCase
{
// phpcs:disable Squiz.Commenting.FunctionComment
public function testDynamicArraysAreSet()
{
$ical = new ICal('./tests/ical/ical-monthly.ics');
foreach ($ical->events() as $event) {
$this->assertTrue(isset($event->dtstart_array));
$this->assertTrue(isset($event->dtend_array));
$this->assertTrue(isset($event->dtstamp_array));
$this->assertTrue(isset($event->uid_array));
$this->assertTrue(isset($event->created_array));
$this->assertTrue(isset($event->last_modified_array));
$this->assertTrue(isset($event->summary_array));
}
}
}

@ -0,0 +1,86 @@
<?php
use ICal\ICal;
use PHPUnit\Framework\TestCase;
class KeyValueTest extends TestCase
{
// phpcs:disable Generic.Arrays.DisallowLongArraySyntax
// phpcs:disable Squiz.Commenting.FunctionComment
public function testBoundaryCharactersInsideQuotes()
{
$checks = array(
0 => 'ATTENDEE',
1 => array(
0 => 'mailto:julien@ag.com',
1 => array(
'PARTSTAT' => 'TENTATIVE',
'CN' => 'ju: @ag.com = Ju ; ',
),
),
);
$this->assertLines(
'ATTENDEE;PARTSTAT=TENTATIVE;CN="ju: @ag.com = Ju ; ":mailto:julien@ag.com',
$checks
);
}
public function testUtf8Characters()
{
$checks = array(
0 => 'ATTENDEE',
1 => array(
0 => 'mailto:juëǯ@ag.com',
1 => array(
'PARTSTAT' => 'TENTATIVE',
'CN' => 'juëǯĻ',
),
),
);
$this->assertLines(
'ATTENDEE;PARTSTAT=TENTATIVE;CN=juëǯĻ:mailto:juëǯ@ag.com',
$checks
);
$checks = array(
0 => 'SUMMARY',
1 => ' I love emojis 😀😁😁 ë, ǯ, Ļ',
);
$this->assertLines(
'SUMMARY: I love emojis 😀😁😁 ë, ǯ, Ļ',
$checks
);
}
public function testParametersOfKeysWithMultipleValues()
{
$checks = array(
0 => 'ATTENDEE',
1 => array(
0 => 'mailto:jsmith@example.com',
1 => array(
'DELEGATED-TO' => array(
0 => 'mailto:jdoe@example.com',
1 => 'mailto:jqpublic@example.com',
),
),
),
);
$this->assertLines(
'ATTENDEE;DELEGATED-TO="mailto:jdoe@example.com","mailto:jqpublic@example.com":mailto:jsmith@example.com',
$checks
);
}
private function assertLines($lines, array $checks)
{
$ical = new ICal();
self::assertSame($ical->keyValueFromString($lines), $checks);
}
}

@ -0,0 +1,580 @@
<?php
use ICal\ICal;
use PHPUnit\Framework\TestCase;
class RecurrencesTest extends TestCase
{
// phpcs:disable Generic.Arrays.DisallowLongArraySyntax
// phpcs:disable Squiz.Commenting.FunctionComment
// phpcs:disable Squiz.Commenting.VariableComment
private $originalTimeZone = null;
/**
* @before
*/
public function setUpFixtures()
{
$this->originalTimeZone = date_default_timezone_get();
}
/**
* @after
*/
public function tearDownFixtures()
{
date_default_timezone_set($this->originalTimeZone);
}
public function testYearlyFullDayTimeZoneBerlin()
{
$checks = array(
array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
array('index' => 1, 'dateString' => '20010301T000000', 'message' => '2nd event, CET: '),
array('index' => 2, 'dateString' => '20020301T000000', 'message' => '3rd event, CET: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART;VALUE=DATE:20000301',
'DTEND;VALUE=DATE:20000302',
'RRULE:FREQ=YEARLY;WKST=SU;COUNT=3',
),
3,
$checks
);
}
public function testMonthlyFullDayTimeZoneBerlin()
{
$checks = array(
array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
array('index' => 1, 'dateString' => '20000401T000000', 'message' => '2nd event, CEST: '),
array('index' => 2, 'dateString' => '20000501T000000', 'message' => '3rd event, CEST: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART;VALUE=DATE:20000301',
'DTEND;VALUE=DATE:20000302',
'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;WKST=SU;COUNT=3',
),
3,
$checks
);
}
public function testMonthlyFullDayTimeZoneBerlinSummerTime()
{
$checks = array(
array('index' => 0, 'dateString' => '20180701', 'message' => '1st event, CEST: '),
array('index' => 1, 'dateString' => '20180801T000000', 'message' => '2nd event, CEST: '),
array('index' => 2, 'dateString' => '20180901T000000', 'message' => '3rd event, CEST: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART;VALUE=DATE:20180701',
'DTEND;VALUE=DATE:20180702',
'RRULE:FREQ=MONTHLY;WKST=SU;COUNT=3',
),
3,
$checks
);
}
public function testMonthlyFullDayTimeZoneBerlinFromFile()
{
$checks = array(
array('index' => 0, 'dateString' => '20180701', 'message' => '1st event, CEST: '),
array('index' => 1, 'dateString' => '20180801T000000', 'message' => '2nd event, CEST: '),
array('index' => 2, 'dateString' => '20180901T000000', 'message' => '3rd event, CEST: '),
);
$this->assertEventFile(
'Europe/Berlin',
'./tests/ical/ical-monthly.ics',
25,
$checks
);
}
public function testIssue196FromFile()
{
$checks = array(
array('index' => 0, 'dateString' => '20191105T190000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
array('index' => 1, 'dateString' => '20191106T190000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
array('index' => 2, 'dateString' => '20191107T190000', 'timezone' => 'Europe/Berlin', 'message' => '3rd event, CEST: '),
array('index' => 3, 'dateString' => '20191108T190000', 'timezone' => 'Europe/Berlin', 'message' => '4th event, CEST: '),
array('index' => 4, 'dateString' => '20191109T170000', 'timezone' => 'Europe/Berlin', 'message' => '5th event, CEST: '),
array('index' => 5, 'dateString' => '20191110T180000', 'timezone' => 'Europe/Berlin', 'message' => '6th event, CEST: '),
);
$this->assertEventFile(
'UTC',
'./tests/ical/issue-196.ics',
7,
$checks
);
}
public function testWeeklyFullDayTimeZoneBerlin()
{
$checks = array(
array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '),
array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '),
array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '),
array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '),
array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART;VALUE=DATE:20000301',
'DTEND;VALUE=DATE:20000302',
'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=6',
),
6,
$checks
);
}
public function testDailyFullDayTimeZoneBerlin()
{
$checks = array(
array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
array('index' => 1, 'dateString' => '20000302T000000', 'message' => '2nd event, CET: '),
array('index' => 30, 'dateString' => '20000331T000000', 'message' => '31st event, CEST: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART;VALUE=DATE:20000301',
'DTEND;VALUE=DATE:20000302',
'RRULE:FREQ=DAILY;WKST=SU;COUNT=31',
),
31,
$checks
);
}
public function testWeeklyFullDayTimeZoneBerlinLocal()
{
$checks = array(
array('index' => 0, 'dateString' => '20000301T000000', 'message' => '1st event, CET: '),
array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '),
array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '),
array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '),
array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '),
array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART;TZID=Europe/Berlin:20000301T000000',
'DTEND;TZID=Europe/Berlin:20000302T000000',
'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=6',
),
6,
$checks
);
}
public function testRFCDaily10NewYork()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'America/New_York', 'message' => '1st event, EDT: '),
array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'America/New_York', 'message' => '2nd event, EDT: '),
array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'America/New_York', 'message' => '10th event, EDT: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=DAILY;COUNT=10',
),
10,
$checks
);
}
public function testRFCDaily10Berlin()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'Europe/Berlin', 'message' => '10th event, CEST: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART;TZID=Europe/Berlin:19970902T090000',
'RRULE:FREQ=DAILY;COUNT=10',
),
10,
$checks
);
}
public function testStartDateIsExdateUsingUntil()
{
$checks = array(
array('index' => 0, 'dateString' => '20190918T095000', 'timezone' => 'Europe/London', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20191002T095000', 'timezone' => 'Europe/London', 'message' => '2nd event: '),
array('index' => 2, 'dateString' => '20191016T095000', 'timezone' => 'Europe/London', 'message' => '3rd event: '),
);
$this->assertVEVENT(
'Europe/London',
array(
'DTSTART;TZID=Europe/London:20190911T095000',
'RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20191027T235959Z;BYDAY=WE',
'EXDATE;TZID=Europe/London:20191023T095000',
'EXDATE;TZID=Europe/London:20191009T095000',
'EXDATE;TZID=Europe/London:20190925T095000',
'EXDATE;TZID=Europe/London:20190911T095000',
),
3,
$checks
);
}
public function testStartDateIsExdateUsingCount()
{
$checks = array(
array('index' => 0, 'dateString' => '20190918T095000', 'timezone' => 'Europe/London', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20191002T095000', 'timezone' => 'Europe/London', 'message' => '2nd event: '),
array('index' => 2, 'dateString' => '20191016T095000', 'timezone' => 'Europe/London', 'message' => '3rd event: '),
);
$this->assertVEVENT(
'Europe/London',
array(
'DTSTART;TZID=Europe/London:20190911T095000',
'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=7;BYDAY=WE',
'EXDATE;TZID=Europe/London:20191023T095000',
'EXDATE;TZID=Europe/London:20191009T095000',
'EXDATE;TZID=Europe/London:20190925T095000',
'EXDATE;TZID=Europe/London:20190911T095000',
),
3,
$checks
);
}
public function testCountWithExdate()
{
$checks = array(
array('index' => 0, 'dateString' => '20200323T050000', 'timezone' => 'Europe/Paris', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20200324T050000', 'timezone' => 'Europe/Paris', 'message' => '2nd event: '),
array('index' => 2, 'dateString' => '20200327T050000', 'timezone' => 'Europe/Paris', 'message' => '3rd event: '),
);
$this->assertVEVENT(
'Europe/London',
array(
'DTSTART;TZID=Europe/Paris:20200323T050000',
'DTEND;TZID=Europe/Paris:20200323T070000',
'RRULE:FREQ=DAILY;COUNT=5',
'EXDATE;TZID=Europe/Paris:20200326T050000',
'EXDATE;TZID=Europe/Paris:20200325T050000',
'DTSTAMP:20200318T141057Z',
),
3,
$checks
);
}
public function testRFCDaily10BerlinFromNewYork()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'Europe/Berlin', 'message' => '10th event, CEST: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=Europe/Berlin:19970902T090000',
'RRULE:FREQ=DAILY;COUNT=10',
),
10,
$checks
);
}
public function testExdatesInDifferentTimezone()
{
$checks = array(
array('index' => 0, 'dateString' => '20170503T190000', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20170510T190000', 'message' => '2nd event: '),
array('index' => 9, 'dateString' => '20170712T190000', 'message' => '10th event: '),
array('index' => 19, 'dateString' => '20171004T190000', 'message' => '20th event: '),
);
$this->assertVEVENT(
'America/Chicago',
array(
'DTSTART;TZID=America/Chicago:20170503T190000',
'RRULE:FREQ=WEEKLY;BYDAY=WE;WKST=SU;UNTIL=20180101',
'EXDATE:20170601T000000Z',
'EXDATE:20170803T000000Z',
'EXDATE:20170824T000000Z',
'EXDATE:20171026T000000Z',
'EXDATE:20171102T000000Z',
'EXDATE:20171123T010000Z',
'EXDATE:20171221T010000Z',
),
28,
$checks
);
}
public function testYearlyWithBySetPos()
{
$checks = array(
array('index' => 0, 'dateString' => '19970306T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970313T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970325T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19980305T090000', 'message' => '4th occurrence: '),
array('index' => 4, 'dateString' => '19980312T090000', 'message' => '5th occurrence: '),
array('index' => 5, 'dateString' => '19980326T090000', 'message' => '6th occurrence: '),
array('index' => 9, 'dateString' => '20000307T090000', 'message' => '10th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970306T090000',
'RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=3;BYDAY=TU,TH;BYSETPOS=2,4,-2',
),
10,
$checks
);
}
public function testDailyWithByMonthDay()
{
$checks = array(
array('index' => 0, 'dateString' => '20000206T120000', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20000211T120000', 'message' => '2nd event: '),
array('index' => 2, 'dateString' => '20000216T120000', 'message' => '3rd event: '),
array('index' => 4, 'dateString' => '20000226T120000', 'message' => '5th event, transition from February to March: '),
array('index' => 5, 'dateString' => '20000301T120000', 'message' => '6th event, transition to March from February: '),
array('index' => 11, 'dateString' => '20000331T120000', 'message' => '12th event, transition from March to April: '),
array('index' => 12, 'dateString' => '20000401T120000', 'message' => '13th event, transition to April from March: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART:20000206T120000',
'DTEND:20000206T130000',
'RRULE:FREQ=DAILY;BYMONTHDAY=1,6,11,16,21,26,31;COUNT=16',
),
16,
$checks
);
}
public function testYearlyWithByMonthDay()
{
$checks = array(
array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
array('index' => 2, 'dateString' => '20010107T120000', 'message' => '3rd event: '),
array('index' => 3, 'dateString' => '20010114T120000', 'message' => '4th event: '),
array('index' => 6, 'dateString' => '20010214T120000', 'message' => '7th event: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART:20001214T120000',
'DTEND:20001214T130000',
'RRULE:FREQ=YEARLY;BYMONTHDAY=7,14,21;COUNT=8',
),
8,
$checks
);
}
public function testYearlyWithByMonthDayAndByDay()
{
$checks = array(
array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
array('index' => 2, 'dateString' => '20010607T120000', 'message' => '3rd event: '),
array('index' => 3, 'dateString' => '20010614T120000', 'message' => '4th event: '),
array('index' => 6, 'dateString' => '20020214T120000', 'message' => '7th event: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART:20001214T120000',
'DTEND:20001214T130000',
'RRULE:FREQ=YEARLY;BYMONTHDAY=7,14,21;BYDAY=TH;COUNT=8',
),
8,
$checks
);
}
public function testYearlyWithByMonthAndByMonthDay()
{
$checks = array(
array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
array('index' => 2, 'dateString' => '20010607T120000', 'message' => '3rd event: '),
array('index' => 3, 'dateString' => '20010614T120000', 'message' => '4th event: '),
array('index' => 6, 'dateString' => '20011214T120000', 'message' => '7th event: '),
);
$this->assertVEVENT(
'Europe/Berlin',
array(
'DTSTART:20001214T120000',
'DTEND:20001214T130000',
'RRULE:FREQ=YEARLY;BYMONTH=12,6;BYMONTHDAY=7,14,21;COUNT=8',
),
8,
$checks
);
}
public function testCountIsOne()
{
$checks = array(
array('index' => 0, 'dateString' => '20211201T090000', 'message' => '1st and only expected event: '),
);
$this->assertVEVENT(
'UTC',
array(
'DTSTART:20211201T090000',
'DTEND:20211201T100000',
'RRULE:FREQ=DAILY;COUNT=1',
),
1,
$checks
);
}
public function test5thByDayOfMonth()
{
$checks = array(
array('index' => 0, 'dateString' => '20200103T090000', 'message' => '1st event: '),
array('index' => 1, 'dateString' => '20200129T090000', 'message' => '2nd event: '),
array('index' => 2, 'dateString' => '20200429T090000', 'message' => '3rd event: '),
array('index' => 3, 'dateString' => '20200501T090000', 'message' => '4th event: '),
array('index' => 4, 'dateString' => '20200703T090000', 'message' => '5th event: '),
array('index' => 5, 'dateString' => '20200729T090000', 'message' => '6th event: '),
array('index' => 6, 'dateString' => '20200930T090000', 'message' => '7th event: '),
array('index' => 7, 'dateString' => '20201002T090000', 'message' => '8th event: '),
array('index' => 8, 'dateString' => '20201230T090000', 'message' => '9th event: '),
array('index' => 9, 'dateString' => '20210101T090000', 'message' => '10th and last event: '),
);
$this->assertVEVENT(
'UTC',
array(
'DTSTART:20200103T090000',
'DTEND:20200103T100000',
'RRULE:FREQ=MONTHLY;BYDAY=5WE,-5FR;UNTIL=20210102T090000',
),
10,
$checks
);
}
public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks)
{
$options = $this->getOptions($defaultTimezone);
$testIcal = implode(PHP_EOL, $this->getIcalHeader());
$testIcal .= PHP_EOL;
$testIcal .= implode(PHP_EOL, $this->formatIcalEvent($veventParts));
$testIcal .= PHP_EOL;
$testIcal .= implode(PHP_EOL, $this->getIcalFooter());
$ical = new ICal(false, $options);
$ical->initString($testIcal);
$events = $ical->events();
$this->assertCount($count, $events);
foreach ($checks as $check) {
$this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
}
}
public function assertEventFile($defaultTimezone, $file, $count, $checks)
{
$options = $this->getOptions($defaultTimezone);
$ical = new ICal($file, $options);
$events = $ical->events();
$this->assertCount($count, $events);
$events = $ical->sortEventsWithOrder($events);
foreach ($checks as $check) {
$this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
}
}
public function assertEvent($event, $expectedDateString, $message, $timeZone = null)
{
if (!is_null($timeZone)) {
date_default_timezone_set($timeZone);
}
$expectedTimeStamp = strtotime($expectedDateString);
$this->assertSame($expectedTimeStamp, $event->dtstart_array[2], $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')');
$this->assertSame($expectedDateString, $event->dtstart, $message . 'dtstart mismatch (timestamp is okay)');
}
public function getOptions($defaultTimezone)
{
$options = array(
'defaultSpan' => 2, // Default value
'defaultTimeZone' => $defaultTimezone, // Default value: UTC
'defaultWeekStart' => 'MO', // Default value
'disableCharacterReplacement' => false, // Default value
'filterDaysAfter' => null, // Default value
'filterDaysBefore' => null, // Default value
'httpUserAgent' => null, // Default value
'skipRecurrence' => false, // Default value
);
return $options;
}
public function formatIcalEvent($veventParts)
{
return array_merge(
array(
'BEGIN:VEVENT',
'CREATED:' . gmdate('Ymd\THis\Z'),
'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209',
),
$veventParts,
array(
'SUMMARY:test',
'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', filemtime(__FILE__)),
'END:VEVENT',
)
);
}
public function getIcalHeader()
{
return array(
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
'X-WR-CALNAME:Private',
'X-APPLE-CALENDAR-COLOR:#FF2968',
'X-WR-CALDESC:',
);
}
public function getIcalFooter()
{
return array('END:VCALENDAR');
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,509 @@
<?php
use ICal\ICal;
use PHPUnit\Framework\TestCase;
class SingleEventsTest extends TestCase
{
// phpcs:disable Generic.Arrays.DisallowLongArraySyntax
// phpcs:disable Squiz.Commenting.FunctionComment
// phpcs:disable Squiz.Commenting.VariableComment
private $originalTimeZone = null;
/**
* @before
*/
public function setUpFixtures()
{
$this->originalTimeZone = date_default_timezone_get();
}
/**
* @after
*/
public function tearDownFixtures()
{
date_default_timezone_set($this->originalTimeZone);
}
public function testFullDayTimeZoneBerlin()
{
$checks = array(
array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
);
$this->assertVEVENT(
'Europe/Berlin',
'DTSTART;VALUE=DATE:20000301',
'DTEND;VALUE=DATE:20000302',
1,
$checks
);
}
public function testSeveralFullDaysTimeZoneBerlin()
{
$checks = array(
array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
);
$this->assertVEVENT(
'Europe/Berlin',
'DTSTART;VALUE=DATE:20000301',
'DTEND;VALUE=DATE:20000304',
1,
$checks
);
}
public function testEventTimeZoneUTC()
{
$checks = array(
array('index' => 0, 'dateString' => '20180626T070000Z', 'message' => '1st event, UTC: '),
);
$this->assertVEVENT(
'Europe/Berlin',
'DTSTART:20180626T070000Z',
'DTEND:20180626T110000Z',
1,
$checks
);
}
public function testEventTimeZoneBerlin()
{
$checks = array(
array('index' => 0, 'dateString' => '20180626T070000', 'message' => '1st event, CEST: '),
);
$this->assertVEVENT(
'Europe/Berlin',
'DTSTART:20180626T070000',
'DTEND:20180626T110000',
1,
$checks
);
}
public function assertVEVENT($defaultTimezone, $dtstart, $dtend, $count, $checks)
{
$options = $this->getOptions($defaultTimezone);
$testIcal = implode(PHP_EOL, $this->getIcalHeader());
$testIcal .= PHP_EOL;
$testIcal .= implode(PHP_EOL, $this->formatIcalEvent($dtstart, $dtend));
$testIcal .= PHP_EOL;
$testIcal .= implode(PHP_EOL, $this->getIcalTimezones());
$testIcal .= PHP_EOL;
$testIcal .= implode(PHP_EOL, $this->getIcalFooter());
date_default_timezone_set('UTC');
$ical = new ICal(false, $options);
$ical->initString($testIcal);
$events = $ical->events();
$this->assertCount($count, $events);
foreach ($checks as $check) {
$this->assertEvent(
$events[$check['index']],
$check['dateString'],
$check['message'],
isset($check['timezone']) ? $check['timezone'] : $defaultTimezone
);
}
}
public function getOptions($defaultTimezone)
{
$options = array(
'defaultSpan' => 2, // Default value
'defaultTimeZone' => $defaultTimezone, // Default value: UTC
'defaultWeekStart' => 'MO', // Default value
'disableCharacterReplacement' => false, // Default value
'filterDaysAfter' => null, // Default value
'filterDaysBefore' => null, // Default value
'httpUserAgent' => null, // Default value
'skipRecurrence' => false, // Default value
);
return $options;
}
public function getIcalHeader()
{
return array(
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
'X-WR-CALNAME:Private',
'X-APPLE-CALENDAR-COLOR:#FF2968',
'X-WR-CALDESC:',
);
}
public function formatIcalEvent($dtstart, $dtend)
{
return array(
'BEGIN:VEVENT',
'CREATED:20090213T195947Z',
'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209',
$dtstart,
$dtend,
'SUMMARY:test',
'DESCRIPTION;LANGUAGE=en-gb:This is a short description\nwith a new line. Some "special" \'s',
' igns\' may be interesting\, too.',
'&nbsp; And a non-breaking space.',
'LAST-MODIFIED:20110429T222101Z',
'DTSTAMP:20170630T105724Z',
'SEQUENCE:0',
'END:VEVENT',
);
}
public function getIcalTimezones()
{
return array(
'BEGIN:VTIMEZONE',
'TZID:Europe/Berlin',
'X-LIC-LOCATION:Europe/Berlin',
'BEGIN:STANDARD',
'DTSTART:18930401T000000',
'RDATE:18930401T000000',
'TZNAME:CEST',
'TZOFFSETFROM:+005328',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19160430T230000',
'RDATE:19160430T230000',
'RDATE:19400401T020000',
'RDATE:19430329T020000',
'RDATE:19460414T020000',
'RDATE:19470406T030000',
'RDATE:19480418T020000',
'RDATE:19490410T020000',
'RDATE:19800406T020000',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19161001T010000',
'RDATE:19161001T010000',
'RDATE:19421102T030000',
'RDATE:19431004T030000',
'RDATE:19441002T030000',
'RDATE:19451118T030000',
'RDATE:19461007T030000',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19170416T020000',
'RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19170917T030000',
'RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19440403T020000',
'RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:DAYLIGHT',
'DTSTART:19450524T020000',
'RDATE:19450524T020000',
'RDATE:19470511T030000',
'TZNAME:CEMT',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0300',
'END:DAYLIGHT',
'BEGIN:DAYLIGHT',
'DTSTART:19450924T030000',
'RDATE:19450924T030000',
'RDATE:19470629T030000',
'TZNAME:CEST',
'TZOFFSETFROM:+0300',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19460101T000000',
'RDATE:19460101T000000',
'RDATE:19800101T000000',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:STANDARD',
'DTSTART:19471005T030000',
'RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:STANDARD',
'DTSTART:19800928T030000',
'RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19810329T020000',
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19961027T030000',
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'END:VTIMEZONE',
'BEGIN:VTIMEZONE',
'TZID:Europe/Paris',
'X-LIC-LOCATION:Europe/Paris',
'BEGIN:STANDARD',
'DTSTART:18910315T000100',
'RDATE:18910315T000100',
'TZNAME:PMT',
'TZOFFSETFROM:+000921',
'TZOFFSETTO:+000921',
'END:STANDARD',
'BEGIN:STANDARD',
'DTSTART:19110311T000100',
'RDATE:19110311T000100',
'TZNAME:WEST',
'TZOFFSETFROM:+000921',
'TZOFFSETTO:+0000',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19160614T230000',
'RDATE:19160614T230000',
'RDATE:19170324T230000',
'RDATE:19180309T230000',
'RDATE:19190301T230000',
'RDATE:19200214T230000',
'RDATE:19210314T230000',
'RDATE:19220325T230000',
'RDATE:19230526T230000',
'RDATE:19240329T230000',
'RDATE:19250404T230000',
'RDATE:19260417T230000',
'RDATE:19270409T230000',
'RDATE:19280414T230000',
'RDATE:19290420T230000',
'RDATE:19300412T230000',
'RDATE:19310418T230000',
'RDATE:19320402T230000',
'RDATE:19330325T230000',
'RDATE:19340407T230000',
'RDATE:19350330T230000',
'RDATE:19360418T230000',
'RDATE:19370403T230000',
'RDATE:19380326T230000',
'RDATE:19390415T230000',
'RDATE:19400225T020000',
'TZNAME:WEST',
'TZOFFSETFROM:+0000',
'TZOFFSETTO:+0100',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19161002T000000',
'RRULE:FREQ=YEARLY;UNTIL=19191005T230000Z;BYMONTH=10;BYMONTHDAY=2,3,4,5,6,',
' 7,8;BYDAY=MO',
'TZNAME:WET',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0000',
'END:STANDARD',
'BEGIN:STANDARD',
'DTSTART:19201024T000000',
'RDATE:19201024T000000',
'RDATE:19211026T000000',
'RDATE:19391119T000000',
'TZNAME:WET',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0000',
'END:STANDARD',
'BEGIN:STANDARD',
'DTSTART:19221008T000000',
'RRULE:FREQ=YEARLY;UNTIL=19381001T230000Z;BYMONTH=10;BYMONTHDAY=2,3,4,5,6,',
' 7,8;BYDAY=SU',
'TZNAME:WET',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0000',
'END:STANDARD',
'BEGIN:STANDARD',
'DTSTART:19400614T230000',
'RDATE:19400614T230000',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:STANDARD',
'BEGIN:STANDARD',
'DTSTART:19421102T030000',
'RDATE:19421102T030000',
'RDATE:19431004T030000',
'RDATE:19760926T010000',
'RDATE:19770925T030000',
'RDATE:19781001T030000',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19430329T020000',
'RDATE:19430329T020000',
'RDATE:19440403T020000',
'RDATE:19760328T010000',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19440825T000000',
'RDATE:19440825T000000',
'TZNAME:WEST',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0200',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19441008T010000',
'RDATE:19441008T010000',
'TZNAME:WEST',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:DAYLIGHT',
'BEGIN:DAYLIGHT',
'DTSTART:19450402T020000',
'RDATE:19450402T020000',
'TZNAME:WEMT',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19450916T030000',
'RDATE:19450916T030000',
'TZNAME:CEST',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:STANDARD',
'DTSTART:19770101T000000',
'RDATE:19770101T000000',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19770403T020000',
'RRULE:FREQ=YEARLY;UNTIL=19800406T010000Z;BYMONTH=4;BYDAY=1SU',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19790930T030000',
'RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19810329T020000',
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19961027T030000',
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'END:VTIMEZONE',
'BEGIN:VTIMEZONE',
'TZID:US-Eastern',
'LAST-MODIFIED:19870101T000000Z',
'TZURL:http://zones.stds_r_us.net/tz/US-Eastern',
'BEGIN:STANDARD',
'DTSTART:19671029T020000',
'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10',
'TZOFFSETFROM:-0400',
'TZOFFSETTO:-0500',
'TZNAME:EST',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19870405T020000',
'RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4',
'TZOFFSETFROM:-0500',
'TZOFFSETTO:-0400',
'TZNAME:EDT',
'END:DAYLIGHT',
'END:VTIMEZONE',
);
}
public function getIcalFooter()
{
return array('END:VCALENDAR');
}
public function assertEvent($event, $expectedDateString, $message, $timezone = null)
{
if ($timezone !== null) {
date_default_timezone_set($timezone);
}
$expectedTimeStamp = strtotime($expectedDateString);
$this->assertSame(
$expectedTimeStamp,
$event->dtstart_array[2],
$message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')'
);
$this->assertSame(
$expectedDateString,
$event->dtstart,
$message . 'dtstart mismatch (timestamp is okay)'
);
}
public function assertEventFile($defaultTimezone, $file, $count, $checks)
{
$options = $this->getOptions($defaultTimezone);
date_default_timezone_set('UTC');
$ical = new ICal($file, $options);
$events = $ical->events();
$this->assertCount($count, $events);
foreach ($checks as $check) {
$this->assertEvent(
$events[$check['index']],
$check['dateString'],
$check['message'],
isset($check['timezone']) ? $check['timezone'] : $defaultTimezone
);
}
}
}

@ -0,0 +1,18 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
X-WR-CALNAME:Private
X-APPLE-CALENDAR-COLOR:#FF2968
X-WR-CALDESC:
BEGIN:VEVENT
CREATED:20090213T195947Z
UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97208
RRULE:FREQ=MONTHLY;BYMONTHDAY=1;WKST=SU;COUNT=25
DTSTART;VALUE=DATE:20180701
DTEND;VALUE=DATE:20180702
SUMMARY:Monthly
LAST-MODIFIED:20110429T222101Z
DTSTAMP:20170630T105724Z
SEQUENCE:0
END:VEVENT
END:VCALENDAR

@ -0,0 +1,64 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
X-WR-CALNAME:Test-Calendar
X-WR-TIMEZONE:Europe/Berlin
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20180101T152047Z
LAST-MODIFIED:20181202T202056Z
DTSTAMP:20181202T202056Z
UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
SUMMARY:test
RRULE:FREQ=DAILY;UNTIL=20191111T180000Z
DTSTART;TZID=Europe/Berlin:20191105T190000
DTEND;TZID=Europe/Berlin:20191105T220000
TRANSP:OPAQUE
SEQUENCE:24
X-MOZ-GENERATION:37
END:VEVENT
BEGIN:VEVENT
CREATED:20181202T202042Z
LAST-MODIFIED:20181202T202053Z
DTSTAMP:20181202T202053Z
UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
SUMMARY:test
RECURRENCE-ID;TZID=Europe/Berlin:20191109T190000
DTSTART;TZID=Europe/Berlin:20191109T170000
DTEND;TZID=Europe/Berlin:20191109T220000
TRANSP:OPAQUE
SEQUENCE:25
X-MOZ-GENERATION:37
DURATION:PT0S
END:VEVENT
BEGIN:VEVENT
CREATED:20181202T202053Z
LAST-MODIFIED:20181202T202056Z
DTSTAMP:20181202T202056Z
UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
SUMMARY:test
RECURRENCE-ID;TZID=Europe/Berlin:20191110T190000
DTSTART;TZID=Europe/Berlin:20191110T180000
DTEND;TZID=Europe/Berlin:20191110T220000
TRANSP:OPAQUE
SEQUENCE:25
X-MOZ-GENERATION:37
DURATION:PT0S
END:VEVENT
END:VCALENDAR
Loading…
Cancel
Save