Skip to content

Commit

Permalink
Implement an optional backend to store incoming caldav events
Browse files Browse the repository at this point in the history
  • Loading branch information
ralflang committed Apr 5, 2021
1 parent 682fbf1 commit 3d58542
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 3 deletions.
19 changes: 19 additions & 0 deletions config/conf.xml
Expand Up @@ -17,6 +17,25 @@
</configswitch>
</configsection>

<configsection name="caldav">
<configheader>CalDAV Driver Settings</configheader>
<configdescription>
Store any caldav attributes Kronolith does not process.
When exposing the event again, these attributes will be added again.
This prevents 'forgetting' unknown fields a client supports.
Storage is independent of the calendar backend.
</configdescription>
<configswitch name="driver" desc="What CalDAV storage driver should we use?">sql
<case name="sql" desc="SQL">
<configsection name="params">
<configsql switchname="driverconfig"/>
</configsection>
</case>
<case name="null" desc="Do not save extra attributes" />
</configswitch>
</configsection>


<configsection name="documents">
<configheader>Virtual File Storage</configheader>
<configvfs switchname="type" />
Expand Down
4 changes: 3 additions & 1 deletion lib/Application.php
Expand Up @@ -67,6 +67,7 @@ protected function _init()
throw new Horde_Exception(_("The Content_Tagger class could not be found. Make sure the Content application is installed."));
}

$GLOBALS['injector']->bindFactory('Kronolith_Icalendar_Storage', 'Kronolith_Factory_IcalendarStorage', 'create');
$GLOBALS['injector']->bindFactory('Kronolith_Geo', 'Kronolith_Factory_Geo', 'create');
$GLOBALS['injector']->bindFactory('Kronolith_Shares', 'Kronolith_Factory_Shares', 'create');

Expand Down Expand Up @@ -1055,6 +1056,7 @@ public function davPutObject($collection, $object, $data)
{
$dav = $GLOBALS['injector']
->getInstance('Horde_Dav_Storage');
$storage = $GLOBALS['injector']->getInstance('Kronolith_Icalendar_Storage');

$internal = $dav->getInternalCollectionId($collection, 'calendar') ?: $collection;
if (!Kronolith::hasPermission($internal, Horde_Perms::EDIT)) {
Expand All @@ -1066,7 +1068,7 @@ public function davPutObject($collection, $object, $data)
throw new Kronolith_Exception(_("There was an error importing the iCalendar data."));
}
$importer = new Kronolith_Icalendar_Handler_Dav(
$ical, Kronolith::getDriver(null, $internal), array('object' => $object)
$ical, Kronolith::getDriver(null, $internal), $storage, ['object' => $object]
);
$importer->process();
}
Expand Down
55 changes: 55 additions & 0 deletions lib/Factory/IcalendarStorage.php
@@ -0,0 +1,55 @@
<?php
/**
* Horde_Injector based factory for Icalendar storage.
*/
class Kronolith_Factory_IcalendarStorage
{
/**
* Instances.
*
* @var array
*/
private $_instances = array();

/**
* Return the caldav driver instance.
*
* @param string $driver The storage backend to use.
* @param array $params Driver params.
*
* @return Kronolith_Caldav_Storage
* @throws Kronolith_Exception
*/
public function create(Horde_Injector $injector): Kronolith_Icalendar_Storage
{
$driver = Horde_String::ucfirst($GLOBALS['conf']['icalendar']['driver']);

if (!empty($this->_instances[$driver])) {
return $this->_instances[$driver];
}

switch ($driver) {
case 'Sql':
$params = Horde::getDriverConfig('icalendar', 'Sql');
if (isset($params['driverconfig']) &&
$params['driverconfig'] != 'horde') {
$customParams = $params;
unset($customParams['driverconfig'], $customParams['table']);
$db = $injector->getInstance('Horde_Core_Factory_Db')->create('kronolith', $customParams);
} else {
$db = $injector->getInstance('Horde_Db_Adapter');
}
$instance = new Kronolith_Icalendar_Storage_Sql($db);
break;

case 'null':
default:
$instance = new Kronolith_Icalendar_Storage_Null($db);
break;
}
$this->_instances[$driver] = $instance;

return $instance;
}

}
17 changes: 15 additions & 2 deletions lib/Icalendar/Handler/Dav.php
Expand Up @@ -29,6 +29,13 @@ class Kronolith_Icalendar_Handler_Dav extends Kronolith_Icalendar_Handler_Base
*/
protected $_dav;

/**
* The iCalendar storage driver.
*
* @var Kronolith_Icalendar_Storage
*/
protected $_storage;

/**
* The calendar id to be imported into.
*
Expand Down Expand Up @@ -62,15 +69,20 @@ class Kronolith_Icalendar_Handler_Dav extends Kronolith_Icalendar_Handler_Base
*
* @param Horde_Icalendar $iCal The iCalendar data.
* @param Kronolith_Driver $driver The Kronolith driver.
* @param Kronolith_Icalendar_Storage $storage The raw Icalendar Storage driver.
* @param array $params Any additional parameters needed for
* the importer. For this driver we
* require: 'object' - contains the DAV
* identifier for the (base) event.
*/
public function __construct(
Horde_Icalendar $iCal, Kronolith_Driver $driver, $params = array())
Horde_Icalendar $iCal,
Kronolith_Driver $driver,
Kronolith_Icalendar_Storage $storage,
array $params = [])
{
parent::__construct($iCal, $driver, $params);
$this->_storage = $storage;
$this->_dav = $GLOBALS['injector']->getInstance('Horde_Dav_Storage');
$this->_calendar = $this->_driver->calendar;
}
Expand Down Expand Up @@ -186,9 +198,10 @@ protected function _postSave(Kronolith_Event $event)
if (!$this->_dav->getInternalObjectId($this->_params['object'], $this->_calendar)) {
$this->_dav->addObjectMap($event->id, $this->_params['object'], $this->_calendar);
}
$this->_storage->put($this->_calendar, $event->id, $this->_iCal->toString());

