1: <?php
2: /**
3: * The Kolab implementation of the free/busy system.
4: *
5: * PHP version 5
6: *
7: * @category Kolab
8: * @package Kolab_FreeBusy
9: * @author Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
10: * @author Gunnar Wrobel <wrobel@pardus.de>
11: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
12: * @link http://pear.horde.org/index.php?package=Kolab_FreeBusy
13: */
14:
15: /**
16: * The Horde_Kolab_FreeBusy class serves as Registry aka ServiceLocator for the
17: * Free/Busy application. It also provides the entry point into the the Horde
18: * MVC system and allows to dispatch a request.
19: *
20: * Copyright 2009-2012 Horde LLC (http://www.horde.org/)
21: *
22: * See the enclosed file COPYING for license information (LGPL). If you did not
23: * receive this file, see
24: * http://www.horde.org/licenses/lgpl21.
25: *
26: * @category Kolab
27: * @package Kolab_FreeBusy
28: * @author Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
29: * @author Gunnar Wrobel <wrobel@pardus.de>
30: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
31: * @link http://pear.horde.org/index.php?package=Kolab_FreeBusy
32: */
33: class Horde_Kolab_FreeBusy_Driver_Freebusy_Base extends Horde_Kolab_FreeBusy_Driver_Base
34: {
35:
36: /**
37: * Parameters for the driver.
38: *
39: * @var array
40: */
41: private $_params;
42:
43: /**
44: * Constructor.
45: *
46: * @param array $params The parameters for the driver.
47: */
48: public function __construct(array $params)
49: {
50: $this->_params = $params;
51: }
52:
53: /**
54: * Trigger regeneration of exported data.
55: *
56: * @param array $params The parameters required to regenerate the freebusy
57: * data.
58: *
59: * @return Horde_Kolab_FreeBusy_Driver_Result The freebusy data.
60: */
61: /* public function trigger(array $params) */
62: /* { */
63: /* $callee = $this->getCallee(); */
64: /* $calendar = $this->getCalendar($callee); */
65:
66: /* $cache = $this->getCache(); */
67:
68: /* if (!$this->cacheRequested() || !$this->cacheValid($calendar, $callee)) { */
69: /* if (!$callee->isAuthenticated()) { */
70: /* throw new Exception(); */
71: /* } */
72: /* $result = $calendar->fetch(); */
73: /* if ($result->isCacheable()) { */
74: /* $cache->store($calendar, $result); */
75: /* } */
76: /* return $result; */
77: /* } */
78: /* return $cache->load($calendar); */
79: /* } */
80: /* $this->logger->debug(sprintf("Partial free/busy data of owner %s on server %s requested by user %s.", */
81: /* $this->callee, $this->freebusyserver, $this->user)); */
82:
83: /* if (!empty($this->remote)) { */
84: /* /\* Try to fetch the data if it is stored on a remote server *\/ */
85: /* //@todo: How to determine which hook/processor to run? */
86: /* return $this->triggerRemote($params); */
87: /* // if (is_a($result, 'PEAR_Error')) { */
88: /* // $error = array('type' => FREEBUSY_ERROR_UNAUTHORIZED, 'error' => $result); */
89: /* } */
90:
91: /* if (!$req_cache) { */
92: /* /\* User wants to regenerate the cache *\/ */
93:
94: /* /\* Here we really need an authenticated IMAP user *\/ */
95: /* $result = $access->authenticated(); */
96: /* if (is_a($result, 'PEAR_Error')) { */
97: /* $error = array('type' => FREEBUSY_ERROR_UNAUTHORIZED, */
98: /* 'error' => $result); */
99: /* $view = new Horde_Kolab_FreeBusy_View_error($error); */
100: /* return $view; */
101: /* } */
102:
103: /* if (empty($access->owner)) { */
104: /* $message = sprintf(_("No such account %s!"), */
105: /* htmlentities($access->req_owner)); */
106: /* $error = array('type' => FREEBUSY_ERROR_NOTFOUND, */
107: /* 'error' => PEAR::raiseError($message)); */
108: /* $view = new Horde_Kolab_FreeBusy_View_error($error); */
109: /* return $view; */
110: /* } */
111:
112: /* /\* Update the cache *\/ */
113: /* $result = $this->_cache->store($access); */
114: /* if (is_a($result, 'PEAR_Error')) { */
115: /* $error = array('type' => FREEBUSY_ERROR_NOTFOUND, */
116: /* 'error' => $result); */
117: /* $view = new Horde_Kolab_FreeBusy_View_error($error); */
118: /* return $view; */
119: /* } */
120: /* } */
121:
122: /* /\* Load the cache data *\/ */
123: /* $vfb = $this->_cache->loadPartial($access, $req_extended); */
124: /* if (is_a($vfb, 'PEAR_Error')) { */
125: /* $error = array('type' => FREEBUSY_ERROR_NOTFOUND, */
126: /* 'error' => $vfb); */
127: /* $view = new Horde_Kolab_FreeBusy_View_error($error); */
128: /* return $view; */
129: /* } */
130:
131: /* /\* Generate the renderer *\/ */
132: /* //$data = array('fb' => $vfb, 'name' => $access->owner . '.ifb'); */
133: /* //$view = new Horde_Kolab_FreeBusy_View_vfb($data); */
134:
135: /* /\* Finish up *\/ */
136: /* return $view; */
137: /* } */
138:
139: /**
140: * Fetch the free/busy data.
141: *
142: * @params array $params Additional options.
143: *
144: * @return array The free/busy data.
145: */
146: public function fetch($params = array())
147: {
148: $this->logger->debug(sprintf("Free/busy data of owner %s requested by user %s (remote: %s).",
149: $this->callee, $this->user, $this->remote));
150:
151: if (!empty($this->remote)) {
152: /* Try to fetch the data if it is stored on a remote server */
153: //@todo: How to determine which hook/processor to run?
154: return $this->fetchRemote($params);
155: // if (is_a($result, 'PEAR_Error')) {
156: // $error = array('type' => FREEBUSY_ERROR_UNAUTHORIZED, 'error' => $result);
157: }
158:
159: global $conf;
160:
161: /* Which files will we access? */
162: if (!empty($conf['fb']['use_acls'])) {
163: $aclcache = &Horde_Kolab_FreeBusy_Cache_DB_acl::singleton('acl', $this->_cache_dir);
164: $files = $aclcache->get($access->owner);
165: if (is_a($files, 'PEAR_Error')) {
166: return $files;
167: }
168: } else {
169: $file_uid = str_replace("\0", '', str_replace(".", "^", $access->owner));
170: $files = array();
171: $this->findAll_readdir($file_uid, $conf['fb']['cache_dir'].'/'.$file_uid, $files);
172: }
173:
174: $owner = $access->owner;
175: if (ereg('(.*)@(.*)', $owner, $regs)) {
176: $owner = $regs[2] . '/' . $regs[1];
177: }
178: $user = $access->user;
179: if (ereg('(.*)@(.*)', $user, $regs)) {
180: $user = $regs[2] . '/' . $regs[1];
181: }
182: $c_file = str_replace("\0", '', str_replace('.', '^', $user . '/' . $owner));
183:
184: $c_vcal = new Horde_Kolab_FreeBusy_Cache_File_vcal($this->_cache_dir,
185: $c_file, $extended);
186:
187: /* If the current vCal cache did not expire, we can deliver it */
188: if (!$this->cache->expired($files)) {
189: return $this->cache->loadVcal();
190: }
191:
192: // Create the new iCalendar.
193: $vCal = new Horde_Icalendar();
194: $vCal->setAttribute('PRODID', '-//kolab.org//NONSGML Kolab Server 2//EN');
195: $vCal->setAttribute('METHOD', 'PUBLISH');
196:
197: // Create new vFreebusy.
198: $vFb = Horde_Icalendar::newComponent('vfreebusy', $vCal);
199: $params = array();
200:
201: $cn = $access->owner_object->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_CN);
202: if (!empty($cn) || is_a($cn, 'PEAR_Error')) {
203: $params['cn'] = $access->owner_object->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_CN);
204: }
205: $vFb->setAttribute('ORGANIZER', 'MAILTO:' . $access->owner, $params);
206:
207: $vFb->setAttribute('DTSTAMP', time());
208: if (isset($_SERVER['SERVER_NAME'])) {
209: $host = $_SERVER['SERVER_NAME'];
210: } else {
211: $host = 'localhost';
212: }
213: if (isset($_SERVER['REQUEST_URI'])) {
214: $uri = $_SERVER['REQUEST_URI'];
215: } else {
216: $uri = '/';
217: }
218: $vFb->setAttribute('URL', 'http://' . $host . $uri);
219:
220: $mtimes = array();
221: foreach ($files as $file) {
222: if ($extended && !empty($conf['fb']['use_acls'])) {
223: $extended_pvc = $this->_allowExtended($file, $access);
224: } else {
225: $extended_pvc = $extended;
226: }
227: $c_pvcal = new Horde_Kolab_FreeBusy_Cache_File_pvcal($this->_cache_dir, $file);
228: $pvCal = $c_pvcal->loadPVcal($extended_pvc);
229: if (is_a($pvCal, 'PEAR_Error')) {
230: Horde::logMessage(sprintf("Ignoring partial free/busy file %s: %s)",
231: $file, $pvCal->getMessage()),
232: __FILE__, __LINE__, PEAR_LOG_INFO);
233: continue;
234: }
235: $pvFb = &$pvCal->findComponent('vfreebusy');
236: if( !$pvFb ) {
237: Horde::logMessage(sprintf("Could not find free/busy info in file %s.)",
238: $file), __FILE__, __LINE__, PEAR_LOG_INFO);
239: continue;
240: }
241: if ($ets = $pvFb->getAttributeDefault('DTEND', false) !== false) {
242: // PENDING(steffen): Make value configurable
243: if ($ets < time()) {
244: Horde::logMessage(sprintf("Free/busy info in file %s is too old.)",
245: $file), __FILE__, __LINE__, PEAR_LOG_INFO);
246: $c_pvcal->purge();
247: continue;
248: }
249: }
250: $vFb->merge($pvFb);
251:
252: /* Store last modification time */
253: $mtimes[$file] = array($c_pvcal->getFile(), $c_pvcal->getMtime());
254: }
255:
256: if (!empty($conf['fb']['remote_servers'])) {
257: $remote_vfb = $this->_fetchRemote($conf['fb']['remote_servers'],
258: $access);
259: if (is_a($remote_vfb, 'PEAR_Error')) {
260: Horde::logMessage(sprintf("Ignoring remote free/busy files: %s)",
261: $remote_vfb->getMessage()),
262: __FILE__, __LINE__, PEAR_LOG_INFO);
263: } else {
264: $vFb->merge($remote_vfb);
265: }
266: }
267:
268: if (!(boolean)$vFb->getBusyPeriods()) {
269: /* No busy periods in fb list. We have to add a
270: * dummy one to be standards compliant
271: */
272: $vFb->setAttribute('COMMENT', 'This is a dummy vfreebusy that indicates an empty calendar');
273: $vFb->addBusyPeriod('BUSY', 0,0, null);
274: }
275:
276: $vCal->addComponent($vFb);
277:
278: $c_vcal->storeVcal($vCal, $mtimes);
279:
280: return $vCal;
281:
282: $result = $this->app->getCache->load($access, $extended);
283: // if (is_a($result, 'PEAR_Error')) {
284: // $error = array('type' => FREEBUSY_ERROR_NOTFOUND, 'error' => $result);
285:
286: //$data = array('fb' => $result, 'name' => $access->owner . '.vfb');
287: //$view = &new Horde_Kolab_FreeBusy_View_vfb($data);
288: }
289:
290: /**
291: * Trigger regeneration of free/busy data in a calender.
292: *
293: * @return NULL
294: */
295: function &trigger($params = array())
296: {
297: $this->logger->debug(sprintf("Partial free/busy data of owner %s on server %s requested by user %s.",
298: $this->callee, $this->freebusyserver, $this->user));
299:
300: if (!empty($this->remote)) {
301: /* Try to fetch the data if it is stored on a remote server */
302: //@todo: How to determine which hook/processor to run?
303: return $this->triggerRemote($params);
304: // if (is_a($result, 'PEAR_Error')) {
305: // $error = array('type' => FREEBUSY_ERROR_UNAUTHORIZED, 'error' => $result);
306: }
307:
308: if (!$req_cache) {
309: /* User wants to regenerate the cache */
310:
311: /* Here we really need an authenticated IMAP user */
312: $result = $access->authenticated();
313: if (is_a($result, 'PEAR_Error')) {
314: $error = array('type' => FREEBUSY_ERROR_UNAUTHORIZED,
315: 'error' => $result);
316: $view = new Horde_Kolab_FreeBusy_View_error($error);
317: return $view;
318: }
319:
320: if (empty($access->owner)) {
321: $message = sprintf(Horde_Kolab_FreeBusy_Translation::t("No such account %s!"),
322: htmlentities($access->req_owner));
323: $error = array('type' => FREEBUSY_ERROR_NOTFOUND,
324: 'error' => PEAR::raiseError($message));
325: $view = new Horde_Kolab_FreeBusy_View_error($error);
326: return $view;
327: }
328:
329: /* Update the cache */
330: $result = $this->_cache->store($access);
331: if (is_a($result, 'PEAR_Error')) {
332: $error = array('type' => FREEBUSY_ERROR_NOTFOUND,
333: 'error' => $result);
334: $view = new Horde_Kolab_FreeBusy_View_error($error);
335: return $view;
336: }
337: }
338:
339: /* Load the cache data */
340: $vfb = $this->_cache->loadPartial($access, $req_extended);
341: if (is_a($vfb, 'PEAR_Error')) {
342: $error = array('type' => FREEBUSY_ERROR_NOTFOUND,
343: 'error' => $vfb);
344: $view = new Horde_Kolab_FreeBusy_View_error($error);
345: return $view;
346: }
347:
348: /* Generate the renderer */
349: //$data = array('fb' => $vfb, 'name' => $access->owner . '.ifb');
350: //$view = new Horde_Kolab_FreeBusy_View_vfb($data);
351:
352: /* Finish up */
353: return $view;
354: }
355:
356: /**
357: * Fetch remote free/busy user if the current user is not local or
358: * redirect to the other server if configured this way.
359: *
360: * @param boolean $trigger Have we been called for triggering?
361: * @param boolean $extended Should the extended information been delivered?
362: */
363: function fetchRemote($trigger = false, $extended = false)
364: {
365: global $conf;
366:
367: if (!empty($conf['kolab']['freebusy']['server'])) {
368: $server = $conf['kolab']['freebusy']['server'];
369: } else {
370: $server = 'https://localhost/freebusy';
371: }
372: if (!empty($conf['fb']['redirect'])) {
373: $do_redirect = $conf['fb']['redirect'];
374: } else {
375: $do_redirect = false;
376: }
377:
378: if ($trigger) {
379: $path = sprintf('/trigger/%s/%s.' . ($extended)?'pxfb':'pfb',
380: urlencode($this->owner), urlencode($this->imap_folder));
381: } else {
382: $path = sprintf('/%s.' . ($extended)?'xfb':'ifb', urlencode($this->owner));
383: }
384:
385: /* Check if we are on the right server and redirect if appropriate */
386: if ($this->freebusyserver && $this->freebusyserver != $server) {
387: $redirect = $this->freebusyserver . $path;
388: Horde::logMessage(sprintf("URL %s indicates remote free/busy server since we only offer %s. Redirecting.",
389: $this->freebusyserver, $server), __FILE__,
390: __LINE__, PEAR_LOG_ERR);
391: if ($do_redirect) {
392: header("Location: $redirect");
393: } else {
394: header("X-Redirect-To: $redirect");
395: $redirect = 'https://' . urlencode($this->user) . ':' . urlencode($GLOBALS['registry']->getAuthCredential('password'))
396: . '@' . $this->freebusyserver . $path;
397: if (!@readfile($redirect)) {
398: $message = sprintf(Horde_Kolab_FreeBusy_Translation::t("Unable to read free/busy information from %s"),
399: 'https://' . urlencode($this->user) . ':XXX'
400: . '@' . $this->freebusyserver . $_SERVER['REQUEST_URI']);
401: return PEAR::raiseError($message);
402: }
403: }
404: exit;
405: }
406: }
407: }
408: