Page MenuHomePhorge

No OneTemporary

diff --git a/config/config.ini.sample b/config/config.ini.sample
index eeb7347..c20a2de 100644
--- a/config/config.ini.sample
+++ b/config/config.ini.sample
@@ -1,86 +1,87 @@
;; Kolab Free/Busy Service configuration
;; Require HTTP authentication to access this service
[httpauth]
;; Example for static auth credentials
; type = static
; username = "<user>"
; password = "<pass>"
;; Example for LDAP-based authentication
; type = ldap
; host = ldap://localhost:389
; bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
; bind_pw = "<service-bind-pw>"
; base_dn = "dc=yourdomain,dc=com"
; filter = "(&(|(mail=%s)(alias=%s)(uid=%s))(objectclass=inetorgperson))" ; optional, %s is replaced by the username
;; Allow privileged access from these IPs
[trustednetworks]
allow = 127.0.0.1,
192.168.0.0/16,
10.10.*,
::1
;; Logging configuration
[log]
driver = file ; supported drivers: file, syslog
path = ./log
name = freebusy
level = 300 ; (100 = Debug, 200 = Info, 300 = Warn, 400 = Error, 500 = Critical)
;; Directories to resolve email addresses and their f/b source locations
;; try local filesystem first
[directory "local"]
type = static
filter = "@yourdomain"
fbsource = file:/var/lib/kolab-freebusy/%s.ifb
;; check if primary email address hits a cache file (saves LDAP lookups)
[directory "local-cache"]
type = static
fbsource = file:/var/cache/kolab-freebusy/%s.ifb
expires = 10m
;; local Kolab directory server
[directory "kolab-ldap"]
type = ldap
host = ldap://localhost:389
bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
bind_pw = "<service-bind-pw>"
base_dn = "dc=yourdomain,dc=com"
filter = "(&(objectClass=kolabInetOrgPerson)(|(uid=%s)(mail=%s)(alias=%s)))"
-attributes = mail, sn
+attributes = mail, sn, alias
lc_attributes = sn
+mail_attributes = mail, alias
fbsource = file:/var/lib/kolab-freebusy/%mail.ifb
loglevel = 200 ; Info
;; resolve Kolab resources from LDAP and fetch calendar from IMAP
[directory "kolab-resources"]
type = ldap
host = ldap://localhost:389
bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
bind_pw = "<service-bind-pw>"
base_dn = "ou=Resources,dc=yourdomain,dc=com"
filter = "(&(objectClass=kolabsharedfolder)(mail=%s))"
attributes = mail, kolabtargetfolder
fbsource = "imap://cyrus-admin:<admin-pass>@localhost/%kolabtargetfolder?acl=lrs"
cacheto = /var/cache/kolab-freebusy/%mail.ifb
expires = 10m
loglevel = 100 ; Debug
;; external MS Exchange 2010 server
[directory "exchange"]
type = static
filter = "@microsoft.com$"
fbsource = https://externalhost/free-busy/%s.ics
format = Exchange2010
;; further examples of fbsource URIs
; - fetch data from another server by HTTP(s)
; fbsource = "https://fb-service-user:imap-password@kolab-server/freebusy/%mail.ifb"
; - read directoy from a users calendars (all) using IMAP proxy authentication
; fbsource = "imap://%mail:<admin-pass>@localhost/?proxy_auth=cyrus-admin"
diff --git a/lib/Kolab/FreeBusy/DirectoryLDAP.php b/lib/Kolab/FreeBusy/DirectoryLDAP.php
index 42d72b1..84248fa 100644
--- a/lib/Kolab/FreeBusy/DirectoryLDAP.php
+++ b/lib/Kolab/FreeBusy/DirectoryLDAP.php
@@ -1,130 +1,133 @@
<?php
/**
* This file is part of the Kolab Server Free/Busy Service
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\FreeBusy;
// PEAR modules operate in global namespace
use \Net_LDAP3;
use \Kolab\Config;
use \Monolog\Logger as Monolog;
/**
* Implementation of an address lookup using an LDAP directory
*/
class DirectoryLDAP extends Directory
{
private $ldap;
private $logger;
private $ready = false;
/**
* Default constructor loading directory configuration
*/
public function __construct($config)
{
$this->config = $config;
$host = parse_url($config['host']);
$ldap_config = array(
'hosts' => array($host['host']),
'port' => $host['port'] ?: 389,
'use_tls' => $host['scheme'] == 'tls' || $host['scheme'] == 'ldaps',
'root_dn' => $config['base_dn'],
'return_attributes' => Config::convert($config['attributes'], Config::ARR),
'log_hook' => array($this, 'log'),
) + $config;
// instantiate Net_LDAP3 and connect with logger
$this->logger = Logger::get('ldap', intval($config['loglevel']));
$this->ldap = new Net_LDAP3($ldap_config);
// connect + bind to LDAP server
if ($this->ldap->connect()) {
$this->ready = $this->ldap->bind($config['bind_dn'], $config['bind_pw']);
}
if ($this->ready) {
$this->logger->addInfo("Connected to $config[host] with '$config[bind_dn]'");
}
else {
$this->logger->addWarning("Connectiion to $config[host] with '$config[bind_dn]' failed!");
}
}
/**
* Callback for Net_LDAP3 logging
*/
public function log($level, $msg)
{
// map PHP log levels to Monolog levels
static $loglevels = array(
LOG_DEBUG => Monolog::DEBUG,
LOG_NOTICE => Monolog::NOTICE,
LOG_INFO => Monolog::INFO,
LOG_WARNING => Monolog::WARNING,
LOG_ERR => Monolog::ERROR,
LOG_CRIT => Monolog::CRITICAL,
LOG_ALERT => Monolog::ALERT,
LOG_EMERG => Monolog::EMERGENCY,
);
$msg = is_array($msg) ? join('; ', $msg) : strval($msg);
$this->logger->addRecord($loglevels[$level], $msg);
}
/**
* @see Directory::resolve()
*/
public function resolve($user)
{
$result = array('s' => $user);
if ($this->ready) {
// search with configured filter
$filter = preg_replace('/%s/i', $user, $this->config['filter']);
$ldapresult = $this->ldap->search($this->config['base_dn'], $filter, 'sub');
// got a valid result
if ($ldapresult && $ldapresult->count()) {
$ldapresult->rewind();
$entry = Net_LDAP3::normalize_entry($ldapresult->current()); // get the first entry
$this->logger->addInfo("Found " . $ldapresult->count() . " entries for $filter", $entry);
// convert entry attributes to strings and add them to the final result hash array
foreach ($entry as $k => $v) {
- if (!empty($v)) {
+ if (is_array($v) && count($v) > 1) {
+ $result[$k] = array_map('strval', $v);
+ }
+ else if (!empty($v)) {
$result[$k] = strval(is_array($v) ? $v[0] : $v);
}
}
return $result;
}
$this->logger->addInfo("No entry found for $filter");
}
return false;
}
}
diff --git a/lib/Kolab/FreeBusy/SourceIMAP.php b/lib/Kolab/FreeBusy/SourceIMAP.php
index 494a4d5..ee2092e 100644
--- a/lib/Kolab/FreeBusy/SourceIMAP.php
+++ b/lib/Kolab/FreeBusy/SourceIMAP.php
@@ -1,257 +1,300 @@
<?php
/**
* This file is part of the Kolab Server Free/Busy Service
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\FreeBusy;
+use Kolab\Config;
use Sabre\VObject;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\FreeBusyGenerator;
use Sabre\VObject\ParseException;
// configure env for Roundcube framework
define('RCUBE_INSTALL_PATH', KOLAB_FREEBUSY_ROOT . '/');
define('RCUBE_CONFIG_DIR', KOLAB_FREEBUSY_ROOT . '/config/');
define('RCUBE_PLUGINS_DIR', KOLAB_FREEBUSY_ROOT . '/lib/plugins/');
/**
* Implementation of a Free/Busy data source reading from IMAP
* (not yet implemented!)
*/
class SourceIMAP extends Source
{
private $folders = array();
public function __construct($config)
{
- parent::__construct($config);
+ parent::__construct($config + array('mail_attributes' => 'mail'));
// load the Roundcube framework with its autoloader
require_once KOLAB_FREEBUSY_ROOT . '/lib/Roundcube/bootstrap.php';
$rcube = \rcube::get_instance(\rcube::INIT_WITH_DB | \rcube::INIT_WITH_PLUGINS);
// Load plugins
$rcube->plugins->init($rcube);
$rcube->plugins->load_plugins(array(), array('libkolab','libcalendaring'));
}
/**
* @see Source::getFreeBusyData()
*/
public function getFreeBusyData($user, $extended)
{
$log = Logger::get('imap', intval($this->config['loglevel']));
$config = $this->getUserConfig($user);
parse_str(strval($config['query']), $param);
$config += $param;
// log this...
$log->addInfo("Fetching data for ", $config);
// caching is enabled
if (!empty($config['cacheto'])) {
// check for cached data
if ($cached = $this->getCached($config)) {
$log->addInfo("Deliver cached data from " . $config['cacheto']);
return $cached;
}
// touch cache file to avoid multiple requests generating the same data
if (file_exists($config['cacheto'])) {
touch($config['cacheto']);
}
else {
file_put_contents($config['cacheto'], Utils::dummyVFreebusy($user['mail']));
+ $tempfile = $config['cacheto'];
+ }
+ }
+
+ // compose a list of user email addresses
+ $user_email = array();
+ foreach (Config::convert($this->config['mail_attributes'], Config::ARR) as $key) {
+ if (!empty($user[$key])) {
+ $user_email = array_merge($user_email, (array)$user[$key]);
}
}
// synchronize with IMAP and read Kolab event objects
if ($imap = $this->imap_login($config)) {
// target folder is specified in source URI
if ($config['path'] && $config['path'] != '/') {
$folders = array(\kolab_storage::get_folder(substr($config['path'], 1)));
$read_all = true;
}
else { // list all folders of type 'event'
$folders = \kolab_storage::get_folders('event', false);
$read_all = false;
}
// make \libvcalendar class available
\libcalendaring::get_ical();
$utc = new \DateTimezone('UTC');
$dtstart = new \DateTime('now - 8 weeks 00:00:00', $utc);
$dtend = new \DateTime('now + 16 weeks 00:00:00', $utc);
$calendar = VObject\Component::create('VCALENDAR');
+ $seen = array();
- $query = array(array('dtstart','>',$dtstart), array('dtend','<',$dtend));
+ $query = array(array('dtstart','<=',$dtend), array('dtend','>=',$dtstart));
foreach ($folders as $folder) {
$log->debug('Reading Kolab folder: ' . $folder->name, $folder->get_folder_info());
// skip other user's shared calendars
if (!$read_all && $folder->get_namespace() == 'other') {
continue;
}
// set ACL (temporarily)
if ($config['acl']) {
$folder->_old_acl = $folder->get_myrights();
$imap->set_acl($folder->name, $config['user'], $config['acl']);
}
foreach ($folder->select($query) as $event) {
- $log->debug('Found event', $event);
+ //$log->debug('Processing event', $event);
- if ($event['cancelled'])
+ if ($event['cancelled']) {
continue;
+ }
- // TODO: only consider shared namespace events if user is a confirmed participant
+ // only consider shared namespace events if user is a confirmed participant (or organizer)
if (!$read_all && $folder->get_namespace() == 'shared') {
- continue; // skip all for now
+ $participant = false;
+ if (is_array($event['organizer']) && !empty($event['organizer']['email'])) {
+ $participant = in_array($event['organizer']['email'], $user_email);
+ }
+ else if (is_array($event['attendees'])) {
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_email)) {
+ if ($attendee['status'] == 'ACCEPTED') {
+ $participant = true;
+ break;
+ }
+ else if ($attendee['status'] == 'TENTATIVE') {
+ $event['free_busy'] = 'tentative';
+ $participant = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!$participant) {
+ $log->debug('Skip shared event', array($event['uid'], $event['title']));
+ continue;
+ }
}
// copied from libvcalendar::_to_ical()
$ve = VObject\Component::create('VEVENT');
// all-day events end the next day
if ($event['allday'] && !empty($event['end'])) {
$event['end'] = clone $event['end'];
$event['end']->add(new \DateInterval('P1D'));
$event['end']->_dateonly = true;
}
+
+ // avoid duplicate entries
+ $key = $event['start']->format('c') . '/' . $event['end']->format('c');
+ if ($seen[$key]++) {
+ $log->debug('Skipping duplicate event at ' . $key, array($event['uid'], $event['title']));
+ continue;
+ }
+
if (!empty($event['start']))
$ve->add(\libvcalendar::datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday']));
if (!empty($event['end']))
$ve->add(\libvcalendar::datetime_prop('DTEND', $event['end'], false, (bool)$event['allday']));
if (!empty($event['free_busy']))
$ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
if ($event['free_busy'] == 'tentative')
$ve->add('STATUS', 'TENTATIVE');
else if (!empty($event['status']))
$ve->add('STATUS', $event['status']);
if ($event['recurrence']) {
if ($exdates = $event['recurrence']['EXDATE'])
unset($event['recurrence']['EXDATE']);
if ($rdates = $event['recurrence']['RDATE'])
unset($event['recurrence']['RDATE']);
if ($event['recurrence']['FREQ'])
$ve->add('RRULE', \libcalendaring::to_rrule($event['recurrence']));
// add EXDATEs each one per line (for Thunderbird Lightning)
if ($exdates) {
foreach ($exdates as $ex) {
if ($ex instanceof \DateTime) {
$exd = clone $event['start'];
$exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j'));
$exd->setTimeZone($utc);
$ve->add(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z')));
}
}
}
// add RDATEs
if (!empty($rdates)) {
$sample = \libvcalendar::datetime_prop('RDATE', $rdates[0]);
$rdprop = new VObject\Property\MultiDateTime('RDATE', null);
$rdprop->setDateTimes($rdates, $sample->getDateType());
$ve->add($rdprop);
}
}
// append to vcalendar container
$calendar->add($ve);
}
}
$this->imap_disconnect($imap, $config, $folders);
// feed the calendar object into the free/busy generator
// we must specify a start and end date, because recurring events are expanded. nice!
$fbgen = new FreeBusyGenerator($dtstart, $dtend, $calendar);
// get the freebusy report
$freebusy = $fbgen->getResult();
$freebusy->PRODID = Utils::PRODID;
$freebusy->METHOD = 'PUBLISH';
- $freebusy->VFREEBUSY->ORGANIZER = 'mailto:' . $user['mail'];
+ $freebusy->VFREEBUSY->ORGANIZER = 'mailto:' . $user_email[0];
// serialize to VCALENDAR format
return $freebusy->serialize();
}
// remove (temporary) cache file again
- else if (!empty($config['cacheto']) && file_exists($config['cacheto'])) {
- unlink($config['cacheto']);
+ else if ($tempfile) {
+ unlink($tempfile);
}
return false;
}
/**
* Helper method to establish connection to the configured IMAP backend
*/
private function imap_login($config)
{
$rcube = \rcube::get_instance();
$imap = $rcube->get_storage();
$host = $config['host'];
$port = $config['port'] ?: ($config['scheme'] == 'imaps' ? 993 : 143);
$ssl = $config['scheme'] == 'imaps' || $port == 993;
// enable proxy authentication
if (!empty($config['proxy_auth'])) {
$imap->set_options(array('auth_cid' => $config['proxy_auth'], 'auth_pw' => $config['pass']));
}
// authenticate user in IMAP
if (!$imap->connect($host, $config['user'], $config['pass'], $port, $ssl)) {
Logger::get('imap')->addWarning("Failed to connect to IMAP server: " . $imap->get_error_code(), $config);
return false;
}
// fake user object to rcube framework
$rcube->set_user(new \rcube_user('0', array('username' => $config['user'])));
return $imap;
}
/**
* Cleanup and close IMAP connection
*/
private function imap_disconnect($imap, $config, $folders)
{
// reset ACL
if ($config['acl'] && !empty($folders)) {
foreach ($folders as $folder) {
$imap->set_acl($folder->name, $config['user'], $folder->_old_acl);
}
}
$imap->close();
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Sep 15, 6:49 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
287603
Default Alt Text
(17 KB)

Event Timeline