// Send iTip messages if necessary.
$type = Kronolith::ITIP_REQUEST;
$type = Kronolith::ITIP_REQUEST;
if ($event->organizer && !Kronolith::isUserEmail($event->creator, $event->organizer)) {
$type = Kronolith::ITIP_REPLY;
}
Expand Down
7 changes: 7 additions & 0 deletions lib/Icalendar/Storage.php
@@ -0,0 +1,7 @@
<?php
interface Kronolith_Icalendar_Storage
{
public function put(string $calendarId, string $eventUid, string $data): void;
public function get(string $calendarId, string $eventUid): string;
public function remove(string $calendarId, string $eventUid): void;
}
19 changes: 19 additions & 0 deletions lib/Icalendar/Storage/Entity.php
@@ -0,0 +1,19 @@
<?php
/**
* @author Ralf Lang <lang@b1-systems.de>
*/
use \Horde_Rdo_Base as EntityBase;
use \Kronolith_Icalendar_Sql as EntityMapper;
/**
* Entity class needed for Horde Rdo
*
* @internal This class is internal to the SQL Backend
* for caldav vevent storage and should not be relied
* upon outside the SQL specific context
*
*
*/
class Kronolith_Icalendar_Storage_Entity extends EntityBase
{

}
24 changes: 24 additions & 0 deletions lib/Icalendar/Storage/Null.php
@@ -0,0 +1,24 @@
<?php
/**
* Noop storage driver for CalDAV data
*
* Simplifies calling code which can just depend on storage interface
*/
class Kronolith_Icalendar_Storage_Null implements Kronolith_Icalendar_Storage
{
public function put(string $calendarId, string $eventUid, string $data): void
{
return;
}

public function remove(string $calendarId, string $eventUid): void
{
return;
}


public function get(string $calendarId, string $eventUid): string
{
return '';
}
}
113 changes: 113 additions & 0 deletions lib/Icalendar/Storage/Sql.php
@@ -0,0 +1,113 @@
<?php
/**
* Simple storage for icalendar objects
*/
declare (strict_types=1);
//use \InvalidArgumentException;
use \Kronolith_Icalendar_Storage_Entity as Entity;
/**
* Simple storage for caldav related data
*/
class Kronolith_Icalendar_Storage_Sql extends \Horde_Rdo_Mapper implements Kronolith_Icalendar_Storage
{

protected $_classname = Entity::class;
protected $_table = 'kronolith_icalendar_storage';

/**
* Remove existing item.
*
* If we wanted to name it delete,
* we would need to make the rdo mapper a dependency
* rather than inheriting from it.
* @param string $calendarId The calendar ID to operate on
* @param string $eventUid The Event to operate on
*
* @throw InvalidArgumentException
* @return void
*/
public function remove(string $calendarId, string $eventUid): void
{
$this->_noNullString($calendarId, $eventUid);
$entity = $this->_getEntity($calendarId, $eventUid);
if ($entity) {
$this->delete($entity);
}
}

/**
* Create or update item.
*
* @param string $calendarId The calendar ID to operate on
* @param string $eventUid The Event to operate on
* @param string $data The actual icalendar data
*
* @throw InvalidArgumentException
* @return void
*/
public function put(string $calendarId, string $eventUid, string $data): void
{
$entity = $this->_getEntity($calendarId, $eventUid);
if (empty($entity)) {
$entity = new Entity([
'calendar_id' => $calendarId,
'event_uid' => $eventUid,
'event_data' => $data
]);
$entity->setMapper($this);
} else {
$entity->event_data = $data;
}
$entity->save();
}

/**
* Retrieve stored item.
*
* @param string $calendarId The calendar ID to operate on
* @param string $eventUid The Event to operate on
*
* @throw InvalidArgumentException
* @return string
*/
public function get(string $calendarId, string $eventUid): string
{
$entity = $this->_getEntity($calendarId, $eventUid);
if ($entity) {
return $entity->event_data;
}
return '';
}


protected function _getEntity(string $calendarId, string $eventUid): ?Entity
{
$this->_noNullString($calendarId, $eventUid);
return $this->findOne([
'calendar_id' => $calendarId,
'event_uid' => $eventUid
]);
}

/**
* Filter out invalid/malicious calls, throw Exception
*
* Rdo does undesirable actions on empty string arguments,
* also they are not valid for our use case.
*
* @param string $calendarId The calendar ID to operate on
* @param string $eventUid The Event to operate on
*
* @throw InvalidArgumentException
* @return void
*/
protected function _noNullString(string $calendarId, string $eventUid)
{
if ($calendarId == '') {
throw new InvalidArgumentException('Calendar ID must not be empty');
}
if ($eventUid == '') {
throw new InvalidArgumentException('Event UID must not be empty');
}
}
}
51 changes: 51 additions & 0 deletions migration/28_kronolith_icalendar_storage.php
@@ -0,0 +1,51 @@
<?php
/**
* Copyright 2020-2021 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (GPL). If you
* did not receive this file, see http://www.horde.org/licenses/gpl.
*
* @author Ralf Lang <lang@b1-systems.de>
* @category Horde
* @license http://www.horde.org/licenses/gpl GPL
* @package Kronolith
*/

/**
* Add a table for storing caldav event details
*
* @author Ralf Lang <lang@b1-systems.de>
* @category Horde
* @license http://www.horde.org/licenses/gpl GPL
* @package Kronolith
*/
class KronolithIcalendarStorage extends Horde_Db_Migration_Base
{
/**
* Upgrade.
*/
public function up()
{
/**
* NOTE: We use horde/rdo for SQL icalendar storage.
* The ical_id is never read but the attribute helps us circumvent
* rdo's notion that an item with an existing primary key must already
* exist in backend. That would issue an UPDATE where an INSERT is more
* appropriate.
*/
$t = $this->createTable('kronolith_icalendar_storage', ['autoincrementKey' => 'ical_id']);
$t->column('calendar_id', 'string', ['limit' => 255, 'null' => false]);
$t->column('event_uid', 'string', ['limit' => 255, 'null' => false]);
$t->column('event_data', 'text', ['null' => false]);
$t->end();
$this->addIndex('kronolith_icalendar_storage', ['calendar_id', 'event_uid'], ['unique' => true]);
}

/**
* Downgrade
*/
public function down()
{
$this->dropTable('kronolith_icalendar_storage');
}
}

0 comments on commit 3d58542

Please sign in to comment.