PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.5.23
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.5.23
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / app / Services / Libs / RRule / RfcParser.php

RfcParser.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 1.5.23, at app/Services/Libs/RRule/RfcParser.php

328 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Licensed under the MIT license.
5 *
6 * For the full copyright and license information, please view the LICENSE file.
7 *
8 * @author Rémi Lanvin <remi@cloudconnected.fr>
9 * @link https://github.com/rlanvin/php-rrule
10 */
11
12 namespace FluentBooking\App\Services\Libs\RRule;
13
14 /**
15 * Collection of static methods to parse RFC strings.
16 *
17 * This is used internally by RRule and RSet. The methods are public, BUT this
18 * isn't part of the public API of this library. Therefore there is no guarantee
19 * they will not break even for a minor version release.
20 *
21 * @internal
22 */
23 class RfcParser
24 {
25 static $tzdata = null;
26
27 /**
28 * High level "line".
29 * Explode a line into property name, property parameters and property value
30 */
31 static public function parseLine($line, array $default = array())
32 {
33 $line = trim($line);
34 $property = array_merge(array(
35 'name' => '',
36 'params' => array(),
37 'value' => null
38 ), $default);
39
40 if (strpos($line,':') === false) {
41 if (! $property['name']) {
42 throw new \InvalidArgumentException('Failed to parse RFC line, missing property name followed by ":"');
43 }
44 $property['value'] = $line;
45 } else {
46 list($property['name'],$property['value']) = explode(':', $line);
47
48 $tmp = explode(';',$property['name']);
49 $property['name'] = $tmp[0];
50 array_splice($tmp,0,1);
51 foreach ($tmp as $pair) {
52 if (strpos($pair,'=') === false) {
53 throw new \InvalidArgumentException('Failed to parse RFC line, invalid property parameters: '. esc_html($pair));
54 }
55 list($key,$value) = explode('=',$pair);
56 $property['params'][$key] = $value;
57 }
58 }
59
60 return $property;
61 }
62
63 /**
64 * Parse both DTSTART and RRULE (and EXRULE).
65 *
66 * It's impossible to accuratly parse a RRULE in isolation (without the DTSTART)
67 * as some tests depends on DTSTART (notably the date format for UNTIL).
68 *
69 * @param string $string The RFC-like string
70 * @param mixed $dtstart The default dtstart to be used (if not in the string)
71 * @return array
72 */
73 static public function parseRRule($string, $dtstart = null)
74 {
75 $string = trim($string);
76 $parts = array();
77 $dtstart_type = null;
78 $rfc_date_regexp = '/\d{6}(T\d{6})?Z?/'; // regexp to check the date, a bit loose
79 $nb_dtstart = 0;
80 $nb_rrule = 0;
81 $lines = explode("\n", $string);
82
83 if ($dtstart) {
84 $nb_dtstart = 1;
85 if (is_string($dtstart)) {
86 if (strlen($dtstart) == 10) {
87 $dtstart_type = 'date';
88 }
89 else {
90 $dtstart_type = 'localtime';
91 }
92 }
93 else {
94 $dtstart_type = 'tzid';
95 }
96 $parts['DTSTART'] = RRule::parseDate($dtstart);
97 }
98
99 foreach ($lines as $line) {
100 $property = self::parseLine($line, array(
101 'name' => sizeof($lines) > 1 ? null : 'RRULE' // allow missing property name for single-line RRULE
102 ));
103
104 switch (strtoupper($property['name'])) {
105 case 'DTSTART':
106 $nb_dtstart += 1;
107 if ($nb_dtstart > 1) {
108 throw new \InvalidArgumentException('Too many DTSTART properties (there can be only one)');
109 }
110 $tmp = null;
111 $dtstart_type = 'date';
112 if (! preg_match($rfc_date_regexp, $property['value'])) {
113 throw new \InvalidArgumentException(
114 'Invalid DTSTART property: date or date time format incorrect'
115 );
116 }
117 if (isset($property['params']['TZID'])) {
118 // TZID must only be specified if this is a date-time (see section 3.3.4 & 3.3.5 of RFC 5545)
119 if (strpos($property['value'], 'T') === false) {
120 throw new \InvalidArgumentException(
121 'Invalid DTSTART property: TZID should not be specified if there is no time component'
122 );
123 }
124 // The "TZID" property parameter MUST NOT be applied to DATE-TIME
125 // properties whose time values are specified in UTC.
126 if (strpos($property['value'], 'Z') !== false) {
127 throw new \InvalidArgumentException(
128 'Invalid DTSTART property: TZID must not be applied when time is specified in UTC'
129 );
130 }
131 $dtstart_type = 'tzid';
132 $tmp = self::parseTimeZone($property['params']['TZID']);
133 }
134 elseif (strpos($property['value'], 'T') !== false) {
135 if (strpos($property['value'], 'Z') === false) {
136 $dtstart_type = 'localtime'; // no timezone
137 }
138 else {
139 $dtstart_type = 'utc';
140 }
141 }
142 $parts['DTSTART'] = new \DateTime($property['value'], $tmp);
143 break;
144 case 'RRULE':
145 case 'EXRULE':
146 $nb_rrule += 1;
147 if ($nb_rrule > 1) {
148 throw new \InvalidArgumentException('Too many RRULE properties (there can be only one)');
149 }
150 foreach (explode(';',$property['value']) as $pair) {
151 $pair = explode('=', $pair);
152 if (! isset($pair[1]) || isset($pair[2])) {
153 throw new \InvalidArgumentException('Failed to parse RFC string, malformed RRULE property: ' . esc_html($property['value']));
154 }
155 list($key, $value) = $pair;
156 if ($key === 'UNTIL') {
157 if (! preg_match($rfc_date_regexp, $value)) {
158 throw new \InvalidArgumentException(
159 'Invalid UNTIL property: date or date time format incorrect'
160 );
161 }
162 switch ($dtstart_type) {
163 case 'date':
164 if (strpos($value, 'T') !== false) {
165 throw new \InvalidArgumentException(
166 'Invalid UNTIL property: The value of the UNTIL rule part MUST be a date if DTSTART is a date.'
167 );
168 }
169 break;
170 case 'localtime':
171 if (strpos($value, 'T') === false || strpos($value, 'Z') !== false) {
172 throw new \InvalidArgumentException(
173 'Invalid UNTIL property: if the "DTSTART" property is specified as a date with local time, then the UNTIL rule part MUST also be specified as a date with local time'
174 );
175 }
176 break;
177 case 'tzid':
178 case 'utc':
179 if (strpos($value, 'T') === false || strpos($value, 'Z') === false) {
180 throw new \InvalidArgumentException(
181 'Invalid UNTIL property: if the "DTSTART" property is specified as a date with UTC time or a date with local time and time zone reference, then the UNTIL rule part MUST be specified as a date with UTC time.'
182 );
183 }
184 break;
185 }
186
187 $value = new \DateTime($value);
188 }
189 elseif ($key === 'DTSTART') {
190 if (isset($parts['DTSTART'])) {
191 throw new \InvalidArgumentException('DTSTART cannot be part of RRULE and has already been defined');
192 }
193 // this is an invalid rule, however we'll support it since the JS lib is broken
194 // see https://github.com/rlanvin/php-rrule/issues/25
195 trigger_error("This string is not compliant with the RFC (DTSTART cannot be part of RRULE). It is accepted as is for compability reasons only.", E_USER_NOTICE);
196 }
197 $parts[$key] = $value;
198 }
199 break;
200 default:
201 throw new \InvalidArgumentException('Failed to parse RFC string, unsupported property: ' . esc_html($property['name']));
202 }
203 }
204
205 return $parts;
206 }
207
208 /**
209 * Parse RDATE and return an array of DateTime
210 */
211 static public function parseRDate($line)
212 {
213 $property = self::parseLine($line);
214 if ($property['name'] !== 'RDATE') {
215 throw new \InvalidArgumentException('Failed to parse RDATE line, this is a ' . esc_html($property['name']) . 'property');
216 }
217
218 $period = false;
219 $tz = null;
220 foreach ($property['params'] as $name => $value) {
221 switch (strtoupper($name)) {
222 case 'TZID':
223 $tz = new \DateTimeZone($value);
224 break;
225 case 'VALUE':
226 switch ($value) {
227 case 'DATE':
228 case 'DATE-TIME':
229 break;
230 case 'PERIOD':
231 $period = true;
232 break;
233 default:
234 throw new \InvalidArgumentException('Unknown VALUE value for RDATE: ' . esc_html($value) . ', must be one of DATE-TIME, DATE or PERIOD');
235 }
236 break;
237 default:
238 throw new \InvalidArgumentException('Unknown property parameter: ' . esc_html($name));
239 }
240 }
241
242 $dates = array();
243
244 foreach (explode(',',$property['value']) as $value) {
245 if ($period) {
246 if (strpos($value,'/') === false) {
247 throw new \InvalidArgumentException('Invalid period in RDATE');
248 }
249 // period is unsupported!
250 trigger_error('VALUE=PERIOD is not supported and ignored', E_USER_NOTICE);
251 }
252 else {
253 if (strpos($value, 'Z')) {
254 if ($tz !== null) {
255 throw new \InvalidArgumentException('Invalid RDATE property: TZID must not be applied when time is specified in UTC');
256 }
257 $dates[] = new \DateTime($value);
258 }
259 else {
260 $dates[] = new \DateTime($value, $tz);
261 }
262 // TODO should check that only dates are provided with VALUE=DATE, and so on.
263 }
264 }
265
266 return $dates;
267 }
268
269 /**
270 * Parse EXDATE and return an array of DateTime
271 */
272 static public function parseExDate($line)
273 {
274 $property = self::parseLine($line);
275 if ($property['name'] !== 'EXDATE') {
276 throw new \InvalidArgumentException('Failed to parse EXDATE line, this is a ' . esc_html($property['name']) . 'property');
277 }
278
279 $tz = null;
280 foreach ($property['params'] as $name => $value) {
281 switch (strtoupper($name)) {
282 case 'VALUE':
283 // Ignore optional words
284 break;
285 case 'TZID':
286 $tz = new \DateTimeZone($value);
287 break;
288 default:
289 throw new \InvalidArgumentException('Unknown property parameter: ' . esc_html($name));
290 }
291 }
292
293 $dates = array();
294
295 foreach (explode(',',$property['value']) as $value) {
296 if (strpos($value, 'Z')) {
297 if ($tz !== null) {
298 throw new \InvalidArgumentException('Invalid EXDATE property: TZID must not be applied when time is specified in UTC');
299 }
300 $dates[] = new \DateTime($value);
301 }
302 else {
303 $dates[] = new \DateTime($value, $tz);
304 }
305 }
306
307 return $dates;
308 }
309
310 /**
311 * Create a new DateTimeZone object, converting non-standard timezone.
312 *
313 * @see https://github.com/rlanvin/php-rrule/issues/69
314 */
315 static public function parseTimeZone($tzid)
316 {
317 if (self::$tzdata === null) {
318 self::$tzdata = require __DIR__.'/tzdata/windows.php';
319 }
320
321 if (isset(self::$tzdata[$tzid])) {
322 return new \DateTimeZone(self::$tzdata[$tzid]);
323 }
324
325 return new \DateTimeZone($tzid);
326 }
327 }
328