LICENSE 0000666 00000002051 13535672167 0005572 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 00000102001 13535672167 0006040 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
### 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.
- :information_source: **Note the parser is limited to [relative date formats](https://www.php.net/manual/en/datetime.formats.relative.php) which can inhibit how complex recurrence rule parts are processed (e.g. `BYDAY` combined with `BYSETPOS`)**
```php
// Dump the whole calendar
var_dump($ical->cal);
// Dump every event
var_dump($ical->events());
```
- Also included are special `{property}_array` arrays which further resolve the contents of a key/value pair.
```php
// Dump a parsed event's start date
var_dump($event->dtstart_array);
// array (size=4)
// 0 =>
// array (size=1)
// 'TZID' => string 'America/Detroit' (length=15)
// 1 => string '20160409T090000' (length=15)
// 2 => int 1460192400
// 3 => string 'TZID=America/Detroit:20160409T090000' (length=36)
```
---
## 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 |
| `$shouldFilterByWindow` | :heavy_multiplication_x: | `false` | `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set |
| `$skipRecurrence` | :ballot_box_with_check: | `false` | Toggles whether to skip the parsing of recurrence rules |
| `$todoCount` | :heavy_multiplication_x: | N/A | Tracks the number of todos in the current iCal feed |
| `$windowMaxTimestamp` | :heavy_multiplication_x: | `null` | If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined by this field and `$windowMinTimestamp` |
| `$windowMinTimestamp` | :heavy_multiplication_x: | `null` | If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined by this field and `$windowMaxTimestamp` |
#### Methods
| Method | Parameter(s) | Visibility | Description |
|-------------------------------------------------|---------------------------------------------------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| `__construct` | `$files = false`, `$options = array()` | `public` | Creates the ICal object |
| `initFile` | `$file` | `protected` | Initialises lines from a file |
| `initLines` | `$lines` | `protected` | Initialises the parser using an array containing each line of iCal content |
| `initString` | `$string` | `protected` | Initialises lines from a string |
| `initUrl` | `$url`, `$username = null`, `$password = null`, `$userAgent = null` | `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 |
| `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 |
| `filterValuesUsingBySetPosRRule` | `$bysetpos`, `$valueslist` | `protected` | Filters a provided values-list by applying a BYSETPOS RRule |
| `freeBusyEvents` | - | `public` | Returns an array of arrays with all free/busy events |
| `getDaysOfMonthMatchingByDayRRule` | `$bydays`, `$initialDateTime` | `protected` | Find all days of a month that match the BYDAY stanza of an RRULE |
| `hasEvents` | - | `public` | Returns a boolean value whether the current calendar has events or not |
| `iCalDateToDateTime` | `$icalDate` | `public` | Returns a `DateTime` object from an iCal date time format |
| `iCalDateToUnixTimestamp` | `$icalDate` | `public` | Returns a Unix timestamp from an iCal date time format |
| `iCalDateWithTimeZone` | `$event`, `$key`, `$format = DATE_TIME_FORMAT` | `public` | Returns a date adapted to the calendar time zone depending on the event `TZID` |
| `doesEventStartOutsideWindow` | `$event` | `protected` | Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp` |
| `isFileOrUrl` | `$filename` | `protected` | Checks if a filename exists as a file or URL |
| `isOutOfRange` | `$calendarDate`, `$minTimestamp`, `$maxTimestamp` | `protected` | Determines whether a valid iCalendar date is within a given range |
| `isValidCldrTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid CLDR time zone |
| `isValidDate` | `$value` | `public` | Checks if a date string is a valid date |
| `isValidIanaTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid IANA time zone |
| `isValidWindowsTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a recognised Windows (non-CLDR) time zone |
| `isValidTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is valid (IANA, CLDR, or Windows) |
| `keyValueFromString` | `$text` | `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 |
| `escapeParamText` | `$candidateText` | `protected` | Places double-quotes around texts that have characters not permitted in parameter-texts, but are permitted in quoted-texts. |
| `parseDuration` | `$date`, `$duration`, `$format = 'U'` | `protected` | Parses a duration and applies it to a date |
| `parseExdates` | `$event` | `public` | Parses a list of excluded dates to be applied to an Event |
| `processDateConversions` | - | `protected` | Processes date conversions using the time zone |
| `processEvents` | - | `protected` | Performs admin tasks on all events as read from the iCal file |
| `processRecurrences` | - | `protected` | Processes recurrence rules |
| `reduceEventsToMinMaxRange` | | `protected` | Reduces the number of events to the defined minimum and maximum range |
| `removeLastEventIfOutsideWindowAndNonRecurring` | | `protected` | Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by `$windowMinTimestamp` / `$windowMaxTimestamp` |
| `removeUnprintableChars` | `$data` | `protected` | Removes unprintable ASCII and UTF-8 characters |
| `sortEventsWithOrder` | `$events`, `$sortOrder = SORT_ASC` | `public` | Sorts events based on a given sort order |
| `timeZoneStringToDateTimeZone` | `$timeZoneString` | `public` | Returns a `DateTimeZone` object based on a string containing a time zone name. |
| `unfold` | `$lines` | `protected` | Unfolds an iCal file in preparation for parsing |
#### Constants
| Name | Description |
|---------------------------|-----------------------------------------------|
| `DATE_TIME_FORMAT_PRETTY` | Default pretty date time format to use |
| `DATE_TIME_FORMAT` | Default date time format to use |
| `ICAL_DATE_TIME_TEMPLATE` | String template to generate an iCal date time |
| `ISO_8601_WEEK_START` | First day of the week, as defined by ISO-8601 |
| `RECURRENCE_EVENT` | Used to isolate generated recurrence events |
| `SECONDS_IN_A_WEEK` | The number of seconds in a week |
| `TIME_FORMAT` | Default time format to use |
| `TIME_ZONE_UTC` | UTC time zone string |
| `UNIX_FORMAT` | Unix timestamp date format |
| `UNIX_MIN_YEAR` | The year Unix time began |
---
### `Event` API (extends `ICal` API)
#### Methods
| Method | Parameter(s) | Visibility | Description |
|---------------|---------------------------------------------|-------------|---------------------------------------------------------------------|
| `__construct` | `$data = array()` | `public` | Creates the Event object |
| `prepareData` | `$value` | `protected` | Prepares the data for output |
| `printData` | `$html = HTML_TEMPLATE` | `public` | Returns Event data excluding anything blank within an HTML template |
| `snakeCase` | `$input`, `$glue = '_'`, `$separator = '-'` | `protected` | Converts the given input to snake_case |
#### Constants
| Name | Description |
|-----------------|-----------------------------------------------------|
| `HTML_TEMPLATE` | String template to use when pretty printing content |
---
## Credits
- [Jonathan Goode](https://github.com/u01jmg3) (programming, bug fixing, 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 00000012111 13535672167 0010221 0 ustar 00 2, // Default value
'defaultTimeZone' => 'UTC',
'defaultWeekStart' => 'MO', // Default value
'disableCharacterReplacement' => false, // Default value
'filterDaysAfter' => null, // Default value
'filterDaysBefore' => null, // Default value
'skipRecurrence' => false, // Default value
));
// $ical->initFile('ICal.ics');
// $ical->initUrl('https://raw.githubusercontent.com/u01jmg3/ics-parser/master/examples/ICal.ics', $username = null, $password = null, $userAgent = null);
} catch (\Exception $e) {
die($e);
}
?>
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]);
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]);
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]);
echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
?>
printData() ?>
1 && $count % 3 === 0) {
echo '
';
}
$count++;
?>
examples/ICal.ics 0000666 00000023717 13535672167 0007727 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:20190101
DTEND;VALUE=DATE:20190101
DTSTAMP;TZID="GMT Standard Time":20190101T195741Z
RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1
UID:4dnsuc3nknin15kv25cn7ridssy@google.com
CREATED:20190101T142059Z
DESCRIPTION;LANGUAGE=en-gb:BYSETPOS First weekday of every month
LAST-MODIFIED:20190101T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYSETPOS First weekday of every month
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20190131
DTEND;VALUE=DATE:20190131
DTSTAMP;TZID="GMT Standard Time":20190121T195741Z
RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1
UID:4dnsuc3nknin15kv25cn7ridssx@google.com
CREATED:20190119T142059Z
DESCRIPTION;LANGUAGE=en-gb:BYSETPOS Last day of every month
LAST-MODIFIED:20190409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYSETPOS Last day of every month
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20170301
DTEND;VALUE=DATE:20170301
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=WE
UID:h6f7sdjbpt47v3dkral8lnsgcc@google.com
CREATED:20170119T142040Z
DESCRIPTION;LANGUAGE=en-gb:BYDAY Test 2
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYDAY Test 2
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20170111
DTEND;VALUE=DATE:20170111
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=5;BYMONTH=1,2,3
UID:f50e8b89a4a3b0070e0b687d03@google.com
CREATED:20170119T142040Z
DESCRIPTION;LANGUAGE=en-gb:BYMONTH Multiple Test 1
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYMONTH Multiple Test 1
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20170405
DTEND;VALUE=DATE:20170405
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=YEARLY;BYMONTH=4,5,6;BYDAY=WE;COUNT=5
UID:675f06aa795665ae50904ebf0e@google.com
CREATED:20170119T142040Z
DESCRIPTION;LANGUAGE=en-gb:BYMONTH Multiple Test 2
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:BYMONTH Multiple Test 2
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT30M
ACTION:DISPLAY
DESCRIPTION:Buzz buzz
END:VALARM
DTSTART;VALUE=DATE;TZID=Germany/Berlin:20170123
DTEND;VALUE=DATE;TZID=Germany/Berlin:20170123
DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
RRULE:FREQ=MONTHLY;BYDAY=-2MO;COUNT=5
EXDATE;VALUE=DATE:20171020
UID:d287b7ec808fcf084983f10837@google.com
CREATED:20170119T142040Z
DESCRIPTION;LANGUAGE=en-gb:Negative BYDAY
LAST-MODIFIED:20170409T150000Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY;LANGUAGE=en-gb:Negative BYDAY
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=Australia/Sydney:20170813T190000
DTEND;TZID=Australia/Sydney:20170813T213000
RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=2SU;COUNT=2
DTSTAMP:20170809T114431Z
UID:testuid@google.com
CREATED:20170802T135539Z
DESCRIPTION:
LAST-MODIFIED:20170802T135935Z
LOCATION:
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Parent Recurrence Event
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=Australia/Sydney:20170813T190000
DTEND;TZID=Australia/Sydney:20170813T213000
DTSTAMP:20170809T114431Z
UID:testuid@google.com
RECURRENCE-ID;TZID=Australia/Sydney:20170813T190000
CREATED:20170802T135539Z
DESCRIPTION:
LAST-MODIFIED:20170809T105604Z
LOCATION:Melbourne VIC\, Australia
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Override Parent Recurrence Event
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
.github/ISSUE_TEMPLATE/bug_report.md 0000666 00000001640 13535672167 0013005 0 ustar 00 ---
name: Bug Report
about: Create a report to help us improve
title: ''
labels: ''
assignees: u01jmg3
---
> :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/PULL_REQUEST_TEMPLATE/pull_request_template.md 0000666 00000001750 13535672167 0016352 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/release_template.md 0000666 00000000676 13535672167 0011775 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 13535672167 0007000 0 ustar 00
tests
src/ICal/ICal.php 0000666 00000255370 13535672167 0007523 0 ustar 00
* @license https://opensource.org/licenses/mit-license.php MIT License
* @version 2.1.15
*/
namespace ICal;
use Carbon\Carbon;
class ICal
{
// phpcs:disable Generic.Arrays.DisallowLongArraySyntax
const DATE_TIME_FORMAT = 'Ymd\THis';
const DATE_TIME_FORMAT_PRETTY = 'F Y H:i:s';
const ICAL_DATE_TIME_TEMPLATE = 'TZID=%s:';
const ISO_8601_WEEK_START = 'MO';
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 = self::ISO_8601_WEEK_START;
/**
* Toggles whether to skip the parsing of recurrence rules
*
* @var boolean
*/
public $skipRecurrence = false;
/**
* Toggles whether to disable all character replacement.
*
* @var boolean
*/
public $disableCharacterReplacement = 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 IANA time zone IDs to avoid unnecessary lookups
*
* @var array
*/
protected $validIanaTimeZones = array();
/**
* Event recurrence instances that have been altered
*
* @var array
*/
protected $alteredRecurrenceInstances = array();
/**
* An associative array containing weekday conversion data
*
* The order of the days in the array follow the ISO-8601 specification of a week.
*
* @var array
*/
protected $weekdays = array(
'MO' => 'monday',
'TU' => 'tuesday',
'WE' => 'wednesday',
'TH' => 'thursday',
'FR' => 'friday',
'SA' => 'saturday',
'SU' => 'sunday',
);
/**
* 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',
'skipRecurrence',
);
/**
* CLDR time zones mapped to IANA time zones.
*
* @var array
*/
private static $cldrTimeZonesMap = 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',
);
/**
* 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',
);
/**
* 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();
}
// 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);
}
$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 ($anEvent === null) {
unset($events[$key]);
continue;
}
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
* @return \DateTime
* @throws \Exception
*/
public function iCalDateToDateTime($icalDate)
{
/**
* 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 = '/^(?:TZID=)?([^:]*|".*")'; // [1]: Time zone
$pattern .= ':?'; // Time zone delimiter
$pattern .= '([0-9]{8})'; // [2]: YYYYMMDD
$pattern .= 'T?'; // Time delimiter
$pattern .= '(?(?<=T)([0-9]{6}))'; // [3]: HHMMSS (filled if delimiter present)
$pattern .= '(Z?)/'; // [4]: UTC flag
preg_match($pattern, $icalDate, $date);
if (empty($date)) {
throw new \Exception('Invalid iCal date format.');
}
// A Unix timestamp usually cannot represent a date prior to 1 Jan 1970.
// PHP, on the other hand, uses negative numbers for that. Thus we don't
// need to special case them.
if ($date[4] === 'Z') {
$dateTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC);
} elseif (!empty($date[1])) {
$dateTimeZone = $this->timeZoneStringToDateTimeZone($date[1]);
} else {
$dateTimeZone = new \DateTimeZone($this->defaultTimeZone);
}
// The exclamation mark at the start of the format string indicates that if a
// time portion is not included, the time in the returned DateTime should be
// set to 00:00:00. Without it, the time would be set to the current system time.
$dateFormat = '!Ymd';
$dateBasic = $date[2];
if (!empty($date[3])) {
$dateBasic .= "T{$date[3]}";
$dateFormat .= '\THis';
}
return \DateTime::createFromFormat($dateFormat, $dateBasic, $dateTimeZone);
}
/**
* Returns a Unix timestamp from an iCal date time format
*
* @param string $icalDate
* @return integer
*/
public function iCalDateToUnixTimestamp($icalDate)
{
return $this->iCalDateToDateTime($icalDate)->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') {
$dateTime = $this->parseDuration($event['DTSTART'], $dateArray[2], null);
} else {
// When constructing from a Unix Timestamp, no time zone needs passing.
$dateTime = new \DateTime("@{$dateArray[2]}");
}
// Set the time zone we wish to use when running `$dateTime->format`.
$dateTime->setTimezone(new \DateTimeZone($this->calendarTimeZone()));
if (is_null($format)) {
return $dateTime;
}
return $dateTime->format($format);
}
/**
* 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'])) {
$timeZone = $this->escapeParamText($anEvent["{$type}_array"][0]['TZID']);
$date = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $timeZone) . $date;
}
$anEvent["{$type}_array"][2] = $this->iCalDateToUnixTimestamp($date);
$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]);
$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]);
// phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
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();
// If there are no events, then we have nothing to process.
if (empty($events)) {
return;
}
$allEventRecurrences = array();
foreach ($events as $anEvent) {
if (!isset($anEvent['RRULE']) || $anEvent['RRULE'] === '') {
continue;
}
// Tag as generated by a recurrence rule
$anEvent['RRULE_array'][2] = self::RECURRENCE_EVENT;
// Create new initial starting point.
$initialEventDate = $this->icalDateToDateTime($anEvent['DTSTART_array'][3]);
// Separate the RRULE stanzas, and explode the values that are lists.
$rrules = array();
foreach (explode(';', $anEvent['RRULE']) as $s) {
list($k, $v) = explode('=', $s);
if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH'))) {
$rrules[$k] = explode(',', $v);
} else {
$rrules[$k] = $v;
}
}
// Get frequency
$frequency = $rrules['FREQ'];
// Reject RRULE if BYDAY stanza is invalid:
// > The BYDAY rule part MUST NOT be specified with a numeric value
// > when the FREQ rule part is not set to MONTHLY or YEARLY.
if (isset($rrules['BYDAY']) && !in_array($frequency, array('MONTHLY', 'YEARLY'))) {
$allByDayStanzasValid = array_reduce($rrules['BYDAY'], function ($carry, $weekday) {
return $carry && substr($weekday, -2) === $weekday;
}, true);
if (!$allByDayStanzasValid) {
error_log("ICal::ProcessRecurrences: A \"{$frequency}\" RRULE should not contain BYDAY values with numeric prefixes");
continue;
}
}
// Get Interval
$interval = (empty($rrules['INTERVAL'])) ? 1 : $rrules['INTERVAL'];
// Throw an error if this isn't an integer.
if (!is_int($this->defaultSpan)) {
trigger_error('ICal::defaultSpan: User defined value is not an integer', E_USER_NOTICE);
}
// Compute EXDATEs
$exdates = $this->parseExdates($anEvent);
/**
* Determine at what point we should stop calculating recurrences
* by looking at the UNTIL or COUNT rrule stanza, or, if neither
* if set, using a fallback.
*
* Syntax:
* UNTIL={enddate}
* COUNT=
*
* Where:
* enddate = ||
*/
$count = 1;
$countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : 0;
$until = date_create()->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp();
if (isset($rrules['UNTIL'])) {
$until = min($until, $this->iCalDateToUnixTimestamp($rrules['UNTIL']));
}
$eventRecurrences = array();
$frequencyRecurringDateTime = clone $initialEventDate;
while ($frequencyRecurringDateTime->getTimestamp() <= $until) {
$candidateDateTimes = [];
// phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault
switch ($frequency) {
case 'DAILY':
$candidateDateTimes[] = clone $frequencyRecurringDateTime;
break;
case 'WEEKLY':
$initialDayOfWeek = $frequencyRecurringDateTime->format('N');
$matchingDays = array($initialDayOfWeek);
if (!empty($rrules['BYDAY'])) {
// setISODate() below uses the ISO-8601 specification of weeks: start on
// a Monday, end on a Sunday. However, RRULEs (or the caller of the
// parser) may state an alternate WeeKSTart.
$wkstTransition = 7;
if (empty($rrules['WKST'])) {
if ($this->defaultWeekStart !== self::ISO_8601_WEEK_START) {
$wkstTransition = array_search($this->defaultWeekStart, array_keys($this->weekdays));
}
} elseif ($rrules['WKST'] !== self::ISO_8601_WEEK_START) {
$wkstTransition = array_search($rrules['WKST'], array_keys($this->weekdays));
}
$matchingDays = array_map(
function ($weekday) use ($initialDayOfWeek, $wkstTransition, $interval) {
$day = array_search($weekday, array_keys($this->weekdays));
if ($day < $initialDayOfWeek) {
$day += 7;
}
if ($day >= $wkstTransition) {
$day += 7 * ($interval - 1);
}
// Ignoring alternate week starts, $day at this point will have a
// value between 0 and 6. But setISODate() expects a value of 1 to 7.
// Even with alternate week starts, we still need to +1 to set the
// correct weekday.
$day += 1;
return $day;
},
$rrules['BYDAY']
);
}
sort($matchingDays);
foreach ($matchingDays as $day) {
$clonedDateTime = clone $frequencyRecurringDateTime;
$candidateDateTimes[] = $clonedDateTime->setISODate(
$frequencyRecurringDateTime->format('Y'),
$frequencyRecurringDateTime->format('W'),
$day
);
}
break;
case 'MONTHLY':
$matchingDays = array();
if (!empty($rrules['BYMONTHDAY'])) {
$matchingDays = $rrules['BYMONTHDAY'];
} elseif (!empty($rrules['BYDAY'])) {
$matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
}
if (!empty($rrules['BYSETPOS'])) {
$matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
}
foreach ($matchingDays as $day) {
// Skip invalid dates (e.g. 30th February)
if ($day > $frequencyRecurringDateTime->format('t')) {
continue;
}
$clonedDateTime = clone $frequencyRecurringDateTime;
$candidateDateTimes[] = $clonedDateTime->setDate(
$frequencyRecurringDateTime->format('Y'),
$frequencyRecurringDateTime->format('m'),
$day
);
}
break;
case 'YEARLY':
if (!empty($rrules['BYMONTH'])) {
foreach ($rrules['BYMONTH'] as $byMonth) {
$clonedDateTime = clone $frequencyRecurringDateTime;
$bymonthRecurringDatetime = $clonedDateTime->setDate(
$frequencyRecurringDateTime->format('Y'),
$byMonth,
$frequencyRecurringDateTime->format('d')
);
if (!empty($rrules['BYDAY'])) {
// Get all days of the month that match the BYDAY rule.
$matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $bymonthRecurringDatetime);
// And add each of them to the list of recurrences
foreach ($matchingDays as $day) {
$clonedDateTime = clone $bymonthRecurringDatetime;
$candidateDateTimes[] = $clonedDateTime->setDate(
$frequencyRecurringDateTime->format('Y'),
$bymonthRecurringDatetime->format('m'),
$day
);
}
} else {
$candidateDateTimes[] = clone $bymonthRecurringDatetime;
}
}
} else {
$candidateDateTimes[] = clone $frequencyRecurringDateTime;
}
break;
}
foreach ($candidateDateTimes as $candidate) {
$timestamp = $candidate->getTimestamp();
if ($timestamp <= $initialEventDate->getTimestamp()) {
continue;
}
if ($timestamp > $until) {
break;
}
// Exclusions
$isExcluded = array_filter($exdates, function ($exdate) use ($timestamp) {
return $exdate->getTimestamp() == $timestamp;
});
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
if (in_array($timestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
}
if (!$isExcluded) {
$eventRecurrences[] = $candidate;
$this->eventCount++;
if (isset($rrules['COUNT'])) {
$count++;
// If RRULE[COUNT] is reached then break
if ($count >= $countLimit) {
break 2;
}
}
}
}
// Move forwards $interval $frequency.
$monthPreMove = $frequencyRecurringDateTime->format('m');
$frequencyRecurringDateTime->modify("{$interval} {$this->frequencyConversion[$frequency]}");
// As noted in Example #2 on https://www.php.net/manual/en/datetime.modify.php,
// there are some occasions where adding months doesn't give the month you might
// expect. For instance: January 31st + 1 month == March 3rd (March 2nd on a leap
// year.) The following code crudely rectifies this.
if ($frequency === 'MONTHLY') {
$monthDiff = $frequencyRecurringDateTime->format('m') - $monthPreMove;
if (($monthDiff > 0 && $monthDiff > $interval) || ($monthDiff < 0 && $monthDiff > $interval - 12)) {
$frequencyRecurringDateTime->modify('-1 month');
}
}
}
// Determine event length
$eventLength = 0;
if (isset($anEvent['DURATION'])) {
$clonedDateTime = clone $initialEventDate;
$endDate = $clonedDateTime->add($anEvent['DURATION_array'][2]);
$eventLength = $endDate->getTimestamp() - $anEvent['DTSTART_array'][2];
} elseif (isset($anEvent['DTEND_array'])) {
$eventLength = $anEvent['DTEND_array'][2] - $anEvent['DTSTART_array'][2];
}
// Whether or not the initial date was UTC
$initialDateWasUTC = substr($anEvent['DTSTART'], -1) === 'Z';
// Build the param array
$dateParamArray = array();
if (!$initialDateWasUTC
&& isset($anEvent['DTSTART_array'][0]['TZID'])
&& $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID'])
) {
$dateParamArray['TZID'] = $anEvent['DTSTART_array'][0]['TZID'];
}
// Populate the `DT{START|END}[_array]`s
$eventRecurrences = array_map(
function ($recurringDatetime) use ($anEvent, $eventLength, $initialDateWasUTC, $dateParamArray) {
$tzidPrefix = (isset($dateParamArray['TZID'])) ? 'TZID=' . $this->escapeParamText($dateParamArray['TZID']) . ':' : '';
foreach (array('DTSTART', 'DTEND') as $dtkey) {
$anEvent[$dtkey] = $recurringDatetime->format(self::DATE_TIME_FORMAT) . (($initialDateWasUTC) ? 'Z' : '');
$anEvent["{$dtkey}_array"] = array(
$dateParamArray, // [0] Array of params (incl. TZID)
$anEvent[$dtkey], // [1] ICalDateTime string w/o TZID
$recurringDatetime->getTimestamp(), // [2] Unix Timestamp
"{$tzidPrefix}{$anEvent[$dtkey]}", // [3] Full ICalDateTime string
);
if ($dtkey !== 'DTEND') {
$recurringDatetime->modify("{$eventLength} seconds");
}
}
return $anEvent;
},
$eventRecurrences
);
$allEventRecurrences = array_merge($allEventRecurrences, $eventRecurrences);
}
$events = array_merge($events, $allEventRecurrences);
$this->cal['VEVENT'] = $events;
}
/**
* Find all days of a month that match the BYDAY stanza of an RRULE.
*
* With no {ordwk}, then return the day number of every {weekday}
* within the month.
*
* With a +ve {ordwk}, then return the {ordwk} {weekday} within the
* month.
*
* With a -ve {ordwk}, then return the {ordwk}-to-last {weekday}
* within the month.
*
* RRule Syntax:
* BYDAY={bywdaylist}
*
* Where:
* bywdaylist = {weekdaynum}[,{weekdaynum}...]
* weekdaynum = [[+]{ordwk} || -{ordwk}]{weekday}
* ordwk = 1 to 53
* weekday = SU || MO || TU || WE || TH || FR || SA
*
* @param array $byDays
* @param \DateTime $initialDateTime
* @return array
*/
protected function getDaysOfMonthMatchingByDayRRule($byDays, $initialDateTime)
{
$matchingDays = array();
foreach ($byDays as $weekday) {
$bydayDateTime = clone $initialDateTime;
$ordwk = intval(substr($weekday, 0, -2));
// Quantise the date to the first instance of the requested day in a month
// (Or last if we have a -ve {ordwk})
$bydayDateTime->modify(
(($ordwk < 0) ? 'Last' : 'First')
. ' '
. $this->weekdays[substr($weekday, -2)] // e.g. "Monday"
. ' of ' . $initialDateTime->format('F') // e.g. "June"
);
if ($ordwk < 0) { // -ve {ordwk}
$bydayDateTime->modify((++$ordwk) . ' week');
$matchingDays[] = $bydayDateTime->format('j');
} elseif ($ordwk > 0) { // +ve {ordwk}
$bydayDateTime->modify((--$ordwk) . ' week');
$matchingDays[] = $bydayDateTime->format('j');
} else { // No {ordwk}
while ($bydayDateTime->format('n') === $initialDateTime->format('n')) {
$matchingDays[] = $bydayDateTime->format('j');
$bydayDateTime->modify('+1 week');
}
}
}
// Sort into ascending order.
sort($matchingDays);
return $matchingDays;
}
/**
* Filters a provided values-list by applying a BYSETPOS RRule.
*
* Where a +ve {daynum} is provided, the {ordday} position'd value as
* measured from the start of the list of values should be retained.
*
* Where a -ve {daynum} is provided, the {ordday} position'd value as
* measured from the end of the list of values should be retained.
*
* RRule Syntax:
* BYSETPOS={bysplist}
*
* Where:
* bysplist = {setposday}[,{setposday}...]
* setposday = {daynum}
* daynum = [+ || -] {ordday}
* ordday = 1 to 366
*
* @param array $bySetPos
* @param array $valuesList
* @return array
*/
protected function filterValuesUsingBySetPosRRule($bySetPos, $valuesList)
{
$filteredMatches = array();
foreach ($bySetPos as $setPosition) {
if ($setPosition < 0) {
$setPosition = count($valuesList) + ++$setPosition;
}
// Positioning starts at 1, array indexes start at 0
$filteredMatches[] = $valuesList[$setPosition - 1];
}
return $filteredMatches;
}
/**
* 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;
}
$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');
} else {
$events[$key]['DTEND_tz'] = $events[$key]['DTSTART_tz'];
}
}
$this->cal['VEVENT'] = $events;
}
}
/**
* 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;
}
// Validate the time zone, falling back to the time zone set in the PHP environment.
$timeZone = $this->timeZoneStringToDateTimeZone($timeZone)->getName();
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'] : array();
}
/**
* 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, CLDR, or Windows)
*
* @param string $timeZone
* @return boolean
*/
protected function isValidTimeZoneId($timeZone)
{
return ($this->isValidIanaTimeZoneId($timeZone) !== false
|| $this->isValidCldrTimeZoneId($timeZone) !== false
|| $this->isValidWindowsTimeZoneId($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->validIanaTimeZones)) {
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->validIanaTimeZones[] = $timeZone;
return true;
}
return false;
}
/**
* Checks if a time zone is a valid CLDR time zone
*
* @param string $timeZone
* @return boolean
*/
public function isValidCldrTimeZoneId($timeZone)
{
return array_key_exists(html_entity_decode($timeZone), self::$cldrTimeZonesMap);
}
/**
* Checks if a time zone is a recognised Windows (non-CLDR) time zone
*
* @param string $timeZone
* @return boolean
*/
public function isValidWindowsTimeZoneId($timeZone)
{
return array_key_exists(html_entity_decode($timeZone), self::$windowsTimeZonesMap);
}
/**
* 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;
}
/**
* 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) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
{
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) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
{
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) : array($search);
$replacements = is_array($replace) ? array_values($replace) : array($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 = array();
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;
}
/**
* Places double-quotes around texts that have characters not permitted
* in parameter-texts, but are permitted in quoted-texts.
*
* @param string $candidateText
* @return string
*/
protected function escapeParamText($candidateText)
{
if (strpbrk($candidateText, ':;,') !== false) {
return '"' . $candidateText . '"';
}
return $candidateText;
}
/**
* 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') {
$currentTimeZone = $this->timeZoneStringToDateTimeZone($subArray[$key]);
} 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);
// phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
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;
}
/**
* Returns a `DateTimeZone` object based on a string containing a time zone name.
* Falls back to the default time zone if string passed not a recognised time zone.
*
* @param string $timeZoneString
* @return \DateTimeZone
*/
public function timeZoneStringToDateTimeZone($timeZoneString)
{
// Some time zones contain characters that are not permitted in param-texts,
// but are within quoted texts. We need to remove the quotes as they're not
// actually part of the time zone.
$timeZoneString = trim($timeZoneString, '"');
$timeZoneString = html_entity_decode($timeZoneString);
if ($this->isValidIanaTimeZoneId($timeZoneString)) {
return new \DateTimeZone($timeZoneString);
}
if ($this->isValidCldrTimeZoneId($timeZoneString)) {
return new \DateTimeZone(self::$cldrTimeZonesMap[$timeZoneString]);
}
if ($this->isValidWindowsTimeZoneId($timeZoneString)) {
return new \DateTimeZone(self::$windowsTimeZonesMap[$timeZoneString]);
}
return new \DateTimeZone($this->defaultTimeZone);
}
}
src/ICal/Event.php 0000666 00000010667 13535672167 0007772 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 = implode($input, $glue);
$input = str_replace($separator, $glue, $input);
return strtolower($input);
}
}
composer.json 0000666 00000002041 13535672167 0007306 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.39.0 || ^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 00000134650 13535672167 0007301 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": "613e6a5bb49b9d4676149640af571d64",
"packages": [
{
"name": "kylekatarnls/update-helper",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/kylekatarnls/update-helper.git",
"reference": "b34a46d7f5ec1795b4a15ac9d46b884377262df9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kylekatarnls/update-helper/zipball/b34a46d7f5ec1795b4a15ac9d46b884377262df9",
"reference": "b34a46d7f5ec1795b4a15ac9d46b884377262df9",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.1.0",
"php": ">=5.3.0"
},
"require-dev": {
"codeclimate/php-test-reporter": "dev-master",
"composer/composer": "^2.0.x-dev",
"phpunit/phpunit": ">=4.8.35 <6.0"
},
"type": "composer-plugin",
"extra": {
"class": "UpdateHelper\\ComposerPlugin"
},
"autoload": {
"psr-0": {
"UpdateHelper\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kyle",
"email": "kylekatarnls@gmail.com"
}
],
"description": "Update helper",
"time": "2019-06-05T08:34:23+00:00"
},
{
"name": "nesbot/carbon",
"version": "1.39.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "dd62a58af4e0775a45ea5f99d0363d81b7d9a1e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/dd62a58af4e0775a45ea5f99d0363d81b7d9a1e0",
"reference": "dd62a58af4e0775a45ea5f99d0363d81b7d9a1e0",
"shasum": ""
},
"require": {
"kylekatarnls/update-helper": "^1.1",
"php": ">=5.3.9",
"symfony/translation": "~2.6 || ~3.0 || ~4.0"
},
"require-dev": {
"composer/composer": "^1.2",
"friendsofphp/php-cs-fixer": "~2",
"phpunit/phpunit": "^4.8.35 || ^5.7"
},
"bin": [
"bin/upgrade-carbon"
],
"type": "library",
"extra": {
"update-helper": "Carbon\\Upgrade",
"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": "2019-06-11T09:07:59+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.50",
"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.1",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
"reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
"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-4": {
"Prophecy\\": "src/Prophecy"
}
},
"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": "2019-06-13T12:50:23+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.50",
"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 13535672167 0007242 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 00000037604 13535672167 0011722 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
'skipRecurrence' => false, // Default value
);
return $options;
}
public function getIcalHeader()
{
return array(
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
'X-WR-CALNAME:Private',
'X-APPLE-CALENDAR-COLOR:#FF2968',
'X-WR-CALDESC:',
);
}
public function formatIcalEvent($dtstart, $dtend)
{
return array(
'BEGIN:VEVENT',
'CREATED:20090213T195947Z',
'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209',
$dtstart,
$dtend,
'SUMMARY:test',
'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 13535672167 0011737 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 13535672167 0011013 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/RecurrencesTest.php 0000666 00000030214 13535672167 0011562 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',
7,
$checks
);
}
public function testWeeklyFullDayTimeZoneBerlin()
{
$checks = array(
array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '),
array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '),
array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '),
array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '),
array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '),
);
$this->assertVEVENT(
'Europe/Berlin',
'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
);
}
public 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);
}
}
public function assertEventFile($defaultTimezone, $file, $count, $checks)
{
$options = $this->getOptions($defaultTimezone);
$ical = new ICal($file, $options);
$events = $ical->events();
$this->assertCount($count, $events);
$events = $ical->sortEventsWithOrder($events);
foreach ($checks as $check) {
$this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
}
}
public function assertEvent($event, $expectedDateString, $message, $timeZone = null)
{
if (!is_null($timeZone)) {
date_default_timezone_set($timeZone);
}
$expectedTimeStamp = strtotime($expectedDateString);
$this->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 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
'skipRecurrence' => false, // Default value
);
return $options;
}
public 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',
);
}
public function getIcalHeader()
{
return array(
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
'X-WR-CALNAME:Private',
'X-APPLE-CALENDAR-COLOR:#FF2968',
'X-WR-CALDESC:',
);
}
public function getIcalFooter()
{
return array('END:VCALENDAR');
}
}
tests/rfc5545RecurrenceExamplesTest.php 0000666 00000123075 13535672167 0014124 0 ustar 00 originalTimeZone = date_default_timezone_get();
}
public function tearDown()
{
date_default_timezone_set($this->originalTimeZone);
}
// Page 123, Test 1 :: Daily, 10 Occurences
public function test_page123_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=DAILY;COUNT=10',
),
10,
$checks
);
}
// Page 123, Test 2 :: Daily, until December 24th
public function test_page123_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=DAILY;UNTIL=19971224T000000Z',
),
113,
$checks
);
}
// Page 124, Test 1 :: Daily, every other day, Forever
//
// UNTIL rule does not exist in original example
public function test_page124_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970906T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=DAILY;INTERVAL=2;UNTIL=19971201Z',
),
45,
$checks
);
}
// Page 124, Test 2 :: Daily, 10-day intervals, 5 occurrences
public function test_page124_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970912T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970922T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5',
),
5,
$checks
);
}
// Page 124, Test 3a :: Every January day, for 3 years (Variant A)
public function test_page124_test3a()
{
$checks = array(
array('index' => 0, 'dateString' => '19980101T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19980102T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19980103T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19980101T090000',
'RRULE:FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA',
),
93,
$checks
);
}
/* Requires support for BYMONTH under DAILY [No ticket]
*
// Page 124, Test 3b :: Every January day, for 3 years (Variant B)
public function test_page124_test3b()
{
$checks = array(
array('index' => 0, 'dateString' => '19980101T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19980102T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19980103T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19980101T090000',
'RRULE:FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1',
),
93,
$checks
);
}
*/
// Page 124, Test 4 :: Weekly, 10 occurrences
public function test_page124_test4()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=WEEKLY;COUNT=10',
),
10,
$checks
);
}
// Page 125, Test 1 :: Weekly, until December 24th
public function test_page125_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
array('index' => 16, 'dateString' => '19971223T090000', 'message' => 'last occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z',
),
17,
$checks
);
}
// Page 125, Test 2 :: Every other week, forever
//
// UNTIL rule does not exist in original example
public function test_page125_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970916T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970930T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19971014T090000', 'message' => '4th occurrence: '),
array('index' => 4, 'dateString' => '19971028T090000', 'message' => '5th occurrence: '),
array('index' => 5, 'dateString' => '19971111T090000', 'message' => '6th occurrence: '),
array('index' => 6, 'dateString' => '19971125T090000', 'message' => '7th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU;UNTIL=19971201Z',
),
7,
$checks
);
}
// Page 125, Test 3a :: Tuesday & Thursday every week, for five weeks (Variant A)
public function test_page125_test3a()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970909T090000', 'message' => '3rd occurrence: '),
array('index' => 9, 'dateString' => '19971002T090000', 'message' => 'final occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH',
),
10,
$checks
);
}
// Page 125, Test 3b :: Tuesday & Thursday every week, for five weeks (Variant B)
public function test_page125_test3b()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970909T090000', 'message' => '3rd occurrence: '),
array('index' => 9, 'dateString' => '19971002T090000', 'message' => 'final occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH',
),
10,
$checks
);
}
// Page 125, Test 4 :: Monday, Wednesday & Friday of every other week until December 24th
public function test_page125_test4()
{
$checks = array(
array('index' => 0, 'dateString' => '19970901T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970905T090000', 'message' => '3rd occurrence: '),
array('index' => 24, 'dateString' => '19971222T090000', 'message' => 'final occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970901T090000',
'RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR',
),
25,
$checks
);
}
// Page 126, Test 1 :: Tuesday & Thursday, every other week, for 8 occurrences
public function test_page126_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH',
),
8,
$checks
);
}
// Page 126, Test 2 :: First Friday of the Month, for 10 occurrences
public function test_page126_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19970905T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19971003T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971107T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970905T090000',
'RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR',
),
10,
$checks
);
}
// Page 126, Test 3 :: First Friday of the Month, until 24th December
public function test_page126_test3()
{
$checks = array(
array('index' => 0, 'dateString' => '19970905T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19971003T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971107T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970905T090000',
'RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR',
),
4,
$checks
);
}
// Page 126, Test 4 :: First and last Sunday, every other Month, for 10 occurrences
public function test_page126_test4()
{
$checks = array(
array('index' => 0, 'dateString' => '19970907T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970928T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971102T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19971130T090000', 'message' => '4th occurrence: '),
array('index' => 4, 'dateString' => '19980104T090000', 'message' => '5th occurrence: '),
array('index' => 5, 'dateString' => '19980125T090000', 'message' => '6th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970907T090000',
'RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU',
),
10,
$checks
);
}
// Page 126, Test 5 :: Second-to-last Monday of the Month, for six months
public function test_page126_test5()
{
$checks = array(
array('index' => 0, 'dateString' => '19970922T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19971020T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971117T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970922T090000',
'RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO',
),
6,
$checks
);
}
/* Requires support for negative BYMONTHDAY rules [No ticket]
*
// Page 127, Test 1 :: Third-to-last day of the month, forever
//
// UNTIL rule does not exist in original example.
public function test_page127_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970928T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19971029T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971128T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19971229T090000', 'message' => '4th occurrence: '),
array('index' => 4, 'dateString' => '19980129T090000', 'message' => '5th occurrence: '),
array('index' => 5, 'dateString' => '19980226T090000', 'message' => '6th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970928T090000',
'RRULE:FREQ=MONTHLY;BYMONTHDAY=-3;UNTIL=19980401',
),
6,
$checks
);
}
*/
// Page 127, Test 2 :: 2nd and 15th of each Month, for 10 occurrences
public function test_page127_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970915T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971002T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19971015T090000', 'message' => '4th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15',
),
10,
$checks
);
}
/* Requires support for negative BYMONTHDAY rules [No ticket]
*
// Page 127, Test 3 :: First and last day of the month, for 10 occurrences
public function test_page127_test3()
{
$checks = array(
array('index' => 0, 'dateString' => '19970930T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19971001T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971031T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19971101T090000', 'message' => '4th occurrence: '),
array('index' => 4, 'dateString' => '19971130T090000', 'message' => '5th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970930T090000',
'RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1',
),
10,
$checks
);
}
*/
// Page 127, Test 4 :: 10th through 15th, every 18 months, for 10 occurrences
public function test_page127_test4()
{
$checks = array(
array('index' => 0, 'dateString' => '19970910T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970911T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970912T090000', 'message' => '3rd occurrence: '),
array('index' => 6, 'dateString' => '19990310T090000', 'message' => '7th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970910T090000',
'RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15',
),
10,
$checks
);
}
// Page 127, Test 5 :: Every Tuesday, every other Month, forever
//
// UNTIL rule does not exist in original example.
public function test_page127_test5()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU;UNTIL=19980101',
),
9,
$checks
);
}
// Page 128, Test 1 :: June & July of each Year, for 10 occurrences
public function test_page128_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970610T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970710T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19980610T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970610T090000',
'RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7',
),
10,
$checks
);
}
// Page 128, Test 2 :: January, February, & March, every other Year, for 10 occurrences
public function test_page128_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19970310T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19990110T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19990210T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970310T090000',
'RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3',
),
10,
$checks
);
}
/* Requires support for BYYEARDAY under YEARLY [#11]
*
// Page 128, Test 3 :: Every third Year on the 1st, 100th, & 200th day for 10 occurrences
public function test_page128_test3()
{
$checks = array(
array('index' => 0, 'dateString' => '19970101T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970410T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970719T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970101T090000',
'RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200',
),
10,
$checks
);
}
*/
/* Requires support for BYDAY *without* BYMONTH under YEARLY [No ticket]
*
// Page 128, Test 4 :: 20th Monday of a Year, forever
//
// COUNT rule does not exist in original example.
public function test_page128_test4()
{
$checks = array(
array('index' => 0, 'dateString' => '19970519T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19980518T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19990517T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970519T090000',
'RRULE:FREQ=YEARLY;BYDAY=20MO;COUNT=4',
),
4,
$checks
);
}
*/
/* Requires support for BYWEEKNO under YEARLY [#11]
*
// Page 129, Test 1 :: Monday of Week 20, where the default start of the week is Monday, forever
//
// COUNT rule does not exist in original example.
public function test_page129_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970512T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19980511T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19990517T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970512T090000',
'RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO;COUNT=4',
),
4,
$checks
);
}
*/
// Page 129, Test 2 :: Every Thursday in March, forever
//
// UNTIL rule does not exist in original example.
public function test_page129_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19970313T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970320T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970327T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970313T090000',
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH;UNTIL=19990401Z',
),
11,
$checks
);
}
// Page 129, Test 3 :: Every Thursday in June, July, & August, forever
//
// UNTIL rule does not exist in original example.
public function test_page129_test3()
{
$checks = array(
array('index' => 0, 'dateString' => '19970605T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970612T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970619T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970605T090000',
'RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8;UNTIL=19970901Z',
),
13,
$checks
);
}
/* 1. Requires support for BYMONTHDAY and BYDAY in the same MONTHLY RRULE [No ticket]
* 2. Parser not excluding the date under EXDATE (which is the date of the initial event) [No ticket]
*
// Page 129, Test 4 :: Every Friday 13th, forever
//
// COUNT rule does not exist in original example.
public function test_page129_test4()
{
$checks = array(
array('index' => 0, 'dateString' => '19980213T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19980313T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19981113T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19990813T090000', 'message' => '4th occurrence: '),
array('index' => 4, 'dateString' => '20001013T090000', 'message' => '5th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'EXDATE;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13;COUNT=5',
),
5,
$checks
);
}
*/
/* Requires support for BYMONTHDAY and BYDAY in the same MONTHLY RRULE [No ticket]
*
// Page 130, Test 1 :: The first Saturday that follows the first Sunday of the month, forever:
//
// COUNT rule does not exist in original example.
public function test_page130_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970913T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19971011T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971108T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970913T090000',
'RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13;COUNT=7',
),
7,
$checks
);
}
*/
/* Requires support for BYMONTHDAY under YEARLY [#11]
*
// Page 130, Test 2 :: The first Tuesday after a Monday in November, every 4 Years (U.S. Presidential Election Day), forever
//
// COUNT rule does not exist in original example.
public function test_page130_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19961105T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '20001107T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '20041102T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19961105T090000',
'RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8;COUNT=4',
),
4,
$checks
);
}
*/
// Page 130, Test 3 :: Third instance of either a Tuesday, Wednesday, or Thursday of a Month, for 3 months.
public function test_page130_test3()
{
$checks = array(
array('index' => 0, 'dateString' => '19970904T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19971007T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971106T090000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970904T090000',
'RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3',
),
3,
$checks
);
}
// Page 130, Test 4 :: Second-to-last weekday of the month, indefinitely
//
// UNTIL rule does not exist in original example.
public function test_page130_test4()
{
$checks = array(
array('index' => 0, 'dateString' => '19970929T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19971030T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19971127T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19971230T090000', 'message' => '4th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970929T090000',
'RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;UNTIL=19980101',
),
4,
$checks
);
}
/* Requires support of HOURLY frequency [#101]
*
// Page 131, Test 1 :: Every 3 hours from 09:00 to 17:00 on a specific day
public function test_page131_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970902T120000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970902T150000', 'message' => '3rd occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z',
),
3,
$checks
);
}
*/
/* Requires support of MINUTELY frequency [#101]
*
// Page 131, Test 2 :: Every 15 minutes for 6 occurrences
public function test_page131_test2()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970902T091500', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970902T093000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19970902T094500', 'message' => '4th occurrence: '),
array('index' => 4, 'dateString' => '19970902T100000', 'message' => '5th occurrence: '),
array('index' => 5, 'dateString' => '19970902T101500', 'message' => '6th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6',
),
6,
$checks
);
}
*/
/* Requires support of MINUTELY frequency [#101]
*
// Page 131, Test 3 :: Every hour and a half for 4 occurrences
public function test_page131_test3()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970902T103000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970902T120000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19970902T133000', 'message' => '4th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4',
),
4,
$checks
);
}
*/
/* Requires support of BYHOUR and BYMINUTE under DAILY [#11]
*
// Page 131, Test 4a :: Every 20 minutes from 9:00 to 16:40 every day, using DAILY
//
// UNTIL rule does not exist in original example
public function test_page131_test4a()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence, Day 1: '),
array('index' => 1, 'dateString' => '19970902T092000', 'message' => '2nd occurrence, Day 1: '),
array('index' => 2, 'dateString' => '19970902T094000', 'message' => '3rd occurrence, Day 1: '),
array('index' => 3, 'dateString' => '19970902T100000', 'message' => '4th occurrence, Day 1: '),
array('index' => 20, 'dateString' => '19970902T164000', 'message' => 'Last occurrence, Day 1: '),
array('index' => 21, 'dateString' => '19970903T090000', 'message' => '1st occurrence, Day 2: '),
array('index' => 41, 'dateString' => '19970903T164000', 'message' => 'Last occurrence, Day 2: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;UNTIL=19970904T000000Z',
),
42,
$checks
);
}
*/
/* Requires support of MINUTELY frequency [#101]
*
// Page 131, Test 4b :: Every 20 minutes from 9:00 to 16:40 every day, using MINUTELY
//
// UNTIL rule does not exist in original example
public function test_page131_test4b()
{
$checks = array(
array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence, Day 1: '),
array('index' => 1, 'dateString' => '19970902T092000', 'message' => '2nd occurrence, Day 1: '),
array('index' => 2, 'dateString' => '19970902T094000', 'message' => '3rd occurrence, Day 1: '),
array('index' => 3, 'dateString' => '19970902T100000', 'message' => '4th occurrence, Day 1: '),
array('index' => 20, 'dateString' => '19970902T164000', 'message' => 'Last occurrence, Day 1: '),
array('index' => 21, 'dateString' => '19970903T090000', 'message' => '1st occurrence, Day 2: '),
array('index' => 41, 'dateString' => '19970903T164000', 'message' => 'Last occurrence, Day 2: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970902T090000',
'RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;UNTIL=19970904T000000Z',
),
42,
$checks
);
}
*/
// Page 131, Test 5a :: Changing the passed WKST rule, before...
public function test_page131_test5a()
{
$checks = array(
array('index' => 0, 'dateString' => '19970805T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970810T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970819T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19970824T090000', 'message' => '4th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970805T090000',
'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO',
),
4,
$checks
);
}
// Page 131, Test 5b :: ...and after
public function test_page131_test5b()
{
$checks = array(
array('index' => 0, 'dateString' => '19970805T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '19970817T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '19970819T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '19970831T090000', 'message' => '4th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:19970805T090000',
'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU',
),
4,
$checks
);
}
// Page 132, Test 1 :: Automatically ignoring an invalid date (30 February)
public function test_page132_test1()
{
$checks = array(
array('index' => 0, 'dateString' => '20070115T090000', 'message' => '1st occurrence: '),
array('index' => 1, 'dateString' => '20070130T090000', 'message' => '2nd occurrence: '),
array('index' => 2, 'dateString' => '20070215T090000', 'message' => '3rd occurrence: '),
array('index' => 3, 'dateString' => '20070315T090000', 'message' => '4th occurrence: '),
array('index' => 4, 'dateString' => '20070330T090000', 'message' => '5th occurrence: '),
);
$this->assertVEVENT(
'America/New_York',
array(
'DTSTART;TZID=America/New_York:20070115T090000',
'RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5',
),
5,
$checks
);
}
public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks)
{
$options = $this->getOptions($defaultTimezone);
$testIcal = implode(PHP_EOL, $this->getIcalHeader());
$testIcal .= PHP_EOL;
$testIcal .= implode(PHP_EOL, $this->formatIcalEvent($veventParts));
$testIcal .= PHP_EOL;
$testIcal .= implode(PHP_EOL, $this->getIcalFooter());
$ical = new ICal(false, $options);
$ical->initString($testIcal);
$events = $ical->events();
$this->assertCount($count, $events);
foreach ($checks as $check) {
$this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
}
}
public function assertEvent($event, $expectedDateString, $message, $timeZone = null)
{
if (!is_null($timeZone)) {
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 getOptions($defaultTimezone)
{
$options = array(
'defaultSpan' => 2, // Default value: 2
'defaultTimeZone' => $defaultTimezone, // Default value: UTC
'defaultWeekStart' => 'MO', // Default value
'disableCharacterReplacement' => false, // Default value
'filterDaysAfter' => null, // Default value
'filterDaysBefore' => null, // Default value
'skipRecurrence' => false, // Default value
);
return $options;
}
public function formatIcalEvent($veventParts)
{
return array_merge(
array(
'BEGIN:VEVENT',
'CREATED:' . gmdate('Ymd\THis\Z'),
'UID:RFC5545-examples-test',
),
$veventParts,
array(
'SUMMARY:test',
'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', filemtime(__FILE__)),
'END:VEVENT',
)
);
}
public function getIcalHeader()
{
return array(
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
'X-WR-CALNAME:Private',
'X-APPLE-CALENDAR-COLOR:#FF2968',
'X-WR-CALDESC:',
);
}
public function getIcalFooter()
{
return array('END:VCALENDAR');
}
}
CONTRIBUTING.md 0000666 00000002441 13535672167 0007021 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 00000001133 13535672167 0006555 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 #
##########
*.git
*-report.*
########
# IDEs #
########
.idea
*.iml