1: <?php
2: /**
3: * Class representing vFreebusy components.
4: *
5: * Copyright 2003-2012 Horde LLC (http://www.horde.org/)
6: *
7: * See the enclosed file COPYING for license information (LGPL). If you
8: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
9: *
10: * @todo Don't use timestamps
11: *
12: * @author Mike Cochrane <mike@graftonhall.co.nz>
13: * @category Horde
14: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
15: * @package Icalendar
16: */
17: class Horde_Icalendar_Vfreebusy extends Horde_Icalendar
18: {
19: /**
20: * The component type of this class.
21: *
22: * @var string
23: */
24: public $type = 'vFreebusy';
25:
26: /**
27: * TODO
28: *
29: * @var array
30: */
31: protected $_busyPeriods = array();
32:
33: /**
34: * TODO
35: *
36: * @var array
37: */
38: protected $_extraParams = array();
39:
40: /**
41: * Parses a string containing vFreebusy data.
42: *
43: * @param string $data The data to parse.
44: * @param $type TODO
45: * @param $charset TODO
46: */
47: public function parsevCalendar($data, $type = null, $charset = null)
48: {
49: parent::parsevCalendar($data, 'VFREEBUSY', $charset);
50:
51: // Do something with all the busy periods.
52: foreach ($this->_attributes as $key => $attribute) {
53: if ($attribute['name'] != 'FREEBUSY') {
54: continue;
55: }
56: foreach ($attribute['values'] as $value) {
57: $params = isset($attribute['params'])
58: ? $attribute['params']
59: : array();
60: if (isset($value['duration'])) {
61: $this->addBusyPeriod('BUSY', $value['start'], null,
62: $value['duration'], $params);
63: } else {
64: $this->addBusyPeriod('BUSY', $value['start'],
65: $value['end'], null, $params);
66: }
67: }
68: unset($this->_attributes[$key]);
69: }
70: }
71:
72: /**
73: * Returns the component exported as string.
74: *
75: * @return string The exported vFreeBusy information according to the
76: * iCalendar format specification.
77: */
78: public function exportvCalendar()
79: {
80: foreach ($this->_busyPeriods as $start => $end) {
81: $periods = array(array('start' => $start, 'end' => $end));
82: $this->setAttribute('FREEBUSY', $periods,
83: isset($this->_extraParams[$start])
84: ? $this->_extraParams[$start] : array());
85: }
86:
87: $res = $this->_exportvData('VFREEBUSY');
88:
89: foreach ($this->_attributes as $key => $attribute) {
90: if ($attribute['name'] == 'FREEBUSY') {
91: unset($this->_attributes[$key]);
92: }
93: }
94:
95: return $res;
96: }
97:
98: /**
99: * Returns a display name for this object.
100: *
101: * @return string A clear text name for displaying this object.
102: */
103: public function getName()
104: {
105: $name = '';
106:
107: try {
108: $method = !empty($this->_container)
109: ? $this->_container->getAttribute('METHOD')
110: : 'PUBLISH';
111: if ($method == 'PUBLISH') {
112: $attr = 'ORGANIZER';
113: } elseif ($method == 'REPLY') {
114: $attr = 'ATTENDEE';
115: }
116: } catch (Horde_Icalendar_Exception $e) {
117: $attr = 'ORGANIZER';
118: }
119:
120: try {
121: $name = $this->getAttribute($attr, true);
122: if (isset($name[0]['CN'])) {
123: return $name[0]['CN'];
124: }
125: } catch (Horde_Icalendar_Exception $e) {}
126:
127: try {
128: $name = parse_url($this->getAttribute($attr));
129: return $name['path'];
130: } catch (Horde_Icalendar_Exception $e) {
131: return '';
132: }
133: }
134:
135: /**
136: * Returns the email address for this object.
137: *
138: * @return string The email address of this object's owner.
139: */
140: public function getEmail()
141: {
142: $name = '';
143:
144: try {
145: $method = !empty($this->_container)
146: ? $this->_container->getAttribute('METHOD')
147: : 'PUBLISH';
148: if ($method == 'PUBLISH') {
149: $attr = 'ORGANIZER';
150: } elseif ($method == 'REPLY') {
151: $attr = 'ATTENDEE';
152: }
153: } catch (Horde_Icalendar_Exception $e) {
154: $attr = 'ORGANIZER';
155: }
156:
157: try {
158: $name = parse_url($this->getAttribute($attr));
159: return $name['path'];
160: } catch (Horde_Icalendar_Exception $e) {
161: return '';
162: }
163: }
164:
165: /**
166: * Returns the busy periods.
167: *
168: * @return array All busy periods.
169: */
170: public function getBusyPeriods()
171: {
172: return $this->_busyPeriods;
173: }
174:
175: /**
176: * Returns any additional freebusy parameters.
177: *
178: * @return array Additional parameters of the freebusy periods.
179: */
180: public function getExtraParams()
181: {
182: return $this->_extraParams;
183: }
184:
185: /**
186: * Returns all the free periods of time in a given period.
187: *
188: * @param integer $startStamp The start timestamp.
189: * @param integer $endStamp The end timestamp.
190: *
191: * @return array A hash with free time periods, the start times as the
192: * keys and the end times as the values.
193: */
194: public function getFreePeriods($startStamp, $endStamp)
195: {
196: $this->simplify();
197: $periods = array();
198:
199: // Check that we have data for some part of this period.
200: if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) {
201: return $periods;
202: }
203:
204: // Locate the first time in the requested period we have data for.
205: $nextstart = max($startStamp, $this->getStart());
206:
207: // Check each busy period and add free periods in between.
208: foreach ($this->_busyPeriods as $start => $end) {
209: if ($start <= $endStamp && $end >= $nextstart) {
210: if ($nextstart <= $start) {
211: $periods[$nextstart] = min($start, $endStamp);
212: }
213: $nextstart = min($end, $endStamp);
214: }
215: }
216:
217: // If we didn't read the end of the requested period but still have
218: // data then mark as free to the end of the period or available data.
219: if ($nextstart < $endStamp && $nextstart < $this->getEnd()) {
220: $periods[$nextstart] = min($this->getEnd(), $endStamp);
221: }
222:
223: return $periods;
224: }
225:
226: /**
227: * Adds a busy period to the info.
228: *
229: * This function may throw away data in case you add a period with a start
230: * date that already exists. The longer of the two periods will be chosen
231: * (and all information associated with the shorter one will be removed).
232: *
233: * @param string $type The type of the period. Either 'FREE' or
234: * 'BUSY'; only 'BUSY' supported at the moment.
235: * @param integer $start The start timestamp of the period.
236: * @param integer $end The end timestamp of the period.
237: * @param integer $duration The duration of the period. If specified, the
238: * $end parameter will be ignored.
239: * @param array $extra Additional parameters for this busy period.
240: */
241: public function addBusyPeriod($type, $start, $end = null, $duration = null,
242: $extra = array())
243: {
244: if ($type == 'FREE') {
245: // Make sure this period is not marked as busy.
246: return false;
247: }
248:
249: // Calculate the end time if duration was specified.
250: $tempEnd = is_null($duration) ? $end : $start + $duration;
251:
252: // Make sure the period length is always positive.
253: $end = max($start, $tempEnd);
254: $start = min($start, $tempEnd);
255:
256: if (isset($this->_busyPeriods[$start])) {
257: // Already a period starting at this time. Change the current
258: // period only if the new one is longer. This might be a problem
259: // if the callee assumes that there is no simplification going
260: // on. But since the periods are stored using the start time of
261: // the busy periods we have to throw away data here.
262: if ($end > $this->_busyPeriods[$start]) {
263: $this->_busyPeriods[$start] = $end;
264: $this->_extraParams[$start] = $extra;
265: }
266: } else {
267: // Add a new busy period.
268: $this->_busyPeriods[$start] = $end;
269: $this->_extraParams[$start] = $extra;
270: }
271:
272: return true;
273: }
274:
275: /**
276: * Returns the timestamp of the start of the time period this free busy
277: * information covers.
278: *
279: * @return integer A timestamp.
280: */
281: public function getStart()
282: {
283: try {
284: return $this->getAttribute('DTSTART');
285: } catch (Horde_Icalendar_Exception $e) {
286: return count($this->_busyPeriods)
287: ? min(array_keys($this->_busyPeriods))
288: : false;
289: }
290: }
291:
292: /**
293: * Returns the timestamp of the end of the time period this free busy
294: * information covers.
295: *
296: * @return integer A timestamp.
297: */
298: public function getEnd()
299: {
300: try {
301: return $this->getAttribute('DTEND');
302: } catch (Horde_Icalendar_Exception $e) {
303: return count($this->_busyPeriods)
304: ? max(array_values($this->_busyPeriods))
305: : false;
306: }
307: }
308:
309: /**
310: * Merges the busy periods of another Horde_Icalendar_Vfreebusy object
311: * into this one.
312: *
313: * This might lead to simplification no matter what you specify for the
314: * "simplify" flag since periods with the same start date will lead to the
315: * shorter period being removed (see addBusyPeriod).
316: *
317: * @param Horde_Icalendar_Vfreebusy $freebusy A freebusy object.
318: * @param boolean $simplify If true, simplify() will
319: * called after the merge.
320: */
321: public function merge(Horde_Icalendar_Vfreebusy $freebusy,
322: $simplify = true)
323: {
324: $extra = $freebusy->getExtraParams();
325: foreach ($freebusy->getBusyPeriods() as $start => $end) {
326: // This might simplify the busy periods without taking the
327: // "simplify" flag into account.
328: $this->addBusyPeriod('BUSY', $start, $end, null,
329: isset($extra[$start])
330: ? $extra[$start] : array());
331: }
332:
333: foreach (array('DTSTART', 'DTEND') as $val) {
334: try {
335: $thisattr = $this->getAttribute($val);
336: } catch (Horde_Icalendar_Exception $e) {
337: $thisattr = null;
338: }
339:
340: try {
341: $thatattr = $freebusy->getAttribute($val);
342: } catch (Horde_Icalendar_Exception $e) {
343: $thatattr = null;
344: }
345:
346: if (is_null($thisattr) && !is_null($thatattr)) {
347: $this->setAttribute($val, $thatattr, array(), false);
348: } elseif (!is_null($thatattr)) {
349: switch ($val) {
350: case 'DTSTART':
351: $set = ($thatattr < $thisattr);
352: break;
353:
354: case 'DTEND':
355: $set = ($thatattr > $thisattr);
356: break;
357: }
358:
359: if ($set) {
360: $this->setAttribute($val, $thatattr, array(), false);
361: }
362: }
363: }
364:
365: if ($simplify) {
366: $this->simplify();
367: }
368:
369: return true;
370: }
371:
372: /**
373: * Removes all overlaps and simplifies the busy periods array as much as
374: * possible.
375: */
376: public function simplify()
377: {
378: $clean = false;
379: $busy = array($this->_busyPeriods, $this->_extraParams);
380: while (!$clean) {
381: $result = $this->_simplify($busy[0], $busy[1]);
382: $clean = $result === $busy;
383: $busy = $result;
384: }
385:
386: ksort($result[1], SORT_NUMERIC);
387: $this->_extraParams = $result[1];
388:
389: ksort($result[0], SORT_NUMERIC);
390: $this->_busyPeriods = $result[0];
391: }
392:
393: /**
394: * TODO
395: *
396: * @param $busyPeriods TODO
397: * @param array $extraParams TODO
398: *
399: * @return array TODO
400: */
401: protected function _simplify($busyPeriods, $extraParams = array())
402: {
403: $checked = $checkedExtra = array();
404: $checkedEmpty = true;
405:
406: foreach ($busyPeriods as $start => $end) {
407: if ($checkedEmpty) {
408: $checked[$start] = $end;
409: $checkedExtra[$start] = isset($extraParams[$start])
410: ? $extraParams[$start]
411: : array();
412: $checkedEmpty = false;
413: } else {
414: $added = false;
415: foreach ($checked as $testStart => $testEnd) {
416: // Replace old period if the new period lies around the
417: // old period.
418: if ($start <= $testStart && $end >= $testEnd) {
419: // Remove old period entry.
420: unset($checked[$testStart]);
421: unset($checkedExtra[$testStart]);
422: // Add replacing entry.
423: $checked[$start] = $end;
424: $checkedExtra[$start] = isset($extraParams[$start])
425: ? $extraParams[$start]
426: : array();
427: $added = true;
428: } elseif ($start >= $testStart && $end <= $testEnd) {
429: // The new period lies fully within the old
430: // period. Just forget about it.
431: $added = true;
432: } elseif (($end <= $testEnd && $end >= $testStart) ||
433: ($start >= $testStart && $start <= $testEnd)) {
434: // Now we are in trouble: Overlapping time periods. If
435: // we allow for additional parameters we cannot simply
436: // choose one of the two parameter sets. It's better
437: // to leave two separated time periods.
438: $extra = isset($extraParams[$start])
439: ? $extraParams[$start]
440: : array();
441: $testExtra = isset($checkedExtra[$testStart])
442: ? $checkedExtra[$testStart]
443: : array();
444: // Remove old period entry.
445: unset($checked[$testStart]);
446: unset($checkedExtra[$testStart]);
447: // We have two periods overlapping. Are their
448: // additional parameters the same or different?
449: $newStart = min($start, $testStart);
450: $newEnd = max($end, $testEnd);
451: if ($extra === $testExtra) {
452: // Both periods have the same information. So we
453: // can just merge.
454: $checked[$newStart] = $newEnd;
455: $checkedExtra[$newStart] = $extra;
456: } else {
457: // Extra parameters are different. Create one
458: // period at the beginning with the params of the
459: // first period and create a trailing period with
460: // the params of the second period. The break
461: // point will be the end of the first period.
462: $break = min($end, $testEnd);
463: $checked[$newStart] = $break;
464: $checkedExtra[$newStart] =
465: isset($extraParams[$newStart])
466: ? $extraParams[$newStart]
467: : array();
468: $checked[$break] = $newEnd;
469: $highStart = max($start, $testStart);
470: $checkedExtra[$break] =
471: isset($extraParams[$highStart])
472: ? $extraParams[$highStart]
473: : array();
474:
475: // Ensure we also have the extra data in the
476: // extraParams.
477: $extraParams[$break] =
478: isset($extraParams[$highStart])
479: ? $extraParams[$highStart]
480: : array();
481: }
482: $added = true;
483: }
484:
485: if ($added) {
486: break;
487: }
488: }
489:
490: if (!$added) {
491: $checked[$start] = $end;
492: $checkedExtra[$start] = isset($extraParams[$start])
493: ? $extraParams[$start]
494: : array();
495: }
496: }
497: }
498:
499: return array($checked, $checkedExtra);
500: }
501:
502: }
503: