2731 lines
		
	
	
		
			104 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			2731 lines
		
	
	
		
			104 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						||
 | 
						||
/**
 | 
						||
 * This PHP class will read an ICS (`.ics`, `.ical`, `.ifb`) file, parse it and return an
 | 
						||
 * array of its contents.
 | 
						||
 *
 | 
						||
 * PHP 5 (≥ 5.6.40)
 | 
						||
 *
 | 
						||
 * @author  Jonathan Goode <https://github.com/u01jmg3>
 | 
						||
 * @license https://opensource.org/licenses/mit-license.php MIT License
 | 
						||
 * @version 3.2.0
 | 
						||
 */
 | 
						||
 | 
						||
 | 
						||
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;
 | 
						||
 | 
						||
    
 | 
						||
    public $events = array();
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * 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|null
 | 
						||
     */
 | 
						||
    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|null
 | 
						||
     */
 | 
						||
    public $filterDaysBefore;
 | 
						||
 | 
						||
    /**
 | 
						||
     * With this being non-null the parser will ignore all events more than roughly this many days before now.
 | 
						||
     *
 | 
						||
     * @var integer|null
 | 
						||
     */
 | 
						||
    public $filterDaysAfter;
 | 
						||
 | 
						||
    /**
 | 
						||
     * 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;
 | 
						||
 | 
						||
    /**
 | 
						||
     * Holds the custom Accept Language string header
 | 
						||
     *
 | 
						||
     * @var string
 | 
						||
     */
 | 
						||
    protected $httpAcceptLanguage;
 | 
						||
 | 
						||
    /**
 | 
						||
     * Holds the custom HTTP Protocol version
 | 
						||
     *
 | 
						||
     * @var string
 | 
						||
     */
 | 
						||
    protected $httpProtocolVersion;
 | 
						||
 | 
						||
    /**
 | 
						||
     * Define which variables can be configured
 | 
						||
     *
 | 
						||
     * @var array
 | 
						||
     */
 | 
						||
    private static $configurableOptions = array(
 | 
						||
        'defaultSpan',
 | 
						||
        'defaultTimeZone',
 | 
						||
        'defaultWeekStart',
 | 
						||
        'disableCharacterReplacement',
 | 
						||
        'filterDaysAfter',
 | 
						||
        'filterDaysBefore',
 | 
						||
        'httpUserAgent',
 | 
						||
        '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;
 | 
						||
 | 
						||
    /**
 | 
						||
     * 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;
 | 
						||
 | 
						||
    /**
 | 
						||
     * `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())
 | 
						||
    {
 | 
						||
        $this->events = [];
 | 
						||
        if (\PHP_VERSION_ID < 80100) {
 | 
						||
            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 = $this->getDefaultTimeZone(true);
 | 
						||
        }
 | 
						||
 | 
						||
        // 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)
 | 
						||
    {
 | 
						||
        $string = str_replace(array("\r\n", "\n\r", "\r"), "\n", $string);
 | 
						||
 | 
						||
        if ($this->cal === array()) {
 | 
						||
            $lines = explode("\n", $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 ($this->cal === array()) {
 | 
						||
            $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
 | 
						||
     * @param  string $acceptLanguage
 | 
						||
     * @param  string $httpProtocolVersion
 | 
						||
     * @return ICal
 | 
						||
     */
 | 
						||
    public function initUrl($url, $username = null, $password = null, $userAgent = null, $acceptLanguage = null, $httpProtocolVersion = null)
 | 
						||
    {
 | 
						||
        if (!is_null($username) && !is_null($password)) {
 | 
						||
            $this->httpBasicAuth['username'] = $username;
 | 
						||
            $this->httpBasicAuth['password'] = $password;
 | 
						||
        }
 | 
						||
 | 
						||
        if (!is_null($userAgent)) {
 | 
						||
            $this->httpUserAgent = $userAgent;
 | 
						||
        }
 | 
						||
 | 
						||
        if (!is_null($acceptLanguage)) {
 | 
						||
            $this->httpAcceptLanguage = $acceptLanguage;
 | 
						||
        }
 | 
						||
 | 
						||
        if (!is_null($httpProtocolVersion)) {
 | 
						||
            $this->httpProtocolVersion = $httpProtocolVersion;
 | 
						||
        }
 | 
						||
 | 
						||
        $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 (empty($line)) {
 | 
						||
                    continue;
 | 
						||
                }
 | 
						||
 | 
						||
                if (!$this->disableCharacterReplacement) {
 | 
						||
                    $line = str_replace(array(
 | 
						||
                        ' ',
 | 
						||
                        "\t",
 | 
						||
                        "\xc2\xa0", // Non-breaking space
 | 
						||
                    ), ' ', $line);
 | 
						||
 | 
						||
                    $line = $this->cleanCharacters($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 one already
 | 
						||
                        $blankArray = array(); // Empty placeholder array
 | 
						||
                        $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;
 | 
						||
 | 
						||
                        case 'BEGIN:VEVENT':
 | 
						||
                            // https://www.kanzaki.com/docs/ical/vevent.html
 | 
						||
                            if (!is_array($value)) {
 | 
						||
                                $this->eventCount++;
 | 
						||
                            }
 | 
						||
 | 
						||
                            $component = 'VEVENT';
 | 
						||
 | 
						||
                            break;
 | 
						||
 | 
						||
                        case 'BEGIN:VFREEBUSY':
 | 
						||
                            // https://www.kanzaki.com/docs/ical/vfreebusy.html
 | 
						||
                            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 ($this->alteredRecurrenceInstances !== array()) {
 | 
						||
                    $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 ($events !== array()) {
 | 
						||
            $lastIndex = count($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 ($events !== array()) {
 | 
						||
            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 = str_ireplace(' ', ' ', $string);
 | 
						||
 | 
						||
        $cleanedString = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string);
 | 
						||
 | 
						||
        $lines = explode(PHP_EOL, $cleanedString ?: $string);
 | 
						||
 | 
						||
        return $lines;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Add one key and value pair to the `$this->cal` array
 | 
						||
     *
 | 
						||
     * @param  string         $component
 | 
						||
     * @param  string|boolean $keyword
 | 
						||
     * @param  string|array   $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
 | 
						||
                    $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
 | 
						||
                    $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);
 | 
						||
                            $this->cal[$key1][$key2]["{$keyword}_array"][] = $duration;
 | 
						||
                        }
 | 
						||
                    }
 | 
						||
 | 
						||
                    if (!is_array($value) && $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;
 | 
						||
        }
 | 
						||
 | 
						||
        if (is_string($keyword)) {
 | 
						||
            $this->lastKeyword = $keyword;
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Gets the key value pair from an iCal string
 | 
						||
     *
 | 
						||
     * @param  string $text
 | 
						||
     * @return array
 | 
						||
     */
 | 
						||
    public function keyValueFromString($text)
 | 
						||
    {
 | 
						||
        $splitLine = $this->parseLine($text);
 | 
						||
        $object    = array();
 | 
						||
        $paramObj  = array();
 | 
						||
        $valueObj  = '';
 | 
						||
        $i         = 0;
 | 
						||
 | 
						||
        while ($i < count($splitLine)) {
 | 
						||
            // The first token corresponds to the property name
 | 
						||
            if ($i === 0) {
 | 
						||
                $object[0] = $splitLine[$i];
 | 
						||
                $i++;
 | 
						||
 | 
						||
                continue;
 | 
						||
            }
 | 
						||
 | 
						||
            // After each semicolon define the property parameters
 | 
						||
            if ($splitLine[$i] == ';') {
 | 
						||
                $i++;
 | 
						||
                $paramName = $splitLine[$i];
 | 
						||
                $i += 2;
 | 
						||
                $paramValue = array();
 | 
						||
                $multiValue = false;
 | 
						||
                // A parameter can have multiple values separated by a comma
 | 
						||
                while ($i + 1 < count($splitLine) && $splitLine[$i + 1] === ',') {
 | 
						||
                    $paramValue[] = $splitLine[$i];
 | 
						||
                    $i += 2;
 | 
						||
                    $multiValue = true;
 | 
						||
                }
 | 
						||
 | 
						||
                if ($multiValue) {
 | 
						||
                    $paramValue[] = $splitLine[$i];
 | 
						||
                } else {
 | 
						||
                    $paramValue = $splitLine[$i];
 | 
						||
                }
 | 
						||
 | 
						||
                // Create object with paramName => paramValue
 | 
						||
                $paramObj[$paramName] = $paramValue;
 | 
						||
            }
 | 
						||
 | 
						||
            // After a colon all tokens are concatenated (non-standard behaviour because the property can have multiple values
 | 
						||
            // according to RFC5545)
 | 
						||
            if ($splitLine[$i] === ':') {
 | 
						||
                $i++;
 | 
						||
                while ($i < count($splitLine)) {
 | 
						||
                    $valueObj .= $splitLine[$i];
 | 
						||
                    $i++;
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            $i++;
 | 
						||
        }
 | 
						||
 | 
						||
        // Object construction
 | 
						||
        if ($paramObj !== array()) {
 | 
						||
            $object[1][0] = $valueObj;
 | 
						||
            $object[1][1] = $paramObj;
 | 
						||
        } else {
 | 
						||
            $object[1] = $valueObj;
 | 
						||
        }
 | 
						||
 | 
						||
        return $object;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Parses a line from an iCal file into an array of tokens
 | 
						||
     *
 | 
						||
     * @param  string $line
 | 
						||
     * @return array
 | 
						||
     */
 | 
						||
    protected function parseLine($line)
 | 
						||
    {
 | 
						||
        $words = array();
 | 
						||
        $word  = '';
 | 
						||
        // The use of str_split is not a problem here even if the character set is in utf8
 | 
						||
        // Indeed we only compare the characters , ; : = " which are on a single byte
 | 
						||
        $arrayOfChar = str_split($line);
 | 
						||
        $inDoubleQuotes = false;
 | 
						||
 | 
						||
        foreach ($arrayOfChar as $char) {
 | 
						||
            // Don't stop the word on ; , : = if it is enclosed in double quotes
 | 
						||
            if ($char === '"') {
 | 
						||
                if ($word !== '') {
 | 
						||
                    $words[] = $word;
 | 
						||
                }
 | 
						||
 | 
						||
                $word = '';
 | 
						||
                $inDoubleQuotes = !$inDoubleQuotes;
 | 
						||
            } elseif (!in_array($char, array(';', ':', ',', '=')) || $inDoubleQuotes) {
 | 
						||
                $word .= $char;
 | 
						||
            } else {
 | 
						||
                if ($word !== '') {
 | 
						||
                    $words[] = $word;
 | 
						||
                }
 | 
						||
 | 
						||
                $words[] = $char;
 | 
						||
                $word = '';
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        $words[] = $word;
 | 
						||
 | 
						||
        return $words;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Returns the default time zone if set.
 | 
						||
     * Falls back to the system default if not set.
 | 
						||
     *
 | 
						||
     * @param  boolean $forceReturnSystemDefault
 | 
						||
     * @return string
 | 
						||
     */
 | 
						||
    private function getDefaultTimeZone($forceReturnSystemDefault = false)
 | 
						||
    {
 | 
						||
        $systemDefault = date_default_timezone_get();
 | 
						||
 | 
						||
        if ($forceReturnSystemDefault) {
 | 
						||
            return $systemDefault;
 | 
						||
        }
 | 
						||
 | 
						||
        return $this->defaultTimeZone ?: $systemDefault;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Returns a `DateTime` object from an iCal date time format
 | 
						||
     *
 | 
						||
     * @param  string $icalDate
 | 
						||
     * @return \DateTime|false
 | 
						||
     * @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 ($date === array()) {
 | 
						||
 | 
						||
            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 (isset($date[1]) && $date[1] !== '') {
 | 
						||
            $dateTimeZone = $this->timeZoneStringToDateTimeZone($date[1]);
 | 
						||
        } else {
 | 
						||
            $dateTimeZone = new \DateTimeZone($this->getDefaultTimeZone());
 | 
						||
        }
 | 
						||
 | 
						||
        // 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 (isset($date[3]) && $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)
 | 
						||
    {
 | 
						||
        $iCalDateToDateTime = $this->iCalDateToDateTime($icalDate);
 | 
						||
 | 
						||
        if ($iCalDateToDateTime === false) {
 | 
						||
            trigger_error("ICal::iCalDateToUnixTimestamp: Invalid date passed ({$icalDate})", E_USER_NOTICE);
 | 
						||
 | 
						||
            return 0;
 | 
						||
        }
 | 
						||
 | 
						||
        return $iCalDateToDateTime->getTimestamp();
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Returns a date adapted to the calendar time zone depending on the event `TZID`
 | 
						||
     *
 | 
						||
     * @param  array       $event
 | 
						||
     * @param  string      $key
 | 
						||
     * @param  string|null $format
 | 
						||
     * @return string|integer|boolean|\DateTime
 | 
						||
     */
 | 
						||
    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]);
 | 
						||
 | 
						||
            if ($dateTime instanceof \DateTime === false) {
 | 
						||
                trigger_error("ICal::iCalDateWithTimeZone: Invalid date passed ({$event['DTSTART']})", E_USER_NOTICE);
 | 
						||
 | 
						||
                return false;
 | 
						||
            }
 | 
						||
        } else {
 | 
						||
            // When constructing from a Unix Timestamp, no time zone needs passing.
 | 
						||
            $dateTime = new \DateTime("@{$dateArray[2]}");
 | 
						||
        }
 | 
						||
 | 
						||
        $calendarTimeZone = $this->calendarTimeZone();
 | 
						||
 | 
						||
        if (!is_null($calendarTimeZone)) {
 | 
						||
            // Set the time zone we wish to use when running `$dateTime->format`.
 | 
						||
            $dateTime->setTimezone(new \DateTimeZone($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()
 | 
						||
    {
 | 
						||
        $checks = null;
 | 
						||
        $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
 | 
						||
 | 
						||
        if ($events !== array()) {
 | 
						||
            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 (($alteredEventKey = array_search($eventDtstartUnix, $this->alteredRecurrenceInstances[$event['UID']], true)) !== false) {
 | 
						||
                        $eventKeysToRemove[] = $alteredEventKey;
 | 
						||
 | 
						||
                        $alteredEvent = array_replace_recursive($events[$key], $events[$alteredEventKey]);
 | 
						||
                        $this->alteredRecurrenceInstances[$event['UID']]['altered-event'] = array($key => $alteredEvent);
 | 
						||
                    }
 | 
						||
                }
 | 
						||
 | 
						||
                unset($checks);
 | 
						||
            }
 | 
						||
 | 
						||
            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 ($events === array()) {
 | 
						||
            return;
 | 
						||
        }
 | 
						||
 | 
						||
        $allEventRecurrences = array();
 | 
						||
        $eventKeysToRemove = array();
 | 
						||
 | 
						||
        foreach ($events as $key => $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]);
 | 
						||
 | 
						||
            if ($initialEventDate === false) {
 | 
						||
                trigger_error("ICal::processRecurrences: Invalid date passed ({$anEvent['DTSTART_array'][3]})", E_USER_NOTICE);
 | 
						||
 | 
						||
                continue;
 | 
						||
            }
 | 
						||
 | 
						||
            // Separate the RRULE stanzas, and explode the values that are lists.
 | 
						||
            $rrules = array();
 | 
						||
            foreach (array_filter(explode(';', $anEvent['RRULE'])) as $s) {
 | 
						||
                list($k, $v) = explode('=', $s);
 | 
						||
                if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH', 'BYYEARDAY', 'BYWEEKNO'))) {
 | 
						||
                    $rrules[$k] = explode(',', $v);
 | 
						||
                } else {
 | 
						||
                    $rrules[$k] = $v;
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            $frequency = $rrules['FREQ'];
 | 
						||
 | 
						||
            if (!is_string($frequency)) {
 | 
						||
                trigger_error('ICal::processRecurrences: Invalid frequency passed', E_USER_NOTICE);
 | 
						||
 | 
						||
                continue;
 | 
						||
            }
 | 
						||
 | 
						||
            // 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.
 | 
						||
            // > Furthermore, the BYDAY rule part MUST NOT be specified with a
 | 
						||
            // > numeric value with the FREQ rule part set to YEARLY when the
 | 
						||
            // > BYWEEKNO rule part is specified.
 | 
						||
            if (isset($rrules['BYDAY'])) {
 | 
						||
                $checkByDays = function ($carry, $weekday) {
 | 
						||
                    return $carry && substr($weekday, -2) === $weekday;
 | 
						||
                };
 | 
						||
                if (!in_array($frequency, array('MONTHLY', 'YEARLY'))) {
 | 
						||
                    if (is_array($rrules['BYDAY']) && !array_reduce($rrules['BYDAY'], $checkByDays, true)) {
 | 
						||
                        trigger_error("ICal::processRecurrences: A {$frequency} RRULE may not contain BYDAY values with numeric prefixes", E_USER_NOTICE);
 | 
						||
 | 
						||
                        continue;
 | 
						||
                    }
 | 
						||
                } elseif ($frequency === 'YEARLY' && (isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array()))) {
 | 
						||
                    if (is_array($rrules['BYDAY']) && !array_reduce($rrules['BYDAY'], $checkByDays, true)) {
 | 
						||
                        trigger_error('ICal::processRecurrences: A YEARLY RRULE with a BYWEEKNO part may not contain BYDAY values with numeric prefixes', E_USER_NOTICE);
 | 
						||
 | 
						||
                        continue;
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            $interval = (empty($rrules['INTERVAL'])) ? 1 : (int) $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 if the initial date is also an EXDATE
 | 
						||
            $initialDateIsExdate = array_reduce($exdates, function ($carry, $exdate) use ($initialEventDate) {
 | 
						||
                return $carry || $exdate->getTimestamp() === $initialEventDate->getTimestamp();
 | 
						||
            }, false);
 | 
						||
 | 
						||
            if ($initialDateIsExdate) {
 | 
						||
                $eventKeysToRemove[] = $key;
 | 
						||
            }
 | 
						||
 | 
						||
            /**
 | 
						||
             * 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.
 | 
						||
             *
 | 
						||
             * If the initial date is also an EXDATE, it shouldn't be included
 | 
						||
             * in the count.
 | 
						||
             *
 | 
						||
             * Syntax:
 | 
						||
             *   UNTIL={enddate}
 | 
						||
             *   COUNT=<positive integer>
 | 
						||
             *
 | 
						||
             * Where:
 | 
						||
             *   enddate = <icalDate> || <icalDateTime>
 | 
						||
             */
 | 
						||
            $count      = 1;
 | 
						||
            $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : PHP_INT_MAX;
 | 
						||
            $now        = date_create();
 | 
						||
 | 
						||
            $until = $now === false
 | 
						||
                ? 0
 | 
						||
                : $now->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp();
 | 
						||
 | 
						||
            $untilWhile = $until;
 | 
						||
 | 
						||
            if (isset($rrules['UNTIL']) && is_string($rrules['UNTIL'])) {
 | 
						||
                $untilDT = $this->iCalDateToDateTime($rrules['UNTIL']);
 | 
						||
                $until   = min($until, ($untilDT === false) ? $until : $untilDT->getTimestamp());
 | 
						||
 | 
						||
                // There are certain edge cases where we need to go a little beyond the UNTIL to
 | 
						||
                // ensure we get all events. Consider:
 | 
						||
                //
 | 
						||
                //   DTSTART:20200103
 | 
						||
                //   RRULE:FREQ=MONTHLY;BYDAY=-5FR;UNTIL=20200502
 | 
						||
                //
 | 
						||
                // In this case the last occurrence should be 1st May, however when we transition
 | 
						||
                // from April to May:
 | 
						||
                //
 | 
						||
                //   $until ~= 2nd May
 | 
						||
                //   $frequencyRecurringDateTime ~= 3rd May
 | 
						||
                //
 | 
						||
                // And as the latter comes after the former, the while loop ends before any dates
 | 
						||
                // in May have the chance to be considered.
 | 
						||
                $untilWhile = min($untilWhile, ($untilDT === false) ? $untilWhile : $untilDT->modify("+1 {$this->frequencyConversion[$frequency]}")->getTimestamp());
 | 
						||
            }
 | 
						||
 | 
						||
            $eventRecurrences = array();
 | 
						||
 | 
						||
            $frequencyRecurringDateTime = clone $initialEventDate;
 | 
						||
            while ($frequencyRecurringDateTime->getTimestamp() <= $untilWhile && $count < $countLimit) {
 | 
						||
                $candidateDateTimes = array();
 | 
						||
 | 
						||
                // phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault
 | 
						||
                switch ($frequency) {
 | 
						||
                    case 'DAILY':
 | 
						||
                        if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
 | 
						||
                            if (!isset($monthDays)) {
 | 
						||
                                // This variable is unset when we change months (see below)
 | 
						||
                                $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
 | 
						||
                            }
 | 
						||
 | 
						||
                            if (!in_array($frequencyRecurringDateTime->format('j'), $monthDays)) {
 | 
						||
                                break;
 | 
						||
                            }
 | 
						||
                        }
 | 
						||
 | 
						||
                        $candidateDateTimes[] = clone $frequencyRecurringDateTime;
 | 
						||
 | 
						||
                        break;
 | 
						||
 | 
						||
                    case 'WEEKLY':
 | 
						||
                        $initialDayOfWeek = $frequencyRecurringDateTime->format('N');
 | 
						||
                        $matchingDays     = array($initialDayOfWeek);
 | 
						||
 | 
						||
                        if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
 | 
						||
                            // 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), true);
 | 
						||
                                }
 | 
						||
                            } elseif ($rrules['WKST'] !== self::ISO_8601_WEEK_START) {
 | 
						||
                                $wkstTransition = array_search($rrules['WKST'], array_keys($this->weekdays), true);
 | 
						||
                            }
 | 
						||
 | 
						||
                            $matchingDays = array_map(
 | 
						||
                                function ($weekday) use ($initialDayOfWeek, $wkstTransition, $interval) {
 | 
						||
                                    $day = array_search($weekday, array_keys($this->weekdays), true);
 | 
						||
 | 
						||
                                    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++;
 | 
						||
 | 
						||
                                    return $day;
 | 
						||
                                },
 | 
						||
                                $rrules['BYDAY']
 | 
						||
                            );
 | 
						||
                        }
 | 
						||
 | 
						||
                        sort($matchingDays);
 | 
						||
 | 
						||
                        foreach ($matchingDays as $day) {
 | 
						||
                            $clonedDateTime = clone $frequencyRecurringDateTime;
 | 
						||
                            $candidateDateTimes[] = $clonedDateTime->setISODate(
 | 
						||
                                (int) $frequencyRecurringDateTime->format('o'),
 | 
						||
                                (int) $frequencyRecurringDateTime->format('W'),
 | 
						||
                                (int) $day
 | 
						||
                            );
 | 
						||
                        }
 | 
						||
                        break;
 | 
						||
 | 
						||
                    case 'MONTHLY':
 | 
						||
                        $matchingDays = array();
 | 
						||
 | 
						||
                        if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
 | 
						||
                            $matchingDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
 | 
						||
                            if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
 | 
						||
                                $matchingDays = array_filter(
 | 
						||
                                    $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime),
 | 
						||
                                    function ($monthDay) use ($matchingDays) {
 | 
						||
                                        return in_array($monthDay, $matchingDays);
 | 
						||
                                    }
 | 
						||
                                );
 | 
						||
                            }
 | 
						||
                        } elseif (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
 | 
						||
                            $matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
 | 
						||
                        } else {
 | 
						||
                            $matchingDays[] = $frequencyRecurringDateTime->format('d');
 | 
						||
                        }
 | 
						||
 | 
						||
                        if (isset($rrules['BYSETPOS']) && (is_array($rrules['BYSETPOS']) && $rrules['BYSETPOS'] !== array())) {
 | 
						||
                            $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(
 | 
						||
                                (int) $frequencyRecurringDateTime->format('Y'),
 | 
						||
                                (int) $frequencyRecurringDateTime->format('m'),
 | 
						||
                                $day
 | 
						||
                            );
 | 
						||
                        }
 | 
						||
                        break;
 | 
						||
 | 
						||
                    case 'YEARLY':
 | 
						||
                        $matchingDays = array();
 | 
						||
 | 
						||
                        if (isset($rrules['BYMONTH']) && (is_array($rrules['BYMONTH']) && $rrules['BYMONTH'] !== array())) {
 | 
						||
                            $bymonthRecurringDatetime = clone $frequencyRecurringDateTime;
 | 
						||
                            foreach ($rrules['BYMONTH'] as $byMonth) {
 | 
						||
                                $bymonthRecurringDatetime->setDate(
 | 
						||
                                    (int) $frequencyRecurringDateTime->format('Y'),
 | 
						||
                                    (int) $byMonth,
 | 
						||
                                    (int) $frequencyRecurringDateTime->format('d')
 | 
						||
                                );
 | 
						||
 | 
						||
                                // Determine the days of the month affected
 | 
						||
                                // (The interaction between BYMONTHDAY and BYDAY is resolved later.)
 | 
						||
                                $monthDays = array();
 | 
						||
                                if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
 | 
						||
                                    $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $bymonthRecurringDatetime);
 | 
						||
                                } elseif (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
 | 
						||
                                    $monthDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $bymonthRecurringDatetime);
 | 
						||
                                } else {
 | 
						||
                                    $monthDays[] = $bymonthRecurringDatetime->format('d');
 | 
						||
                                }
 | 
						||
 | 
						||
                                // And add each of them to the list of recurrences
 | 
						||
                                foreach ($monthDays as $day) {
 | 
						||
                                    $matchingDays[] = $bymonthRecurringDatetime->setDate(
 | 
						||
                                        (int) $frequencyRecurringDateTime->format('Y'),
 | 
						||
                                        (int) $bymonthRecurringDatetime->format('m'),
 | 
						||
                                        $day
 | 
						||
                                    )->format('z') + 1;
 | 
						||
                                }
 | 
						||
                            }
 | 
						||
                        } elseif (isset($rrules['BYWEEKNO']) && (is_array($rrules['BYWEEKNO']) && $rrules['BYWEEKNO'] !== array())) {
 | 
						||
                            $matchingDays = $this->getDaysOfYearMatchingByWeekNoRRule($rrules['BYWEEKNO'], $frequencyRecurringDateTime);
 | 
						||
                        } elseif (isset($rrules['BYYEARDAY']) && (is_array($rrules['BYYEARDAY']) && $rrules['BYYEARDAY'] !== array())) {
 | 
						||
                            $matchingDays = $this->getDaysOfYearMatchingByYearDayRRule($rrules['BYYEARDAY'], $frequencyRecurringDateTime);
 | 
						||
                        } elseif (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
 | 
						||
                            $matchingDays = $this->getDaysOfYearMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
 | 
						||
                        }
 | 
						||
 | 
						||
                        if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
 | 
						||
                            if (isset($rrules['BYYEARDAY']) && ($rrules['BYYEARDAY'] !== '' && $rrules['BYYEARDAY'] !== array()) || isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== '' && $rrules['BYMONTHDAY'] !== array()) || isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array())) {
 | 
						||
                                $matchingDays = array_filter(
 | 
						||
                                    $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime),
 | 
						||
                                    function ($yearDay) use ($matchingDays) {
 | 
						||
                                        return in_array($yearDay, $matchingDays);
 | 
						||
                                    }
 | 
						||
                                );
 | 
						||
                            } elseif ($matchingDays === array()) {
 | 
						||
                                $matchingDays = $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
 | 
						||
                            }
 | 
						||
                        }
 | 
						||
 | 
						||
                        if ($matchingDays === array()) {
 | 
						||
                            $matchingDays = array($frequencyRecurringDateTime->format('z') + 1);
 | 
						||
                        } else {
 | 
						||
                            sort($matchingDays);
 | 
						||
                        }
 | 
						||
 | 
						||
                        if (isset($rrules['BYSETPOS']) && (is_array($rrules['BYSETPOS']) && $rrules['BYSETPOS'] !== array())) {
 | 
						||
                            $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
 | 
						||
                        }
 | 
						||
 | 
						||
                        foreach ($matchingDays as $day) {
 | 
						||
                            $clonedDateTime = clone $frequencyRecurringDateTime;
 | 
						||
                            $candidateDateTimes[] = $clonedDateTime->setDate(
 | 
						||
                                (int) $frequencyRecurringDateTime->format('Y'),
 | 
						||
                                1,
 | 
						||
                                $day
 | 
						||
                            );
 | 
						||
                        }
 | 
						||
                        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++;
 | 
						||
                    }
 | 
						||
 | 
						||
                    // Count all evaluated candidates including excluded ones,
 | 
						||
                    // and if RRULE[COUNT] (if set) is reached then break.
 | 
						||
                    $count++;
 | 
						||
                    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');
 | 
						||
                    }
 | 
						||
                }
 | 
						||
 | 
						||
                // $monthDays is set in the DAILY frequency if the BYMONTHDAY stanza is present in
 | 
						||
                // the RRULE. The variable only needs to be updated when we change months, so we
 | 
						||
                // unset it here, prompting a recreation next iteration.
 | 
						||
                if (isset($monthDays) && $frequencyRecurringDateTime->format('m') !== $monthPreMove) {
 | 
						||
                    unset($monthDays);
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            unset($monthDays); // Unset it here as well, so it doesn't bleed into the calculation of the next recurring event.
 | 
						||
 | 
						||
            // 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);
 | 
						||
        }
 | 
						||
 | 
						||
        // Nullify the initial events that are also EXDATEs
 | 
						||
        foreach ($eventKeysToRemove as $eventKeyToRemove) {
 | 
						||
            $events[$eventKeyToRemove] = null;
 | 
						||
        }
 | 
						||
 | 
						||
        $events = array_merge($events, $allEventRecurrences);
 | 
						||
 | 
						||
        $this->cal['VEVENT'] = $events;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Resolves values from indices of the range 1 -> $limit.
 | 
						||
     *
 | 
						||
     * For instance, if passed [1, 4, -16] and 28, this will return [1, 4, 13].
 | 
						||
     *
 | 
						||
     * @param  array   $indexes
 | 
						||
     * @param  integer $limit
 | 
						||
     * @return array
 | 
						||
     */
 | 
						||
    protected function resolveIndicesOfRange(array $indexes, $limit)
 | 
						||
    {
 | 
						||
        $matching = array();
 | 
						||
        foreach ($indexes as $index) {
 | 
						||
            if ($index > 0 && $index <= $limit) {
 | 
						||
                $matching[] = $index;
 | 
						||
            } elseif ($index < 0 && -$index <= $limit) {
 | 
						||
                $matching[] = $index + $limit + 1;
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        sort($matching);
 | 
						||
 | 
						||
        return $matching;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * 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(array $byDays, $initialDateTime)
 | 
						||
    {
 | 
						||
        $matchingDays = array();
 | 
						||
        $currentMonth = $initialDateTime->format('n');
 | 
						||
 | 
						||
        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');
 | 
						||
                if ($bydayDateTime->format('n') === $currentMonth) {
 | 
						||
                    $matchingDays[] = $bydayDateTime->format('j');
 | 
						||
                }
 | 
						||
            } elseif ($ordwk > 0) { // +ve {ordwk}
 | 
						||
                $bydayDateTime->modify((--$ordwk) . ' week');
 | 
						||
                if ($bydayDateTime->format('n') === $currentMonth) {
 | 
						||
                    $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;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Find all days of a month that match the BYMONTHDAY stanza of an RRULE.
 | 
						||
     *
 | 
						||
     * RRUle Syntax:
 | 
						||
     *   BYMONTHDAY={bymodaylist}
 | 
						||
     *
 | 
						||
     * Where:
 | 
						||
     *   bymodaylist = {monthdaynum}[,{monthdaynum}...]
 | 
						||
     *   monthdaynum = ([+] || -) {ordmoday}
 | 
						||
     *   ordmoday    = 1 to 31
 | 
						||
     *
 | 
						||
     * @param  array     $byMonthDays
 | 
						||
     * @param  \DateTime $initialDateTime
 | 
						||
     * @return array
 | 
						||
     */
 | 
						||
    protected function getDaysOfMonthMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime)
 | 
						||
    {
 | 
						||
        return $this->resolveIndicesOfRange($byMonthDays, (int) $initialDateTime->format('t'));
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Find all days of a year that match the BYDAY stanza of an RRULE.
 | 
						||
     *
 | 
						||
     * With no {ordwk}, then return the day number of every {weekday}
 | 
						||
     * within the year.
 | 
						||
     *
 | 
						||
     * With a +ve {ordwk}, then return the {ordwk} {weekday} within the
 | 
						||
     * year.
 | 
						||
     *
 | 
						||
     * With a -ve {ordwk}, then return the {ordwk}-to-last {weekday}
 | 
						||
     * within the year.
 | 
						||
     *
 | 
						||
     * 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 getDaysOfYearMatchingByDayRRule(array $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 year
 | 
						||
            // (Or last if we have a -ve {ordwk})
 | 
						||
            $bydayDateTime->modify(
 | 
						||
                (($ordwk < 0) ? 'Last' : 'First') .
 | 
						||
                ' ' .
 | 
						||
                $this->weekdays[substr($weekday, -2)] . // e.g. "Monday"
 | 
						||
                ' of ' . (($ordwk < 0) ? 'December' : 'January') .
 | 
						||
                ' ' . $initialDateTime->format('Y') // e.g. "2018"
 | 
						||
            );
 | 
						||
 | 
						||
            if ($ordwk < 0) { // -ve {ordwk}
 | 
						||
                $bydayDateTime->modify((++$ordwk) . ' week');
 | 
						||
                $matchingDays[] = $bydayDateTime->format('z') + 1;
 | 
						||
            } elseif ($ordwk > 0) { // +ve {ordwk}
 | 
						||
                $bydayDateTime->modify((--$ordwk) . ' week');
 | 
						||
                $matchingDays[] = $bydayDateTime->format('z') + 1;
 | 
						||
            } else { // No {ordwk}
 | 
						||
                while ($bydayDateTime->format('Y') === $initialDateTime->format('Y')) {
 | 
						||
                    $matchingDays[] = $bydayDateTime->format('z') + 1;
 | 
						||
                    $bydayDateTime->modify('+1 week');
 | 
						||
                }
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        // Sort into ascending order
 | 
						||
        sort($matchingDays);
 | 
						||
 | 
						||
        return $matchingDays;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Find all days of a year that match the BYYEARDAY stanza of an RRULE.
 | 
						||
     *
 | 
						||
     * RRUle Syntax:
 | 
						||
     *   BYYEARDAY={byyrdaylist}
 | 
						||
     *
 | 
						||
     * Where:
 | 
						||
     *   byyrdaylist = {yeardaynum}[,{yeardaynum}...]
 | 
						||
     *   yeardaynum  = ([+] || -) {ordyrday}
 | 
						||
     *   ordyrday    = 1 to 366
 | 
						||
     *
 | 
						||
     * @param  array     $byYearDays
 | 
						||
     * @param  \DateTime $initialDateTime
 | 
						||
     * @return array
 | 
						||
     */
 | 
						||
    protected function getDaysOfYearMatchingByYearDayRRule(array $byYearDays, $initialDateTime)
 | 
						||
    {
 | 
						||
        // `\DateTime::format('L')` returns 1 if leap year, 0 if not.
 | 
						||
        $daysInThisYear = $initialDateTime->format('L') ? 366 : 365;
 | 
						||
 | 
						||
        return $this->resolveIndicesOfRange($byYearDays, $daysInThisYear);
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Find all days of a year that match the BYWEEKNO stanza of an RRULE.
 | 
						||
     *
 | 
						||
     * Unfortunately, the RFC5545 specification does not specify exactly
 | 
						||
     * how BYWEEKNO should expand on the initial DTSTART when provided
 | 
						||
     * without any other stanzas.
 | 
						||
     *
 | 
						||
     * A comparison of expansions used by other ics parsers may be found
 | 
						||
     * at https://github.com/s0600204/ics-parser-1/wiki/byweekno
 | 
						||
     *
 | 
						||
     * This method uses the same expansion as the python-dateutil module.
 | 
						||
     *
 | 
						||
     * RRUle Syntax:
 | 
						||
     *   BYWEEKNO={bywknolist}
 | 
						||
     *
 | 
						||
     * Where:
 | 
						||
     *   bywknolist = {weeknum}[,{weeknum}...]
 | 
						||
     *   weeknum    = ([+] || -) {ordwk}
 | 
						||
     *   ordwk      = 1 to 53
 | 
						||
     *
 | 
						||
     * @param  array     $byWeekNums
 | 
						||
     * @param  \DateTime $initialDateTime
 | 
						||
     * @return array
 | 
						||
     */
 | 
						||
    protected function getDaysOfYearMatchingByWeekNoRRule(array $byWeekNums, $initialDateTime)
 | 
						||
    {
 | 
						||
        // `\DateTime::format('L')` returns 1 if leap year, 0 if not.
 | 
						||
        $isLeapYear        = $initialDateTime->format('L');
 | 
						||
        $initialYear       = date_create("first day of January {$initialDateTime->format('Y')}");
 | 
						||
        $firstDayOfTheYear = ($initialYear === false) ? null : $initialYear->format('D');
 | 
						||
        $weeksInThisYear   = ($firstDayOfTheYear === 'Thu' || $isLeapYear && $firstDayOfTheYear === 'Wed') ? 53 : 52;
 | 
						||
 | 
						||
        $matchingWeeks = $this->resolveIndicesOfRange($byWeekNums, $weeksInThisYear);
 | 
						||
        $matchingDays = array();
 | 
						||
        $byweekDateTime = clone $initialDateTime;
 | 
						||
        foreach ($matchingWeeks as $weekNum) {
 | 
						||
            $dayNum = $byweekDateTime->setISODate(
 | 
						||
                (int) $initialDateTime->format('Y'),
 | 
						||
                $weekNum,
 | 
						||
                1
 | 
						||
            )->format('z') + 1;
 | 
						||
            for ($x = 0; $x < 7; ++$x) {
 | 
						||
                $matchingDays[] = $x + $dayNum;
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        sort($matchingDays);
 | 
						||
 | 
						||
        return $matchingDays;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Find all days of a year that match the BYMONTHDAY stanza of an RRULE.
 | 
						||
     *
 | 
						||
     * RRule Syntax:
 | 
						||
     *   BYMONTHDAY={bymodaylist}
 | 
						||
     *
 | 
						||
     * Where:
 | 
						||
     *   bymodaylist = {monthdaynum}[,{monthdaynum}...]
 | 
						||
     *   monthdaynum = ([+] || -) {ordmoday}
 | 
						||
     *   ordmoday    = 1 to 31
 | 
						||
     *
 | 
						||
     * @param  array     $byMonthDays
 | 
						||
     * @param  \DateTime $initialDateTime
 | 
						||
     * @return array
 | 
						||
     */
 | 
						||
    protected function getDaysOfYearMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime)
 | 
						||
    {
 | 
						||
        $matchingDays = array();
 | 
						||
        $monthDateTime = clone $initialDateTime;
 | 
						||
        for ($month = 1; $month < 13; $month++) {
 | 
						||
            $monthDateTime->setDate(
 | 
						||
                (int) $initialDateTime->format('Y'),
 | 
						||
                $month,
 | 
						||
                1
 | 
						||
            );
 | 
						||
 | 
						||
            $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($byMonthDays, $monthDateTime);
 | 
						||
            foreach ($monthDays as $day) {
 | 
						||
                $matchingDays[] = $monthDateTime->setDate(
 | 
						||
                    (int) $initialDateTime->format('Y'),
 | 
						||
                    (int) $monthDateTime->format('m'),
 | 
						||
                    $day
 | 
						||
                )->format('z') + 1;
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        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(array $bySetPos, array $valuesList)
 | 
						||
    {
 | 
						||
        $filteredMatches = array();
 | 
						||
 | 
						||
        foreach ($bySetPos as $setPosition) {
 | 
						||
            if ($setPosition < 0) {
 | 
						||
                $setPosition = count($valuesList) + ++$setPosition;
 | 
						||
            }
 | 
						||
 | 
						||
            // Positioning starts at 1, array indexes start at 0
 | 
						||
            if (isset($valuesList[$setPosition - 1])) {
 | 
						||
                $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
 | 
						||
     * @throws \Exception
 | 
						||
     */
 | 
						||
    protected function processDateConversions()
 | 
						||
    {
 | 
						||
        $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
 | 
						||
 | 
						||
        if ($events !== array()) {
 | 
						||
            foreach ($events as $key => $anEvent) {
 | 
						||
                if (is_null($anEvent) || !$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();
 | 
						||
 | 
						||
        foreach ($array as $event) {
 | 
						||
            $events[] = new \Event($event);
 | 
						||
        }
 | 
						||
 | 
						||
        $this->events = $events;
 | 
						||
    }
 | 
						||
 | 
						||
    public function filterByDate(string $dateString) {
 | 
						||
        $dateArray = explode('.', $dateString);
 | 
						||
        $dateStart = mktime(0,0,0,$dateArray[1],$dateArray[0],$dateArray[2]);
 | 
						||
        $tomorrow = $dateStart + 86400;
 | 
						||
 | 
						||
        $filteredEvents = [];
 | 
						||
        foreach ($this->events as $event) {
 | 
						||
            if ($event->startInt < $dateStart && $event->endInt > $dateStart) {
 | 
						||
                $filteredEvents[] = $event; //Event hat in der Vergangenheit begonnen und endet nach heute Mitternacht
 | 
						||
            } elseif ($event->startInt >= $dateStart && $event->endInt <= $tomorrow) {
 | 
						||
                $filteredEvents[] = $event; //Event findet genau heute statt
 | 
						||
            } elseif ($event->startInt >= $dateStart && $event->startInt <= $tomorrow) {
 | 
						||
                $filteredEvents[] = $event; //Event beginnt heute und endet in der Zukunft
 | 
						||
            }
 | 
						||
        }
 | 
						||
        return $filteredEvents;
 | 
						||
    }
 | 
						||
 | 
						||
 | 
						||
    /**
 | 
						||
     * 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|null
 | 
						||
     */
 | 
						||
    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 ($this->events() !== array()) ?: 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());
 | 
						||
 | 
						||
        if ($events === array()) {
 | 
						||
            return array();
 | 
						||
        }
 | 
						||
 | 
						||
        $extendedEvents = array();
 | 
						||
 | 
						||
        if (!is_null($rangeStart)) {
 | 
						||
            try {
 | 
						||
                $rangeStart = new \DateTime($rangeStart, new \DateTimeZone($this->getDefaultTimeZone()));
 | 
						||
            } catch (\Exception $exception) {
 | 
						||
                error_log("ICal::eventsFromRange: Invalid date passed ({$rangeStart})");
 | 
						||
                $rangeStart = false;
 | 
						||
            }
 | 
						||
        } else {
 | 
						||
            $rangeStart = new \DateTime('now', new \DateTimeZone($this->getDefaultTimeZone()));
 | 
						||
        }
 | 
						||
 | 
						||
        if (!is_null($rangeEnd)) {
 | 
						||
            try {
 | 
						||
                $rangeEnd = new \DateTime($rangeEnd, new \DateTimeZone($this->getDefaultTimeZone()));
 | 
						||
            } catch (\Exception $exception) {
 | 
						||
                error_log("ICal::eventsFromRange: Invalid date passed ({$rangeEnd})");
 | 
						||
                $rangeEnd = false;
 | 
						||
            }
 | 
						||
        } else {
 | 
						||
            $rangeEnd = new \DateTime('now', new \DateTimeZone($this->getDefaultTimeZone()));
 | 
						||
            $rangeEnd->modify('+20 years');
 | 
						||
        }
 | 
						||
 | 
						||
        if ($rangeEnd !== false && $rangeStart !== false) {
 | 
						||
            // 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;
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        return $extendedEvents;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Returns a sorted array of the events following a given string
 | 
						||
     *
 | 
						||
     * @param  string $interval
 | 
						||
     * @return array
 | 
						||
     */
 | 
						||
    public function eventsFromInterval($interval)
 | 
						||
    {
 | 
						||
        $timeZone   = $this->getDefaultTimeZone();
 | 
						||
        $rangeStart = new \DateTime('now', new \DateTimeZone($timeZone));
 | 
						||
        $rangeEnd   = new \DateTime('now', new \DateTimeZone($timeZone));
 | 
						||
 | 
						||
        $dateInterval = \DateInterval::createFromDateString($interval);
 | 
						||
 | 
						||
        if ($dateInterval instanceof \DateInterval) {
 | 
						||
            $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  \DateInterval $duration
 | 
						||
     * @return \DateTime|false
 | 
						||
     */
 | 
						||
    protected function parseDuration($date, $duration)
 | 
						||
    {
 | 
						||
        $dateTime = date_create($date);
 | 
						||
 | 
						||
        if ($dateTime === false) {
 | 
						||
            return false;
 | 
						||
        }
 | 
						||
 | 
						||
        $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");
 | 
						||
 | 
						||
        return $dateTime;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Removes unprintable ASCII and UTF-8 characters
 | 
						||
     *
 | 
						||
     * @param  string $data
 | 
						||
     * @return string|null
 | 
						||
     */
 | 
						||
    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 (($code %= 0x200000) < 0x80) {
 | 
						||
                $s = chr($code);
 | 
						||
            } elseif ($code < 0x800) {
 | 
						||
                $s = chr(0xc0 | $code >> 6) . chr(0x80 | $code & 0x3f);
 | 
						||
            } elseif ($code < 0x10000) {
 | 
						||
                $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;
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * 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;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Replace curly quotes and other special characters with their standard equivalents
 | 
						||
     * @see https://utf8-chartable.de/unicode-utf8-table.pl?start=8211&utf8=string-literal
 | 
						||
     *
 | 
						||
     * @param  string $input
 | 
						||
     * @return string
 | 
						||
     */
 | 
						||
    protected function cleanCharacters($input)
 | 
						||
    {
 | 
						||
        return strtr(
 | 
						||
            $input,
 | 
						||
            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"     => '...', // …
 | 
						||
                $this->mb_chr(145) => "'",   // ‘
 | 
						||
                $this->mb_chr(146) => "'",   // ’
 | 
						||
                $this->mb_chr(147) => '"',   // “
 | 
						||
                $this->mb_chr(148) => '"',   // ”
 | 
						||
                $this->mb_chr(150) => '-',   // –
 | 
						||
                $this->mb_chr(151) => '--',  // —
 | 
						||
                $this->mb_chr(133) => '...', // …
 | 
						||
            )
 | 
						||
        );
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * 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 = new \DateTimeZone($this->getDefaultTimeZone());
 | 
						||
 | 
						||
        foreach ($exdates as $subArray) {
 | 
						||
            end($subArray);
 | 
						||
            $finalKey = key($subArray);
 | 
						||
 | 
						||
            foreach (array_keys($subArray) as $key) {
 | 
						||
                if ($key === 'TZID') {
 | 
						||
                    $currentTimeZone = $this->timeZoneStringToDateTimeZone($subArray[$key]);
 | 
						||
                } elseif (is_numeric($key)) {
 | 
						||
                    $icalDate = $subArray[$key];
 | 
						||
 | 
						||
                    if (substr($icalDate, -1) === 'Z') {
 | 
						||
                        $currentTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC);
 | 
						||
                    }
 | 
						||
 | 
						||
                    $output[] = new \DateTime($icalDate, $currentTimeZone);
 | 
						||
 | 
						||
                    if ($key === $finalKey) {
 | 
						||
                        // Reset to default
 | 
						||
                        $currentTimeZone = new \DateTimeZone($this->getDefaultTimeZone());
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        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 $exception) {
 | 
						||
            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();
 | 
						||
        $options['http']           = array();
 | 
						||
        $options['http']['header'] = array();
 | 
						||
 | 
						||
        if ($this->httpBasicAuth === array() || !empty($this->httpUserAgent) || !empty($this->httpAcceptLanguage)) {
 | 
						||
            if ($this->httpBasicAuth !== array()) {
 | 
						||
                $username  = $this->httpBasicAuth['username'];
 | 
						||
                $password  = $this->httpBasicAuth['password'];
 | 
						||
                $basicAuth = base64_encode("{$username}:{$password}");
 | 
						||
 | 
						||
                $options['http']['header'][] = "Authorization: Basic {$basicAuth}";
 | 
						||
            }
 | 
						||
 | 
						||
            if (!empty($this->httpUserAgent)) {
 | 
						||
                $options['http']['header'][] = "User-Agent: {$this->httpUserAgent}";
 | 
						||
            }
 | 
						||
 | 
						||
            if (!empty($this->httpAcceptLanguage)) {
 | 
						||
                $options['http']['header'][] = "Accept-language: {$this->httpAcceptLanguage}";
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        if (empty($this->httpUserAgent)) {
 | 
						||
            if (mb_stripos($filename, 'outlook.office365.com') !== false) {
 | 
						||
                $options['http']['header'][] = 'User-Agent: A User Agent';
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        if (!empty($this->httpProtocolVersion)) {
 | 
						||
            $options['http']['protocol_version'] = $this->httpProtocolVersion;
 | 
						||
        } else {
 | 
						||
            $options['http']['protocol_version'] = '1.1';
 | 
						||
        }
 | 
						||
 | 
						||
        $options['http']['header'][] = 'Connection: close';
 | 
						||
 | 
						||
        $context = stream_context_create($options);
 | 
						||
 | 
						||
            if (($lines = @file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES, $context)) === false) {
 | 
						||
                #    throw new \Exception("The file path or URL '{$filename}' does not exist.");
 | 
						||
            }
 | 
						||
            if (false === $lines) {
 | 
						||
                return [];
 | 
						||
            }
 | 
						||
            return $lines;
 | 
						||
 | 
						||
        // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
 | 
						||
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * 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->getDefaultTimeZone());
 | 
						||
    }
 | 
						||
}
 |