LICENSE 0000666 00000002051 13454100516 0005552 0 ustar 00 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.
README.md 0000666 00000104055 13454100516 0006033 0 ustar 00 # 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.3.9)
- [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
- If Windows time zones are present in your feed you will need to set `$replaceWindowsTimeZoneIds` to `true`
### 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 (`v2`) 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": "^2"
}
}
```
## 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.
```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)
```
---
## 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` | :heavy_multiplication_x: | `null` | Holds the custom User Agent string header |
| `$replaceWindowsTimeZoneIds` | :ballot_box_with_check: | `false` | Toggles whether to replace (non-CLDR) Windows time zone IDs with their IANA equivalent e.g. "Mountain Standard Time" would be replaced with "America/Denver". As there are 130+ Windows time zone IDs that need to be searched and replaced this flag should only be turned on if you know that your calendar file contains such time zone IDs. **Microsoft Exchange** calendars are often seen using such IDs. |
| `$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 |
| `$useTimeZoneWithRRules` | :ballot_box_with_check: | `false` | Toggles whether to use time zone info when parsing recurrence rules |
| `$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` | `protected` | Initialises lines from a URL. Accepts a username/password combination for HTTP basic authentication |
| `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 |
| `cleanData` | `$data` | `protected` | Replaces curly quotes and other special characters with their standard equivalents |
| `convertDayOrdinalToPositive` | `$dayNumber`, `$weekday`, `$timestamp` | `protected` | Converts a negative day ordinal to its equivalent positive form |
| `eventsFromInterval` | `$interval` | `public` | Returns a sorted array of events following a given string, or `false` if no events exist in the range |
| `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 |
| `freeBusyEvents` | - | `public` | Returns an array of arrays with all free/busy events |
| `hasEvents` | - | `public` | Returns a boolean value whether the current calendar has events or not |
| `iCalDateToDateTime` | `$icalDate`, `$forceTimeZone = false`, `$forceUtc = false` | `public` | Returns a `DateTime` object from an iCal date time format |
| `iCalDateToUnixTimestamp` | `$icalDate`, `$forceTimeZone = false`, `$forceUtc = false` | `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` |
| `isExdateMatch` | `$exdate`, `$anEvent`, `$recurringOffset` | `protected` | Checks if an excluded date matches a given date by reconciling time zones |
| `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`, `doConversion = false` | `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 |
| `isValidTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is valid (IANA or CLDR) |
| `keyValueFromString` | `$text` | `protected` | Gets the key value pair from an iCal string |
| `mb_chr` | `$code` | `protected` | Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()` |
| `mb_str_replace` | `$search`, `$replace`, `$subject`, `$count = 0` | `protected` | Replaces all occurrences of a search string with a given replacement string |
| `numberOfDays` | `$days`, `$start`, `$end` | `protected` | Gets the number of days between a start and end date |
| `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 |
| `processEventIcalDateTime` | `$event`, `$index = 3` | `protected` | Extends the `{DTSTART\|DTEND\|RECURRENCE-ID}_array` array to include an iCal date time for each event |
| `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 |
| `sortEventsWithOrder` | `$events`, `$sortOrder = SORT_ASC` | `public` | Sorts events based on a given sort order |
| `trimToRecurrenceCount` | `$rrules`, `$recurrenceEvents` | `protected` | Ensures the recurrence count is enforced against generated recurrence events |
| `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 |
| `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, enhancement, coding standard)
- [John Grogg](john.grogg@gmail.com) (programming, addition of event recurrence handling)
---
## 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)
examples/index.php 0000666 00000012432 13454100516 0010207 0 ustar 00 2, // Default value
'defaultTimeZone' => 'UTC',
'defaultWeekStart' => 'MO', // Default value
'disableCharacterReplacement' => false, // Default value
'filterDaysAfter' => null, // Default value
'filterDaysBefore' => null, // Default value
'replaceWindowsTimeZoneIds' => false, // Default value
'skipRecurrence' => false, // Default value
'useTimeZoneWithRRules' => 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);
}
$forceTimeZone = false;
?>
PHP ICS Parser example
PHP ICS Parser example
eventCount ?>
The number of events
freeBusyCount ?>
The number of free/busy time slots
todoCount ?>
The number of todos
alarmCount ?>
The number of alarms
true,
'range' => true,
'all' => true,
);
?>
eventsFromInterval('1 week');
if ($events) {
echo '
Events in the next 7 days: ';
}
$count = 1;
?>
iCalDateToDateTime($event->dtstart_array[3], $forceTimeZone);
echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
?>
printData() ?>
1 && $count % 3 === 0) {
echo '
';
}
$count++;
?>
eventsFromRange('2017-03-01 12:00:00', '2017-04-31 17:00:00');
if ($events) {
echo '
Events March through April: ';
}
$count = 1;
?>
iCalDateToDateTime($event->dtstart_array[3], $forceTimeZone);
echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
?>
printData() ?>
1 && $count % 3 === 0) {
echo '
';
}
$count++;
?>
sortEventsWithOrder($ical->events());
if ($events) {
echo '
All Events: ';
}
?>
iCalDateToDateTime($event->dtstart_array[3], $forceTimeZone);
echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
?>
printData() ?>
1 && $count % 3 === 0) {
echo '
';
}
$count++;
?>
examples/ICal.ics 0000666 00000022066 13454100516 0007703 0 ustar 00 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.
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)";ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:l.page@google.com
ATTENDEE;CN="Brin, Sergey (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=2WE
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: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
.github/PULL_REQUEST_TEMPLATE.md 0000666 00000001750 13454100516 0011713 0 ustar 00 > :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`
.github/ISSUE_TEMPLATE.md 0000666 00000001463 13454100516 0010620 0 ustar 00 > :information_source:
> - Firstly, check you are using the latest version (`dev-master`) as the problem may already have been fixed.
> - It is **essential** to be provided with the offending iCal causing the parser to behave incorrectly.
> - Best to upload the iCal file directly to this issue
> - [Create a Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve)
> - Minimal - use as little code as possible that still produces the same problem
> - Complete - provide all parts needed to reproduce the problem
> - Verifiable - test the code you're about to provide to make sure it reproduces the problem
- PHP Version: `7.#.#`
- PHP date.timezone: `[Country]/[City]`
- ICS Parser Version: `2.#.#`
- Windows/Mac/Linux
### Description of the Issue:
### Steps to Reproduce:
1.
1.
1.
.github/RELEASE_CHECKLIST.md 0000666 00000000676 13454100516 0011113 0 ustar 00 # 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
phpunit.xml 0000666 00000000235 13454100516 0006760 0 ustar 00
tests
src/ICal/ICal.php 0000666 00000362670 13454100516 0007505 0 ustar 00
* @license https://opensource.org/licenses/mit-license.php MIT License
* @version 2.1.9
*/
namespace ICal;
use Carbon\Carbon;
class ICal
{
// phpcs:disable Generic.Arrays.DisallowLongArraySyntax.Found
const DATE_TIME_FORMAT = 'Ymd\THis';
const DATE_TIME_FORMAT_PRETTY = 'F Y H:i:s';
const ICAL_DATE_TIME_TEMPLATE = 'TZID=%s:';
const RECURRENCE_EVENT = 'Generated recurrence event';
const SECONDS_IN_A_WEEK = 604800;
const TIME_FORMAT = 'His';
const TIME_ZONE_UTC = 'UTC';
const UNIX_FORMAT = 'U';
const UNIX_MIN_YEAR = 1970;
/**
* Tracks the number of alarms in the current iCal feed
*
* @var integer
*/
public $alarmCount = 0;
/**
* Tracks the number of events in the current iCal feed
*
* @var integer
*/
public $eventCount = 0;
/**
* Tracks the free/busy count in the current iCal feed
*
* @var integer
*/
public $freeBusyCount = 0;
/**
* Tracks the number of todos in the current iCal feed
*
* @var integer
*/
public $todoCount = 0;
/**
* The value in years to use for indefinite, recurring events
*
* @var integer
*/
public $defaultSpan = 2;
/**
* Enables customisation of the default time zone
*
* @var string
*/
public $defaultTimeZone;
/**
* The two letter representation of the first day of the week
*
* @var string
*/
public $defaultWeekStart = 'MO';
/**
* Toggles whether to skip the parsing of recurrence rules
*
* @var boolean
*/
public $skipRecurrence = false;
/**
* Toggles whether to use time zone info when parsing recurrence rules
*
* @var boolean
*/
public $useTimeZoneWithRRules = false;
/**
* Toggles whether to disable all character replacement.
*
* @var boolean
*/
public $disableCharacterReplacement = false;
/**
* Toggles whether to replace (non-CLDR) Windows time zone IDs with their IANA equivalent.
*
* @var boolean
*/
public $replaceWindowsTimeZoneIds = false;
/**
* With this being non-null the parser will ignore all events more than roughly this many days after now.
*
* @var integer
*/
public $filterDaysBefore = null;
/**
* With this being non-null the parser will ignore all events more than roughly this many days before now.
*
* @var integer
*/
public $filterDaysAfter = null;
/**
* The parsed calendar
*
* @var array
*/
public $cal = array();
/**
* Tracks the VFREEBUSY component
*
* @var integer
*/
protected $freeBusyIndex = 0;
/**
* Variable to track the previous keyword
*
* @var string
*/
protected $lastKeyword;
/**
* Cache valid time zones to avoid unnecessary lookups
*
* @var array
*/
protected $validTimeZones = array();
/**
* Event recurrence instances that have been altered
*
* @var array
*/
protected $alteredRecurrenceInstances = array();
/**
* An associative array containing ordinal data
*
* @var array
*/
protected $dayOrdinals = array(
1 => 'first',
2 => 'second',
3 => 'third',
4 => 'fourth',
5 => 'fifth',
);
/**
* An associative array containing weekday conversion data
*
* @var array
*/
protected $weekdays = array(
'SU' => 'sunday',
'MO' => 'monday',
'TU' => 'tuesday',
'WE' => 'wednesday',
'TH' => 'thursday',
'FR' => 'friday',
'SA' => 'saturday',
);
/**
* An associative array containing week conversion data
* (UK = SU, Europe = MO)
*
* @var array
*/
protected $weeks = array(
'SA' => array('SA', 'SU', 'MO', 'TU', 'WE', 'TH', 'FR'),
'SU' => array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'),
'MO' => array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'),
);
/**
* An associative array containing month names
*
* @var array
*/
protected $monthNames = array(
1 => 'January',
2 => 'February',
3 => 'March',
4 => 'April',
5 => 'May',
6 => 'June',
7 => 'July',
8 => 'August',
9 => 'September',
10 => 'October',
11 => 'November',
12 => 'December',
);
/**
* An associative array containing frequency conversion terms
*
* @var array
*/
protected $frequencyConversion = array(
'DAILY' => 'day',
'WEEKLY' => 'week',
'MONTHLY' => 'month',
'YEARLY' => 'year',
);
/**
* Holds the username and password for HTTP basic authentication
*
* @var array
*/
protected $httpBasicAuth = array();
/**
* Holds the custom User Agent string header
*
* @var string
*/
protected $httpUserAgent = null;
/**
* Define which variables can be configured
*
* @var array
*/
private static $configurableOptions = array(
'defaultSpan',
'defaultTimeZone',
'defaultWeekStart',
'disableCharacterReplacement',
'filterDaysAfter',
'filterDaysBefore',
'replaceWindowsTimeZoneIds',
'skipRecurrence',
'useTimeZoneWithRRules',
);
/**
* Maps Windows (non-CLDR) time zone ID to IANA ID. This is pragmatic but not 100% precise as one Windows zone ID
* maps to multiple IANA IDs (one for each territory). For all practical purposes this should be good enough, though.
*
* Source: http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml
*
* @var array
*/
private static $windowsTimeZonesMap = array(
'AUS Central Standard Time' => 'Australia/Darwin',
'AUS Eastern Standard Time' => 'Australia/Sydney',
'Afghanistan Standard Time' => 'Asia/Kabul',
'Alaskan Standard Time' => 'America/Anchorage',
'Aleutian Standard Time' => 'America/Adak',
'Altai Standard Time' => 'Asia/Barnaul',
'Arab Standard Time' => 'Asia/Riyadh',
'Arabian Standard Time' => 'Asia/Dubai',
'Arabic Standard Time' => 'Asia/Baghdad',
'Argentina Standard Time' => 'America/Buenos_Aires',
'Astrakhan Standard Time' => 'Europe/Astrakhan',
'Atlantic Standard Time' => 'America/Halifax',
'Aus Central W. Standard Time' => 'Australia/Eucla',
'Azerbaijan Standard Time' => 'Asia/Baku',
'Azores Standard Time' => 'Atlantic/Azores',
'Bahia Standard Time' => 'America/Bahia',
'Bangladesh Standard Time' => 'Asia/Dhaka',
'Belarus Standard Time' => 'Europe/Minsk',
'Bougainville Standard Time' => 'Pacific/Bougainville',
'Canada Central Standard Time' => 'America/Regina',
'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
'Caucasus Standard Time' => 'Asia/Yerevan',
'Cen. Australia Standard Time' => 'Australia/Adelaide',
'Central America Standard Time' => 'America/Guatemala',
'Central Asia Standard Time' => 'Asia/Almaty',
'Central Brazilian Standard Time' => 'America/Cuiaba',
'Central Europe Standard Time' => 'Europe/Budapest',
'Central European Standard Time' => 'Europe/Warsaw',
'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
'Central Standard Time (Mexico)' => 'America/Mexico_City',
'Central Standard Time' => 'America/Chicago',
'Chatham Islands Standard Time' => 'Pacific/Chatham',
'China Standard Time' => 'Asia/Shanghai',
'Cuba Standard Time' => 'America/Havana',
'Dateline Standard Time' => 'Etc/GMT+12',
'E. Africa Standard Time' => 'Africa/Nairobi',
'E. Australia Standard Time' => 'Australia/Brisbane',
'E. Europe Standard Time' => 'Europe/Chisinau',
'E. South America Standard Time' => 'America/Sao_Paulo',
'Easter Island Standard Time' => 'Pacific/Easter',
'Eastern Standard Time (Mexico)' => 'America/Cancun',
'Eastern Standard Time' => 'America/New_York',
'Egypt Standard Time' => 'Africa/Cairo',
'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
'FLE Standard Time' => 'Europe/Kiev',
'Fiji Standard Time' => 'Pacific/Fiji',
'GMT Standard Time' => 'Europe/London',
'GTB Standard Time' => 'Europe/Bucharest',
'Georgian Standard Time' => 'Asia/Tbilisi',
'Greenland Standard Time' => 'America/Godthab',
'Greenwich Standard Time' => 'Atlantic/Reykjavik',
'Haiti Standard Time' => 'America/Port-au-Prince',
'Hawaiian Standard Time' => 'Pacific/Honolulu',
'India Standard Time' => 'Asia/Calcutta',
'Iran Standard Time' => 'Asia/Tehran',
'Israel Standard Time' => 'Asia/Jerusalem',
'Jordan Standard Time' => 'Asia/Amman',
'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
'Korea Standard Time' => 'Asia/Seoul',
'Libya Standard Time' => 'Africa/Tripoli',
'Line Islands Standard Time' => 'Pacific/Kiritimati',
'Lord Howe Standard Time' => 'Australia/Lord_Howe',
'Magadan Standard Time' => 'Asia/Magadan',
'Magallanes Standard Time' => 'America/Punta_Arenas',
'Marquesas Standard Time' => 'Pacific/Marquesas',
'Mauritius Standard Time' => 'Indian/Mauritius',
'Middle East Standard Time' => 'Asia/Beirut',
'Montevideo Standard Time' => 'America/Montevideo',
'Morocco Standard Time' => 'Africa/Casablanca',
'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
'Mountain Standard Time' => 'America/Denver',
'Myanmar Standard Time' => 'Asia/Rangoon',
'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
'Namibia Standard Time' => 'Africa/Windhoek',
'Nepal Standard Time' => 'Asia/Katmandu',
'New Zealand Standard Time' => 'Pacific/Auckland',
'Newfoundland Standard Time' => 'America/St_Johns',
'Norfolk Standard Time' => 'Pacific/Norfolk',
'North Asia East Standard Time' => 'Asia/Irkutsk',
'North Asia Standard Time' => 'Asia/Krasnoyarsk',
'North Korea Standard Time' => 'Asia/Pyongyang',
'Omsk Standard Time' => 'Asia/Omsk',
'Pacific SA Standard Time' => 'America/Santiago',
'Pacific Standard Time (Mexico)' => 'America/Tijuana',
'Pacific Standard Time' => 'America/Los_Angeles',
'Pakistan Standard Time' => 'Asia/Karachi',
'Paraguay Standard Time' => 'America/Asuncion',
'Romance Standard Time' => 'Europe/Paris',
'Russia Time Zone 10' => 'Asia/Srednekolymsk',
'Russia Time Zone 11' => 'Asia/Kamchatka',
'Russia Time Zone 3' => 'Europe/Samara',
'Russian Standard Time' => 'Europe/Moscow',
'SA Eastern Standard Time' => 'America/Cayenne',
'SA Pacific Standard Time' => 'America/Bogota',
'SA Western Standard Time' => 'America/La_Paz',
'SE Asia Standard Time' => 'Asia/Bangkok',
'Saint Pierre Standard Time' => 'America/Miquelon',
'Sakhalin Standard Time' => 'Asia/Sakhalin',
'Samoa Standard Time' => 'Pacific/Apia',
'Sao Tome Standard Time' => 'Africa/Sao_Tome',
'Saratov Standard Time' => 'Europe/Saratov',
'Singapore Standard Time' => 'Asia/Singapore',
'South Africa Standard Time' => 'Africa/Johannesburg',
'Sri Lanka Standard Time' => 'Asia/Colombo',
'Sudan Standard Time' => 'Africa/Tripoli',
'Syria Standard Time' => 'Asia/Damascus',
'Taipei Standard Time' => 'Asia/Taipei',
'Tasmania Standard Time' => 'Australia/Hobart',
'Tocantins Standard Time' => 'America/Araguaina',
'Tokyo Standard Time' => 'Asia/Tokyo',
'Tomsk Standard Time' => 'Asia/Tomsk',
'Tonga Standard Time' => 'Pacific/Tongatapu',
'Transbaikal Standard Time' => 'Asia/Chita',
'Turkey Standard Time' => 'Europe/Istanbul',
'Turks And Caicos Standard Time' => 'America/Grand_Turk',
'US Eastern Standard Time' => 'America/Indianapolis',
'US Mountain Standard Time' => 'America/Phoenix',
'UTC' => 'Etc/GMT',
'UTC+12' => 'Etc/GMT-12',
'UTC+13' => 'Etc/GMT-13',
'UTC-02' => 'Etc/GMT+2',
'UTC-08' => 'Etc/GMT+8',
'UTC-09' => 'Etc/GMT+9',
'UTC-11' => 'Etc/GMT+11',
'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
'Venezuela Standard Time' => 'America/Caracas',
'Vladivostok Standard Time' => 'Asia/Vladivostok',
'W. Australia Standard Time' => 'Australia/Perth',
'W. Central Africa Standard Time' => 'Africa/Lagos',
'W. Europe Standard Time' => 'Europe/Berlin',
'W. Mongolia Standard Time' => 'Asia/Hovd',
'West Asia Standard Time' => 'Asia/Tashkent',
'West Bank Standard Time' => 'Asia/Hebron',
'West Pacific Standard Time' => 'Pacific/Port_Moresby',
'Yakutsk Standard Time' => 'Asia/Yakutsk',
);
/**
* Store the Windows Time Zone IDs to search and replace
*
* @var array
*/
private $windowsTimeZones;
/**
* Store the IANA IDs to be used as a replacement for Windows Time Zone IDs
*
* @var array
*/
private $windowsTimeZonesIana;
/**
* If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined
* by this field and `$windowMaxTimestamp`.
*
* @var integer
*/
private $windowMinTimestamp = null;
/**
* If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined
* by this field and `$windowMinTimestamp`.
*
* @var integer
*/
private $windowMaxTimestamp = null;
/**
* `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set.
*
* @var boolean
*/
private $shouldFilterByWindow = false;
/**
* Creates the ICal object
*
* @param mixed $files
* @param array $options
* @return void
*/
public function __construct($files = false, array $options = array())
{
ini_set('auto_detect_line_endings', '1');
foreach ($options as $option => $value) {
if (in_array($option, self::$configurableOptions)) {
$this->{$option} = $value;
}
}
// Fallback to use the system default time zone
if (!isset($this->defaultTimeZone) || !$this->isValidTimeZoneId($this->defaultTimeZone)) {
$this->defaultTimeZone = date_default_timezone_get();
}
$this->windowsTimeZones = array_keys(self::$windowsTimeZonesMap);
$this->windowsTimeZonesIana = array_values(self::$windowsTimeZonesMap);
// Ideally you would use `PHP_INT_MIN` from PHP 7
$php_int_min = -2147483648;
$this->windowMinTimestamp = is_null($this->filterDaysBefore) ? $php_int_min : (new \DateTime('now'))->sub(new \DateInterval('P' . $this->filterDaysBefore . 'D'))->getTimestamp();
$this->windowMaxTimestamp = is_null($this->filterDaysAfter) ? PHP_INT_MAX : (new \DateTime('now'))->add(new \DateInterval('P' . $this->filterDaysAfter . 'D'))->getTimestamp();
$this->shouldFilterByWindow = !is_null($this->filterDaysBefore) || !is_null($this->filterDaysAfter);
if ($files !== false) {
$files = is_array($files) ? $files : array($files);
foreach ($files as $file) {
if (!is_array($file) && $this->isFileOrUrl($file)) {
$lines = $this->fileOrUrl($file);
} else {
$lines = is_array($file) ? $file : array($file);
}
$this->initLines($lines);
}
}
}
/**
* Initialises lines from a string
*
* @param string $string
* @return ICal
*/
public function initString($string)
{
if (empty($this->cal)) {
$lines = explode(PHP_EOL, $string);
$this->initLines($lines);
} else {
trigger_error('ICal::initString: Calendar already initialised in constructor', E_USER_NOTICE);
}
return $this;
}
/**
* Initialises lines from a file
*
* @param string $file
* @return ICal
*/
public function initFile($file)
{
if (empty($this->cal)) {
$lines = $this->fileOrUrl($file);
$this->initLines($lines);
} else {
trigger_error('ICal::initFile: Calendar already initialised in constructor', E_USER_NOTICE);
}
return $this;
}
/**
* Initialises lines from a URL
*
* @param string $url
* @param string $username
* @param string $password
* @param string $userAgent
* @return ICal
*/
public function initUrl($url, $username = null, $password = null, $userAgent = null)
{
if (!is_null($username) && !is_null($password)) {
$this->httpBasicAuth['username'] = $username;
$this->httpBasicAuth['password'] = $password;
}
if (!is_null($userAgent)) {
$this->httpUserAgent = $userAgent;
}
$this->initFile($url);
return $this;
}
/**
* Initialises the parser using an array
* containing each line of iCal content
*
* @param array $lines
* @return void
*/
protected function initLines(array $lines)
{
$lines = $this->unfold($lines);
if (stristr($lines[0], 'BEGIN:VCALENDAR') !== false) {
$component = '';
foreach ($lines as $line) {
$line = rtrim($line); // Trim trailing whitespace
$line = $this->removeUnprintableChars($line);
if (!$this->disableCharacterReplacement) {
$line = $this->cleanData($line);
}
if ($this->replaceWindowsTimeZoneIds && strpos($line, 'TZID') !== false) {
$line = $this->replaceWindowsTimeZoneId($line);
}
$add = $this->keyValueFromString($line);
$keyword = $add[0];
$values = $add[1]; // May be an array containing multiple values
if (!is_array($values)) {
if (!empty($values)) {
$values = array($values); // Make an array as not already
$blankArray = array(); // Empty placeholder array
array_push($values, $blankArray);
} else {
$values = array(); // Use blank array to ignore this line
}
} elseif (empty($values[0])) {
$values = array(); // Use blank array to ignore this line
}
// Reverse so that our array of properties is processed first
$values = array_reverse($values);
foreach ($values as $value) {
switch ($line) {
// https://www.kanzaki.com/docs/ical/vtodo.html
case 'BEGIN:VTODO':
if (!is_array($value)) {
$this->todoCount++;
}
$component = 'VTODO';
break;
// https://www.kanzaki.com/docs/ical/vevent.html
case 'BEGIN:VEVENT':
if (!is_array($value)) {
$this->eventCount++;
}
$component = 'VEVENT';
break;
// https://www.kanzaki.com/docs/ical/vfreebusy.html
case 'BEGIN:VFREEBUSY':
if (!is_array($value)) {
$this->freeBusyIndex++;
}
$component = 'VFREEBUSY';
break;
case 'BEGIN:VALARM':
if (!is_array($value)) {
$this->alarmCount++;
}
$component = 'VALARM';
break;
case 'END:VALARM':
$component = 'VEVENT';
break;
case 'BEGIN:DAYLIGHT':
case 'BEGIN:STANDARD':
case 'BEGIN:VCALENDAR':
case 'BEGIN:VTIMEZONE':
$component = $value;
break;
case 'END:DAYLIGHT':
case 'END:STANDARD':
case 'END:VCALENDAR':
case 'END:VFREEBUSY':
case 'END:VTIMEZONE':
case 'END:VTODO':
$component = 'VCALENDAR';
break;
case 'END:VEVENT':
if ($this->shouldFilterByWindow) {
$this->removeLastEventIfOutsideWindowAndNonRecurring();
}
$component = 'VCALENDAR';
break;
default:
$this->addCalendarComponentWithKeyAndValue($component, $keyword, $value);
break;
}
}
}
$this->processEvents();
if (!$this->skipRecurrence) {
$this->processRecurrences();
// Apply changes to altered recurrence instances
if (!empty($this->alteredRecurrenceInstances)) {
$events = $this->cal['VEVENT'];
foreach ($this->alteredRecurrenceInstances as $alteredRecurrenceInstance) {
if (isset($alteredRecurrenceInstance['altered-event'])) {
$alteredEvent = $alteredRecurrenceInstance['altered-event'];
$key = key($alteredEvent);
$events[$key] = $alteredEvent[$key];
}
}
$this->cal['VEVENT'] = $events;
}
}
if ($this->shouldFilterByWindow) {
$this->reduceEventsToMinMaxRange();
}
$this->processDateConversions();
}
}
/**
* Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by
* `$windowMinTimestamp` / `$windowMaxTimestamp`.
*
* @return void
*/
protected function removeLastEventIfOutsideWindowAndNonRecurring()
{
$events = $this->cal['VEVENT'];
if (!empty($events)) {
$lastIndex = sizeof($events) - 1;
$lastEvent = $events[$lastIndex];
if (!isset($lastEvent['RRULE']) || $lastEvent['RRULE'] === '' && $this->doesEventStartOutsideWindow($lastEvent)) {
$this->eventCount--;
unset($events[$lastIndex]);
}
$this->cal['VEVENT'] = $events;
}
}
/**
* Reduces the number of events to the defined minimum and maximum range
*
* @return void
*/
protected function reduceEventsToMinMaxRange()
{
$events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
if (!empty($events)) {
foreach ($events as $key => $anEvent) {
if ($this->doesEventStartOutsideWindow($anEvent)) {
$this->eventCount--;
unset($events[$key]);
continue;
}
}
$this->cal['VEVENT'] = $events;
}
}
/**
* Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp`.
* Returns `true` for invalid dates.
*
* @param array $event
* @return boolean
*/
protected function doesEventStartOutsideWindow(array $event)
{
return !$this->isValidDate($event['DTSTART']) || $this->isOutOfRange($event['DTSTART'], $this->windowMinTimestamp, $this->windowMaxTimestamp);
}
/**
* Determines whether a valid iCalendar date is within a given range
*
* @param string $calendarDate
* @param integer $minTimestamp
* @param integer $maxTimestamp
* @return boolean
*/
protected function isOutOfRange($calendarDate, $minTimestamp, $maxTimestamp)
{
$timestamp = strtotime(explode('T', $calendarDate)[0]);
return $timestamp < $minTimestamp || $timestamp > $maxTimestamp;
}
/**
* Unfolds an iCal file in preparation for parsing
* (https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html)
*
* @param array $lines
* @return array
*/
protected function unfold(array $lines)
{
$string = implode(PHP_EOL, $lines);
$string = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string);
$lines = explode(PHP_EOL, $string);
return $lines;
}
/**
* Add one key and value pair to the `$this->cal` array
*
* @param string $component
* @param string|boolean $keyword
* @param string $value
* @return void
*/
protected function addCalendarComponentWithKeyAndValue($component, $keyword, $value)
{
if ($keyword == false) {
$keyword = $this->lastKeyword;
}
switch ($component) {
case 'VALARM':
$key1 = 'VEVENT';
$key2 = ($this->eventCount - 1);
$key3 = $component;
if (!isset($this->cal[$key1][$key2][$key3]["{$keyword}_array"])) {
$this->cal[$key1][$key2][$key3]["{$keyword}_array"] = array();
}
if (is_array($value)) {
// Add array of properties to the end
array_push($this->cal[$key1][$key2][$key3]["{$keyword}_array"], $value);
} else {
if (!isset($this->cal[$key1][$key2][$key3][$keyword])) {
$this->cal[$key1][$key2][$key3][$keyword] = $value;
}
if ($this->cal[$key1][$key2][$key3][$keyword] !== $value) {
$this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value;
}
}
break;
case 'VEVENT':
$key1 = $component;
$key2 = ($this->eventCount - 1);
if (!isset($this->cal[$key1][$key2]["{$keyword}_array"])) {
$this->cal[$key1][$key2]["{$keyword}_array"] = array();
}
if (is_array($value)) {
// Add array of properties to the end
array_push($this->cal[$key1][$key2]["{$keyword}_array"], $value);
} else {
if (!isset($this->cal[$key1][$key2][$keyword])) {
$this->cal[$key1][$key2][$keyword] = $value;
}
if ($keyword === 'EXDATE') {
if (trim($value) === $value) {
$array = array_filter(explode(',', $value));
$this->cal[$key1][$key2]["{$keyword}_array"][] = $array;
} else {
$value = explode(',', implode(',', $this->cal[$key1][$key2]["{$keyword}_array"][1]) . trim($value));
$this->cal[$key1][$key2]["{$keyword}_array"][1] = $value;
}
} else {
$this->cal[$key1][$key2]["{$keyword}_array"][] = $value;
if ($keyword === 'DURATION') {
$duration = new \DateInterval($value);
array_push($this->cal[$key1][$key2]["{$keyword}_array"], $duration);
}
}
if ($this->cal[$key1][$key2][$keyword] !== $value) {
$this->cal[$key1][$key2][$keyword] .= ',' . $value;
}
}
break;
case 'VFREEBUSY':
$key1 = $component;
$key2 = ($this->freeBusyIndex - 1);
$key3 = $keyword;
if ($keyword === 'FREEBUSY') {
if (is_array($value)) {
$this->cal[$key1][$key2][$key3][][] = $value;
} else {
$this->freeBusyCount++;
end($this->cal[$key1][$key2][$key3]);
$key = key($this->cal[$key1][$key2][$key3]);
$value = explode('/', $value);
$this->cal[$key1][$key2][$key3][$key][] = $value;
}
} else {
$this->cal[$key1][$key2][$key3][] = $value;
}
break;
case 'VTODO':
$this->cal[$component][$this->todoCount - 1][$keyword] = $value;
break;
default:
$this->cal[$component][$keyword] = $value;
break;
}
$this->lastKeyword = $keyword;
}
/**
* Gets the key value pair from an iCal string
*
* @param string $text
* @return array|boolean
*/
protected function keyValueFromString($text)
{
$text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
$colon = strpos($text, ':');
$quote = strpos($text, '"');
if ($colon === false) {
$matches = array();
} elseif ($quote === false || $colon < $quote) {
list($before, $after) = explode(':', $text, 2);
$matches = array($text, $before, $after);
} else {
list($before, $text) = explode('"', $text, 2);
$text = '"' . $text;
$matches = str_getcsv($text, ':');
$combinedValue = '';
foreach ($matches as $key => $match) {
if ($key === 0) {
if (!empty($before)) {
$matches[$key] = $before . '"' . $matches[$key] . '"';
}
} else {
if ($key > 1) {
$combinedValue .= ':';
}
$combinedValue .= $matches[$key];
}
}
$matches = array_slice($matches, 0, 2);
$matches[1] = $combinedValue;
array_unshift($matches, $before . $text);
}
if (count($matches) === 0) {
return false;
}
if (preg_match('/^([A-Z-]+)([;][\w\W]*)?$/', $matches[1])) {
$matches = array_splice($matches, 1, 2); // Remove first match and re-align ordering
// Process properties
if (preg_match('/([A-Z-]+)[;]([\w\W]*)/', $matches[0], $properties)) {
// Remove first match
array_shift($properties);
// Fix to ignore everything in keyword after a ; (e.g. Language, TZID, etc.)
$matches[0] = $properties[0];
array_shift($properties); // Repeat removing first match
$formatted = array();
foreach ($properties as $property) {
// Match semicolon separator outside of quoted substrings
preg_match_all('~[^' . PHP_EOL . '";]+(?:"[^"\\\]*(?:\\\.[^"\\\]*)*"[^' . PHP_EOL . '";]*)*~', $property, $attributes);
// Remove multi-dimensional array and use the first key
$attributes = (sizeof($attributes) === 0) ? array($property) : reset($attributes);
if (is_array($attributes)) {
foreach ($attributes as $attribute) {
// Match equals sign separator outside of quoted substrings
preg_match_all(
'~[^' . PHP_EOL . '"=]+(?:"[^"\\\]*(?:\\\.[^"\\\]*)*"[^' . PHP_EOL . '"=]*)*~',
$attribute,
$values
);
// Remove multi-dimensional array and use the first key
$value = (sizeof($values) === 0) ? null : reset($values);
if (is_array($value) && isset($value[1])) {
// Remove double quotes from beginning and end only
$formatted[$value[0]] = trim($value[1], '"');
}
}
}
}
// Assign the keyword property information
$properties[0] = $formatted;
// Add match to beginning of array
array_unshift($properties, $matches[1]);
$matches[1] = $properties;
}
return $matches;
} else {
return false; // Ignore this match
}
}
/**
* Returns a `DateTime` object from an iCal date time format
*
* @param string $icalDate
* @param boolean $forceTimeZone
* @param boolean $forceUtc
* @return \DateTime
* @throws \Exception
*/
public function iCalDateToDateTime($icalDate, $forceTimeZone = false, $forceUtc = false)
{
/**
* iCal times may be in 3 formats, (https://www.kanzaki.com/docs/ical/dateTime.html)
*
* UTC: Has a trailing 'Z'
* Floating: No time zone reference specified, no trailing 'Z', use local time
* TZID: Set time zone as specified
*
* Use DateTime class objects to get around limitations with `mktime` and `gmmktime`.
* Must have a local time zone set to process floating times.
*/
$pattern = '/\AT?Z?I?D?=?(.*):?'; // [1]: Time zone
$pattern .= '([0-9]{4})'; // [2]: YYYY
$pattern .= '([0-9]{2})'; // [3]: MM
$pattern .= '([0-9]{2})'; // [4]: DD
$pattern .= 'T?'; // Time delimiter
$pattern .= '([0-9]{0,2})'; // [5]: HH
$pattern .= '([0-9]{0,2})'; // [6]: MM
$pattern .= '([0-9]{0,2})'; // [7]: SS
$pattern .= '(Z?)/'; // [8]: UTC flag
preg_match($pattern, $icalDate, $date);
if (empty($date)) {
throw new \Exception('Invalid iCal date format.');
}
// A Unix timestamp cannot represent a date prior to 1 Jan 1970
$year = $date[2];
$isUtc = false;
if ($year <= self::UNIX_MIN_YEAR) {
$eventTimeZone = ltrim(strstr($icalDate, ':', true), 'TZID=');
if (empty($eventTimeZone)) {
$dateTime = new \DateTime($icalDate, new \DateTimeZone($this->defaultTimeZone));
} else {
$icalDate = ltrim(strstr($icalDate, ':'), ':');
$dateTime = new \DateTime($icalDate, new \DateTimeZone($eventTimeZone));
}
} else {
if ($forceTimeZone) {
// TZID={Time Zone}:
if (isset($date[1])) {
$eventTimeZone = rtrim($date[1], ':');
}
if ($date[8] === 'Z') {
$isUtc = true;
$dateTime = new \DateTime('now', new \DateTimeZone(self::TIME_ZONE_UTC));
} elseif (isset($eventTimeZone) && $this->isValidIanaTimeZoneId($eventTimeZone)) {
$dateTime = new \DateTime('now', new \DateTimeZone($eventTimeZone));
} elseif (isset($eventTimeZone) && $this->isValidCldrTimeZoneId($eventTimeZone)) {
$dateTime = new \DateTime('now', new \DateTimeZone($this->isValidCldrTimeZoneId($eventTimeZone, true)));
} else {
$dateTime = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
}
} else {
if ($forceUtc) {
$dateTime = new \DateTime('now', new \DateTimeZone(self::TIME_ZONE_UTC));
} else {
$dateTime = new \DateTime('now');
}
}
$dateTime->setDate((int) $date[2], (int) $date[3], (int) $date[4]);
$dateTime->setTime((int) $date[5], (int) $date[6], (int) $date[7]);
}
if ($forceTimeZone && $isUtc) {
$dateTime->setTimezone(new \DateTimeZone($this->defaultTimeZone));
} elseif ($forceUtc) {
$dateTime->setTimezone(new \DateTimeZone(self::TIME_ZONE_UTC));
}
return $dateTime;
}
/**
* Returns a Unix timestamp from an iCal date time format
*
* @param string $icalDate
* @param boolean $forceTimeZone
* @param boolean $forceUtc
* @return integer
*/
public function iCalDateToUnixTimestamp($icalDate, $forceTimeZone = false, $forceUtc = false)
{
$dateTime = $this->iCalDateToDateTime($icalDate, $forceTimeZone, $forceUtc);
return $dateTime->getTimestamp();
}
/**
* Returns a date adapted to the calendar time zone depending on the event `TZID`
*
* @param array $event
* @param string $key
* @param string $format
* @return string|boolean
*/
public function iCalDateWithTimeZone(array $event, $key, $format = self::DATE_TIME_FORMAT)
{
if (!isset($event[$key . '_array']) || !isset($event[$key])) {
return false;
}
$dateArray = $event[$key . '_array'];
if ($key === 'DURATION') {
$duration = end($dateArray);
$dateTime = $this->parseDuration($event['DTSTART'], $duration, null);
} else {
$dateTime = new \DateTime($dateArray[1], new \DateTimeZone(self::TIME_ZONE_UTC));
$dateTime->setTimezone(new \DateTimeZone($this->calendarTimeZone()));
}
// Force time zone
if (isset($dateArray[0]['TZID'])) {
if ($this->isValidIanaTimeZoneId($dateArray[0]['TZID'])) {
$dateTime->setTimezone(new \DateTimeZone($dateArray[0]['TZID']));
} elseif ($this->isValidCldrTimeZoneId($dateArray[0]['TZID'])) {
$dateTime->setTimezone(new \DateTimeZone($this->isValidCldrTimeZoneId($dateArray[0]['TZID'], true)));
} else {
$dateTime->setTimezone(new \DateTimeZone($this->defaultTimeZone));
}
}
if (is_null($format)) {
$output = $dateTime;
} else {
if ($format === self::UNIX_FORMAT) {
$output = $dateTime->getTimestamp();
} else {
$output = $dateTime->format($format);
}
}
return $output;
}
/**
* Performs admin tasks on all events as read from the iCal file.
* Adds a Unix timestamp to all `{DTSTART|DTEND|RECURRENCE-ID}_array` arrays
* Tracks modified recurrence instances
*
* @return void
*/
protected function processEvents()
{
$events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
if (!empty($events)) {
foreach ($events as $key => $anEvent) {
foreach (array('DTSTART', 'DTEND', 'RECURRENCE-ID') as $type) {
if (isset($anEvent[$type])) {
$date = $anEvent[$type . '_array'][1];
if (isset($anEvent[$type . '_array'][0]['TZID'])) {
$date = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $anEvent[$type . '_array'][0]['TZID']) . $date;
}
$anEvent[$type . '_array'][2] = $this->iCalDateToUnixTimestamp($date, true, true);
$anEvent[$type . '_array'][3] = $date;
}
}
if (isset($anEvent['RECURRENCE-ID'])) {
$uid = $anEvent['UID'];
if (!isset($this->alteredRecurrenceInstances[$uid])) {
$this->alteredRecurrenceInstances[$uid] = array();
}
$recurrenceDateUtc = $this->iCalDateToUnixTimestamp($anEvent['RECURRENCE-ID_array'][3], true, true);
$this->alteredRecurrenceInstances[$uid][$key] = $recurrenceDateUtc;
}
$events[$key] = $anEvent;
}
$eventKeysToRemove = array();
foreach ($events as $key => $event) {
$checks[] = !isset($event['RECURRENCE-ID']);
$checks[] = isset($event['UID']);
$checks[] = isset($event['UID']) && isset($this->alteredRecurrenceInstances[$event['UID']]);
if ((bool) array_product($checks)) {
$eventDtstartUnix = $this->iCalDateToUnixTimestamp($event['DTSTART_array'][3], true, true);
if (false !== $alteredEventKey = array_search($eventDtstartUnix, $this->alteredRecurrenceInstances[$event['UID']])) {
$eventKeysToRemove[] = $alteredEventKey;
$alteredEvent = array_replace_recursive($events[$key], $events[$alteredEventKey]);
$this->alteredRecurrenceInstances[$event['UID']]['altered-event'] = array($key => $alteredEvent);
}
}
unset($checks);
}
if (!empty($eventKeysToRemove)) {
foreach ($eventKeysToRemove as $eventKeyToRemove) {
$events[$eventKeyToRemove] = null;
}
}
$this->cal['VEVENT'] = $events;
}
}
/**
* Processes recurrence rules
*
* @return void
*/
protected function processRecurrences()
{
$events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
$recurrenceEvents = array();
$allRecurrenceEvents = array();
if (!empty($events)) {
foreach ($events as $anEvent) {
if (isset($anEvent['RRULE']) && $anEvent['RRULE'] !== '') {
// Tag as generated by a recurrence rule
$anEvent['RRULE_array'][2] = self::RECURRENCE_EVENT;
$countNb = 0;
$initialStart = new \DateTime($anEvent['DTSTART_array'][1]);
$initialStartTimeZoneName = $initialStart->getTimezone()->getName();
if (isset($anEvent['DTEND'])) {
$initialEnd = new \DateTime($anEvent['DTEND_array'][1]);
$initialEndTimeZoneName = $initialEnd->getTimezone()->getName();
} else {
$initialEndTimeZoneName = $initialStartTimeZoneName;
}
// Recurring event, parse RRULE and add appropriate duplicate events
$rrules = array();
$rruleStrings = explode(';', $anEvent['RRULE']);
foreach ($rruleStrings as $s) {
list($k, $v) = explode('=', $s);
$rrules[$k] = $v;
}
// Get frequency
$frequency = $rrules['FREQ'];
// Get Start timestamp
$startTimestamp = $initialStart->getTimestamp();
if (isset($anEvent['DTEND'])) {
$endTimestamp = $initialEnd->getTimestamp();
} elseif (isset($anEvent['DURATION'])) {
$duration = end($anEvent['DURATION_array']);
$endTimestamp = $this->parseDuration($anEvent['DTSTART'], $duration);
} else {
$endTimestamp = $anEvent['DTSTART_array'][2];
}
$eventTimestampOffset = $endTimestamp - $startTimestamp;
// Get Interval
$interval = (isset($rrules['INTERVAL']) && $rrules['INTERVAL'] !== '') ? $rrules['INTERVAL'] : 1;
$dayNumber = null;
$weekday = null;
if (in_array($frequency, array('MONTHLY', 'YEARLY')) && isset($rrules['BYDAY']) && $rrules['BYDAY'] !== '') {
// Deal with BYDAY
$byDay = $rrules['BYDAY'];
$dayNumber = intval($byDay);
if (empty($dayNumber)) { // Returns 0 when no number defined in BYDAY
if (!isset($rrules['BYSETPOS'])) {
$dayNumber = 1; // Set first as default
} elseif (is_numeric($rrules['BYSETPOS'])) {
$dayNumber = $rrules['BYSETPOS'];
}
}
$weekday = substr($byDay, -2);
}
if (is_int($this->defaultSpan)) {
$untilDefault = date_create('now');
$untilDefault->modify($this->defaultSpan . ' year');
$untilDefault->setTime(23, 59, 59); // End of the day
} else {
trigger_error('ICal::defaultSpan: User defined value is not an integer', E_USER_NOTICE);
}
// Compute EXDATEs
$exdates = $this->parseExdates($anEvent);
$countOrig = null;
if (isset($rrules['UNTIL'])) {
// Get Until
$until = strtotime($rrules['UNTIL']);
} elseif (isset($rrules['COUNT'])) {
$countOrig = (is_numeric($rrules['COUNT']) && $rrules['COUNT'] > 1) ? $rrules['COUNT'] : 0;
// Increment count by the number of excluded dates
$countOrig += sizeof($exdates);
// Remove one to exclude the occurrence that initialises the rule
$count = ($countOrig - 1);
if ($interval >= 2) {
$count += ($count > 0) ? ($count * $interval) : 0;
}
$countNb = 1;
$offset = "+{$count} " . $this->frequencyConversion[$frequency];
$until = strtotime($offset, $startTimestamp);
if (in_array($frequency, array('MONTHLY', 'YEARLY'))
&& isset($rrules['BYDAY']) && $rrules['BYDAY'] !== ''
) {
$dtstart = date_create($anEvent['DTSTART']);
if (!$dtstart) {
continue;
}
for ($i = 1; $i <= $count; $i++) {
$dtstartClone = clone $dtstart;
$dtstartClone->modify('next ' . $this->frequencyConversion[$frequency]);
$offset = "{$this->convertDayOrdinalToPositive($dayNumber, $weekday, $dtstartClone)} {$this->weekdays[$weekday]} of " . $dtstartClone->format('F Y H:i:01');
$dtstart->modify($offset);
}
// Jumping X months forwards doesn't mean
// the end date will fall on the same day defined in BYDAY
// Use the largest of these to ensure we are going far enough
// in the future to capture our final end day
$until = max($until, $dtstart->format(self::UNIX_FORMAT));
}
unset($offset);
} elseif (isset($untilDefault)) {
$until = $untilDefault->getTimestamp();
}
$until = intval($until);
// Decide how often to add events and do so
switch ($frequency) {
case 'DAILY':
// Simply add a new event each interval of days until UNTIL is reached
$offset = "+{$interval} day";
$recurringTimestamp = strtotime($offset, $startTimestamp);
while ($recurringTimestamp <= $until) {
$dayRecurringTimestamp = $recurringTimestamp;
// Adjust time zone from initial event
$dayRecurringOffset = 0;
if ($this->useTimeZoneWithRRules) {
$recurringTimeZone = \DateTime::createFromFormat(self::UNIX_FORMAT, $dayRecurringTimestamp);
$recurringTimeZone->setTimezone($initialStart->getTimezone());
$dayRecurringOffset = $recurringTimeZone->getOffset();
$dayRecurringTimestamp += $dayRecurringOffset;
}
// Add event
$anEvent['DTSTART'] = date(self::DATE_TIME_FORMAT, $dayRecurringTimestamp) . (($initialStartTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTSTART_array'][1] = $anEvent['DTSTART'];
$anEvent['DTSTART_array'][2] = $dayRecurringTimestamp;
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = date(
self::DATE_TIME_FORMAT,
$anEvent['DTEND_array'][2]
) . (($initialEndTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
// Exclusions
$isExcluded = array_filter($exdates, function ($exdate) use ($anEvent, $dayRecurringOffset) {
return self::isExdateMatch($exdate, $anEvent, $dayRecurringOffset);
});
if (isset($anEvent['UID'])) {
$searchDate = $anEvent['DTSTART'];
if (isset($anEvent['DTSTART_array'][0]['TZID'])) {
$searchDate = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $anEvent['DTSTART_array'][0]['TZID']) . $searchDate;
}
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
$searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true);
if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
}
}
if (!$isExcluded) {
$anEvent = $this->processEventIcalDateTime($anEvent);
$recurrenceEvents[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break;
}
}
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
$recurrenceEvents = $this->trimToRecurrenceCount($rrules, $recurrenceEvents);
$allRecurrenceEvents = array_merge($allRecurrenceEvents, $recurrenceEvents);
$recurrenceEvents = array(); // Reset
break;
case 'WEEKLY':
// Create offset
$offset = "+{$interval} week";
$wkst = (isset($rrules['WKST']) && in_array($rrules['WKST'], array('SA', 'SU', 'MO'))) ? $rrules['WKST'] : $this->defaultWeekStart;
$aWeek = $this->weeks[$wkst];
$days = array('SA' => 'Saturday', 'SU' => 'Sunday', 'MO' => 'Monday');
// Build list of days of week to add events
$weekdays = $aWeek;
if (isset($rrules['BYDAY']) && $rrules['BYDAY'] !== '') {
$byDays = explode(',', $rrules['BYDAY']);
} else {
// A textual representation of a day, two letters (e.g. SU)
$byDays = array(mb_substr(strtoupper($initialStart->format('D')), 0, 2));
}
// Get timestamp of first day of start week
$weekRecurringTimestamp = (strcasecmp($initialStart->format('l'), $this->weekdays[$wkst]) === 0)
? $startTimestamp
: strtotime("last {$days[$wkst]} " . $initialStart->format('H:i:s'), $startTimestamp);
// Step through weeks
while ($weekRecurringTimestamp <= $until) {
$dayRecurringTimestamp = $weekRecurringTimestamp;
// Adjust time zone from initial event
$dayRecurringOffset = 0;
if ($this->useTimeZoneWithRRules) {
$dayRecurringTimeZone = \DateTime::createFromFormat(self::UNIX_FORMAT, $dayRecurringTimestamp);
$dayRecurringTimeZone->setTimezone($initialStart->getTimezone());
$dayRecurringOffset = $dayRecurringTimeZone->getOffset();
$dayRecurringTimestamp += $dayRecurringOffset;
}
foreach ($weekdays as $day) {
// Check if day should be added
if (in_array($day, $byDays) && $dayRecurringTimestamp > $startTimestamp
&& $dayRecurringTimestamp <= $until
) {
// Add event
$anEvent['DTSTART'] = date(self::DATE_TIME_FORMAT, $dayRecurringTimestamp) . (($initialStartTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTSTART_array'][1] = $anEvent['DTSTART'];
$anEvent['DTSTART_array'][2] = $dayRecurringTimestamp;
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = date(
self::DATE_TIME_FORMAT,
$anEvent['DTEND_array'][2]
) . (($initialEndTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
// Exclusions
$isExcluded = array_filter($exdates, function ($exdate) use ($anEvent, $dayRecurringOffset) {
return self::isExdateMatch($exdate, $anEvent, $dayRecurringOffset);
});
if (isset($anEvent['UID'])) {
$searchDate = $anEvent['DTSTART'];
if (isset($anEvent['DTSTART_array'][0]['TZID'])) {
$searchDate = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $anEvent['DTSTART_array'][0]['TZID']) . $searchDate;
}
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
$searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true);
if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
}
}
if (!$isExcluded) {
$anEvent = $this->processEventIcalDateTime($anEvent);
$recurrenceEvents[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
// Move forwards a day
$dayRecurringTimestamp = strtotime('+1 day', $dayRecurringTimestamp);
}
// Move forwards $interval weeks
$weekRecurringTimestamp = strtotime($offset, $weekRecurringTimestamp);
}
$recurrenceEvents = $this->trimToRecurrenceCount($rrules, $recurrenceEvents);
$allRecurrenceEvents = array_merge($allRecurrenceEvents, $recurrenceEvents);
$recurrenceEvents = array(); // Reset
break;
case 'MONTHLY':
// Create offset
$recurringTimestamp = $startTimestamp;
$offset = "+{$interval} month";
if (isset($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== '') {
// Deal with BYMONTHDAY
$monthdays = explode(',', $rrules['BYMONTHDAY']);
while ($recurringTimestamp <= $until) {
foreach ($monthdays as $key => $monthday) {
$monthRecurringTimestamp = null;
if ($key === 0) {
// Ensure original event conforms to monthday rule
$anEvent['DTSTART'] = gmdate(
'Ym' . sprintf('%02d', $monthday) . '\T' . self::TIME_FORMAT,
strtotime($anEvent['DTSTART'])
) . (($initialStartTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTEND'] = gmdate(
'Ym' . sprintf('%02d', $monthday) . '\T' . self::TIME_FORMAT,
isset($anEvent['DURATION'])
? $this->parseDuration($anEvent['DTSTART'], end($anEvent['DURATION_array']))
: strtotime($anEvent['DTEND'])
) . (($initialEndTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTSTART_array'][1] = $anEvent['DTSTART'];
$anEvent['DTSTART_array'][2] = $this->iCalDateToUnixTimestamp($anEvent['DTSTART']);
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
$anEvent['DTEND_array'][2] = $this->iCalDateToUnixTimestamp($anEvent['DTEND']);
// Ensure recurring timestamp confirms to BYMONTHDAY rule
$monthRecurringTimestamp = $this->iCalDateToUnixTimestamp(
gmdate(
'Ym' . sprintf('%02d', $monthday) . '\T' . self::TIME_FORMAT,
$recurringTimestamp
) . (($initialStartTimeZoneName === 'Z') ? 'Z' : '')
);
}
// Adjust time zone from initial event
$monthRecurringOffset = 0;
if ($this->useTimeZoneWithRRules) {
$recurringTimeZone = \DateTime::createFromFormat(self::UNIX_FORMAT, $monthRecurringTimestamp);
$recurringTimeZone->setTimezone($initialStart->getTimezone());
$monthRecurringOffset = $recurringTimeZone->getOffset();
$monthRecurringTimestamp += $monthRecurringOffset;
}
if (($monthRecurringTimestamp > $startTimestamp) && ($monthRecurringTimestamp <= $until)) {
// Add event
$anEvent['DTSTART'] = date(
'Ym' . sprintf('%02d', $monthday) . '\T' . self::TIME_FORMAT,
$monthRecurringTimestamp
) . (($initialStartTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTSTART_array'][1] = $anEvent['DTSTART'];
$anEvent['DTSTART_array'][2] = $monthRecurringTimestamp;
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = date(
self::DATE_TIME_FORMAT,
$anEvent['DTEND_array'][2]
) . (($initialEndTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
// Exclusions
$isExcluded = array_filter($exdates, function ($exdate) use ($anEvent, $monthRecurringOffset) {
return self::isExdateMatch($exdate, $anEvent, $monthRecurringOffset);
});
if (isset($anEvent['UID'])) {
$searchDate = $anEvent['DTSTART'];
if (isset($anEvent['DTSTART_array'][0]['TZID'])) {
$searchDate = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $anEvent['DTSTART_array'][0]['TZID']) . $searchDate;
}
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
$searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true);
if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
}
}
if (!$isExcluded) {
$anEvent = $this->processEventIcalDateTime($anEvent);
$recurrenceEvents[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
} elseif (isset($rrules['BYDAY']) && $rrules['BYDAY'] !== '') {
while ($recurringTimestamp <= $until) {
$monthRecurringTimestamp = $recurringTimestamp;
// Adjust time zone from initial event
$monthRecurringOffset = 0;
if ($this->useTimeZoneWithRRules) {
$recurringTimeZone = \DateTime::createFromFormat(self::UNIX_FORMAT, $monthRecurringTimestamp);
$recurringTimeZone->setTimezone($initialStart->getTimezone());
$monthRecurringOffset = $recurringTimeZone->getOffset();
$monthRecurringTimestamp += $monthRecurringOffset;
}
$eventStartDesc = "{$this->convertDayOrdinalToPositive($dayNumber, $weekday, $monthRecurringTimestamp)} {$this->weekdays[$weekday]} of "
. date(self::DATE_TIME_FORMAT_PRETTY, $monthRecurringTimestamp);
$eventStartTimestamp = strtotime($eventStartDesc);
if (intval($rrules['BYDAY']) === 0) {
$lastDayDesc = "last {$this->weekdays[$weekday]} of "
. date(self::DATE_TIME_FORMAT_PRETTY, $monthRecurringTimestamp);
} else {
$lastDayDesc = "{$this->convertDayOrdinalToPositive($dayNumber, $weekday, $monthRecurringTimestamp)} {$this->weekdays[$weekday]} of "
. date(self::DATE_TIME_FORMAT_PRETTY, $monthRecurringTimestamp);
}
$lastDayTimestamp = strtotime($lastDayDesc);
do {
// Prevent 5th day of a month from showing up on the next month
// If BYDAY and the event falls outside the current month, skip the event
$compareCurrentMonth = date('F', $monthRecurringTimestamp);
$compareEventMonth = date('F', $eventStartTimestamp);
if ($compareCurrentMonth !== $compareEventMonth) {
$monthRecurringTimestamp = strtotime($offset, $monthRecurringTimestamp);
continue;
}
if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp <= $until) {
$anEvent['DTSTART'] = date(self::DATE_TIME_FORMAT, $eventStartTimestamp) . (($initialStartTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTSTART_array'][1] = $anEvent['DTSTART'];
$anEvent['DTSTART_array'][2] = $eventStartTimestamp;
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = date(
self::DATE_TIME_FORMAT,
$anEvent['DTEND_array'][2]
) . (($initialEndTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
// Exclusions
$isExcluded = array_filter($exdates, function ($exdate) use ($anEvent, $monthRecurringOffset) {
return self::isExdateMatch($exdate, $anEvent, $monthRecurringOffset);
});
if (isset($anEvent['UID'])) {
$searchDate = $anEvent['DTSTART'];
if (isset($anEvent['DTSTART_array'][0]['TZID'])) {
$searchDate = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $anEvent['DTSTART_array'][0]['TZID']) . $searchDate;
}
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
$searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true);
if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
}
}
if (!$isExcluded) {
$anEvent = $this->processEventIcalDateTime($anEvent);
$recurrenceEvents[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
if (isset($rrules['BYSETPOS'])) {
// BYSETPOS is defined so skip
// looping through each week
$lastDayTimestamp = $eventStartTimestamp;
}
$eventStartTimestamp += self::SECONDS_IN_A_WEEK;
} while ($eventStartTimestamp <= $lastDayTimestamp);
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
}
$recurrenceEvents = $this->trimToRecurrenceCount($rrules, $recurrenceEvents);
$allRecurrenceEvents = array_merge($allRecurrenceEvents, $recurrenceEvents);
$recurrenceEvents = array(); // Reset
break;
case 'YEARLY':
// Create offset
$recurringTimestamp = $startTimestamp;
$offset = "+{$interval} year";
// Deal with BYMONTH
if (isset($rrules['BYMONTH']) && $rrules['BYMONTH'] !== '') {
$bymonths = explode(',', $rrules['BYMONTH']);
} else {
$bymonths = array();
}
// Check if BYDAY rule exists
if (isset($rrules['BYDAY']) && $rrules['BYDAY'] !== '') {
while ($recurringTimestamp <= $until) {
$yearRecurringTimestamp = $recurringTimestamp;
// Adjust time zone from initial event
$yearRecurringOffset = 0;
if ($this->useTimeZoneWithRRules) {
$recurringTimeZone = \DateTime::createFromFormat(self::UNIX_FORMAT, $yearRecurringTimestamp);
$recurringTimeZone->setTimezone($initialStart->getTimezone());
$yearRecurringOffset = $recurringTimeZone->getOffset();
$yearRecurringTimestamp += $yearRecurringOffset;
}
foreach ($bymonths as $bymonth) {
$eventStartDesc = "{$this->convertDayOrdinalToPositive($dayNumber, $weekday, $yearRecurringTimestamp)} {$this->weekdays[$weekday]}"
. " of {$this->monthNames[$bymonth]} "
. gmdate('Y H:i:s', $yearRecurringTimestamp);
$eventStartTimestamp = strtotime($eventStartDesc);
if (intval($rrules['BYDAY']) === 0) {
$lastDayDesc = "last {$this->weekdays[$weekday]}"
. " of {$this->monthNames[$bymonth]} "
. gmdate('Y H:i:s', $yearRecurringTimestamp);
} else {
$lastDayDesc = "{$this->convertDayOrdinalToPositive($dayNumber, $weekday, $yearRecurringTimestamp)} {$this->weekdays[$weekday]}"
. " of {$this->monthNames[$bymonth]} "
. gmdate('Y H:i:s', $yearRecurringTimestamp);
}
$lastDayTimestamp = strtotime($lastDayDesc);
do {
if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp <= $until) {
$anEvent['DTSTART'] = date(self::DATE_TIME_FORMAT, $eventStartTimestamp) . (($initialStartTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTSTART_array'][1] = $anEvent['DTSTART'];
$anEvent['DTSTART_array'][2] = $eventStartTimestamp;
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = date(
self::DATE_TIME_FORMAT,
$anEvent['DTEND_array'][2]
) . (($initialEndTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
// Exclusions
$isExcluded = array_filter($exdates, function ($exdate) use ($anEvent, $yearRecurringOffset) {
return self::isExdateMatch($exdate, $anEvent, $yearRecurringOffset);
});
if (isset($anEvent['UID'])) {
$searchDate = $anEvent['DTSTART'];
if (isset($anEvent['DTSTART_array'][0]['TZID'])) {
$searchDate = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $anEvent['DTSTART_array'][0]['TZID']) . $searchDate;
}
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
$searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true);
if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
}
}
if (!$isExcluded) {
$anEvent = $this->processEventIcalDateTime($anEvent);
$recurrenceEvents[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 3;
}
}
}
}
$eventStartTimestamp += self::SECONDS_IN_A_WEEK;
} while ($eventStartTimestamp <= $lastDayTimestamp);
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
} else {
$day = $initialStart->format('d');
// Step through years
while ($recurringTimestamp <= $until) {
$yearRecurringTimestamp = $recurringTimestamp;
// Adjust time zone from initial event
$yearRecurringOffset = 0;
if ($this->useTimeZoneWithRRules) {
$recurringTimeZone = \DateTime::createFromFormat(self::UNIX_FORMAT, $yearRecurringTimestamp);
$recurringTimeZone->setTimezone($initialStart->getTimezone());
$yearRecurringOffset = $recurringTimeZone->getOffset();
$yearRecurringTimestamp += $yearRecurringOffset;
}
$eventStartDescs = array();
if (isset($rrules['BYMONTH']) && $rrules['BYMONTH'] !== '') {
foreach ($bymonths as $bymonth) {
array_push($eventStartDescs, "{$day} {$this->monthNames[$bymonth]} " . gmdate('Y H:i:s', $yearRecurringTimestamp));
}
} else {
array_push($eventStartDescs, $day . gmdate(self::DATE_TIME_FORMAT_PRETTY, $yearRecurringTimestamp));
}
foreach ($eventStartDescs as $eventStartDesc) {
$eventStartTimestamp = strtotime($eventStartDesc);
if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp <= $until) {
$anEvent['DTSTART'] = date(self::DATE_TIME_FORMAT, $eventStartTimestamp) . (($initialStartTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTSTART_array'][1] = $anEvent['DTSTART'];
$anEvent['DTSTART_array'][2] = $eventStartTimestamp;
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = date(
self::DATE_TIME_FORMAT,
$anEvent['DTEND_array'][2]
) . (($initialEndTimeZoneName === 'Z') ? 'Z' : '');
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
// Exclusions
$isExcluded = array_filter($exdates, function ($exdate) use ($anEvent, $yearRecurringOffset) {
return self::isExdateMatch($exdate, $anEvent, $yearRecurringOffset);
});
if (isset($anEvent['UID'])) {
$searchDate = $anEvent['DTSTART'];
if (isset($anEvent['DTSTART_array'][0]['TZID'])) {
$searchDate = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $anEvent['DTSTART_array'][0]['TZID']) . $searchDate;
}
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
$searchDateUtc = $this->iCalDateToUnixTimestamp($searchDate, true, true);
if (in_array($searchDateUtc, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
}
}
if (!$isExcluded) {
$anEvent = $this->processEventIcalDateTime($anEvent);
$recurrenceEvents[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
}
$recurrenceEvents = $this->trimToRecurrenceCount($rrules, $recurrenceEvents);
$allRecurrenceEvents = array_merge($allRecurrenceEvents, $recurrenceEvents);
$recurrenceEvents = array(); // Reset
break;
}
}
}
$events = array_merge($events, $allRecurrenceEvents);
$this->cal['VEVENT'] = $events;
}
}
/**
* Processes date conversions using the time zone
*
* Add keys `DTSTART_tz` and `DTEND_tz` to each Event
* These keys contain dates adapted to the calendar
* time zone depending on the event `TZID`.
*
* @return void
*/
protected function processDateConversions()
{
$events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
if (!empty($events)) {
foreach ($events as $key => $anEvent) {
if (!$this->isValidDate($anEvent['DTSTART'])) {
unset($events[$key]);
$this->eventCount--;
continue;
}
if ($this->useTimeZoneWithRRules && isset($anEvent['RRULE_array'][2]) && $anEvent['RRULE_array'][2] === self::RECURRENCE_EVENT) {
$events[$key]['DTSTART_tz'] = $anEvent['DTSTART'];
$events[$key]['DTEND_tz'] = isset($anEvent['DTEND']) ? $anEvent['DTEND'] : $anEvent['DTSTART'];
} else {
$events[$key]['DTSTART_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTSTART');
if ($this->iCalDateWithTimeZone($anEvent, 'DTEND')) {
$events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTEND');
} elseif ($this->iCalDateWithTimeZone($anEvent, 'DURATION')) {
$events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DURATION');
} elseif ($this->iCalDateWithTimeZone($anEvent, 'DTSTART')) {
$events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTSTART');
}
}
}
$this->cal['VEVENT'] = $events;
}
}
/**
* Extends the `{DTSTART|DTEND|RECURRENCE-ID}_array`
* array to include an iCal date time for each event
* (`TZID=Timezone:YYYYMMDD[T]HHMMSS`)
*
* @param array $event
* @param integer $index
* @return array
*/
protected function processEventIcalDateTime(array $event, $index = 3)
{
$calendarTimeZone = $this->calendarTimeZone(true);
foreach (array('DTSTART', 'DTEND', 'RECURRENCE-ID') as $type) {
if (isset($event["{$type}_array"])) {
$timeZone = (isset($event["{$type}_array"][0]['TZID'])) ? $event["{$type}_array"][0]['TZID'] : $calendarTimeZone;
$event["{$type}_array"][$index] = ((is_null($timeZone)) ? '' : sprintf(self::ICAL_DATE_TIME_TEMPLATE, $timeZone)) . $event["{$type}_array"][1];
$event["{$type}_array"][2] = $this->iCalDateToUnixTimestamp($event["{$type}_array"][3], true, true);
}
}
return $event;
}
/**
* Returns an array of Events.
* Every event is a class with the event
* details being properties within it.
*
* @return array
*/
public function events()
{
$array = $this->cal;
$array = isset($array['VEVENT']) ? $array['VEVENT'] : array();
$events = array();
if (!empty($array)) {
foreach ($array as $event) {
$events[] = new Event($event);
}
}
return $events;
}
/**
* Returns the calendar name
*
* @return string
*/
public function calendarName()
{
return isset($this->cal['VCALENDAR']['X-WR-CALNAME']) ? $this->cal['VCALENDAR']['X-WR-CALNAME'] : '';
}
/**
* Returns the calendar description
*
* @return string
*/
public function calendarDescription()
{
return isset($this->cal['VCALENDAR']['X-WR-CALDESC']) ? $this->cal['VCALENDAR']['X-WR-CALDESC'] : '';
}
/**
* Returns the calendar time zone
*
* @param boolean $ignoreUtc
* @return string
*/
public function calendarTimeZone($ignoreUtc = false)
{
if (isset($this->cal['VCALENDAR']['X-WR-TIMEZONE'])) {
$timeZone = $this->cal['VCALENDAR']['X-WR-TIMEZONE'];
} elseif (isset($this->cal['VTIMEZONE']['TZID'])) {
$timeZone = $this->cal['VTIMEZONE']['TZID'];
} else {
$timeZone = $this->defaultTimeZone;
}
// Use default time zone if the calendar's is invalid
if ($this->isValidIanaTimeZoneId($timeZone) === false) {
// phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition.Warning
if (($timeZone = $this->isValidCldrTimeZoneId($timeZone, true)) === false) {
$timeZone = $this->defaultTimeZone;
}
}
if ($ignoreUtc && strtoupper($timeZone) === self::TIME_ZONE_UTC) {
return null;
}
return $timeZone;
}
/**
* Returns an array of arrays with all free/busy events.
* Every event is an associative array and each property
* is an element it.
*
* @return array
*/
public function freeBusyEvents()
{
$array = $this->cal;
return isset($array['VFREEBUSY']) ? $array['VFREEBUSY'] : [];
}
/**
* Returns a boolean value whether the
* current calendar has events or not
*
* @return boolean
*/
public function hasEvents()
{
return (count($this->events()) > 0) ?: false;
}
/**
* Returns a sorted array of the events in a given range,
* or an empty array if no events exist in the range.
*
* Events will be returned if the start or end date is contained within the
* range (inclusive), or if the event starts before and end after the range.
*
* If a start date is not specified or of a valid format, then the start
* of the range will default to the current time and date of the server.
*
* If an end date is not specified or of a valid format, then the end of
* the range will default to the current time and date of the server,
* plus 20 years.
*
* Note that this function makes use of Unix timestamps. This might be a
* problem for events on, during, or after 29 Jan 2038.
* See https://en.wikipedia.org/wiki/Unix_time#Representing_the_number
*
* @param string|null $rangeStart
* @param string|null $rangeEnd
* @return array
* @throws \Exception
*/
public function eventsFromRange($rangeStart = null, $rangeEnd = null)
{
// Sort events before processing range
$events = $this->sortEventsWithOrder($this->events(), SORT_ASC);
if (empty($events)) {
return array();
}
$extendedEvents = array();
if (!is_null($rangeStart)) {
try {
$rangeStart = new \DateTime($rangeStart, new \DateTimeZone($this->defaultTimeZone));
} catch (\Exception $e) {
error_log("ICal::eventsFromRange: Invalid date passed ({$rangeStart})");
$rangeStart = false;
}
} else {
$rangeStart = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
}
if (!is_null($rangeEnd)) {
try {
$rangeEnd = new \DateTime($rangeEnd, new \DateTimeZone($this->defaultTimeZone));
} catch (\Exception $e) {
error_log("ICal::eventsFromRange: Invalid date passed ({$rangeEnd})");
$rangeEnd = false;
}
} else {
$rangeEnd = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
$rangeEnd->modify('+20 years');
}
// If start and end are identical and are dates with no times...
if ($rangeEnd->format('His') == 0 && $rangeStart->getTimestamp() == $rangeEnd->getTimestamp()) {
$rangeEnd->modify('+1 day');
}
$rangeStart = $rangeStart->getTimestamp();
$rangeEnd = $rangeEnd->getTimestamp();
foreach ($events as $anEvent) {
$eventStart = $anEvent->dtstart_array[2];
$eventEnd = (isset($anEvent->dtend_array[2])) ? $anEvent->dtend_array[2] : null;
if (($eventStart >= $rangeStart && $eventStart < $rangeEnd) // Event start date contained in the range
|| ($eventEnd !== null
&& (
($eventEnd > $rangeStart && $eventEnd <= $rangeEnd) // Event end date contained in the range
|| ($eventStart < $rangeStart && $eventEnd > $rangeEnd) // Event starts before and finishes after range
)
)
) {
$extendedEvents[] = $anEvent;
}
}
if (empty($extendedEvents)) {
return array();
}
return $extendedEvents;
}
/**
* Returns a sorted array of the events following a given string,
* or `false` if no events exist in the range.
*
* @param string $interval
* @return array
*/
public function eventsFromInterval($interval)
{
$rangeStart = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
$rangeEnd = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
$dateInterval = \DateInterval::createFromDateString($interval);
$rangeEnd->add($dateInterval);
return $this->eventsFromRange($rangeStart->format('Y-m-d'), $rangeEnd->format('Y-m-d'));
}
/**
* Sorts events based on a given sort order
*
* @param array $events
* @param integer $sortOrder Either SORT_ASC, SORT_DESC, SORT_REGULAR, SORT_NUMERIC, SORT_STRING
* @return array
*/
public function sortEventsWithOrder(array $events, $sortOrder = SORT_ASC)
{
$extendedEvents = array();
$timestamp = array();
foreach ($events as $key => $anEvent) {
$extendedEvents[] = $anEvent;
$timestamp[$key] = $anEvent->dtstart_array[2];
}
array_multisort($timestamp, $sortOrder, $extendedEvents);
return $extendedEvents;
}
/**
* Checks if a time zone is valid (IANA or CLDR)
*
* @param string $timeZone
* @return boolean
*/
protected function isValidTimeZoneId($timeZone)
{
return ($this->isValidIanaTimeZoneId($timeZone) !== false || $this->isValidCldrTimeZoneId($timeZone) !== false);
}
/**
* Checks if a time zone is a valid IANA time zone
*
* @param string $timeZone
* @return boolean
*/
protected function isValidIanaTimeZoneId($timeZone)
{
if (in_array($timeZone, $this->validTimeZones)) {
return true;
}
$valid = array();
$tza = timezone_abbreviations_list();
foreach ($tza as $zone) {
foreach ($zone as $item) {
$valid[$item['timezone_id']] = true;
}
}
unset($valid['']);
if (isset($valid[$timeZone]) || in_array($timeZone, timezone_identifiers_list(\DateTimeZone::ALL_WITH_BC))) {
$this->validTimeZones[] = $timeZone;
return true;
}
return false;
}
/**
* Checks if a time zone is a valid CLDR time zone
*
* @param string $timeZone
* @param boolean $doConversion
* @return boolean|string
*/
public function isValidCldrTimeZoneId($timeZone, $doConversion = false)
{
$timeZone = html_entity_decode($timeZone);
$cldrTimeZones = array(
'(UTC-12:00) International Date Line West' => 'Etc/GMT+12',
'(UTC-11:00) Coordinated Universal Time-11' => 'Etc/GMT+11',
'(UTC-10:00) Hawaii' => 'Pacific/Honolulu',
'(UTC-09:00) Alaska' => 'America/Anchorage',
'(UTC-08:00) Pacific Time (US & Canada)' => 'America/Los_Angeles',
'(UTC-07:00) Arizona' => 'America/Phoenix',
'(UTC-07:00) Chihuahua, La Paz, Mazatlan' => 'America/Chihuahua',
'(UTC-07:00) Mountain Time (US & Canada)' => 'America/Denver',
'(UTC-06:00) Central America' => 'America/Guatemala',
'(UTC-06:00) Central Time (US & Canada)' => 'America/Chicago',
'(UTC-06:00) Guadalajara, Mexico City, Monterrey' => 'America/Mexico_City',
'(UTC-06:00) Saskatchewan' => 'America/Regina',
'(UTC-05:00) Bogota, Lima, Quito, Rio Branco' => 'America/Bogota',
'(UTC-05:00) Chetumal' => 'America/Cancun',
'(UTC-05:00) Eastern Time (US & Canada)' => 'America/New_York',
'(UTC-05:00) Indiana (East)' => 'America/Indianapolis',
'(UTC-04:00) Asuncion' => 'America/Asuncion',
'(UTC-04:00) Atlantic Time (Canada)' => 'America/Halifax',
'(UTC-04:00) Caracas' => 'America/Caracas',
'(UTC-04:00) Cuiaba' => 'America/Cuiaba',
'(UTC-04:00) Georgetown, La Paz, Manaus, San Juan' => 'America/La_Paz',
'(UTC-04:00) Santiago' => 'America/Santiago',
'(UTC-03:30) Newfoundland' => 'America/St_Johns',
'(UTC-03:00) Brasilia' => 'America/Sao_Paulo',
'(UTC-03:00) Cayenne, Fortaleza' => 'America/Cayenne',
'(UTC-03:00) City of Buenos Aires' => 'America/Buenos_Aires',
'(UTC-03:00) Greenland' => 'America/Godthab',
'(UTC-03:00) Montevideo' => 'America/Montevideo',
'(UTC-03:00) Salvador' => 'America/Bahia',
'(UTC-02:00) Coordinated Universal Time-02' => 'Etc/GMT+2',
'(UTC-01:00) Azores' => 'Atlantic/Azores',
'(UTC-01:00) Cabo Verde Is.' => 'Atlantic/Cape_Verde',
'(UTC) Coordinated Universal Time' => 'Etc/GMT',
'(UTC+00:00) Casablanca' => 'Africa/Casablanca',
'(UTC+00:00) Dublin, Edinburgh, Lisbon, London' => 'Europe/London',
'(UTC+00:00) Monrovia, Reykjavik' => 'Atlantic/Reykjavik',
'(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
'(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest',
'(UTC+01:00) Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
'(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb' => 'Europe/Warsaw',
'(UTC+01:00) West Central Africa' => 'Africa/Lagos',
'(UTC+02:00) Amman' => 'Asia/Amman',
'(UTC+02:00) Athens, Bucharest' => 'Europe/Bucharest',
'(UTC+02:00) Beirut' => 'Asia/Beirut',
'(UTC+02:00) Cairo' => 'Africa/Cairo',
'(UTC+02:00) Chisinau' => 'Europe/Chisinau',
'(UTC+02:00) Damascus' => 'Asia/Damascus',
'(UTC+02:00) Harare, Pretoria' => 'Africa/Johannesburg',
'(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' => 'Europe/Kiev',
'(UTC+02:00) Jerusalem' => 'Asia/Jerusalem',
'(UTC+02:00) Kaliningrad' => 'Europe/Kaliningrad',
'(UTC+02:00) Tripoli' => 'Africa/Tripoli',
'(UTC+02:00) Windhoek' => 'Africa/Windhoek',
'(UTC+03:00) Baghdad' => 'Asia/Baghdad',
'(UTC+03:00) Istanbul' => 'Europe/Istanbul',
'(UTC+03:00) Kuwait, Riyadh' => 'Asia/Riyadh',
'(UTC+03:00) Minsk' => 'Europe/Minsk',
'(UTC+03:00) Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
'(UTC+03:00) Nairobi' => 'Africa/Nairobi',
'(UTC+03:30) Tehran' => 'Asia/Tehran',
'(UTC+04:00) Abu Dhabi, Muscat' => 'Asia/Dubai',
'(UTC+04:00) Baku' => 'Asia/Baku',
'(UTC+04:00) Izhevsk, Samara' => 'Europe/Samara',
'(UTC+04:00) Port Louis' => 'Indian/Mauritius',
'(UTC+04:00) Tbilisi' => 'Asia/Tbilisi',
'(UTC+04:00) Yerevan' => 'Asia/Yerevan',
'(UTC+04:30) Kabul' => 'Asia/Kabul',
'(UTC+05:00) Ashgabat, Tashkent' => 'Asia/Tashkent',
'(UTC+05:00) Ekaterinburg' => 'Asia/Yekaterinburg',
'(UTC+05:00) Islamabad, Karachi' => 'Asia/Karachi',
'(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi' => 'Asia/Calcutta',
'(UTC+05:30) Sri Jayawardenepura' => 'Asia/Colombo',
'(UTC+05:45) Kathmandu' => 'Asia/Katmandu',
'(UTC+06:00) Astana' => 'Asia/Almaty',
'(UTC+06:00) Dhaka' => 'Asia/Dhaka',
'(UTC+06:30) Yangon (Rangoon)' => 'Asia/Rangoon',
'(UTC+07:00) Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
'(UTC+07:00) Krasnoyarsk' => 'Asia/Krasnoyarsk',
'(UTC+07:00) Novosibirsk' => 'Asia/Novosibirsk',
'(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi' => 'Asia/Shanghai',
'(UTC+08:00) Irkutsk' => 'Asia/Irkutsk',
'(UTC+08:00) Kuala Lumpur, Singapore' => 'Asia/Singapore',
'(UTC+08:00) Perth' => 'Australia/Perth',
'(UTC+08:00) Taipei' => 'Asia/Taipei',
'(UTC+08:00) Ulaanbaatar' => 'Asia/Ulaanbaatar',
'(UTC+09:00) Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
'(UTC+09:00) Pyongyang' => 'Asia/Pyongyang',
'(UTC+09:00) Seoul' => 'Asia/Seoul',
'(UTC+09:00) Yakutsk' => 'Asia/Yakutsk',
'(UTC+09:30) Adelaide' => 'Australia/Adelaide',
'(UTC+09:30) Darwin' => 'Australia/Darwin',
'(UTC+10:00) Brisbane' => 'Australia/Brisbane',
'(UTC+10:00) Canberra, Melbourne, Sydney' => 'Australia/Sydney',
'(UTC+10:00) Guam, Port Moresby' => 'Pacific/Port_Moresby',
'(UTC+10:00) Hobart' => 'Australia/Hobart',
'(UTC+10:00) Vladivostok' => 'Asia/Vladivostok',
'(UTC+11:00) Chokurdakh' => 'Asia/Srednekolymsk',
'(UTC+11:00) Magadan' => 'Asia/Magadan',
'(UTC+11:00) Solomon Is., New Caledonia' => 'Pacific/Guadalcanal',
'(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky' => 'Asia/Kamchatka',
'(UTC+12:00) Auckland, Wellington' => 'Pacific/Auckland',
'(UTC+12:00) Coordinated Universal Time+12' => 'Etc/GMT-12',
'(UTC+12:00) Fiji' => 'Pacific/Fiji',
"(UTC+13:00) Nuku'alofa" => 'Pacific/Tongatapu',
'(UTC+13:00) Samoa' => 'Pacific/Apia',
'(UTC+14:00) Kiritimati Island' => 'Pacific/Kiritimati',
);
if (array_key_exists($timeZone, $cldrTimeZones)) {
if ($doConversion) {
return $cldrTimeZones[$timeZone];
} else {
return true;
}
}
return false;
}
/**
* Parses a duration and applies it to a date
*
* @param string $date
* @param string $duration
* @param string $format
* @return integer|\DateTime
*/
protected function parseDuration($date, $duration, $format = self::UNIX_FORMAT)
{
$dateTime = date_create($date);
$dateTime->modify($duration->y . ' year');
$dateTime->modify($duration->m . ' month');
$dateTime->modify($duration->d . ' day');
$dateTime->modify($duration->h . ' hour');
$dateTime->modify($duration->i . ' minute');
$dateTime->modify($duration->s . ' second');
if (is_null($format)) {
$output = $dateTime;
} else {
if ($format === self::UNIX_FORMAT) {
$output = $dateTime->getTimestamp();
} else {
$output = $dateTime->format($format);
}
}
return $output;
}
/**
* Gets the number of days between a start and end date
*
* @param integer $days
* @param integer $start
* @param integer $end
* @return integer
*/
protected function numberOfDays($days, $start, $end)
{
$w = array(date('w', $start), date('w', $end));
$base = floor(($end - $start) / self::SECONDS_IN_A_WEEK);
$sum = 0;
for ($day = 0; $day < 7; ++$day) {
if ($days & pow(2, $day)) {
$sum += $base + (($w[0] > $w[1]) ? $w[0] <= $day || $day <= $w[1] : $w[0] <= $day && $day <= $w[1]);
}
}
return $sum;
}
/**
* Converts a negative day ordinal to
* its equivalent positive form
*
* @param integer $dayNumber
* @param integer $weekday
* @param integer|\DateTime $timestamp
* @return string
*/
protected function convertDayOrdinalToPositive($dayNumber, $weekday, $timestamp)
{
$dayNumber = empty($dayNumber) ? 1 : $dayNumber; // Returns 0 when no number defined in BYDAY
$dayOrdinals = $this->dayOrdinals;
// We only care about negative BYDAY values
if ($dayNumber >= 1) {
return $dayOrdinals[$dayNumber];
}
$timestamp = (is_object($timestamp)) ? $timestamp : \DateTime::createFromFormat(self::UNIX_FORMAT, $timestamp);
$start = strtotime('first day of ' . $timestamp->format(self::DATE_TIME_FORMAT_PRETTY));
$end = strtotime('last day of ' . $timestamp->format(self::DATE_TIME_FORMAT_PRETTY));
// Used with pow(2, X) so pow(2, 4) is THURSDAY
$weekdays = array_flip(array_keys($this->weekdays));
$numberOfDays = $this->numberOfDays(pow(2, $weekdays[$weekday]), $start, $end);
// Create subset
$dayOrdinals = array_slice($dayOrdinals, 0, $numberOfDays, true);
// Reverse only the values
$dayOrdinals = array_combine(array_keys($dayOrdinals), array_reverse(array_values($dayOrdinals)));
return $dayOrdinals[$dayNumber * -1];
}
/**
* Removes unprintable ASCII and UTF-8 characters
*
* @param string $data
* @return string
*/
protected function removeUnprintableChars($data)
{
return preg_replace('/[\x00-\x1F\x7F\xA0]/u', '', $data);
}
/**
* Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()`.
* Multibyte safe.
*
* @param integer $code
* @return string
*/
protected function mb_chr($code)
{
if (function_exists('mb_chr')) {
return mb_chr($code);
} else {
if (0x80 > $code %= 0x200000) {
$s = chr($code);
} elseif (0x800 > $code) {
$s = chr(0xc0 | $code >> 6) . chr(0x80 | $code & 0x3f);
} elseif (0x10000 > $code) {
$s = chr(0xe0 | $code >> 12) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f);
} else {
$s = chr(0xf0 | $code >> 18) . chr(0x80 | $code >> 12 & 0x3f) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f);
}
return $s;
}
}
/**
* Replace all occurrences of the search string with the replacement string.
* Multibyte safe.
*
* @param string|array $search
* @param string|array $replace
* @param string|array $subject
* @param string $encoding
* @param integer $count
* @return array|string
*/
protected static function mb_str_replace($search, $replace, $subject, $encoding = null, &$count = 0)
{
if (is_array($subject)) {
// Call `mb_str_replace()` for each subject in the array, recursively
foreach ($subject as $key => $value) {
$subject[$key] = self::mb_str_replace($search, $replace, $value, $encoding, $count);
}
} else {
// Normalize $search and $replace so they are both arrays of the same length
$searches = is_array($search) ? array_values($search) : [$search];
$replacements = is_array($replace) ? array_values($replace) : [$replace];
$replacements = array_pad($replacements, count($searches), '');
foreach ($searches as $key => $search) {
if (is_null($encoding)) {
$encoding = mb_detect_encoding($search, 'UTF-8', true);
}
$replace = $replacements[$key];
$searchLen = mb_strlen($search, $encoding);
$sb = [];
while (($offset = mb_strpos($subject, $search, 0, $encoding)) !== false) {
$sb[] = mb_substr($subject, 0, $offset, $encoding);
$subject = mb_substr($subject, $offset + $searchLen, null, $encoding);
++$count;
}
$sb[] = $subject;
$subject = implode($replace, $sb);
}
}
return $subject;
}
/**
* Replaces curly quotes and other special characters
* with their standard equivalents
*
* @param string $data
* @return string
*/
protected function cleanData($data)
{
$replacementChars = array(
"\xe2\x80\x98" => "'", // ‘
"\xe2\x80\x99" => "'", // ’
"\xe2\x80\x9a" => "'", // ‚
"\xe2\x80\x9b" => "'", // ‛
"\xe2\x80\x9c" => '"', // “
"\xe2\x80\x9d" => '"', // ”
"\xe2\x80\x9e" => '"', // „
"\xe2\x80\x9f" => '"', // ‟
"\xe2\x80\x93" => '-', // –
"\xe2\x80\x94" => '--', // —
"\xe2\x80\xa6" => '...', // …
"\xc2\xa0" => ' ',
);
// Replace UTF-8 characters
$cleanedData = strtr($data, $replacementChars);
// Replace Windows-1252 equivalents
$charsToReplace = array_map(function ($code) {
return $this->mb_chr($code);
}, array(133, 145, 146, 147, 148, 150, 151, 194));
$cleanedData = $this->mb_str_replace($charsToReplace, $replacementChars, $cleanedData);
return $cleanedData;
}
/**
* Parses a list of excluded dates
* to be applied to an Event
*
* @param array $event
* @return array
*/
public function parseExdates(array $event)
{
if (empty($event['EXDATE_array'])) {
return array();
} else {
$exdates = $event['EXDATE_array'];
}
$output = array();
$currentTimeZone = $this->defaultTimeZone;
foreach ($exdates as $subArray) {
end($subArray);
$finalKey = key($subArray);
foreach ($subArray as $key => $value) {
if ($key === 'TZID') {
$checkTimeZone = $subArray[$key];
if ($this->isValidIanaTimeZoneId($checkTimeZone)) {
$currentTimeZone = $checkTimeZone;
} elseif ($this->isValidCldrTimeZoneId($checkTimeZone)) {
$currentTimeZone = $this->isValidCldrTimeZoneId($checkTimeZone, true);
} else {
$currentTimeZone = $this->defaultTimeZone;
}
} elseif (is_numeric($key)) {
$icalDate = $subArray[$key];
if (substr($icalDate, -1) === 'Z') {
$currentTimeZone = self::TIME_ZONE_UTC;
}
$output[] = new Carbon($icalDate, $currentTimeZone);
if ($key === $finalKey) {
// Reset to default
$currentTimeZone = $this->defaultTimeZone;
}
}
}
}
return $output;
}
/**
* Checks if a date string is a valid date
*
* @param string $value
* @return boolean
* @throws \Exception
*/
public function isValidDate($value)
{
if (!$value) {
return false;
}
try {
new \DateTime($value);
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* Checks if a filename exists as a file or URL
*
* @param string $filename
* @return boolean
*/
protected function isFileOrUrl($filename)
{
return (file_exists($filename) || filter_var($filename, FILTER_VALIDATE_URL)) ?: false;
}
/**
* Reads an entire file or URL into an array
*
* @param string $filename
* @return array
* @throws \Exception
*/
protected function fileOrUrl($filename)
{
$options = array();
if (!empty($this->httpBasicAuth) || !empty($this->httpUserAgent)) {
$options['http'] = array();
$options['http']['header'] = array();
if (!empty($this->httpBasicAuth)) {
$username = $this->httpBasicAuth['username'];
$password = $this->httpBasicAuth['password'];
$basicAuth = base64_encode("{$username}:{$password}");
array_push($options['http']['header'], "Authorization: Basic {$basicAuth}");
}
if (!empty($this->httpUserAgent)) {
array_push($options['http']['header'], "User-Agent: {$this->httpUserAgent}");
}
}
$context = stream_context_create($options);
if (($lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES, $context)) === false) {
throw new \Exception("The file path or URL '{$filename}' does not exist.");
}
return $lines;
}
/**
* Ensures the recurrence count is enforced against generated recurrence events.
*
* @param array $rrules
* @param array $recurrenceEvents
* @return array
*/
protected function trimToRecurrenceCount(array $rrules, array $recurrenceEvents)
{
if (isset($rrules['COUNT'])) {
$recurrenceCount = (intval($rrules['COUNT']) - 1);
$surplusCount = (sizeof($recurrenceEvents) - $recurrenceCount);
if ($surplusCount > 0) {
$recurrenceEvents = array_slice($recurrenceEvents, 0, $recurrenceCount);
$this->eventCount -= $surplusCount;
}
}
return $recurrenceEvents;
}
/**
* Checks if an excluded date matches a given date by reconciling time zones.
*
* @param Carbon $exdate
* @param array $anEvent
* @param integer $recurringOffset
* @return boolean
*/
protected function isExdateMatch($exdate, array $anEvent, $recurringOffset)
{
$searchDate = $anEvent['DTSTART'];
if (substr($searchDate, -1) === 'Z') {
$timeZone = self::TIME_ZONE_UTC;
} else {
if (isset($anEvent['DTSTART_array'][0]['TZID'])) {
$checkTimeZone = $anEvent['DTSTART_array'][0]['TZID'];
if ($this->isValidIanaTimeZoneId($checkTimeZone)) {
$timeZone = $checkTimeZone;
} elseif ($this->isValidCldrTimeZoneId($checkTimeZone)) {
$timeZone = $this->isValidCldrTimeZoneId($checkTimeZone, true);
} else {
$timeZone = $this->defaultTimeZone;
}
} else {
$timeZone = $this->defaultTimeZone;
}
}
$a = new Carbon($searchDate, $timeZone);
$b = $exdate->addSeconds($recurringOffset);
return $a->eq($b);
}
/**
* Replaces non-CLDR Windows time zone ID like 'W. Europe Standard Time' with its IANA equivalent.
*
* @param string $lineWithTzid
* @return string
*/
protected function replaceWindowsTimeZoneId($lineWithTzid)
{
return str_replace($this->windowsTimeZones, $this->windowsTimeZonesIana, $lineWithTzid);
}
}
src/ICal/Event.php 0000666 00000010672 13454100516 0007746 0 ustar 00 %s: %s';
/**
* https://www.kanzaki.com/docs/ical/summary.html
*
* @var $summary
*/
public $summary;
/**
* https://www.kanzaki.com/docs/ical/dtstart.html
*
* @var $dtstart
*/
public $dtstart;
/**
* https://www.kanzaki.com/docs/ical/dtend.html
*
* @var $dtend
*/
public $dtend;
/**
* https://www.kanzaki.com/docs/ical/duration.html
*
* @var $duration
*/
public $duration;
/**
* https://www.kanzaki.com/docs/ical/dtstamp.html
*
* @var $dtstamp
*/
public $dtstamp;
/**
* https://www.kanzaki.com/docs/ical/uid.html
*
* @var $uid
*/
public $uid;
/**
* https://www.kanzaki.com/docs/ical/created.html
*
* @var $created
*/
public $created;
/**
* https://www.kanzaki.com/docs/ical/lastModified.html
*
* @var $lastmodified
*/
public $lastmodified;
/**
* https://www.kanzaki.com/docs/ical/description.html
*
* @var $description
*/
public $description;
/**
* https://www.kanzaki.com/docs/ical/location.html
*
* @var $location
*/
public $location;
/**
* https://www.kanzaki.com/docs/ical/sequence.html
*
* @var $sequence
*/
public $sequence;
/**
* https://www.kanzaki.com/docs/ical/status.html
*
* @var $status
*/
public $status;
/**
* https://www.kanzaki.com/docs/ical/transp.html
*
* @var $transp
*/
public $transp;
/**
* https://www.kanzaki.com/docs/ical/organizer.html
*
* @var $organizer
*/
public $organizer;
/**
* https://www.kanzaki.com/docs/ical/attendee.html
*
* @var $attendee
*/
public $attendee;
/**
* Creates the Event object
*
* @param array $data
* @return void
*/
public function __construct(array $data = array())
{
if (!empty($data)) {
foreach ($data as $key => $value) {
$variable = self::snakeCase($key);
$this->{$variable} = self::prepareData($value);
}
}
}
/**
* 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)));
} elseif (is_array($value)) {
return array_map('self::prepareData', $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->lastmodified,
'DESCRIPTION' => $this->description,
'LOCATION' => $this->location,
'SEQUENCE' => $this->sequence,
'STATUS' => $this->status,
'TRANSP' => $this->transp,
'ORGANISER' => $this->organizer,
'ATTENDEE(S)' => $this->attendee,
);
$data = array_filter($data); // Remove any blank values
$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 = join($input, $glue);
$input = str_replace($separator, $glue, $input);
return strtolower($input);
}
}
composer.json 0000666 00000002041 13454100516 0007266 0 ustar 00 {
"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"
}
],
"require": {
"php": ">=5.3.9",
"ext-mbstring": "*",
"nesbot/carbon": "^1.36.2 || ^2.0"
},
"require-dev": {
"squizlabs/php_codesniffer": "~2.9.1",
"phpunit/phpunit": "^4"
},
"autoload": {
"psr-0": {
"ICal": "src/"
}
},
"config": {
"platform": {
"php": "5.3.29"
}
},
"scripts": {
"test": [
"phpunit --colors=always"
]
}
}
composer.lock 0000666 00000131542 13454100516 0007256 0 ustar 00 {
"_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": "119f8b4b8ebba22819faad3757a6272a",
"packages": [
{
"name": "nesbot/carbon",
"version": "1.36.2",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"shasum": ""
},
"require": {
"php": ">=5.3.9",
"symfony/translation": "~2.6 || ~3.0 || ~4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7"
},
"suggest": {
"friendsofphp/php-cs-fixer": "Needed for the `composer phpcs` command. Allow to automatically fix code style.",
"phpstan/phpstan": "Needed for the `composer phpstan` command. Allow to detect potential errors."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Carbon\\Laravel\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "http://carbon.nesbot.com",
"keywords": [
"date",
"datetime",
"time"
],
"time": "2018-12-28T10:07:33+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fe5e94c604826c35a32fa832f35bd036b6799609"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609",
"reference": "fe5e94c604826c35a32fa832f35bd036b6799609",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"time": "2019-02-06T07:57:58+00:00"
},
{
"name": "symfony/translation",
"version": "v2.8.49",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "fc58c2a19e56c29f5ba2736ec40d0119a0de2089"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/fc58c2a19e56c29f5ba2736ec40d0119a0de2089",
"reference": "fc58c2a19e56c29f5ba2736ec40d0119a0de2089",
"shasum": ""
},
"require": {
"php": ">=5.3.9",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"symfony/config": "<2.7"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "~2.8",
"symfony/intl": "~2.7.25|^2.8.18|~3.2.5",
"symfony/yaml": "~2.2|~3.0.0"
},
"suggest": {
"psr/log-implementation": "To use logging capability in translator",
"symfony/config": "",
"symfony/yaml": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Translation\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2018-11-24T21:16:41+00:00"
}
],
"packages-dev": [
{
"name": "doctrine/instantiator",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
"shasum": ""
},
"require": {
"php": ">=5.3,<8.0-DEV"
},
"require-dev": {
"athletic/athletic": "~0.1.8",
"ext-pdo": "*",
"ext-phar": "*",
"phpunit/phpunit": "~4.0",
"squizlabs/php_codesniffer": "~2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
"homepage": "http://ocramius.github.com/"
}
],
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
"homepage": "https://github.com/doctrine/instantiator",
"keywords": [
"constructor",
"instantiate"
],
"time": "2015-06-14T21:17:01+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "2.0.5",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e6a969a640b00d8daa3c66518b0405fb41ae0c4b",
"reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"suggest": {
"dflydev/markdown": "~1.0",
"erusev/parsedown": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-0": {
"phpDocumentor": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "mike.vanriel@naenius.com"
}
],
"time": "2016-01-25T08:17:30+00:00"
},
{
"name": "phpspec/prophecy",
"version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
"reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
"sebastian/comparator": "^1.1|^2.0|^3.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
"require-dev": {
"phpspec/phpspec": "^2.5|^3.2",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8.x-dev"
}
},
"autoload": {
"psr-0": {
"Prophecy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
},
{
"name": "Marcello Duarte",
"email": "marcello.duarte@gmail.com"
}
],
"description": "Highly opinionated mocking framework for PHP 5.3+",
"homepage": "https://github.com/phpspec/prophecy",
"keywords": [
"Double",
"Dummy",
"fake",
"mock",
"spy",
"stub"
],
"time": "2018-08-05T17:53:17+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "2.2.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979",
"reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"phpunit/php-file-iterator": "~1.3",
"phpunit/php-text-template": "~1.2",
"phpunit/php-token-stream": "~1.3",
"sebastian/environment": "^1.3.2",
"sebastian/version": "~1.0"
},
"require-dev": {
"ext-xdebug": ">=2.1.4",
"phpunit/phpunit": "~4"
},
"suggest": {
"ext-dom": "*",
"ext-xdebug": ">=2.2.1",
"ext-xmlwriter": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
"homepage": "https://github.com/sebastianbergmann/php-code-coverage",
"keywords": [
"coverage",
"testing",
"xunit"
],
"time": "2015-10-06T15:47:00+00:00"
},
{
"name": "phpunit/php-file-iterator",
"version": "1.4.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
"reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "FilterIterator implementation that filters files based on a list of suffixes.",
"homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
"keywords": [
"filesystem",
"iterator"
],
"time": "2017-11-27T13:52:08+00:00"
},
{
"name": "phpunit/php-text-template",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Simple template engine.",
"homepage": "https://github.com/sebastianbergmann/php-text-template/",
"keywords": [
"template"
],
"time": "2015-06-21T13:50:34+00:00"
},
{
"name": "phpunit/php-timer",
"version": "1.0.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
"reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
"reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Utility class for timing",
"homepage": "https://github.com/sebastianbergmann/php-timer/",
"keywords": [
"timer"
],
"time": "2017-02-26T11:10:40+00:00"
},
{
"name": "phpunit/php-token-stream",
"version": "1.4.12",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
"reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16",
"reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Wrapper around PHP's tokenizer extension.",
"homepage": "https://github.com/sebastianbergmann/php-token-stream/",
"keywords": [
"tokenizer"
],
"time": "2017-12-04T08:55:13+00:00"
},
{
"name": "phpunit/phpunit",
"version": "4.8.36",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "46023de9a91eec7dfb06cc56cb4e260017298517"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517",
"reference": "46023de9a91eec7dfb06cc56cb4e260017298517",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-json": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=5.3.3",
"phpspec/prophecy": "^1.3.1",
"phpunit/php-code-coverage": "~2.1",
"phpunit/php-file-iterator": "~1.4",
"phpunit/php-text-template": "~1.2",
"phpunit/php-timer": "^1.0.6",
"phpunit/phpunit-mock-objects": "~2.3",
"sebastian/comparator": "~1.2.2",
"sebastian/diff": "~1.2",
"sebastian/environment": "~1.3",
"sebastian/exporter": "~1.2",
"sebastian/global-state": "~1.0",
"sebastian/version": "~1.0",
"symfony/yaml": "~2.1|~3.0"
},
"suggest": {
"phpunit/php-invoker": "~1.1"
},
"bin": [
"phpunit"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.8.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "The PHP Unit Testing framework.",
"homepage": "https://phpunit.de/",
"keywords": [
"phpunit",
"testing",
"xunit"
],
"time": "2017-06-21T08:07:12+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "2.3.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983",
"reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": ">=5.3.3",
"phpunit/php-text-template": "~1.2",
"sebastian/exporter": "~1.2"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"suggest": {
"ext-soap": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Mock Object library for PHPUnit",
"homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
"keywords": [
"mock",
"xunit"
],
"abandoned": true,
"time": "2015-10-02T06:51:40+00:00"
},
{
"name": "sebastian/comparator",
"version": "1.2.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
"reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"sebastian/diff": "~1.2",
"sebastian/exporter": "~1.2 || ~2.0"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Provides the functionality to compare PHP values for equality",
"homepage": "http://www.github.com/sebastianbergmann/comparator",
"keywords": [
"comparator",
"compare",
"equality"
],
"time": "2017-01-29T09:50:25+00:00"
},
{
"name": "sebastian/diff",
"version": "1.4.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4",
"reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff"
],
"time": "2017-05-22T07:24:03+00:00"
},
{
"name": "sebastian/environment",
"version": "1.3.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea",
"reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8 || ^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Provides functionality to handle HHVM/PHP environments",
"homepage": "http://www.github.com/sebastianbergmann/environment",
"keywords": [
"Xdebug",
"environment",
"hhvm"
],
"time": "2016-08-18T05:49:44+00:00"
},
{
"name": "sebastian/exporter",
"version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "42c4c2eec485ee3e159ec9884f95b431287edde4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4",
"reference": "42c4c2eec485ee3e159ec9884f95b431287edde4",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"sebastian/recursion-context": "~1.0"
},
"require-dev": {
"ext-mbstring": "*",
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
}
],
"description": "Provides the functionality to export PHP variables for visualization",
"homepage": "http://www.github.com/sebastianbergmann/exporter",
"keywords": [
"export",
"exporter"
],
"time": "2016-06-17T09:04:28+00:00"
},
{
"name": "sebastian/global-state",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
"reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.2"
},
"suggest": {
"ext-uopz": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Snapshotting of global state",
"homepage": "http://www.github.com/sebastianbergmann/global-state",
"keywords": [
"global state"
],
"time": "2015-10-12T03:26:01+00:00"
},
{
"name": "sebastian/recursion-context",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7",
"reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
}
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"time": "2016-10-03T07:41:43+00:00"
},
{
"name": "sebastian/version",
"version": "1.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/version.git",
"reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
"reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
"shasum": ""
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
"time": "2015-06-21T13:59:46+00:00"
},
{
"name": "squizlabs/php_codesniffer",
"version": "2.9.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "2acf168de78487db620ab4bc524135a13cfe6745"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/2acf168de78487db620ab4bc524135a13cfe6745",
"reference": "2acf168de78487db620ab4bc524135a13cfe6745",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": ">=5.1.2"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"bin": [
"scripts/phpcs",
"scripts/phpcbf"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"classmap": [
"CodeSniffer.php",
"CodeSniffer/CLI.php",
"CodeSniffer/Exception.php",
"CodeSniffer/File.php",
"CodeSniffer/Fixer.php",
"CodeSniffer/Report.php",
"CodeSniffer/Reporting.php",
"CodeSniffer/Sniff.php",
"CodeSniffer/Tokens.php",
"CodeSniffer/Reports/",
"CodeSniffer/Tokenizers/",
"CodeSniffer/DocGenerators/",
"CodeSniffer/Standards/AbstractPatternSniff.php",
"CodeSniffer/Standards/AbstractScopeSniff.php",
"CodeSniffer/Standards/AbstractVariableSniff.php",
"CodeSniffer/Standards/IncorrectPatternException.php",
"CodeSniffer/Standards/Generic/Sniffs/",
"CodeSniffer/Standards/MySource/Sniffs/",
"CodeSniffer/Standards/PEAR/Sniffs/",
"CodeSniffer/Standards/PSR1/Sniffs/",
"CodeSniffer/Standards/PSR2/Sniffs/",
"CodeSniffer/Standards/Squiz/Sniffs/",
"CodeSniffer/Standards/Zend/Sniffs/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Greg Sherwood",
"role": "lead"
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "http://www.squizlabs.com/php-codesniffer",
"keywords": [
"phpcs",
"standards"
],
"time": "2018-11-07T22:31:41+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "82ebae02209c21113908c229e9883c419720738a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a",
"reference": "82ebae02209c21113908c229e9883c419720738a",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
},
{
"name": "Gert de Pagter",
"email": "backendtea@gmail.com"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"time": "2019-02-06T07:57:58+00:00"
},
{
"name": "symfony/yaml",
"version": "v2.8.49",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "02c1859112aa779d9ab394ae4f3381911d84052b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/02c1859112aa779d9ab394ae4f3381911d84052b",
"reference": "02c1859112aa779d9ab394ae4f3381911d84052b",
"shasum": ""
},
"require": {
"php": ">=5.3.9",
"symfony/polyfill-ctype": "~1.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2018-11-11T11:18:13+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.3.9",
"ext-mbstring": "*"
},
"platform-dev": [],
"platform-overrides": {
"php": "5.3.29"
}
}
.editorconfig 0000666 00000000260 13454100516 0007222 0 ustar 00 # 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
tests/SingleEventsTest.php 0000666 00000037470 13454100516 0011703 0 ustar 00 originalTimeZone = date_default_timezone_get();
}
public function tearDown()
{
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
'replaceWindowsTimeZoneIds' => false, // Default value
'skipRecurrence' => false, // Default value
'useTimeZoneWithRRules' => 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',
'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->assertEquals(
$expectedTimeStamp,
$event->dtstart_array[2],
$message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')'
);
$this->assertAttributeEquals(
$expectedDateString,
'dtstart',
$event,
$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
);
}
}
}
tests/ical/ical-monthly.ics 0000666 00000000701 13454100516 0011717 0 ustar 00 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
tests/ical/issue-196.ics 0000666 00000002751 13454100516 0010773 0 ustar 00 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
tests/icalmonthly.txt 0000666 00000000631 13454100516 0010775 0 ustar 00 BEGIN:VCALENDAR
VERSION:2.0
X-WR-CALNAME:Privat
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
tests/RecurrencesTest.php 0000666 00000030130 13454100516 0011537 0 ustar 00 originalTimeZone = date_default_timezone_get();
}
public function tearDown()
{
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',
'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',
'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',
'DTSTART;VALUE=DATE:20180701',
'DTEND;VALUE=DATE:20180702',
'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;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',
6,
$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',
'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',
'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',
'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',
'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',
'DTSTART;TZID=Europe/Berlin:19970902T090000',
'',
'RRULE:FREQ=DAILY;COUNT=10',
10,
$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',
'DTSTART;TZID=Europe/Berlin:19970902T090000',
'',
'RRULE:FREQ=DAILY;COUNT=10',
10,
$checks
);
}
function assertVEVENT($defaultTimezone, $dtstart, $dtend, $rrule, $count, $checks)
{
$options = $this->getOptions($defaultTimezone);
$testIcal = implode(PHP_EOL, $this->getIcalHeader());
$testIcal .= PHP_EOL;
$testIcal .= implode(PHP_EOL, $this->formatIcalEvent($dtstart, $dtend, $rrule));
$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);
}
}
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);
}
}
function assertEvent($event, $expectedDateString, $message, $timezone = null)
{
if ($timezone !== null) {
date_default_timezone_set($timezone);
}
$expectedTimeStamp = strtotime($expectedDateString);
$this->assertEquals($expectedTimeStamp, $event->dtstart_array[2], $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')');
$this->assertAttributeEquals($expectedDateString, 'dtstart', $event, $message . 'dtstart mismatch (timestamp is okay)');
}
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
'replaceWindowsTimeZoneIds' => false, // Default value
'skipRecurrence' => false, // Default value
'useTimeZoneWithRRules' => $this->useTimeZoneWithRRules, // Default value: false
);
return $options;
}
function formatIcalEvent($dtstart, $dtend, $rrule)
{
return array(
'BEGIN:VEVENT',
'CREATED:20090213T195947Z',
'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209',
$rrule,
$dtstart,
$dtend,
'SUMMARY:test',
'LAST-MODIFIED:20110429T222101Z',
'DTSTAMP:20170630T105724Z',
'SEQUENCE:0',
'END:VEVENT',
);
}
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:',
);
}
function getIcalFooter()
{
return array('END:VCALENDAR');
}
}
CONTRIBUTING.md 0000666 00000002441 13454100516 0007001 0 ustar 00 ## 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
.gitignore 0000666 00000001125 13454100516 0006536 0 ustar 00 ###################
# 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
ehthumbs.db
Thumbs.db
workbench
####################
# Package Managers #
####################
auth.json
node_modules
vendor
##########
# Custom #
##########
*-report.*
########
# IDEs #
########
.idea
*.iml