Convert to new API framework.
authorMichael M Slusarz <slusarz@curecanti.org>
Mon, 3 Aug 2009 21:25:43 +0000 (15:25 -0600)
committerMichael M Slusarz <slusarz@curecanti.org>
Tue, 4 Aug 2009 00:10:44 +0000 (18:10 -0600)
56 files changed:
ansel/lib/Api.php [new file with mode: 0644]
ansel/lib/api.php [deleted file]
ansel/lib/version.php [deleted file]
ansel/rss.php
ansel/test.php
babel/lib/Api.php [new file with mode: 0644]
babel/lib/api.php [deleted file]
chora/lib/Api.php [new file with mode: 0644]
chora/lib/api.php [deleted file]
chora/lib/version.php [deleted file]
chora/test.php
crumb/lib/Api.php [new file with mode: 0644]
crumb/lib/version.php [deleted file]
fima/lib/Api.php [new file with mode: 0644]
fima/lib/version.php [deleted file]
folks/lib/Api.php [new file with mode: 0644]
folks/lib/api.php [deleted file]
folks/lib/version.php [deleted file]
gollem/lib/Api.php [new file with mode: 0644]
gollem/lib/api.php [deleted file]
gollem/lib/version.php [deleted file]
gollem/test.php
imp/acl.php
imp/lib/Api.php [new file with mode: 0644]
imp/lib/api.php [deleted file]
imp/lib/version.php [deleted file]
imp/test.php
ingo/lib/Api.php [new file with mode: 0644]
ingo/lib/api.php [deleted file]
ingo/lib/version.php [deleted file]
ingo/test.php
jeta/lib/Api.php [new file with mode: 0644]
jeta/lib/version.php [deleted file]
jeta/test.php
kastalia/lib/Api.php [new file with mode: 0644]
kastalia/lib/version.php [deleted file]
kronolith/lib/Api.php [new file with mode: 0644]
kronolith/lib/api.php [deleted file]
kronolith/lib/version.php [deleted file]
kronolith/test.php
nag/lib/Api.php [new file with mode: 0644]
nag/lib/api.php [deleted file]
nag/lib/version.php [deleted file]
news/lib/Api.php [new file with mode: 0644]
news/lib/api.php [deleted file]
news/lib/version.php [deleted file]
skeleton/lib/Api.php [new file with mode: 0644]
skeleton/lib/version.php [deleted file]
skoli/lib/Api.php [new file with mode: 0644]
skoli/lib/version.php [deleted file]
timeobjects/lib/Api.php [new file with mode: 0644]
timeobjects/lib/api.php [deleted file]
turba/lib/Api.php [new file with mode: 0644]
turba/lib/api.php [deleted file]
turba/lib/version.php [deleted file]
turba/test.php

diff --git a/ansel/lib/Api.php b/ansel/lib/Api.php
new file mode 100644 (file)
index 0000000..e4d600d
--- /dev/null
@@ -0,0 +1,1342 @@
+<?php
+/**
+ * Ansel external API interface.
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Jan Schneider <jan@horde.org>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Ansel
+ */
+class Ansel_Api extends Horde_Registry_Api
+{
+    public $version = 'H4 (2.0-git)';
+
+    public $services = array(
+        'perms' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'browse' => array(
+            'args' => array('path' => 'string'),
+            'type' => '{urn:horde}hashHash',
+        ),
+
+        'put' => array(
+            'args' => array('path' => 'string', 'content' => 'string', 'content_type' => 'string'),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        // 'path_delete' => array(
+        //     'args' => array('path' => 'string'),
+        //     'type' => 'boolean',
+        // ),
+
+        'commentCallback' => array(
+            'args' => array('image_id' => 'string'),
+            'type' => 'string'
+        ),
+
+        'hasComments' => array(
+            'args' => array(),
+            'type' => 'boolean'
+        ),
+
+        'saveImage' => array(
+            'args' => array('app'          => 'string',
+            'gallery_id'   => 'string',
+            'image'        => '{urn:horde}hashHash',
+            'default'      => 'boolean',
+            'gallery_data' => '{urn:horde}hashHash',
+            'encoding'     => 'string',
+            'slug'         => 'string',
+            'compression'  => 'string',
+            'skiphook'     => 'boolean'),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'postBatchUpload' => array(
+            'args' => array('image_ids' => '{urn:horde}hash'),
+            'type' => 'int'
+        ),
+
+        'createGallery' => array(
+            'args' => array('app'        => 'string',
+            'attributes' => '{urn:horde}hashHash',
+            'perm'       => '{urn:horde}hashHash'),
+            'type' => 'int'
+        ),
+
+        'removeImage' => array(
+            'args' => array('app'        => 'string',
+            'gallery_id' => 'integer',
+            'image_id'   => 'integer'),
+            'type' => 'int'
+        ),
+
+        'removeGallery' => array(
+            'args' => array('app'        => 'string',
+            'gallery_id' => 'integer'),
+            'type' => 'int'
+        ),
+
+        'getImageUrl' => array(
+            'args' => array('app'        => 'string',
+            'image_id'   => 'integer',
+            'view'       => 'string',
+            'full'       => 'boolean',
+            'style'      => 'string'),
+            'type' => 'string'
+        ),
+
+        'getImageContent' => array(
+            'args' => array('image_id'   => 'integer',
+            'view'       => 'string',
+            'style'      => 'string',
+            'app'        => 'string'),
+            'type' => 'string'
+        ),
+
+        'count' => array(
+            'args' => array('app'        => 'string',
+            'gallery_id' => 'integer'),
+            'type' => 'int'
+        ),
+
+        'getDefaultImage' => array(
+            'args' => array('app'        => 'string',
+            'gallery_id' => 'integer',
+            'style'      => 'string'),
+            'type' => 'string'
+        ),
+
+        'listGalleries' => array(
+            'args' => array('app'        => 'string',
+            'perm'       => 'integer',
+            'parent'     => 'string',
+            'allLevels'  => 'string',
+            'from'       => 'integer',
+            'count'      => 'integer'),
+            'type' => 'string'
+        ),
+
+        'getGalleries' => array(
+            'args' => array('ids' => '{urn:horde}hash',
+            'app' => 'string'),
+            'type' => '{urn:horde}hash'
+        ),
+
+        'selectGalleries' => array(
+            'args' => array('app'        => 'string',
+            'perm'       => 'integer',
+            'parent'     => 'string',
+            'allLevels'  => 'string',
+            'from'       => 'integer',
+            'count'      => 'integer'),
+            'type' => 'string'
+        ),
+
+        'listImages' => array(
+            'args' => array('app'        => 'string',
+            'gallery_id' => 'integer',
+            'perm'       => 'integer',
+            'view'       => 'string',
+            'full'       => 'boolean',
+            'from'       => 'integer',
+            'count'      => 'integer',
+            'style'      => 'string'),
+            'type' => 'string'
+        ),
+
+        'getRecentImages' => array(
+            'args' => array('app' => 'string',
+            'galleries' => '{urn:horde}hash',
+            'perms' => 'integer',
+            'view' => 'string',
+            'full' => 'boolean',
+            'limit' => 'integer',
+            'style' => 'string',
+            'slugs' => '{urn:horde}hashHash'),
+            'type' => '{urn:horde}hash'
+        ),
+
+        'countGalleries' => array(
+            'args' => array('app'        => 'string',
+            'perm'       => 'string',
+            'attributes' => '{urn:horde}hash',
+            'parent'     => 'string',
+            'allLevels'  => 'boolean'),
+            'type' => 'int'
+        ),
+
+        'listTagInfo' => array(
+            'args' => array('tags' => '{urn:horde}stringArray'),
+            'type' => '{urn:horde}hash'
+        ),
+
+        'searchTags' => array(
+            'args' => array('tags' => '{urn:horde}stringArray',
+            'resource_type' => 'string',
+            'count' => 'int',
+            'user' => 'string'),
+            'type' => '{urn:horde}hash'
+        ),
+
+        'galleryExists' => array(
+            'args' => array('app' => 'string',
+            'gallery_name' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'renderView' => array(
+            'args' => array('parameters' => '{urn:horde]stringArray',
+            'app' => 'string',
+            'view' => 'string'),
+            'type' => 'string'
+        ),
+
+        'getGalleryStyles' => array(
+            'args' => array(),
+            'type' => '{urn:horde}hash'
+        )
+    );
+
+    /**
+     * Returns a list of available permissions.
+     *
+     * @return array  An array describing all available permissions.
+     */
+    public function perms()
+    {
+        $perms = array();
+        $perms['tree']['ansel']['admin'] = false;
+        $perms['title']['ansel:admin'] = _("Administrators");
+
+        return $perms;
+    }
+
+    /**
+     * Browse through Ansel's gallery tree.
+     *
+     * @param string $path       The level of the tree to browse.
+     * @param array $properties  The item properties to return. Defaults to 'name',
+     *                           'icon', and 'browseable'.
+     *
+     * @return array  The contents of $path
+     */
+    public function browse($path = '', $properties = array())
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        // Default properties.
+        if (!$properties) {
+            $properties = array('name', 'icon', 'browseable');
+        }
+
+        if (substr($path, 0, 5) == 'ansel') {
+            $path = substr($path, 5);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        if (empty($path)) {
+            $owners = array();
+            $galleries = $GLOBALS['ansel_storage']->listGalleries(PERMS_SHOW, null, null, false);
+            foreach ($galleries  as $gallery) {
+                $owners[$gallery->data['share_owner']] = true;
+            }
+
+            $results = array();
+            foreach (array_keys($owners) as $owner) {
+                if (in_array('name', $properties)) {
+                    $results['ansel/' . $owner]['name'] = $owner;
+                }
+                if (in_array('icon', $properties)) {
+                    $results['ansel/' . $owner]['icon'] =
+                        $registry->getImageDir('horde') . '/user.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results['ansel/' . $owner]['browseable'] = true;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results['ansel/' . $owner]['contenttype'] =
+                        'httpd/unix-directory';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results['ansel/' . $owner]['contentlength'] = 0;
+                }
+                if (in_array('modified', $properties)) {
+                    $results['ansel/' . $owner]['modified'] = time();
+                }
+                if (in_array('created', $properties)) {
+                    $results['ansel/' . $owner]['created'] = 0;
+                }
+            }
+            return $results;
+
+        } else {
+            if (count($parts) == 1) {
+                // This request is for all galleries owned by the requested user.
+                $galleries = $GLOBALS['ansel_storage']->listGalleries(
+                    PERMS_SHOW, $parts[0], null, false);
+                $images = array();
+            } elseif ($this->galleryExists(null, end($parts))) {
+                // This request if for a certain gallery, list all sub-galleries
+                // and images.
+                $gallery_id = end($parts);
+                $galleries = $GLOBALS['ansel_storage']->getGalleries(
+                    array($gallery_id));
+                if (!isset($galleries[$gallery_id]) ||
+                    !$galleries[$gallery_id]->hasPermission(Horde_Auth::getAuth(),
+                        PERMS_READ)) {
+                            return PEAR::raiseError(_("Invalid gallery specified."), 404);
+                        }
+                $galleries = $GLOBALS['ansel_storage']->listGalleries(
+                    PERMS_SHOW, null, $gallery_id, false);
+
+                $images = $this->listImages(null, $gallery_id, PERMS_SHOW, 'mini');
+            } elseif (count($parts) > 2 &&
+                $this->galleryExists(null, $parts[count($parts) - 2]) &&
+                !is_a($image = $GLOBALS['ansel_storage']->getImage(end($parts)), 'PEAR_Error')) {
+                    return array('data' => $image->raw(),
+                        'mimetype' => $image->type,
+                        'mtime' => $image->uploaded);
+                } else {
+                    return PEAR::raiseError(_("File not found."), 404);
+                }
+
+            $results = array();
+            foreach ($galleries as $galleryId => $gallery) {
+                $retpath = 'ansel/' . implode('/', $parts) . '/' . $galleryId;
+                if (in_array('name', $properties)) {
+                    $results[$retpath]['name'] = $gallery->data['attribute_name'];
+                }
+                if (in_array('displayname', $properties)) {
+                    $results[$retpath]['displayname'] = rawurlencode(
+                        $gallery->data['attribute_name']);
+                }
+                if (in_array('icon', $properties)) {
+                    $results[$retpath]['icon'] = $registry->getImageDir()
+                        . '/ansel.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results[$retpath]['browseable'] = $gallery->hasPermission(
+                        Horde_Auth::getAuth(), PERMS_READ);
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results[$retpath]['contenttype'] = 'httpd/unix-directory';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results[$retpath]['contentlength'] = 0;
+                }
+                if (in_array('modified', $properties)) {
+                    $results[$retpath]['modified'] = time();
+                }
+                if (in_array('created', $properties)) {
+                    $results[$retpath]['created'] = 0;
+                }
+            }
+
+            foreach ($images as $imageId => $image) {
+                $retpath = 'ansel/' . implode('/', $parts) . '/' . $imageId;
+                if (in_array('name', $properties)) {
+                    $results[$retpath]['name'] = $image['name'];
+                }
+                if (in_array('displayname', $properties)) {
+                    $results[$retpath]['displayname'] = rawurlencode($image['name']);
+                }
+                if (in_array('icon', $properties)) {
+                    $results[$retpath]['icon'] = Horde::url($image['url'], true);
+                }
+                if (in_array('browseable', $properties)) {
+                    $results[$retpath]['browseable'] = false;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results[$retpath]['contenttype'] = $image['type'];
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results[$retpath]['contentlength'] = 0;
+                }
+                if (in_array('modified', $properties)) {
+                    $results[$retpath]['modified'] = $image['uploaded'];
+                }
+                if (in_array('created', $properties)) {
+                    $results[$retpath]['created'] = $image['uploaded'];
+                }
+            }
+            return $results;
+
+        }
+
+        return PEAR::raiseError(_("File not found."), 404);
+    }
+
+    /**
+     * Saves an image into the gallery tree.
+     *
+     * @param string $path          The path where to PUT the file.
+     * @param string $content       The file content.
+     * @param string $content_type  The file's content type.
+     *
+     * @return array  The event UIDs, or a PEAR_Error on failure.
+     */
+    public function put($path, $content, $content_type)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (substr($path, 0, 5) == 'ansel') {
+            $path = substr($path, 9);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        if (count($parts) < 3) {
+            return PEAR::raiseError("Gallery does not exist");
+        }
+        $image_name = array_pop($parts);
+        $gallery_id = end($parts);
+        if (!$GLOBALS['ansel_storage']->galleryExists($gallery_id)) {
+            return PEAR::raiseError("Gallery does not exist");
+        }
+        $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
+        if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
+            return PEAR::raiseError(_("Access denied adding photos to \"%s\"."));
+        }
+
+        return $gallery->addImage(array('image_type' => $content_type,
+            'image_filename' => $image_name,
+            'image_caption' => '',
+            'data' => $content));
+    }
+
+    /**
+     * Callback for Agora comments.
+     *
+     * @param integer $image_id  Image id to check
+     *
+     * @return mixed Image filename on success | false on failure
+     */
+    public function commentCallback($image_id)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!$GLOBALS['conf']['comments']['allow']) {
+            return false;
+        }
+
+        $image = $GLOBALS['ansel_storage']->getImage($image_id);
+        if (!$image || is_a($image, 'PEAR_Error')) {
+            return false;
+        }
+
+        return $image->filename;
+    }
+
+    /**
+     * Checks if applications allows comments
+     *
+     * @return boolean
+     */
+    public function hasComments()
+    {
+        if (($GLOBALS['conf']['comments']['allow'] == 'all' ||
+            ($GLOBALS['conf']['comments']['allow'] == 'authenticated' &&
+            Horde_Auth::getAuth()))) {
+                return true;
+            } else {
+                return false;
+            }
+    }
+
+    /**
+     * Returns decoded image data
+     *
+     * @param string $data         The id of the image.
+     * @param string $encoding     The encoding type for the image data.
+     *                             (none, base64, or binhex)
+     * @param string $compression  The compression type for the image data.
+     *                             (none, gzip, or lzf)
+     * @param boolean $upload      Process direction (true of encode/compress or false if decode/decompress)
+     *
+     * @return string  The image path.
+     */
+    protected function _getImageData($data, $encoding = 'none', $compression = 'none', $upload = true)
+    {
+        switch ($encoding) {
+        case 'base64':
+            $data = $upload ? base64_decode($data) : base64_encode($data);
+            break;
+
+        case 'binhex':
+            $data = $upload ? pack('H*', $data) : unpack('H*', $data);
+        }
+
+        switch ($compression) {
+        case 'gzip':
+            if (Horde_Util::loadExtension('zlib')) {
+                return $upload ? gzuncompress($data) : gzcompress($data);
+            }
+            break;
+
+        case 'lzf':
+            if (Horde_Util::loadExtension('lzf')) {
+                return $upload ? lzf_decompress($data) : lzf_compress($data);
+            }
+            break;
+
+        default:
+            return $data;
+        }
+    }
+
+    /**
+     * Stores an image in a gallery and returns gallery and image data.
+     *
+     * @param integer $app         Application used if null then use default.
+     * @param integer $gallery_id  The gallery id to add the image to.
+     * @param array $image         Image data array.  This can either be the return
+     *                             from Horde_Form_Type_image:: or an array with
+     *                             the following four fields:
+     *                             'filename', 'description', 'data', 'type'
+     * @param integer $default     Set this image as default in the gallery?
+     * @param array $gallery_data  Any gallery parameters to change at this time.
+     * @param string $encoding     The encoding type for the image data.
+     *                             (none, base64, or binhex)
+     * @param string $slug         Use gallery slug instead of id. (Pass '0' or null
+     *                             to gallery_id parameter).
+     * @param string $compression  The compression type for the image data.
+     *                             (none, gzip, or lzf)
+     *
+     * @return mixed  An array of image/gallery data || PEAR_Error
+     */
+    public function saveImage($app = null, $gallery_id, $image, $default = false,
+        $gallery_data = null, $encoding = null, $slug = null,
+        $compression = 'none', $skiphook = false)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $image_data = null;
+
+        /* If no app is given use Ansel's own gallery which is initialized
+         * in base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        if (isset($image['filename']) &&
+            isset($image['description']) &&
+            isset($image['data']) &&
+            isset($image['type'])) {
+                Horde::logMessage(sprintf("Receiving image %s in saveImage() with a raw filesize of %i", $image['filename'], strlen($image['data'])), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $image_data = array('image_filename' => $image['filename'],
+                    'image_caption' => $image['description'],
+                    'image_type' => $image['type'],
+                    'data' => $this->_getImageData($image['data'], $encoding, $compression, true));
+            } else {
+                Horde::logMessage(sprintf("Receiving image %s in saveImage() with a raw filesize of %i", $image['file'], filesize($image['file'])), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            }
+
+        if (is_null($image_data) && getimagesize($image['file']) === false) {
+            return PEAR::raiseError(_("The file you uploaded does not appear to be a valid photo."));
+        }
+        if (empty($slug) && empty($gallery_id)) {
+            return PEAR::raiseError(_("A gallery to add this photo to is required."));
+        }
+        if (!empty($slug)) {
+            $gallery = $GLOBALS['ansel_storage']->getGalleryBySlug($slug);
+            if (is_a($gallery, 'PEAR_Error')) {
+                return $gallery;
+            }
+        } elseif ($GLOBALS['ansel_storage']->galleryExists($gallery_id)) {
+            $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
+            if (is_a($gallery, 'PEAR_Error')) {
+                return $gallery;
+            }
+        }
+        if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
+            return PEAR::raiseError(sprintf(_("Access denied adding photos to \"%s\"."), $gallery->get('name')));
+        }
+        if (!empty($gallery_data)) {
+            foreach ($gallery_data as $key => $value) {
+                $gallery->set($key, $value);
+            }
+            $gallery->save();
+        }
+
+        if (is_null($image_data)) {
+            $image_data = array(
+                'image_filename' => $image['name'],
+                'image_caption' => $image['name'],
+                'image_type' => $image['name']['type'],
+                'data' => file_get_contents($image['file']),
+            );
+        }
+
+        if (isset($image['tags']) && is_array($image['tags']) &&
+            count($image['tags'])) {
+                $image_data['tags'] = $image['tags'];
+            }
+
+        $image_id = $gallery->addImage($image_data, $default);
+        if (is_a($image_id, 'PEAR_Error')) {
+            return $image_id;
+        }
+
+        // Call the postupload hook if needed
+        if (!empty($GLOBALS['conf']['hooks']['postupload']) && !$skiphook) {
+            Horde::callHook('_ansel_hook_postupload', array(array($image_id)), 'ansel');
+        }
+
+        return array('image_id'   => (int)$image_id,
+            'gallery_id' => (int)$gallery->id,
+            'gallery_slug' => $gallery->get('slug'),
+            'image_count' => (int)$gallery->countImages());
+    }
+
+    /**
+     * Notify Ansel that a group of images has just been uploaded. Used for when
+     * the _ansel_hook_postupload hook should be called with a group of recently
+     * uploaded images, as opposed to calling it once after each image is saved.
+     *
+     * @param array $image_ids  An array of image ids.
+     */
+    public function postBatchUpload($image_ids)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        if (!empty($conf['hooks']['postupload'])) {
+            return Horde::callHook('_ansel_hook_postupload', array($image_ids), 'ansel');
+        }
+    }
+
+    /**
+     * Removes an image from a gallery.
+     *
+     * @param string $app         Application scope to use, if not the default.
+     * @param integer $gallery_id The id of gallery.
+     * @param string $image_id    The id of image to remove.
+     */
+    public function removeImage($app = null, $gallery_id, $image_id)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        /* Check global Ansel permissions */
+        if (!($GLOBALS['perms']->getPermissions('ansel'))) {
+            return PEAR::raiseError(_("Access denied deleting galleries."));
+        }
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        $image = $GLOBALS['ansel_storage']->getImage($image_id);
+        if (is_a($image, 'PEAR_Error')) {
+            return $image;
+        }
+        $gallery = $GLOBALS['ansel_storage']->getGallery($image->gallery);
+        if (is_a($gallery, 'PEAR_Error') ||
+            !$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_DELETE)) {
+
+                return PEAR::raiseError(sprintf(_("Access denied deleting photos from \"%s\"."), $gallery->get('name')));
+            }
+        return $gallery->removeImage($image);
+    }
+
+    /**
+     * Add a new gallery to any arbitrary application's Ansel_Shares.
+     *
+     * @param string $app            Application scope to use, if not the default.
+     * @param array $attributes      The gallery attributes
+     *                               (@see Ansel_Storage::createGallery).
+     * @param array $perm            An array of permission data if Ansel's defaults
+     *                               are not desired. Takes an array like:
+     *                               array('guest' => PERMS_SHOW | PERMS_READ,
+     *                                     'default' => PERMS_SHOW | PERMS_READ);
+     * @param integer $parent        The gallery id of the parent gallery, if any.
+     *
+     * @return mixed  The gallery id of the new gallery | PEAR_Error
+     */
+    public function createGallery($app = null, $attributes = array(), $perm = null, $parent = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!(Horde_Auth::isAdmin() ||
+            (!$GLOBALS['perms']->exists('ansel') && Horde_Auth::getAuth()) ||
+            $GLOBALS['perms']->hasPermission('ansel', Horde_Auth::getAuth(), PERMS_EDIT))) {
+                return PEAR::raiseError(_("Access denied creating new galleries."));
+            }
+
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        if (!empty($perm)) {
+            // The name is inconsequential; it is only used as a container to
+            // represent permissions when passed to the Ansel backend.
+            $permobj = new Horde_Permission('');
+            $permobj->data = $perm;
+        } else {
+            $permobj = null;
+        }
+
+        $gallery = $GLOBALS['ansel_storage']->createGallery($attributes, $permobj, $parent);
+        if (is_a($gallery, 'PEAR_Error')) {
+            return $gallery;
+        }
+        return $gallery->id;
+    }
+
+    /**
+     * Removes a gallery and its images.
+     *
+     * @param string $app          Application scope to use, if not the default.
+     * @param integer $gallery_id  The id of gallery.
+     *
+     * @return mixed boolean true | PEAR_Error
+     */
+    public function removeGallery($app = null, $gallery_id)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        /* Check global Ansel permissions */
+        if (!($GLOBALS['perms']->getPermissions('ansel'))) {
+            return PEAR::raiseError(_("Access denied deleting galleries."));
+        }
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
+        if (is_a($gallery, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Access denied deleting gallery \"%s\"."),
+                $gallery->getMessage()));
+        } elseif (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_DELETE)) {
+            return PEAR::raiseError(sprintf(_("Access denied deleting gallery \"%s\"."),
+                $gallery->get('name')));
+        } else {
+            $imageList = $gallery->listImages();
+            if ($imageList) {
+                foreach ($imageList as $id) {
+                    $gallery->removeImage($id);
+                }
+            }
+            $result = $GLOBALS['ansel_storage']->removeGallery($gallery);
+            if (!is_a($result, 'PEAR_Error')) {
+                return true;
+            } else {
+                return PEAR::raiseError(sprintf(_("There was a problem deleting %s: %s"),
+                    $gallery->get('name'),
+                    $result->getMessage()));
+            }
+        }
+    }
+
+    /**
+     * Returns the number of images in a gallery.
+     *
+     * @param integer $app          Application used; if null then use default.
+     * @param integer $gallery_id   The gallery id.
+     * @param string  $slug         The gallery slug.
+     *
+     * @return integer  The number of images in the gallery.
+     */
+    public function count($app = null, $gallery_id = null, $slug = '')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        if (!empty($slug)) {
+            $gallery = $GLOBALS['ansel_storage']->getGalleryBySlug($slug);
+        } else {
+            $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
+        }
+
+        if (is_a($gallery, 'PEAR_Error')) {
+            return 0;
+        } else {
+            return (int)$gallery->countImages();
+        }
+    }
+
+    /**
+     * Returns the default image id of a gallery.
+     *
+     * @param string $app            Application scope to use, if not the default.
+     * @param integer $gallery_id    The gallery id.
+     * @param string $style          The named style.
+     * @param string $slug           The gallery slug.
+     *
+     * @return integer  The default image id.
+     */
+    public function getDefaultImage($app = null, $gallery_id = null,
+        $style = 'ansel_default', $slug = '')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        if (!empty($slug)) {
+            $gallery = $GLOBALS['ansel_storage']->getGalleryBySlug($slug);
+        } else {
+            $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
+        }
+
+        if (is_a($gallery, 'PEAR_Error')) {
+            return $gallery;
+        } else {
+            return $gallery->getDefaultImage($style);
+        }
+    }
+
+    /**
+     * Returns image URL.
+     *
+     * @param integer $app       Application used.
+     * @param integer $image_id  The id of the image.
+     * @param string $view       The view ('screen', 'thumb', 'full', etc) to show.
+     * @param boolean $full      Return a path that includes the server name?
+     * @param string $style      Use this gallery style
+     *
+     * @return string  The image path.
+     */
+    public function getImageUrl($app = null, $image_id, $view = 'screen',
+        $full = false, $style = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        return Ansel::getImageUrl($image_id, $view, $full, $style);
+    }
+
+    /**
+     * Returns image file content.
+     *
+     * @param integer $image_id  The id of the image.
+     * @param string $view       The view ('screen', 'thumb', 'full', etc) to show.
+     * @param string $style      Force use of this gallery style.
+     * @param integer $app       Application used.
+     * @param string $encoding     The encoding type for the image data.
+     *                             (none, base64, or binhex)
+     * @param string $compression  The compression type for the image data.
+     *                             (none, gzip, or lzf)
+     *
+     * @return string  The image path.
+     */
+    public function getImageContent($image_id, $view = 'screen', $style = null,
+        $app = null, $encoding = null, $compression = 'none')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        // Get image
+        $image = $GLOBALS['ansel_storage']->getImage($image_id);
+        if (is_a($image, 'PEAR_Error')) {
+            return $image;
+        }
+
+        // Get gallery
+        $gallery = $GLOBALS['ansel_storage']->getGallery($image->gallery);
+        if (is_a($gallery, 'PEAR_Error')) {
+            return $gallery;
+        }
+
+        // Check age and password
+        if (!$gallery->hasPasswd() || !$gallery->isOldEnough()) {
+            return PEAR::raiseError(_("Locked galleries are not viewable via the api."));
+        }
+
+        if ($view == 'full') {
+            // Check permissions for full view
+            if (!$gallery->canDownload()) {
+                return PEAR::RaiseError(sprintf(_("Access denied downloading photos from \"%s\"."), $gallery->get('name')));
+            }
+
+            $data = $GLOBALS['ansel_vfs']->read($image->getVFSPath('full'),
+                $image->getVFSName('full'));
+        } else {
+            // Load View
+            $result = $image->load($view, $style);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            // Return image content
+            $data = $image->_image->raw();
+        }
+
+        if (is_a($data, 'PEAR_Error')) {
+            return $data;
+        }
+
+        return $this->_getImageData($data, $encoding, $compression, false);
+    }
+
+    /**
+     * Returns a list of all galleries.
+     *
+     * @param string $app         Application scope to use, if not the default.
+     * @param integer $perm       The level of permissions to require for a gallery
+     *                            to be returned.
+     * @param integer $parent     The parent gallery id to start searching from.
+     *                            This should be either a gallery id or null.
+     * @param boolean $allLevels  Return all levels, or just the direct children of
+     *                            $parent?
+     * @param integer $from       The gallery to start listing at.
+     * @param integer $count      The number of galleries to return.
+     * @param array $attributes   Restrict the returned galleries to those matching
+     *                            $attributes. An array of attribute names => values
+     *
+     * @return array  An array of gallery information.
+     */
+    public function listGalleries($app = null, $perm = PERMS_SHOW,
+        $parent = null,
+        $allLevels = true, $from = 0, $count = 0,
+        $attributes = null, $sort_by = null, $direction = 0)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+
+        $galleries = $GLOBALS['ansel_storage']->listGalleries(
+            $perm, $attributes, $parent, $allLevels, $from, $count, $sort_by, $direction);
+
+        if (is_a($galleries, 'PEAR_Error')) {
+            return $galleries;
+        }
+
+        $return = array();
+        foreach ($galleries as $gallery) {
+            $return[$gallery->id] = array_merge($gallery->data, array('crumbs' => $gallery->getGalleryCrumbData()));
+        }
+
+        return $return;
+    }
+
+    /**
+     * Returns an array of gallery information.
+     *
+     * @param array $ids   An array of gallery ids.
+     * @param string $app  Application scope to use, if not the default.
+     * @param array $slugs An array of gallery slugs.
+     *
+     * @return mixed  An array of gallery data arrays | PEAR_Error
+     */
+    public function getGalleries($ids = array(), $app = null, $slugs = array())
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        if (count($slugs)) {
+            $results = $GLOBALS['ansel_storage']->getGalleriesBySlugs($slugs);
+        } else {
+            $results = $GLOBALS['ansel_storage']->getGalleries($ids);
+        }
+
+        if (is_a($results, 'PEAR_Error')) {
+            return $results;
+        }
+
+    /* We can't just return the results of the getGalleries call - we need to
+    ensure the caller has at least PERMS_READ on the galleries. */
+        $galleries = array();
+        foreach ($results as $gallery) {
+            if ($gallery->hasPermission(Horde_Auth::getAuth(), PERMS_READ)) {
+                $galleries[$gallery->id] = array_merge($gallery->data, array('crumbs' => $gallery->getGalleryCrumbData()));
+            }
+        }
+
+        return $galleries;
+    }
+
+    /**
+     * Returns a 'select' menu from the list of galleries created by
+     * listGalleries().
+     *
+     * @param integer $app        Application used if null then use default.
+     * @param integer $perm       The permissions filter to use.
+     * @param string $parent      The parent share to start listing at.
+     * @param boolean $allLevels  Return all levels, or just the direct
+     * @param integer $from       The gallery to start listing at.
+     * @param integer $count      The number of galleries to return.
+     * @param boolean $default    The gallery_id of the  gallery that is
+     *                            selected by default in the returned option
+     *                            list.
+     */
+    public function selectGalleries($app = null, $perm = PERMS_SHOW,
+        $parent = null,
+        $allLevels = true, $from = 0, $count = 0,
+        $default = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        return Ansel::selectGalleries($default, $perm, null, $parent, $allLevels,
+            $from, $count);
+    }
+
+    /**
+     * Returns a list of all images in a gallery.
+     *
+     * The return has the URL because in a lot of cases you'll want the url
+     * also. Using api call getImageURL results in a lot of overhead when
+     * e.g. generating a select list.
+     *
+     * @param string $app          Application scope to use, if not the default.
+     * @param integer $gallery_id  Gallery id to get images from.
+     * @param integer $perm        The level of permissions to require for a
+     *                             gallery to return it.
+     * @param string $view         Viewsize to generate URLs for.
+     * @param boolean $full        Return a full URL
+     * @param integer $from        Start image.
+     * @param integer $count       End image.
+     * @param string $style        Use this gallery style.
+     * @param string $slug         Gallery slug
+     *
+     * @return array  Two dimensional array with image names ids (key) and urls.
+     */
+    public function listImages($app = null, $gallery_id = null, $perm = PERMS_SHOW,
+        $view = 'screen', $full = false, $from = 0,
+        $count = 0, $style = null, $slug = '')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+    /* If no app is given use Ansel's own gallery which is initialized in
+    base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+    /* Determine the default gallery when none is given. The first gallery in
+    the list is the default gallery. */
+        if (is_null($gallery_id) && empty($slug)) {
+            $galleries = $GLOBALS['ansel_storage']->listGalleries($perm);
+            if (!count($galleries)) {
+                return array();
+            }
+            $keys = array_keys($galleries);
+            $gallery_names = array_keys($galleries[$keys[0]]['galleries']);
+            $gallery_id = $gallery_names[0];
+        } elseif (!empty($slug)) {
+            $gallery = $GLOBALS['ansel_storage']->getGalleryBySlug($slug);
+        } else {
+            $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
+        }
+        if (is_a($gallery, 'PEAR_Error')) {
+            return $gallery;
+        }
+
+        $images = $gallery->listImages();
+        if (is_a($images, 'PEAR_Error')) {
+            return $images;
+        }
+
+        $counter = 0;
+        $imagelist = array();
+        foreach ($images as $id) {
+            $image = $GLOBALS['ansel_storage']->getImage($id);
+            if (is_a($image, 'PEAR_Error')) {
+                return $image;
+            }
+            $imagelist[$id]['name'] = $image->filename;
+            $imagelist[$id]['caption'] = $image->caption;
+            $imagelist[$id]['type'] = $image->type;
+            $imagelist[$id]['uploaded'] = $image->uploaded;
+            $imagelist[$id]['original_date'] = $image->originalDate;
+            $imagelist[$id]['url'] = Ansel::getImageUrl($id, $view, $full, $style);
+            if (!is_null($app) && $GLOBALS['conf']['vfs']['src'] != 'direct') {
+                $imagelist[$id]['url'] = Horde_Util::addParameter($imagelist[$id]['url'],
+                    'app', $app);
+            }
+        }
+        return $imagelist;
+    }
+
+    /**
+     * Return a list of recently added images
+     *
+     * @param string $app       Application used if null then use default.
+     * @param array $galleries  An array of gallery ids to check.  If left empty,
+     *                          will search all galleries with the given
+     *                          permissions for the current user.
+     * @param integer $perms    PERMS_* constant
+     * @param string $view      The type of image view to return.
+     * @param boolean $full     Return a full URL if this is true.
+     * @param integer  $limit   The maximum number of images to return.
+     * @param string $style     Force the use of this gallery style
+     * @param string $slugs     An array of gallery slugs
+     *
+     * @return array  An array of image objects.
+     */
+    public function getRecentImages($app = null, $galleries = array(),
+        $perms = PERMS_SHOW, $view = 'screen',
+        $full = false, $limit = 10, $style = null,
+        $slugs = array())
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+        $images = $GLOBALS['ansel_storage']->getRecentImages($galleries, $limit, $slugs);
+        $imagelist = array();
+        foreach ($images as $image) {
+            $id = $image->id;
+            $imagelist[$id]['id'] = $id;
+            $imagelist[$id]['name'] = $image->filename;
+            $imagelist[$id]['url'] = Ansel::getImageUrl($id, $view, $full, $style);
+            $imagelist[$id]['caption'] = $image->caption;
+            $imagelist[$id]['filename'] = $image->filename;
+            $imagelist[$id]['gallery'] = $image->gallery;
+            $imagelist[$id]['uploaded'] = $image->uploaded;
+            $imagelist[$id]['original_date'] = $image->originalDate;
+
+            if (!is_null($app) && $GLOBALS['conf']['vfs']['src'] != 'direct') {
+                $imagelist[$id]['url'] = Horde_Util::addParameter($imagelist[$id]['url'],
+                    'app', $app);
+            }
+        }
+        return $imagelist;
+
+    }
+
+    /**
+     * Counts the number of galleries.
+     *
+     * @param string $app         Application scope to use, if not the default.
+     * @param integer $perm       The level of permissions to require for a gallery
+     *                            to return it.
+     * @param mixed $attributes   Restrict the galleries counted to those matching
+     *                            $attributes. An array of attribute/value pairs or
+     *                            a gallery owner username.
+     * @param integer $parent     The parent gallery id to start searching at.
+     * @param boolean $allLevels  Return all levels, or just the direct children of
+     *                            $parent?
+     *
+     * @return integer  Returns the number of matching galleries.
+     */
+    public function countGalleries($app = null, $perm = PERMS_SHOW, $attributes = null,
+        $parent = null, $allLevels = true)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        /* If no app is given use Ansel's own gallery which is initialized
+         * in base.php */
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        return $GLOBALS['ansel_storage']->countGalleries(Horde_Auth::getAuth(), $perm,
+            $attributes, $parent,
+            $allLevels);
+    }
+
+    /**
+     * Retrieve the list of used tag_names, tag_ids and the total number
+     * of resources that are linked to that tag.
+     *
+     * @param array $tags  An optional array of tag_ids. If omitted, all tags
+     *                     will be included.
+     *
+     * @return mixed  An array containing tag_name, and total | PEAR_Error
+     */
+    public function listTagInfo($tags = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return Ansel_Tags::listTagInfo($tags);
+    }
+
+    /**
+     * Searches images/galleries tagged with all requested tags.
+     * Returns an application-agnostic array (useful for when doing a tag search
+     * across multiple applications) containing the following keys:
+     * <pre>
+     *  'title'    - The title for this resource.
+     *  'desc'     - A terse description of this resource.
+     *  'view_url' - The URL to view this resource.
+     *  'app'      - The Horde application this resource belongs to.
+     * </pre>
+     *
+     * The 'raw' results array can be returned instead by setting $raw = true.
+     *
+     * @param array $names           An array of tag_names to search for.
+     * @param integer $max           The maximum number of stories to return.
+     * @param integer $from          The number of the story to start with.
+     * @param string $resource_type  An array of channel_ids to limit the search to.
+     * @param string $user           Restrict results to resources owned by $user.
+     * @param boolean $raw           Return the raw story data?
+     * @param string $app            Application scope to use, if not the default.
+     *
+     * @return mixed  An array of results | PEAR_Error
+     */
+    public function searchTags($names, $max = 10, $from = 0,
+        $resource_type = 'all', $user = null, $raw = false,
+        $app = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        } else {
+            $app = 'ansel';
+        }
+
+        $results = Ansel_Tags::searchTags($names, $max, $from,  $resource_type,
+            $user);
+
+        /* Check for error or if we requested the raw data array */
+        if (is_a($results, 'PEAR_Error') || $raw) {
+            return $results;
+        }
+
+        $return = array();
+        if (!empty($results['images'])) {
+            foreach ($results['images'] as $image_id) {
+                $image = $GLOBALS['ansel_storage']->getImage($image_id);
+                $desc = $image->caption;
+                $title = $image->filename;
+                $view_url = Ansel::getUrlFor('view',
+                    array('gallery' => $image->gallery,
+                    'image' => $image_id,
+                    'view' => 'Image'),
+                    true);
+                $return[] = array('title' => $image->filename,
+                    'desc'=> $image->caption,
+                    'view_url' => $view_url,
+                    'app' => $app);
+            }
+
+        }
+
+        if (!empty($results['galleries'])) {
+            foreach ($results['galleries'] as $gallery) {
+                $gal = $GLOBALS['ansel_storage']->getGallery($gallery);
+                $view_url = Horde_Util::addParameter(Horde::applicationUrl('view.php'), array('gallery' => $gallery,
+                    'view' => 'Gallery'));
+                $return[] = array('title' => $gal->get('name'),
+                    'desc' => $gal->get('desc'),
+                    'view_url' => $view_url,
+                    'app' => $app);
+
+            }
+        }
+
+
+        return $return;
+    }
+
+    /**
+     * Checks if the gallery exists
+     *
+     * @param string $app          Application scope to use, if not the default.
+     * @param integer $gallery_id  The gallery id
+     * @param string $slug         The gallery slug
+     *
+     * @return boolean
+     */
+    public function galleryExists($app, $gallery_id = null, $slug = '')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+
+        return $GLOBALS['ansel_storage']->galleryExists($gallery_id, $slug);
+    }
+
+    /**
+     * Get a list of all configured styles.
+     *
+     * @return hash of style definitions.
+     */
+    public function getGalleryStyles()
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return Ansel::getAvailableStyles();
+    }
+
+    /**
+     * Renders a gallery view
+     *
+     * @param array $params         Any parameters that the view might need.
+     *                              @see Ansel_View_* classes for descriptions of
+     *                              available parameters to use here.
+     * @param string $app           Application scope to use, if not the default.
+     * @param string $view          The generic type of view we want.
+     *                              (Gallery, Image, List, Embedded)
+     *
+     * @return array  An array containing 'html' and 'crumbs' keys.
+     */
+    public function renderView($params = array(), $app = null,
+        $view = 'Gallery')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!is_null($app)) {
+            $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
+        }
+        $classname = 'Ansel_View_' . basename($view);
+        $params['api'] = true;
+        $params['view'] = $view;
+        $trail = array();
+        $return = array();
+        try {
+            $view = new $classname($params);
+        } catch (Horde_Exception $e) {
+            $return['html'] = $e->getMessage();
+            $return['crumbs'] = array();
+            return $return;
+        }
+        $return['html'] = $view->html();
+        if ($params['view'] == 'Gallery' || $params['view'] == 'Image') {
+            $trail = $view->getGalleryCrumbData();
+        }
+        $return['crumbs'] = $trail;
+
+        return $return;
+
+    }
+
+}
diff --git a/ansel/lib/api.php b/ansel/lib/api.php
deleted file mode 100644 (file)
index 214ea48..0000000
+++ /dev/null
@@ -1,1335 +0,0 @@
-<?php
-/**
- * Ansel external API interface.
- *
- * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (GPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- *
- * @author  Jan Schneider <jan@horde.org>
- * @author  Chuck Hagenbuch <chuck@horde.org>
- * @author  Michael J. Rubinsky <mrubinsk@horde.org>
- * @package Ansel
- */
-
-$_services['perms'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['browse'] = array(
-    'args' => array('path' => 'string'),
-    'type' => '{urn:horde}hashHash',
-);
-
-$_services['put'] = array(
-    'args' => array('path' => 'string', 'content' => 'string', 'content_type' => 'string'),
-    'type' => '{urn:horde}stringArray',
-);
-
-// $_services['path_delete'] = array(
-//     'args' => array('path' => 'string'),
-//     'type' => 'boolean',
-// );
-
-$_services['commentCallback'] = array(
-    'args' => array('image_id' => 'string'),
-    'type' => 'string'
-);
-
-$_services['hasComments'] = array(
-    'args' => array(),
-    'type' => 'boolean'
-);
-
-$_services['saveImage'] = array(
-    'args' => array('app'          => 'string',
-                    'gallery_id'   => 'string',
-                    'image'        => '{urn:horde}hashHash',
-                    'default'      => 'boolean',
-                    'gallery_data' => '{urn:horde}hashHash',
-                    'encoding'     => 'string',
-                    'slug'         => 'string',
-                    'compression'  => 'string',
-                    'skiphook'     => 'boolean'),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['postBatchUpload'] = array(
-    'args' => array('image_ids' => '{urn:horde}hash'),
-    'type' => 'int'
-);
-
-$_services['createGallery'] = array(
-    'args' => array('app'        => 'string',
-                    'attributes' => '{urn:horde}hashHash',
-                    'perm'       => '{urn:horde}hashHash'),
-    'type' => 'int'
-);
-
-$_services['removeImage'] = array(
-    'args' => array('app'        => 'string',
-                    'gallery_id' => 'integer',
-                    'image_id'   => 'integer'),
-    'type' => 'int'
-);
-
-$_services['removeGallery'] = array(
-    'args' => array('app'        => 'string',
-                    'gallery_id' => 'integer'),
-    'type' => 'int'
-);
-
-$_services['getImageUrl'] = array(
-    'args' => array('app'        => 'string',
-                    'image_id'   => 'integer',
-                    'view'       => 'string',
-                    'full'       => 'boolean',
-                    'style'      => 'string'),
-    'type' => 'string'
-);
-
-$_services['getImageContent'] = array(
-    'args' => array('image_id'   => 'integer',
-                    'view'       => 'string',
-                    'style'      => 'string',
-                    'app'        => 'string'),
-    'type' => 'string'
-);
-
-$_services['count'] = array(
-    'args' => array('app'        => 'string',
-                    'gallery_id' => 'integer'),
-    'type' => 'int'
-);
-
-$_services['getDefaultImage'] = array(
-    'args' => array('app'        => 'string',
-                    'gallery_id' => 'integer',
-                    'style'      => 'string'),
-    'type' => 'string'
-);
-
-$_services['listGalleries'] = array(
-    'args' => array('app'        => 'string',
-                    'perm'       => 'integer',
-                    'parent'     => 'string',
-                    'allLevels'  => 'string',
-                    'from'       => 'integer',
-                    'count'      => 'integer'),
-    'type' => 'string'
-);
-
-$_services['getGalleries'] = array(
-    'args' => array('ids' => '{urn:horde}hash',
-                    'app' => 'string'),
-    'type' => '{urn:horde}hash'
-);
-
-$_services['selectGalleries'] = array(
-    'args' => array('app'        => 'string',
-                    'perm'       => 'integer',
-                    'parent'     => 'string',
-                    'allLevels'  => 'string',
-                    'from'       => 'integer',
-                    'count'      => 'integer'),
-    'type' => 'string'
-);
-
-$_services['listImages'] = array(
-    'args' => array('app'        => 'string',
-                    'gallery_id' => 'integer',
-                    'perm'       => 'integer',
-                    'view'       => 'string',
-                    'full'       => 'boolean',
-                    'from'       => 'integer',
-                    'count'      => 'integer',
-                    'style'      => 'string'),
-    'type' => 'string'
-);
-
-$_services['getRecentImages'] = array(
-    'args' => array('app' => 'string',
-                    'galleries' => '{urn:horde}hash',
-                    'perms' => 'integer',
-                    'view' => 'string',
-                    'full' => 'boolean',
-                    'limit' => 'integer',
-                    'style' => 'string',
-                    'slugs' => '{urn:horde}hashHash'),
-    'type' => '{urn:horde}hash'
-);
-
-$_services['countGalleries'] = array(
-    'args' => array('app'        => 'string',
-                    'perm'       => 'string',
-                    'attributes' => '{urn:horde}hash',
-                    'parent'     => 'string',
-                    'allLevels'  => 'boolean'),
-    'type' => 'int'
-);
-
-$_services['listTagInfo'] = array(
-    'args' => array('tags' => '{urn:horde}stringArray'),
-    'type' => '{urn:horde}hash'
-);
-
-$_services['searchTags'] = array(
-    'args' => array('tags' => '{urn:horde}stringArray',
-                    'resource_type' => 'string',
-                    'count' => 'int',
-                    'user' => 'string'),
-    'type' => '{urn:horde}hash'
-);
-
-$_services['galleryExists'] = array(
-    'args' => array('app' => 'string',
-                    'gallery_name' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['renderView'] = array(
-    'args' => array('parameters' => '{urn:horde]stringArray',
-                    'app' => 'string',
-                    'view' => 'string'),
-    'type' => 'string'
-);
-
-$_services['getGalleryStyles'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}hash');
-
-/**
- * Returns a list of available permissions.
- *
- * @return array  An array describing all available permissions.
- */
-function _ansel_perms()
-{
-    $perms = array();
-    $perms['tree']['ansel']['admin'] = false;
-    $perms['title']['ansel:admin'] = _("Administrators");
-
-    return $perms;
-}
-
-/**
- * Browse through Ansel's gallery tree.
- *
- * @param string $path       The level of the tree to browse.
- * @param array $properties  The item properties to return. Defaults to 'name',
- *                           'icon', and 'browseable'.
- *
- * @return array  The contents of $path
- */
-function _ansel_browse($path = '', $properties = array())
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    // Default properties.
-    if (!$properties) {
-        $properties = array('name', 'icon', 'browseable');
-    }
-
-    if (substr($path, 0, 5) == 'ansel') {
-        $path = substr($path, 5);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    if (empty($path)) {
-        $owners = array();
-        $galleries = $GLOBALS['ansel_storage']->listGalleries(PERMS_SHOW, null, null, false);
-        foreach ($galleries  as $gallery) {
-            $owners[$gallery->data['share_owner']] = true;
-        }
-
-        $results = array();
-        foreach (array_keys($owners) as $owner) {
-            if (in_array('name', $properties)) {
-                $results['ansel/' . $owner]['name'] = $owner;
-            }
-            if (in_array('icon', $properties)) {
-                $results['ansel/' . $owner]['icon'] =
-                    $registry->getImageDir('horde') . '/user.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results['ansel/' . $owner]['browseable'] = true;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results['ansel/' . $owner]['contenttype'] =
-                    'httpd/unix-directory';
-            }
-            if (in_array('contentlength', $properties)) {
-                $results['ansel/' . $owner]['contentlength'] = 0;
-            }
-            if (in_array('modified', $properties)) {
-                $results['ansel/' . $owner]['modified'] = time();
-            }
-            if (in_array('created', $properties)) {
-                $results['ansel/' . $owner]['created'] = 0;
-            }
-        }
-        return $results;
-
-    } else {
-        if (count($parts) == 1) {
-            // This request is for all galleries owned by the requested user.
-            $galleries = $GLOBALS['ansel_storage']->listGalleries(
-                PERMS_SHOW, $parts[0], null, false);
-            $images = array();
-        } elseif (_ansel_galleryExists(null, end($parts))) {
-            // This request if for a certain gallery, list all sub-galleries
-            // and images.
-            $gallery_id = end($parts);
-            $galleries = $GLOBALS['ansel_storage']->getGalleries(
-                array($gallery_id));
-            if (!isset($galleries[$gallery_id]) ||
-                !$galleries[$gallery_id]->hasPermission(Horde_Auth::getAuth(),
-                                                        PERMS_READ)) {
-                return PEAR::raiseError(_("Invalid gallery specified."), 404);
-            }
-            $galleries = $GLOBALS['ansel_storage']->listGalleries(
-                PERMS_SHOW, null, $gallery_id, false);
-
-            $images = _ansel_listImages(null, $gallery_id, PERMS_SHOW, 'mini');
-        } elseif (count($parts) > 2 &&
-                  _ansel_galleryExists(null, $parts[count($parts) - 2]) &&
-                  !is_a($image = $GLOBALS['ansel_storage']->getImage(end($parts)), 'PEAR_Error')) {
-            return array('data' => $image->raw(),
-                         'mimetype' => $image->type,
-                         'mtime' => $image->uploaded);
-        } else {
-            return PEAR::raiseError(_("File not found."), 404);
-        }
-
-        $results = array();
-        foreach ($galleries as $galleryId => $gallery) {
-            $retpath = 'ansel/' . implode('/', $parts) . '/' . $galleryId;
-            if (in_array('name', $properties)) {
-                $results[$retpath]['name'] = $gallery->data['attribute_name'];
-            }
-            if (in_array('displayname', $properties)) {
-                $results[$retpath]['displayname'] = rawurlencode(
-                    $gallery->data['attribute_name']);
-            }
-            if (in_array('icon', $properties)) {
-                $results[$retpath]['icon'] = $registry->getImageDir()
-                    . '/ansel.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results[$retpath]['browseable'] = $gallery->hasPermission(
-                    Horde_Auth::getAuth(), PERMS_READ);
-            }
-            if (in_array('contenttype', $properties)) {
-                $results[$retpath]['contenttype'] = 'httpd/unix-directory';
-            }
-            if (in_array('contentlength', $properties)) {
-                $results[$retpath]['contentlength'] = 0;
-            }
-            if (in_array('modified', $properties)) {
-                $results[$retpath]['modified'] = time();
-            }
-            if (in_array('created', $properties)) {
-                $results[$retpath]['created'] = 0;
-            }
-        }
-
-        foreach ($images as $imageId => $image) {
-            $retpath = 'ansel/' . implode('/', $parts) . '/' . $imageId;
-            if (in_array('name', $properties)) {
-                $results[$retpath]['name'] = $image['name'];
-            }
-            if (in_array('displayname', $properties)) {
-                $results[$retpath]['displayname'] = rawurlencode($image['name']);
-            }
-            if (in_array('icon', $properties)) {
-                $results[$retpath]['icon'] = Horde::url($image['url'], true);
-            }
-            if (in_array('browseable', $properties)) {
-                $results[$retpath]['browseable'] = false;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results[$retpath]['contenttype'] = $image['type'];
-            }
-            if (in_array('contentlength', $properties)) {
-                $results[$retpath]['contentlength'] = 0;
-            }
-            if (in_array('modified', $properties)) {
-                $results[$retpath]['modified'] = $image['uploaded'];
-            }
-            if (in_array('created', $properties)) {
-                $results[$retpath]['created'] = $image['uploaded'];
-            }
-        }
-        return $results;
-
-    }
-
-    return PEAR::raiseError(_("File not found."), 404);
-}
-
-/**
- * Saves an image into the gallery tree.
- *
- * @param string $path          The path where to PUT the file.
- * @param string $content       The file content.
- * @param string $content_type  The file's content type.
- *
- * @return array  The event UIDs, or a PEAR_Error on failure.
- */
-function _ansel_put($path, $content, $content_type)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (substr($path, 0, 5) == 'ansel') {
-        $path = substr($path, 9);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    if (count($parts) < 3) {
-        return PEAR::raiseError("Gallery does not exist");
-    }
-    $image_name = array_pop($parts);
-    $gallery_id = end($parts);
-    if (!$GLOBALS['ansel_storage']->galleryExists($gallery_id)) {
-        return PEAR::raiseError("Gallery does not exist");
-    }
-    $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
-    if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
-        return PEAR::raiseError(_("Access denied adding photos to \"%s\"."));
-    }
-
-    return $gallery->addImage(array('image_type' => $content_type,
-                                    'image_filename' => $image_name,
-                                    'image_caption' => '',
-                                    'data' => $content));
-}
-
-/**
- * Callback for Agora comments.
- *
- * @param integer $image_id  Image id to check
- *
- * @return mixed Image filename on success | false on failure
- */
-function _ansel_commentCallback($image_id)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!$GLOBALS['conf']['comments']['allow']) {
-        return false;
-    }
-
-    $image = $GLOBALS['ansel_storage']->getImage($image_id);
-    if (!$image || is_a($image, 'PEAR_Error')) {
-        return false;
-    }
-
-    return $image->filename;
-}
-
-/**
- * Checks if applications allows comments
- *
- * @return boolean
- */
-function _ansel_hasComments()
-{
-    if (($GLOBALS['conf']['comments']['allow'] == 'all' ||
-        ($GLOBALS['conf']['comments']['allow'] == 'authenticated' &&
-         Horde_Auth::getAuth()))) {
-        return true;
-    } else {
-        return false;
-    }
-}
-
-/**
- * Returns decoded image data
- *
- * @param string $data         The id of the image.
- * @param string $encoding     The encoding type for the image data.
- *                             (none, base64, or binhex)
- * @param string $compression  The compression type for the image data.
- *                             (none, gzip, or lzf)
- * @param boolean $upload      Process direction (true of encode/compress or false if decode/decompress)
- *
- * @return string  The image path.
- */
-function _getImageData($data, $encoding = 'none', $compression = 'none', $upload = true)
-{
-    switch ($encoding) {
-    case 'base64':
-        $data = $upload ? base64_decode($data) : base64_encode($data);
-        break;
-
-    case 'binhex':
-        $data = $upload ? pack('H*', $data) : unpack('H*', $data);
-    }
-
-    switch ($compression) {
-    case 'gzip':
-        if (Horde_Util::loadExtension('zlib')) {
-            return $upload ? gzuncompress($data) : gzcompress($data);
-        }
-        break;
-
-    case 'lzf':
-        if (Horde_Util::loadExtension('lzf')) {
-            return $upload ? lzf_decompress($data) : lzf_compress($data);
-        }
-        break;
-
-    default:
-        return $data;
-    }
-}
-
-/**
- * Stores an image in a gallery and returns gallery and image data.
- *
- * @param integer $app         Application used if null then use default.
- * @param integer $gallery_id  The gallery id to add the image to.
- * @param array $image         Image data array.  This can either be the return
- *                             from Horde_Form_Type_image:: or an array with
- *                             the following four fields:
- *                             'filename', 'description', 'data', 'type'
- * @param integer $default     Set this image as default in the gallery?
- * @param array $gallery_data  Any gallery parameters to change at this time.
- * @param string $encoding     The encoding type for the image data.
- *                             (none, base64, or binhex)
- * @param string $slug         Use gallery slug instead of id. (Pass '0' or null
- *                             to gallery_id parameter).
- * @param string $compression  The compression type for the image data.
- *                             (none, gzip, or lzf)
- *
- * @return mixed  An array of image/gallery data || PEAR_Error
- */
-function _ansel_saveImage($app = null, $gallery_id, $image, $default = false,
-                          $gallery_data = null, $encoding = null, $slug = null,
-                          $compression = 'none', $skiphook = false)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $image_data = null;
-
-    /* If no app is given use Ansel's own gallery which is initialized
-     * in base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    if (isset($image['filename']) &&
-        isset($image['description']) &&
-        isset($image['data']) &&
-        isset($image['type'])) {
-        Horde::logMessage(sprintf("Receiving image %s in _ansel_saveImage() with a raw filesize of %i", $image['filename'], strlen($image['data'])), __FILE__, __LINE__, PEAR_LOG_DEBUG);
-        $image_data = array('image_filename' => $image['filename'],
-                            'image_caption' => $image['description'],
-                            'image_type' => $image['type'],
-                            'data' => _getImageData($image['data'], $encoding, $compression, true));
-    } else {
-        Horde::logMessage(sprintf("Receiving image %s in _ansel_saveImage() with a raw filesize of %i", $image['file'], filesize($image['file'])), __FILE__, __LINE__, PEAR_LOG_DEBUG);
-    }
-
-    if (is_null($image_data) && getimagesize($image['file']) === false) {
-        return PEAR::raiseError(_("The file you uploaded does not appear to be a valid photo."));
-    }
-    if (empty($slug) && empty($gallery_id)) {
-        return PEAR::raiseError(_("A gallery to add this photo to is required."));
-    }
-    if (!empty($slug)) {
-        $gallery = $GLOBALS['ansel_storage']->getGalleryBySlug($slug);
-        if (is_a($gallery, 'PEAR_Error')) {
-            return $gallery;
-        }
-    } elseif ($GLOBALS['ansel_storage']->galleryExists($gallery_id)) {
-        $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
-        if (is_a($gallery, 'PEAR_Error')) {
-            return $gallery;
-        }
-    }
-    if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
-        return PEAR::raiseError(sprintf(_("Access denied adding photos to \"%s\"."), $gallery->get('name')));
-    }
-    if (!empty($gallery_data)) {
-        foreach ($gallery_data as $key => $value) {
-            $gallery->set($key, $value);
-        }
-        $gallery->save();
-    }
-
-    if (is_null($image_data)) {
-        $image_data = array(
-            'image_filename' => $image['name'],
-            'image_caption' => $image['name'],
-            'image_type' => $image['name']['type'],
-            'data' => file_get_contents($image['file']),
-        );
-    }
-
-    if (isset($image['tags']) && is_array($image['tags']) &&
-        count($image['tags'])) {
-            $image_data['tags'] = $image['tags'];
-    }
-
-    $image_id = $gallery->addImage($image_data, $default);
-    if (is_a($image_id, 'PEAR_Error')) {
-        return $image_id;
-    }
-
-    // Call the postupload hook if needed
-    if (!empty($GLOBALS['conf']['hooks']['postupload']) && !$skiphook) {
-        Horde::callHook('_ansel_hook_postupload', array(array($image_id)), 'ansel');
-    }
-
-    return array('image_id'   => (int)$image_id,
-                 'gallery_id' => (int)$gallery->id,
-                 'gallery_slug' => $gallery->get('slug'),
-                 'image_count' => (int)$gallery->countImages());
-}
-
-/**
- * Notify Ansel that a group of images has just been uploaded. Used for when
- * the _ansel_hook_postupload hook should be called with a group of recently
- * uploaded images, as opposed to calling it once after each image is saved.
- *
- * @param array $image_ids  An array of image ids.
- */
-function _ansel_postBatchUpload($image_ids)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    if (!empty($conf['hooks']['postupload'])) {
-        return Horde::callHook('_ansel_hook_postupload', array($image_ids), 'ansel');
-    }
-}
-
-/**
- * Removes an image from a gallery.
- *
- * @param string $app         Application scope to use, if not the default.
- * @param integer $gallery_id The id of gallery.
- * @param string $image_id    The id of image to remove.
- */
-function _ansel_removeImage($app = null, $gallery_id, $image_id)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* Check global Ansel permissions */
-    if (!($GLOBALS['perms']->getPermissions('ansel'))) {
-        return PEAR::raiseError(_("Access denied deleting galleries."));
-    }
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    $image = $GLOBALS['ansel_storage']->getImage($image_id);
-    if (is_a($image, 'PEAR_Error')) {
-        return $image;
-    }
-    $gallery = $GLOBALS['ansel_storage']->getGallery($image->gallery);
-    if (is_a($gallery, 'PEAR_Error') ||
-        !$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_DELETE)) {
-
-        return PEAR::raiseError(sprintf(_("Access denied deleting photos from \"%s\"."), $gallery->get('name')));
-    }
-    return $gallery->removeImage($image);
-}
-
-/**
- * Add a new gallery to any arbitrary application's Ansel_Shares.
- *
- * @param string $app            Application scope to use, if not the default.
- * @param array $attributes      The gallery attributes
- *                               (@see Ansel_Storage::createGallery).
- * @param array $perm            An array of permission data if Ansel's defaults
- *                               are not desired. Takes an array like:
- *                               array('guest' => PERMS_SHOW | PERMS_READ,
- *                                     'default' => PERMS_SHOW | PERMS_READ);
- * @param integer $parent        The gallery id of the parent gallery, if any.
- *
- * @return mixed  The gallery id of the new gallery | PEAR_Error
- */
-function _ansel_createGallery($app = null, $attributes = array(), $perm = null, $parent = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!(Horde_Auth::isAdmin() ||
-          (!$GLOBALS['perms']->exists('ansel') && Horde_Auth::getAuth()) ||
-          $GLOBALS['perms']->hasPermission('ansel', Horde_Auth::getAuth(), PERMS_EDIT))) {
-        return PEAR::raiseError(_("Access denied creating new galleries."));
-    }
-
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    if (!empty($perm)) {
-        // The name is inconsequential; it is only used as a container to
-        // represent permissions when passed to the Ansel backend.
-        $permobj = new Horde_Permission('');
-        $permobj->data = $perm;
-    } else {
-        $permobj = null;
-    }
-
-    $gallery = $GLOBALS['ansel_storage']->createGallery($attributes, $permobj, $parent);
-    if (is_a($gallery, 'PEAR_Error')) {
-        return $gallery;
-    }
-    return $gallery->id;
-}
-
-/**
- * Removes a gallery and its images.
- *
- * @param string $app          Application scope to use, if not the default.
- * @param integer $gallery_id  The id of gallery.
- *
- * @return mixed boolean true | PEAR_Error
- */
-function _ansel_removeGallery($app = null, $gallery_id)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* Check global Ansel permissions */
-    if (!($GLOBALS['perms']->getPermissions('ansel'))) {
-        return PEAR::raiseError(_("Access denied deleting galleries."));
-    }
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
-    if (is_a($gallery, 'PEAR_Error')) {
-        return PEAR::raiseError(sprintf(_("Access denied deleting gallery \"%s\"."),
-                                        $gallery->getMessage()));
-    } elseif (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_DELETE)) {
-        return PEAR::raiseError(sprintf(_("Access denied deleting gallery \"%s\"."),
-                                        $gallery->get('name')));
-    } else {
-        $imageList = $gallery->listImages();
-        if ($imageList) {
-            foreach ($imageList as $id) {
-                $gallery->removeImage($id);
-            }
-        }
-        $result = $GLOBALS['ansel_storage']->removeGallery($gallery);
-        if (!is_a($result, 'PEAR_Error')) {
-            return true;
-        } else {
-            return PEAR::raiseError(sprintf(_("There was a problem deleting %s: %s"),
-                                            $gallery->get('name'),
-                                            $result->getMessage()));
-        }
-    }
-}
-
-/**
- * Returns the number of images in a gallery.
- *
- * @param integer $app          Application used; if null then use default.
- * @param integer $gallery_id   The gallery id.
- * @param string  $slug         The gallery slug.
- *
- * @return integer  The number of images in the gallery.
- */
-function _ansel_count($app = null, $gallery_id = null, $slug = '')
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    if (!empty($slug)) {
-        $gallery = $GLOBALS['ansel_storage']->getGalleryBySlug($slug);
-    } else {
-        $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
-    }
-
-    if (is_a($gallery, 'PEAR_Error')) {
-        return 0;
-    } else {
-        return (int)$gallery->countImages();
-    }
-}
-
-/**
- * Returns the default image id of a gallery.
- *
- * @param string $app            Application scope to use, if not the default.
- * @param integer $gallery_id    The gallery id.
- * @param string $style          The named style.
- * @param string $slug           The gallery slug.
- *
- * @return integer  The default image id.
- */
-function _ansel_getDefaultImage($app = null, $gallery_id = null,
-                                $style = 'ansel_default', $slug = '')
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    if (!empty($slug)) {
-        $gallery = $GLOBALS['ansel_storage']->getGalleryBySlug($slug);
-    } else {
-        $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
-    }
-
-    if (is_a($gallery, 'PEAR_Error')) {
-        return $gallery;
-    } else {
-        return $gallery->getDefaultImage($style);
-    }
-}
-
-/**
- * Returns image URL.
- *
- * @param integer $app       Application used.
- * @param integer $image_id  The id of the image.
- * @param string $view       The view ('screen', 'thumb', 'full', etc) to show.
- * @param boolean $full      Return a path that includes the server name?
- * @param string $style      Use this gallery style
- *
- * @return string  The image path.
- */
-function _ansel_getImageUrl($app = null, $image_id, $view = 'screen',
-                            $full = false, $style = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    return Ansel::getImageUrl($image_id, $view, $full, $style);
-}
-
-/**
- * Returns image file content.
- *
- * @param integer $image_id  The id of the image.
- * @param string $view       The view ('screen', 'thumb', 'full', etc) to show.
- * @param string $style      Force use of this gallery style.
- * @param integer $app       Application used.
- * @param string $encoding     The encoding type for the image data.
- *                             (none, base64, or binhex)
- * @param string $compression  The compression type for the image data.
- *                             (none, gzip, or lzf)
- *
- * @return string  The image path.
- */
-function _ansel_getImageContent($image_id, $view = 'screen', $style = null,
-                                $app = null, $encoding = null, $compression = 'none')
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    // Get image
-    $image = $GLOBALS['ansel_storage']->getImage($image_id);
-    if (is_a($image, 'PEAR_Error')) {
-        return $image;
-    }
-
-    // Get gallery
-    $gallery = $GLOBALS['ansel_storage']->getGallery($image->gallery);
-    if (is_a($gallery, 'PEAR_Error')) {
-        return $gallery;
-    }
-
-    // Check age and password
-    if (!$gallery->hasPasswd() || !$gallery->isOldEnough()) {
-        return PEAR::raiseError(_("Locked galleries are not viewable via the api."));
-    }
-
-    if ($view == 'full') {
-        // Check permissions for full view
-        if (!$gallery->canDownload()) {
-            return PEAR::RaiseError(sprintf(_("Access denied downloading photos from \"%s\"."), $gallery->get('name')));
-        }
-
-        $data = $GLOBALS['ansel_vfs']->read($image->getVFSPath('full'),
-                                            $image->getVFSName('full'));
-    } else {
-        // Load View
-        $result = $image->load($view, $style);
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        }
-
-        // Return image content
-        $data = $image->_image->raw();
-    }
-
-    if (is_a($data, 'PEAR_Error')) {
-        return $data;
-    }
-
-    return _getImageData($data, $encoding, $compression, false);
-}
-
-/**
- * Returns a list of all galleries.
- *
- * @param string $app         Application scope to use, if not the default.
- * @param integer $perm       The level of permissions to require for a gallery
- *                            to be returned.
- * @param integer $parent     The parent gallery id to start searching from.
- *                            This should be either a gallery id or null.
- * @param boolean $allLevels  Return all levels, or just the direct children of
- *                            $parent?
- * @param integer $from       The gallery to start listing at.
- * @param integer $count      The number of galleries to return.
- * @param array $attributes   Restrict the returned galleries to those matching
- *                            $attributes. An array of attribute names => values
- *
- * @return array  An array of gallery information.
- */
-function _ansel_listGalleries($app = null, $perm = PERMS_SHOW,
-                              $parent = null,
-                              $allLevels = true, $from = 0, $count = 0,
-                              $attributes = null, $sort_by = null, $direction = 0)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-
-    $galleries = $GLOBALS['ansel_storage']->listGalleries(
-        $perm, $attributes, $parent, $allLevels, $from, $count, $sort_by, $direction);
-
-    if (is_a($galleries, 'PEAR_Error')) {
-        return $galleries;
-    }
-
-    $return = array();
-    foreach ($galleries as $gallery) {
-        $return[$gallery->id] = array_merge($gallery->data, array('crumbs' => $gallery->getGalleryCrumbData()));
-    }
-
-    return $return;
-}
-
-/**
- * Returns an array of gallery information.
- *
- * @param array $ids   An array of gallery ids.
- * @param string $app  Application scope to use, if not the default.
- * @param array $slugs An array of gallery slugs.
- *
- * @return mixed  An array of gallery data arrays | PEAR_Error
- */
-function _ansel_getGalleries($ids = array(), $app = null, $slugs = array())
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    if (count($slugs)) {
-        $results = $GLOBALS['ansel_storage']->getGalleriesBySlugs($slugs);
-    } else {
-        $results = $GLOBALS['ansel_storage']->getGalleries($ids);
-    }
-
-    if (is_a($results, 'PEAR_Error')) {
-        return $results;
-    }
-
-    /* We can't just return the results of the getGalleries call - we need to
-       ensure the caller has at least PERMS_READ on the galleries. */
-    $galleries = array();
-    foreach ($results as $gallery) {
-        if ($gallery->hasPermission(Horde_Auth::getAuth(), PERMS_READ)) {
-            $galleries[$gallery->id] = array_merge($gallery->data, array('crumbs' => $gallery->getGalleryCrumbData()));
-        }
-    }
-
-    return $galleries;
-}
-
-/**
- * Returns a 'select' menu from the list of galleries created by
- * _ansel_listGalleries().
- *
- *
- * @param integer $app        Application used if null then use default.
- * @param integer $perm       The permissions filter to use.
- * @param string $parent      The parent share to start listing at.
- * @param boolean $allLevels  Return all levels, or just the direct
- * @param integer $from       The gallery to start listing at.
- * @param integer $count      The number of galleries to return.
- * @param boolean $default    The gallery_id of the  gallery that is
- *                            selected by default in the returned option
- *                            list.
- */
-function _ansel_selectGalleries($app = null, $perm = PERMS_SHOW,
-                                $parent = null,
-                                $allLevels = true, $from = 0, $count = 0,
-                                $default = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    return Ansel::selectGalleries($default, $perm, null, $parent, $allLevels,
-                                  $from, $count);
-}
-
-/**
- * Returns a list of all images in a gallery.
- *
- * The return has the URL because in a lot of cases you'll want the url
- * also. Using api call getImageURL results in a lot of overhead when
- * e.g. generating a select list.
- *
- * @param string $app          Application scope to use, if not the default.
- * @param integer $gallery_id  Gallery id to get images from.
- * @param integer $perm        The level of permissions to require for a
- *                             gallery to return it.
- * @param string $view         Viewsize to generate URLs for.
- * @param boolean $full        Return a full URL
- * @param integer $from        Start image.
- * @param integer $count       End image.
- * @param string $style        Use this gallery style.
- * @param string $slug         Gallery slug
- *
- * @return array  Two dimensional array with image names ids (key) and urls.
- */
-function _ansel_listImages($app = null, $gallery_id = null, $perm = PERMS_SHOW,
-                           $view = 'screen', $full = false, $from = 0,
-                           $count = 0, $style = null, $slug = '')
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* If no app is given use Ansel's own gallery which is initialized in
-       base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    /* Determine the default gallery when none is given. The first gallery in
-       the list is the default gallery. */
-    if (is_null($gallery_id) && empty($slug)) {
-        $galleries = $GLOBALS['ansel_storage']->listGalleries($perm);
-        if (!count($galleries)) {
-            return array();
-        }
-        $keys = array_keys($galleries);
-        $gallery_names = array_keys($galleries[$keys[0]]['galleries']);
-        $gallery_id = $gallery_names[0];
-    } elseif (!empty($slug)) {
-        $gallery = $GLOBALS['ansel_storage']->getGalleryBySlug($slug);
-    } else {
-        $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id);
-    }
-    if (is_a($gallery, 'PEAR_Error')) {
-        return $gallery;
-    }
-
-    $images = $gallery->listImages();
-    if (is_a($images, 'PEAR_Error')) {
-        return $images;
-    }
-
-    $counter = 0;
-    $imagelist = array();
-    foreach ($images as $id) {
-        $image = $GLOBALS['ansel_storage']->getImage($id);
-        if (is_a($image, 'PEAR_Error')) {
-            return $image;
-        }
-        $imagelist[$id]['name'] = $image->filename;
-        $imagelist[$id]['caption'] = $image->caption;
-        $imagelist[$id]['type'] = $image->type;
-        $imagelist[$id]['uploaded'] = $image->uploaded;
-        $imagelist[$id]['original_date'] = $image->originalDate;
-        $imagelist[$id]['url'] = Ansel::getImageUrl($id, $view, $full, $style);
-        if (!is_null($app) && $GLOBALS['conf']['vfs']['src'] != 'direct') {
-            $imagelist[$id]['url'] = Horde_Util::addParameter($imagelist[$id]['url'],
-                                                        'app', $app);
-        }
-    }
-    return $imagelist;
-}
-
-/**
- * Return a list of recently added images
- *
- * @param string $app       Application used if null then use default.
- * @param array $galleries  An array of gallery ids to check.  If left empty,
- *                          will search all galleries with the given
- *                          permissions for the current user.
- * @param integer $perms    PERMS_* constant
- * @param string $view      The type of image view to return.
- * @param boolean $full     Return a full URL if this is true.
- * @param integer  $limit   The maximum number of images to return.
- * @param string $style     Force the use of this gallery style
- * @param string $slugs     An array of gallery slugs
- *
- * @return array  An array of image objects.
- */
-function _ansel_getRecentImages($app = null, $galleries = array(),
-                                $perms = PERMS_SHOW, $view = 'screen',
-                                $full = false, $limit = 10, $style = null,
-                                $slugs = array())
-{
-    require_once dirname(__FILE__) . '/base.php';
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-    $images = $GLOBALS['ansel_storage']->getRecentImages($galleries, $limit, $slugs);
-    $imagelist = array();
-    foreach ($images as $image) {
-        $id = $image->id;
-        $imagelist[$id]['id'] = $id;
-        $imagelist[$id]['name'] = $image->filename;
-        $imagelist[$id]['url'] = Ansel::getImageUrl($id, $view, $full, $style);
-        $imagelist[$id]['caption'] = $image->caption;
-        $imagelist[$id]['filename'] = $image->filename;
-        $imagelist[$id]['gallery'] = $image->gallery;
-        $imagelist[$id]['uploaded'] = $image->uploaded;
-        $imagelist[$id]['original_date'] = $image->originalDate;
-
-        if (!is_null($app) && $GLOBALS['conf']['vfs']['src'] != 'direct') {
-            $imagelist[$id]['url'] = Horde_Util::addParameter($imagelist[$id]['url'],
-                                                        'app', $app);
-        }
-    }
-    return $imagelist;
-
-}
-
-/**
- * Counts the number of galleries.
- *
- * @param string $app         Application scope to use, if not the default.
- * @param integer $perm       The level of permissions to require for a gallery
- *                            to return it.
- * @param mixed $attributes   Restrict the galleries counted to those matching
- *                            $attributes. An array of attribute/value pairs or
- *                            a gallery owner username.
- * @param integer $parent     The parent gallery id to start searching at.
- * @param boolean $allLevels  Return all levels, or just the direct children of
- *                            $parent?
- *
- * @return integer  Returns the number of matching galleries.
- */
-function _ansel_countGalleries($app = null, $perm = PERMS_SHOW, $attributes = null,
-                               $parent = null, $allLevels = true)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* If no app is given use Ansel's own gallery which is initialized
-     * in base.php */
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    return $GLOBALS['ansel_storage']->countGalleries(Horde_Auth::getAuth(), $perm,
-                                                     $attributes, $parent,
-                                                     $allLevels);
-}
-
-/**
- * Retrieve the list of used tag_names, tag_ids and the total number
- * of resources that are linked to that tag.
- *
- * @param array $tags  An optional array of tag_ids. If omitted, all tags
- *                     will be included.
- *
- * @return mixed  An array containing tag_name, and total | PEAR_Error
- */
-function _ansel_listTagInfo($tags = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return Ansel_Tags::listTagInfo($tags);
-}
-
-/**
- * Searches images/galleries tagged with all requested tags.
- * Returns an application-agnostic array (useful for when doing a tag search
- * across multiple applications) containing the following keys:
- * <pre>
- *  'title'    - The title for this resource.
- *  'desc'     - A terse description of this resource.
- *  'view_url' - The URL to view this resource.
- *  'app'      - The Horde application this resource belongs to.
- * </pre>
- *
- * The 'raw' results array can be returned instead by setting $raw = true.
- *
- * @param array $names           An array of tag_names to search for.
- * @param integer $max           The maximum number of stories to return.
- * @param integer $from          The number of the story to start with.
- * @param string $resource_type  An array of channel_ids to limit the search to.
- * @param string $user           Restrict results to resources owned by $user.
- * @param boolean $raw           Return the raw story data?
- * @param string $app            Application scope to use, if not the default.
- *
- * @return mixed  An array of results | PEAR_Error
- */
-function _ansel_searchTags($names, $max = 10, $from = 0,
-                           $resource_type = 'all', $user = null, $raw = false,
-                           $app = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    } else {
-        $app = 'ansel';
-    }
-
-    $results = Ansel_Tags::searchTags($names, $max, $from,  $resource_type,
-                                      $user);
-
-    /* Check for error or if we requested the raw data array */
-    if (is_a($results, 'PEAR_Error') || $raw) {
-        return $results;
-    }
-
-    $return = array();
-    if (!empty($results['images'])) {
-        foreach ($results['images'] as $image_id) {
-            $image = $GLOBALS['ansel_storage']->getImage($image_id);
-            $desc = $image->caption;
-            $title = $image->filename;
-            $view_url = Ansel::getUrlFor('view',
-                                         array('gallery' => $image->gallery,
-                                               'image' => $image_id,
-                                               'view' => 'Image'),
-                                         true);
-            $return[] = array('title' => $image->filename,
-                              'desc'=> $image->caption,
-                              'view_url' => $view_url,
-                              'app' => $app);
-        }
-
-    }
-
-    if (!empty($results['galleries'])) {
-        foreach ($results['galleries'] as $gallery) {
-            $gal = $GLOBALS['ansel_storage']->getGallery($gallery);
-            $view_url = Horde_Util::addParameter(Horde::applicationUrl('view.php'), array('gallery' => $gallery,
-                                                                                    'view' => 'Gallery'));
-            $return[] = array('title' => $gal->get('name'),
-                              'desc' => $gal->get('desc'),
-                              'view_url' => $view_url,
-                              'app' => $app);
-
-        }
-    }
-
-
-    return $return;
-}
-
-/**
- * Checks if the gallery exists
- *
- * @param string $app          Application scope to use, if not the default.
- * @param integer $gallery_id  The gallery id
- * @param string $slug         The gallery slug
- *
- * @return boolean
- */
-function _ansel_galleryExists($app, $gallery_id = null, $slug = '')
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-
-    return $GLOBALS['ansel_storage']->galleryExists($gallery_id, $slug);
-}
-
-/**
- * Get a list of all configured styles.
- *
- * @return hash of style definitions.
- */
-function _ansel_getGalleryStyles()
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return Ansel::getAvailableStyles();
-}
-
-/**
- * Renders a gallery view
- *
- * @param array $params         Any parameters that the view might need.
- *                              @see Ansel_View_* classes for descriptions of
- *                              available parameters to use here.
- * @param string $app           Application scope to use, if not the default.
- * @param string $view          The generic type of view we want.
- *                              (Gallery, Image, List, Embedded)
- *
- * @return array  An array containing 'html' and 'crumbs' keys.
- */
-function _ansel_renderView($params = array(), $app = null,
-                           $view = 'Gallery')
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!is_null($app)) {
-        $GLOBALS['ansel_storage'] = new Ansel_Storage($app);
-    }
-    $classname = 'Ansel_View_' . basename($view);
-    $params['api'] = true;
-    $params['view'] = $view;
-    $trail = array();
-    $return = array();
-    try {
-        $view = new $classname($params);
-    } catch (Horde_Exception $e) {
-        $return['html'] = $e->getMessage();
-        $return['crumbs'] = array();
-        return $return;
-    }
-    $return['html'] = $view->html();
-    if ($params['view'] == 'Gallery' || $params['view'] == 'Image') {
-        $trail = $view->getGalleryCrumbData();
-    }
-    $return['crumbs'] = $trail;
-
-    return $return;
-
-}
diff --git a/ansel/lib/version.php b/ansel/lib/version.php
deleted file mode 100755 (executable)
index 4b58583..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('ANSEL_VERSION', '2.0-git') ?>
index 5f0670d..d7684a3 100644 (file)
@@ -12,7 +12,6 @@
 
 $ansel_session_control = 'readonly';
 require_once dirname(__FILE__) . '/lib/base.php';
-require_once ANSEL_BASE . '/lib/version.php';
 
 // Get form data
 $stream_type = Horde_Util::getFormData('stream_type', 'all');
@@ -266,7 +265,7 @@ if (empty($rss)) {
     $image_url = htmlspecialchars($params['image_url']);
     $image_link = htmlspecialchars($params['image_link']);
     $image_alt = htmlspecialchars($params['image_alt']);
-    $ansel = 'Ansel ' . ANSEL_VERSION . ' (http://www.horde.org/)';
+    $ansel = 'Ansel ' . $registry->getVersion('ansel') . ' (http://www.horde.org/)';
 
     if ($stream_type != 'all' && $type != 'rss2') {
         $getparams = array('stream_type' => $stream_type, 'type' => $type);
index 5ac1597..2218bc7 100644 (file)
@@ -30,8 +30,9 @@ $horde_test = new Horde_Test;
 
 /* Ansel version. */
 $module = 'Ansel';
-require_once ANSEL_BASE . '/lib/version.php';
-$module_version = ANSEL_VERSION;
+require_once dirname(__FILE__) . '/lib/Api.php';
+$api = new Ansel_Api();
+$module_version = $api->version;
 
 /* Ansel configuration files. */
 $file_list = array(
diff --git a/babel/lib/Api.php b/babel/lib/Api.php
new file mode 100644 (file)
index 0000000..6e7cb5b
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Babel external API interface.
+ *
+ * This file defines Babel's external API interface. Other applications can
+ * interact with Babel through this API.
+ *
+ * Copyright 2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+class Babel_Api extends Horde_Registry_Api
+{
+    public function perms()
+    {
+        global $registry;
+
+        static $perms = array();
+        if (!empty($perms)) {
+               return $perms;
+        }
+
+        $perms['tree']['babel']['language'] = array();
+        $perms['title']['babel:language'] = _("Languages");
+        $perms['type']['babel:language']  = 'none';
+
+        foreach(Horde_Nls::$config['languages'] as $langcode => $langdesc) {
+               $perms['tree']['babel']['language'][$langcode] = false;
+               $perms['title']['babel:language:' . $langcode] = sprintf("%s (%s)", $langdesc, $langcode);
+               $perms['type']['babel:language:' . $langcode] = 'boolean';
+        }
+
+        $perms['tree']['babel']['module'] = array();
+        $perms['title']['babel:module'] = _("Modules");
+        $perms['type']['babel:module']  = 'none';
+
+        foreach ($registry->applications as $app => $params) {
+               if ($params['status'] == 'heading' || $params['status'] == 'block') {
+                   continue;
+               }
+
+               if (isset($params['fileroot']) && !is_dir($params['fileroot'])) {
+                   continue;
+               }
+
+               if (preg_match('/_reports$/', $app) || preg_match('/_tools$/', $app)) {
+                   continue;
+               }
+
+               $perms['tree']['babel']['module'][$app] = false;
+               $perms['title']['babel:module:' . $app] = sprintf("%s (%s)", $params['name'], $app);
+               $perms['type']['babel:module:' . $app] = 'boolean';
+        }
+
+        $tabdesc['download']   = _("Download");
+        $tabdesc['upload']     = _("Upload");
+        $tabdesc['stats']      = _("Statistics");
+        $tabdesc['view']       = _("View/Edit");
+        $tabdesc['viewsource'] = _("View Source");
+        $tabdesc['extract']    = _("Extract");
+        $tabdesc['make']       = _("Make");
+        $tabdesc['commit']     = _("Commit");
+        $tabdesc['reset']      = _("Reset");
+
+        foreach ($tabdesc as $cat => $desc) {
+               $perms['tree']['babel'][$cat] = array();
+               $perms['title']['babel:' . $cat] = $desc;
+        }
+
+        return $perms;
+    }
+
+}
diff --git a/babel/lib/api.php b/babel/lib/api.php
deleted file mode 100644 (file)
index 5a5be33..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-/**
- * Babel external API interface.
- *
- * This file defines Babel's external API interface. Other applications can
- * interact with Babel through this API.
- *
- * Copyright 2009 The Horde Project (http://www.horde.org/)
- *
- * @author  Joel Vandal <joel@scopserv.com>
- * @package Babel
- */
-
-$_services['perms'] = array(
-    'args' => array(),
-    // This is actually a hash of hashes
-    'type' => '{urn:horde}hash'
-);
-
-function _babel_perms()
-{
-
-    global $registry;
-    
-    static $perms = array();
-    if (!empty($perms)) {
-       return $perms;
-    }
-
-    $perms['tree']['babel']['language'] = array();
-    $perms['title']['babel:language'] = _("Languages");
-    $perms['type']['babel:language']  = 'none';
-    
-    foreach(Horde_Nls::$config['languages'] as $langcode => $langdesc) {
-       $perms['tree']['babel']['language'][$langcode] = false;
-       $perms['title']['babel:language:' . $langcode] = sprintf("%s (%s)", $langdesc, $langcode);
-       $perms['type']['babel:language:' . $langcode] = 'boolean';
-    }
-
-    $perms['tree']['babel']['module'] = array();
-    $perms['title']['babel:module'] = _("Modules");
-    $perms['type']['babel:module']  = 'none';
-    
-    foreach ($registry->applications as $app => $params) {
-       if ($params['status'] == 'heading' || $params['status'] == 'block') {
-           continue;
-       }
-       
-       if (isset($params['fileroot']) && !is_dir($params['fileroot'])) {
-           continue;
-       }
-       
-       if (preg_match('/_reports$/', $app) || preg_match('/_tools$/', $app)) {
-           continue;
-       }
-       
-       $perms['tree']['babel']['module'][$app] = false;
-       $perms['title']['babel:module:' . $app] = sprintf("%s (%s)", $params['name'], $app);
-       $perms['type']['babel:module:' . $app] = 'boolean';
-    }
-
-    $tabdesc['download']   = _("Download");
-    $tabdesc['upload']     = _("Upload");
-    $tabdesc['stats']      = _("Statistics");
-    $tabdesc['view']       = _("View/Edit");
-    $tabdesc['viewsource'] = _("View Source");
-    $tabdesc['extract']    = _("Extract");
-    $tabdesc['make']       = _("Make");
-    $tabdesc['commit']     = _("Commit");
-    $tabdesc['reset']      = _("Reset");
-    
-    foreach ($tabdesc as $cat => $desc) {
-       $perms['tree']['babel'][$cat] = array();
-       $perms['title']['babel:' . $cat] = $desc;
-    }
-    
-    return $perms;
-    
-}
diff --git a/chora/lib/Api.php b/chora/lib/Api.php
new file mode 100644 (file)
index 0000000..bd458bc
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Chora external API interface.
+ *
+ * This file defines Chora's external API interface. Other applications can
+ * interact with Chora through this API.
+ *
+ * @package Chora
+ */
+class Chora_Api extends Horde_Registry_Api
+{
+    /**
+     * The application's version.
+     *
+     * @var string
+     */
+    public $version = 'H4 (3.0-git)';
+
+    public function perms()
+    {
+        static $perms = array();
+
+        if (!empty($perms)) {
+            return $perms;
+        }
+
+        require_once dirname(__FILE__) . '/../config/sourceroots.php';
+
+        $perms['tree']['chora']['sourceroots'] = false;
+        $perms['title']['chora:sourceroots'] = _("Repositories");
+
+        // Run through every source repository
+        foreach ($sourceroots as $sourceroot => $srconfig) {
+            $perms['tree']['chora']['sourceroots'][$sourceroot] = false;
+            $perms['title']['chora:sourceroots:' . $sourceroot] = $srconfig['name'];
+        }
+
+        return $perms;
+    }
+
+}
diff --git a/chora/lib/api.php b/chora/lib/api.php
deleted file mode 100644 (file)
index e1b05d2..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-/**
- * Chora external API interface.
- *
- * This file defines Chora's external API interface. Other applications can
- * interact with Chora through this API.
- *
- * @package Chora
- */
-@define('CHORA_BASE', dirname(__FILE__) . "/..");
-
-$_services['perms'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray'
-);
-
-function _chora_perms()
-{
-    static $perms = array();
-
-    if (!empty($perms)) {
-        return $perms;
-    }
-
-    @define('CHORA_BASE', dirname(__FILE__) . '/..');
-    require_once CHORA_BASE . '/config/sourceroots.php';
-
-    $perms['tree']['chora']['sourceroots'] = false;
-    $perms['title']['chora:sourceroots'] = _("Repositories");
-
-    // Run through every source repository
-    foreach ($sourceroots as $sourceroot => $srconfig) {
-        $perms['tree']['chora']['sourceroots'][$sourceroot] = false;
-        $perms['title']['chora:sourceroots:' . $sourceroot] = $srconfig['name'];
-    }
-
-    return $perms;
-}
-
diff --git a/chora/lib/version.php b/chora/lib/version.php
deleted file mode 100644 (file)
index 4b0a8e5..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('CHORA_VERSION', 'H4 (3.0-git)') ?>
index b375144..b2beca2 100644 (file)
@@ -30,8 +30,9 @@ $horde_test = new Horde_Test;
 
 /* Chora version. */
 $module = 'Chora';
-require_once CHORA_BASE . '/lib/version.php';
-$module_version = CHORA_VERSION;
+require_once dirname(__FILE__) . '/lib/Api.php';
+$api = new Chora_Api();
+$module_version = $api->version;
 
 /* Chora configuration files. */
 $file_list = array(
diff --git a/crumb/lib/Api.php b/crumb/lib/Api.php
new file mode 100644 (file)
index 0000000..54fa300
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+class Crumb_Api extends Horde_Registry_Api
+{
+    public $version = 'H4 (0.1-git)';
+}
diff --git a/crumb/lib/version.php b/crumb/lib/version.php
deleted file mode 100644 (file)
index e5486e3..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('CRUMB_VERSION', 'H4 (0.1-git)') ?>
diff --git a/fima/lib/Api.php b/fima/lib/Api.php
new file mode 100644 (file)
index 0000000..6500598
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+class Fima_Api extends Horde_Regsitry_Api
+{
+    public $version = '1.0.1';
+}
diff --git a/fima/lib/version.php b/fima/lib/version.php
deleted file mode 100644 (file)
index 3f4500b..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('FIMA_VERSION', '1.0.1') ?>
\ No newline at end of file
diff --git a/folks/lib/Api.php b/folks/lib/Api.php
new file mode 100644 (file)
index 0000000..f853dbb
--- /dev/null
@@ -0,0 +1,576 @@
+<?php
+/**
+ * Folks api
+ *
+ * Copyright 2008 Obala d.o.o (www.obala.si)
+ *
+ * See the enclosed file LICENSE for license information (BSD). If you
+ * did not receive this file, see http://cvs.horde.org/co.php/folks/LICENSE.
+ *
+ * $Id: api.php 1235 2009-01-28 19:25:04Z duck $
+ *
+ * @author Duck <duck@obala.net>
+ * @package Folks
+ */
+class Folks_Api extends Horde_Registry_Api
+{
+    public $version = 'H4 (0.1-git)';
+
+    public $services = array(
+        'commentCallback' => array(
+            'args' => array('id' => 'string'),
+            'type' => 'string'
+        ),
+
+        'removeUserData' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'hasComments' => array(
+            'args' => array(),
+            'type' => 'boolean'
+        ),
+
+        'getOnlineUsers' => array(
+            'args' => array(),
+            'type' => 'array'
+        ),
+
+        'getProfile' => array(
+            'args' => array(),
+            'type' => 'array'
+        ),
+
+        'getFriends' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'array'
+        ),
+
+        'addFriend' => array(
+            'args' => array('friend' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'getBlacklist' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'array'
+        ),
+
+        'addBlacklisted' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'removeBlacklisted' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'isBlacklisted' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'show' => array(
+            'link' => '%application%/user.php?user=|user|'
+        ),
+
+        'listTimeObjectCategories' => array(
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'listTimeObjects' => array(
+            'args' => array('start' => 'int', 'end' => 'int'),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'logActivity' => array(
+            'args' => array('activity_message' => 'string',
+            'scope' => 'string',
+            'user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'getActivity' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'setStatus' => array(
+            'args' => array('status' => 'boolean'),
+            'args' => array('user' => 'string'),
+            'type' => 'array'
+        ),
+
+        'getStatus' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'array'
+        ),
+
+        'authenticate' => array(
+            'args' => array('userID'      => 'string',
+            'credentials' => '{urn:horde}hash',
+            'params'      => '{urn:horde}hash'),
+            'checkperms' => false,
+            'type' => 'boolean'
+        ),
+
+        'userExists' => array(
+            'args' => array('userId' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'addUser' => array(
+            'args' => array('userId' => 'string')
+        ),
+
+        'getImageUrl' => array(
+            'args' => array(
+                'user' => 'string',
+                'view' => 'string',
+                'full' => 'boolean'
+            ),
+            'type' => 'string'
+        ),
+
+        'userList' => array(
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'removeUser' => array(
+            'args' => array('userId' => 'string')
+        )
+    );
+
+    public function __construct()
+    {
+        if (!Horde_Auth::isAdmin()) {
+            unset($this->services['userList'], $this->services['removeUser']);
+        }
+    }
+
+    /**
+     * Returns profile image URL.
+     *
+     * @param string  $user      User uid
+     * @param string $view       The view ('small', 'big') to show.
+     * @param boolean $full      Return a path that includes the server name?
+     *
+     * @return string  The image path.
+     */
+    public function getImageUrl($user, $view = 'small', $full = false)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        return Folks::getImageUrl($user, $view, $full);
+    }
+
+    /**
+     * Callback for comment API.
+     *
+     * @param int $id       Internal data identifier.
+     * @param string $type  Type of data to retreive (title, owner...).
+     * @param array $params  Parameter to be passed to callback function
+     */
+    public function commentCallback($id, $type = 'title', $params = null)
+    {
+        static $info;
+
+        if (!empty($info[$id][$type])) {
+            return $info[$id][$type];
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+
+        switch ($type) {
+
+        case 'owner':
+            return $id;
+
+        case 'link':
+            return Folks::getUrlFor('user', $id);
+
+        case 'messages':
+
+            // Update comments count
+            $result = $GLOBALS['folks_driver']->updateComments($id);
+            if ($result instanceof PEAR_Error) {
+                return $result;
+            }
+
+            // Update activity log
+            $link = '<a href="' . Folks::getUrlFor('user', $id) . '">' . $id . '</a>';
+            return $GLOBALS['folks_driver']->logActivity(sprintf(_("Commented user %s."), $link), 'folks:comments');
+
+            return true;
+
+        default:
+            return $id;
+        }
+    }
+
+    /**
+     * Comments are enebled
+     */
+    public function hasComments()
+    {
+        return $GLOBALS['conf']['comments']['allow'];
+    }
+
+    /**
+     * Get online users
+     */
+    public function getOnlineUsers()
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['folks_driver']->getOnlineUsers();
+    }
+
+    /**
+     * Get user profile
+     *
+     * @param string $user User to get profile for
+     */
+    public function getProfile($user = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['folks_driver']->getProfile($user);
+    }
+
+
+    /**
+     * Get user friends
+     *
+     * @param string $user  Username to get friends for
+     *
+     * @return array of users
+     */
+    public function getFriends($user = null)
+    {
+        require_once dirname(__FILE__) . '/Friends.php';
+
+        $friends = Folks_Friends::singleton('sql', array('user' => $user));
+
+        return $friends->getFriends();
+    }
+
+    /**
+     * Add user to our friend list
+     *
+     * @param string $friend   Friend's usersame
+     *
+     * @return true or PEAR_Error
+     */
+    public function addFriend($user = null)
+    {
+        require_once dirname(__FILE__) . '/Friends.php';
+
+        $friends = Folks_Friends::singleton('sql', array('user' => $user));
+
+        return $friends->addFriend($user);
+    }
+
+    /**
+     * Remove user from a fiend list
+     *
+     * @param string $friend   Friend's usersame
+     *
+     * @return true or PEAR_Error
+     */
+    public function removeFriend($user = null)
+    {
+        require_once dirname(__FILE__) . '/Friends.php';
+
+        $friends = Folks_Friends::singleton('sql', array('user' => $user));
+
+        return $friends->removeFriend($user);
+    }
+
+    /**
+     * Get user blacklist
+     *
+     * @param string $user  Username to get blacklist for
+     *
+     * @return array of users
+     */
+    public function _getBlacklist($user = null)
+    {
+        require_once dirname(__FILE__) . '/Friends.php';
+
+        $friends = Folks_Friends::singleton('sql', array('user' => $user));
+
+        return $friends->getBlacklist();
+    }
+
+    /**
+     * Add user to a blacklist list
+     *
+     * @param string $user   Usersame
+     */
+    public function addBlacklisted($user = null)
+    {
+        require_once dirname(__FILE__) . '/Friends.php';
+
+        $friends = Folks_Friends::singleton('sql', array('user' => $user));
+
+        return $friends->addBlacklisted($user);
+    }
+
+    /**
+     * Remove user from a blacklist list
+     *
+     * @param string $user   Usersame
+     */
+    public function removeBlacklisted($user = null)
+    {
+        require_once dirname(__FILE__) . '/Friends.php';
+
+        $friends = Folks_Friends::singleton('sql', array('user' => $user));
+
+        return $friends->removeBlacklisted($user);
+    }
+
+    /**
+     * Are we blackisted by user this user?
+     *
+     * @param string $user  Username to get blacklist for
+     *
+     * @return array of users
+     */
+    public function isBlacklisted($user = null)
+    {
+        require_once dirname(__FILE__) . '/Friends.php';
+
+        $friends = Folks_Friends::singleton('sql', array('user' => $user));
+
+        return $friends->isBlacklisted(Horde_Auth::getAuth());
+    }
+
+    /**
+     * Users categories
+     */
+    public function listTimeObjectCategories()
+    {
+        return array('birthday_friends' => _("Friends Birthdays"));
+    }
+
+    /**
+     * Lists users with birthdays/goout dates as time objects.
+     *
+     * @param array $categories  The time categories (from listTimeObjectCategories) to list.
+     * @param Horde_Date $start       The start date of the period.
+     * @param Horde_Date $end         The end date of the period.
+     */
+    public function listTimeObjects($categories, $start, $end)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        require_once FOLKS_BASE . '/lib/Friends.php';
+
+        $friends_driver = Folks_Friends::singleton('sql');
+        $friends = $friends_driver->getFriends();
+        if ($friends instanceof PEAR_Error) {
+            return array();
+        }
+
+        $objects = array();
+
+        foreach ($friends as $friend) {
+            $user = $GLOBALS['folks_driver']->getProfile($friend);
+            if ($user instanceof PEAR_Error) {
+                continue;
+            }
+
+            $user['user_birthday'] = date('Y') . substr($user['user_birthday'], 4);
+            $born = strtotime($user['user_birthday']);
+            if ($born === false ||
+                $born < $start->timestamp() ||
+                $born > $end->timestamp()) {
+                    continue;
+                }
+
+            $age = Folks::calcAge($user['user_birthday']);
+            $desc = $age['age'] . ' (' . $age['sign'] . ')';
+
+            $objects[$friend] = array(
+                'title' => $friend,
+                'description' => $desc,
+                'id' => $friend,
+                'start' => date('Y-m-d\TH:i:s', $born),
+                'end' => date('Y-m-d\TH:i:s', $born + 1),
+                'params' => array('user' => $friend),
+                'link' => Folks::getUrlFor('user', $friend, true));
+        }
+
+        return $objects;
+    }
+
+    /**
+     * Log user's activity
+     *
+     * @param mixed $message    Activity message or details
+     * @param string $scope    Scope
+     * @param string $user    $user
+     *
+     * @return boolean  True on success or a PEAR_Error object on failure.
+     */
+    public function logActivity($message, $scope = 'folks', $user = null)
+    {
+        if (empty($user)) {
+            $user = Horde_Auth::getAuth();
+        } elseif ($user !== Horde_Auth::getAuth() && !Horde_Auth::isAdmin('admin:' . $scope)) {
+            return PEAR::raiseError(_("You cannot log activities for other users."));
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['folks_driver']->logActivity($message, $scope, $user);
+    }
+
+    /**
+     * Get user's activity
+     *
+     * @param string $user    Username
+     * @param int $limit    Number of actions to return
+     *
+     * @return array    Activity log
+     */
+    public function getActivity($user, $limit = 10)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['folks_driver']->getActivity($user, $limit);
+    }
+
+    /**
+     * Set user status
+     *
+     * @param booelan $online True to set user online, false to push it offline
+     * @param string $user    Username
+     *
+     * @return boolean True if succes, PEAR_Error on failure
+     */
+    public function setStatus($online = true, $user = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if ($user == null) {
+            $user = Horde_Auth::getAuth();
+        }
+
+        if ($online) {
+            return $GLOBALS['folks_driver']->resetOnlineUsers();
+        } else {
+            $result = $GLOBALS['folks_driver']->deleteOnlineUser($user);
+            $GLOBALS['cache']->expire('folksOnlineUsers');
+            return $result;
+        }
+    }
+
+    /**
+     * Get user status
+     *
+     * @param string $user    Username
+     *
+     * @return boolean True if user is online, false otherwise
+     */
+    public function getStatus($user = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if ($user == null) {
+            $user = Horde_Auth::getAuth();
+        }
+
+        return $GLOBALS['folks_driver']->isOnline($user);
+    }
+
+    /**
+     * Authenticate a givern user
+     *
+     * @param string $userID       Username
+     * @param array  $credentials  Array of criedentials (password requied)
+     * @param array  $params       Additional params
+     *
+     * @return boolean  Whether IMP authentication was successful.
+     */
+    public function authenticate($userID, $credentials, $params)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['folks_driver']->comparePassword($userID, $credentials['password']);
+    }
+
+    /**
+     * Check if a user exists
+     *
+     * @param string $userID       Username
+     *
+     * @return boolean  True if user exists
+     */
+    public function userExists($userId)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['folks_driver']->userExists($userId);
+    }
+
+    /**
+     * Lists all users in the system.
+     *
+     * @return array  The array of userIds, or a PEAR_Error object on failure.
+     */
+    public function userList()
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $users = array();
+        foreach ($GLOBALS['folks_driver']->getUsers() as $user) {
+            $users[] = $user['user_uid'];
+        }
+
+        return $users;
+    }
+
+    /**
+     * Adds a set of authentication credentials.
+     *
+     * @param string $userId  The userId to add.
+     *
+     * @return boolean  True on success or a PEAR_Error object on failure.
+     */
+    public function addUser($userId)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['folks_driver']->addUser($userId);
+    }
+
+    /**
+     * Deletes a set of authentication credentials.
+     *
+     * @param string $userId  The userId to delete.
+     *
+     * @return boolean  True on success or a PEAR_Error object on failure.
+     */
+    public function removeUser($userId)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['folks_driver']->deleteUser($userId);
+    }
+
+    /**
+     * Deletes a user and its data
+     *
+     * @param string $userId  The userId to delete.
+     *
+     * @return boolean  True on success or a PEAR_Error object on failure.
+     */
+    public function removeUserData($user = null)
+    {
+        return $this->removeUser($user);
+    }
+
+}
diff --git a/folks/lib/api.php b/folks/lib/api.php
deleted file mode 100644 (file)
index 77d38a2..0000000
+++ /dev/null
@@ -1,563 +0,0 @@
-<?php
-/**
- * Folks api
- *
- * Copyright 2008 Obala d.o.o (www.obala.si)
- *
- * See the enclosed file LICENSE for license information (BSD). If you
- * did not receive this file, see http://cvs.horde.org/co.php/folks/LICENSE.
- *
- * $Id: api.php 1235 2009-01-28 19:25:04Z duck $
- *
- * @author Duck <duck@obala.net>
- * @package Folks
- */
-
-$_services['commentCallback'] = array(
-    'args' => array('id' => 'string'),
-    'type' => 'string'
-);
-
-$_services['removeUserData'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['hasComments'] = array(
-    'args' => array(),
-    'type' => 'boolean'
-);
-
-$_services['getOnlineUsers'] = array(
-    'args' => array(),
-    'type' => 'array'
-);
-
-$_services['getProfile'] = array(
-    'args' => array(),
-    'type' => 'array'
-);
-
-$_services['getFriends'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'array'
-);
-
-$_services['addFriend'] = array(
-    'args' => array('friend' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['getBlacklist'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'array'
-);
-
-$_services['addBlacklisted'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['removeBlacklisted'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['isBlacklisted'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['show'] = array(
-    'link' => '%application%/user.php?user=|user|'
-);
-
-$_services['listTimeObjectCategories'] = array(
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['listTimeObjects'] = array(
-    'args' => array('start' => 'int', 'end' => 'int'),
-    'type' => '{urn:horde}hashHash'
-);
-
-$_services['logActivity'] = array(
-    'args' => array('activity_message' => 'string',
-                    'scope' => 'string',
-                    'user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['getActivity'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['setStatus'] = array(
-    'args' => array('status' => 'boolean'),
-    'args' => array('user' => 'string'),
-    'type' => 'array'
-);
-
-$_services['getStatus'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'array'
-);
-
-$_services['authenticate'] = array(
-    'args' => array('userID'      => 'string',
-                    'credentials' => '{urn:horde}hash',
-                    'params'      => '{urn:horde}hash'),
-    'checkperms' => false,
-    'type' => 'boolean'
-);
-
-$_services['userExists'] = array(
-    'args' => array('userId' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['addUser'] = array(
-    'args' => array('userId' => 'string')
-);
-
-$_services['getImageUrl'] = array(
-    'args' => array('user'       => 'string',
-                    'view'       => 'string',
-                    'full'       => 'boolean'),
-    'type' => 'string'
-);
-
-if (Horde_Auth::isAdmin()) {
-    $_services['userList'] = array(
-        'type' => '{urn:horde}stringArray'
-    );
-
-    $_services['removeUser'] = array(
-        'args' => array('userId' => 'string')
-    );
-}
-
-/**
- * Returns profile image URL.
- *
- * @param string  $user      User uid
- * @param string $view       The view ('small', 'big') to show.
- * @param boolean $full      Return a path that includes the server name?
- *
- * @return string  The image path.
- */
-function _folks_getImageUrl($user, $view = 'small', $full = false)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return Folks::getImageUrl($user, $view, $full);
-}
-
-/**
- * Callback for comment API.
- *
- * @param int $id       Internal data identifier.
- * @param string $type  Type of data to retreive (title, owner...).
- * @param array $params  Parameter to be passed to callback function
- */
-function _folks_commentCallback($id, $type = 'title', $params = null)
-{
-    static $info;
-
-    if (!empty($info[$id][$type])) {
-        return $info[$id][$type];
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-
-    switch ($type) {
-
-    case 'owner':
-        return $id;
-
-    case 'link':
-        return Folks::getUrlFor('user', $id);
-
-    case 'messages':
-
-        // Update comments count
-        $result = $GLOBALS['folks_driver']->updateComments($id);
-        if ($result instanceof PEAR_Error) {
-            return $result;
-        }
-
-        // Update activity log
-        $link = '<a href="' . Folks::getUrlFor('user', $id) . '">' . $id . '</a>';
-        return $GLOBALS['folks_driver']->logActivity(sprintf(_("Commented user %s."), $link), 'folks:comments');
-
-        return true;
-
-    default:
-        return $id;
-    }
-}
-
-/**
- * Comments are enebled
- */
-function _folks_hasComments()
-{
-    return $GLOBALS['conf']['comments']['allow'];
-}
-
-/**
- * Get online users
- */
-function _folks_getOnlineUsers()
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['folks_driver']->getOnlineUsers();
-}
-
-/**
- * Get user profile
- *
- * @param string $user User to get profile for
- */
-function _folks_getProfile($user = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['folks_driver']->getProfile($user);
-}
-
-
-/**
- * Get user friends
- *
- * @param string $user  Username to get friends for
- *
- * @return array of users
- */
-function _folks_getFriends($user = null)
-{
-    require_once dirname(__FILE__) . '/Friends.php';
-
-    $friends = Folks_Friends::singleton('sql', array('user' => $user));
-
-    return $friends->getFriends();
-}
-
-/**
- * Add user to our friend list
- *
- * @param string $friend   Friend's usersame
- *
- * @return true or PEAR_Error
- */
-function _folks_addFriend($user = null)
-{
-    require_once dirname(__FILE__) . '/Friends.php';
-
-    $friends = Folks_Friends::singleton('sql', array('user' => $user));
-
-    return $friends->addFriend($user);
-}
-
-/**
- * Remove user from a fiend list
- *
- * @param string $friend   Friend's usersame
- *
- * @return true or PEAR_Error
- */
-function _folks_removeFriend($user = null)
-{
-    require_once dirname(__FILE__) . '/Friends.php';
-
-    $friends = Folks_Friends::singleton('sql', array('user' => $user));
-
-    return $friends->removeFriend($user);
-}
-
-/**
- * Get user blacklist
- *
- * @param string $user  Username to get blacklist for
- *
- * @return array of users
- */
-function _folks_getBlacklist($user = null)
-{
-    require_once dirname(__FILE__) . '/Friends.php';
-
-    $friends = Folks_Friends::singleton('sql', array('user' => $user));
-
-    return $friends->getBlacklist();
-}
-
-/**
- * Add user to a blacklist list
- *
- * @param string $user   Usersame
- */
-function _folks_addBlacklisted($user = null)
-{
-    require_once dirname(__FILE__) . '/Friends.php';
-
-    $friends = Folks_Friends::singleton('sql', array('user' => $user));
-
-    return $friends->addBlacklisted($user);
-}
-
-/**
- * Remove user from a blacklist list
- *
- * @param string $user   Usersame
- */
-function _folks_removeBlacklisted($user = null)
-{
-    require_once dirname(__FILE__) . '/Friends.php';
-
-    $friends = Folks_Friends::singleton('sql', array('user' => $user));
-
-    return $friends->removeBlacklisted($user);
-}
-
-/**
- * Are we blackisted by user this user?
- *
- * @param string $user  Username to get blacklist for
- *
- * @return array of users
- */
-function _folks_isBlacklisted($user = null)
-{
-    require_once dirname(__FILE__) . '/Friends.php';
-
-    $friends = Folks_Friends::singleton('sql', array('user' => $user));
-
-    return $friends->isBlacklisted(Horde_Auth::getAuth());
-}
-
-/**
- * Users categories
- */
-function _folks_listTimeObjectCategories()
-{
-    return array('birthday_friends' => _("Friends Birthdays"));
-}
-
-/**
- * Lists users with birthdays/goout dates as time objects.
- *
- * @param array $categories  The time categories (from listTimeObjectCategories) to list.
- * @param Horde_Date $start       The start date of the period.
- * @param Horde_Date $end         The end date of the period.
- */
-function _folks_listTimeObjects($categories, $start, $end)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    require_once FOLKS_BASE . '/lib/Friends.php';
-
-    $friends_driver = Folks_Friends::singleton('sql');
-    $friends = $friends_driver->getFriends();
-    if ($friends instanceof PEAR_Error) {
-        return array();
-    }
-
-    $objects = array();
-
-    foreach ($friends as $friend) {
-        $user = $GLOBALS['folks_driver']->getProfile($friend);
-        if ($user instanceof PEAR_Error) {
-            continue;
-        }
-
-        $user['user_birthday'] = date('Y') . substr($user['user_birthday'], 4);
-        $born = strtotime($user['user_birthday']);
-        if ($born === false ||
-            $born < $start->timestamp() ||
-            $born > $end->timestamp()) {
-            continue;
-        }
-
-        $age = Folks::calcAge($user['user_birthday']);
-        $desc = $age['age'] . ' (' . $age['sign'] . ')';
-
-        $objects[$friend] = array(
-            'title' => $friend,
-            'description' => $desc,
-            'id' => $friend,
-            'start' => date('Y-m-d\TH:i:s', $born),
-            'end' => date('Y-m-d\TH:i:s', $born + 1),
-            'params' => array('user' => $friend),
-            'link' => Folks::getUrlFor('user', $friend, true));
-    }
-
-    return $objects;
-}
-
-/**
- * Log user's activity
- *
- * @param mixed $message    Activity message or details
- * @param string $scope    Scope
- * @param string $user    $user
- *
- * @return boolean  True on success or a PEAR_Error object on failure.
- */
-function _folks_logActivity($message, $scope = 'folks', $user = null)
-{
-    if (empty($user)) {
-        $user = Horde_Auth::getAuth();
-    } elseif ($user !== Horde_Auth::getAuth() && !Horde_Auth::isAdmin('admin:' . $scope)) {
-        return PEAR::raiseError(_("You cannot log activities for other users."));
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['folks_driver']->logActivity($message, $scope, $user);
-}
-
-/**
- * Get user's activity
- *
- * @param string $user    Username
- * @param int $limit    Number of actions to return
- *
- * @return array    Activity log
- */
-function _folks_getActivity($user, $limit = 10)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['folks_driver']->getActivity($user, $limit);
-}
-
-/**
- * Set user status
- *
- * @param booelan $online True to set user online, false to push it offline
- * @param string $user    Username
- *
- * @return boolean True if succes, PEAR_Error on failure
- */
-function _folks_setStatus($online = true, $user = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if ($user == null) {
-        $user = Horde_Auth::getAuth();
-    }
-
-    if ($online) {
-        return $GLOBALS['folks_driver']->resetOnlineUsers();
-    } else {
-        $result = $GLOBALS['folks_driver']->deleteOnlineUser($user);
-        $GLOBALS['cache']->expire('folksOnlineUsers');
-        return $result;
-    }
-}
-
-/**
- * Get user status
- *
- * @param string $user    Username
- *
- * @return boolean True if user is online, false otherwise
- */
-function _folks_getStatus($user = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if ($user == null) {
-        $user = Horde_Auth::getAuth();
-    }
-
-    return $GLOBALS['folks_driver']->isOnline($user);
-}
-
-/**
- * Authenticate a givern user
- *
- * @param string $userID       Username
- * @param array  $credentials  Array of criedentials (password requied)
- * @param array  $params       Additional params
- *
- * @return boolean  Whether IMP authentication was successful.
- */
-function _folks_authenticate($userID, $credentials, $params)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['folks_driver']->comparePassword($userID, $credentials['password']);
-}
-
-/**
- * Check if a user exists
- *
- * @param string $userID       Username
- *
- * @return boolean  True if user exists
- */
-function _folks_userExists($userId)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['folks_driver']->userExists($userId);
-}
-
-/**
- * Lists all users in the system.
- *
- * @return array  The array of userIds, or a PEAR_Error object on failure.
- */
-function _folks_userList()
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $users = array();
-    foreach ($GLOBALS['folks_driver']->getUsers() as $user) {
-        $users[] = $user['user_uid'];
-    }
-
-    return $users;
-}
-
-/**
- * Adds a set of authentication credentials.
- *
- * @param string $userId  The userId to add.
- *
- * @return boolean  True on success or a PEAR_Error object on failure.
- */
-function _folks_addUser($userId)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['folks_driver']->addUser($userId);
-}
-
-/**
- * Deletes a set of authentication credentials.
- *
- * @param string $userId  The userId to delete.
- *
- * @return boolean  True on success or a PEAR_Error object on failure.
- */
-function _folks_removeUser($userId)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['folks_driver']->deleteUser($userId);
-}
-
-/**
- * Deletes a user and its data
- *
- * @param string $userId  The userId to delete.
- *
- * @return boolean  True on success or a PEAR_Error object on failure.
- */
-function _folks_removeUserData($user = null)
-{
-    return _folks_removeUser($user);
-}
diff --git a/folks/lib/version.php b/folks/lib/version.php
deleted file mode 100644 (file)
index a1f769b..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('FOLKS_VERSION', 'H4 (0.1-git)') ?>
\ No newline at end of file
diff --git a/gollem/lib/Api.php b/gollem/lib/Api.php
new file mode 100644 (file)
index 0000000..6c2fd25
--- /dev/null
@@ -0,0 +1,651 @@
+<?php
+/**
+ * Gollem external API interface.
+ *
+ * This file defines Gollem's external API interface. Other
+ * applications can interact with Gollem through this API.
+ *
+ * @author  Amith Varghese (amith@xalan.com)
+ * @author  Michael Slusarz (slusarz@curecanti.org)
+ * @author  Ben Klang (bklang@alkaloid.net)
+ * @package Gollem
+ */
+class Gollem_Api extends Horde_Registry_Api
+{
+    /**
+     * The application's version.
+     *
+     * @var string
+     */
+    public $version = 'H4 (5.0-git)';
+
+    /**
+     * The services provided by this application.
+     *
+     * @var array
+     */
+    public $services = array(
+        'perms' => array(
+            'args' => array(),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'browse' => array(
+            'args' => array('path' => 'string'),
+            'type' => '{urn:horde}hashHash',
+        ),
+
+        'put' => array(
+            'args' => array(
+                'path' => 'string',
+                'content' => 'string',
+                'content_type' => 'string'
+            ),
+            'type' => 'int',
+        ),
+
+        'mkcol' => array(
+            'args' => array('path' => 'string'),
+            'type' => 'int',
+        ),
+
+        'move' => array(
+            'args' => array(
+                'path' => 'string',
+                'dest' => 'string'
+            ),
+            'type' => 'int',
+        ),
+
+        'path_delete' => array(
+            'args' => array('path' => 'string'),
+            'type' => 'int',
+        ),
+
+        'selectlistLink' => array(
+            'args' => array(
+                'link_text' => 'string',
+                'link_style' => 'string',
+                'formid' => 'string',
+                'icon' => 'boolean',
+                'selectid' => 'string'
+            ),
+            'type' => 'string'
+        ),
+
+        'selectlistResults' => array(
+            'args' => array('selectid' => 'string'),
+            'type' => 'array'
+        ),
+
+        'returnFromSelectlist' => array(
+            'args' => array(
+                'selectid' => 'string',
+                'index' => 'string'
+            ),
+            'type' => 'string'
+        ),
+
+        'setSelectList' => array(
+            'args' => array(
+                'selectid' => 'string',
+                'files' => 'array'
+            ),
+            'type' => 'string'
+        ),
+
+        'getViewLink' => array(
+            'args' => array(
+                'dir' => 'string',
+                'file' => 'string',
+                'backend' => 'string'
+            ),
+            'type' => 'string'
+        )
+    );
+
+    /**
+     * Browses through the VFS tree.
+     *
+     * Each VFS backend is listed as a directory at the top level.  No modify
+     * operations are allowed outside any VFS area.
+     *
+     * @param string $path       The level of the tree to browse.
+     * @param array $properties  The item properties to return. Defaults to 'name',
+     *                           'icon', and 'browseable'.
+     *
+     * @return array  The contents of $path.
+     */
+    public function browse($path = '', $properties = array())
+    {
+        $GLOBALS['gollem_authentication'] = 'none';
+        require_once dirname(__FILE__) . '/base.php';
+        require GOLLEM_BASE . '/config/backends.php';
+        require GOLLEM_BASE . '/config/credentials.php';
+
+        $path = Gollem::stripAPIPath($path);
+
+        // Default properties.
+        if (!$properties) {
+            $properties = array('name', 'icon', 'browseable');
+        }
+
+        $results = array();
+        if ($path == '') {
+            // We are at the root of gollem.  Return a set of folders, one for
+            // each backend available.
+            foreach ($backends as $backend => $curBackend) {
+                if (Gollem::checkPermissions('backend', PERMS_SHOW, $backend)) {
+                    $results['gollem/' . $backend]['name'] = $curBackend['name'];
+                    $results['gollem/' . $backend]['browseable'] = true;
+                }
+            }
+        } else {
+            // A file or directory has been requested.
+
+            // Locate the backend_key in the path.
+            if (strchr($path, '/')) {
+                $backend_key = substr($path, 0, strpos($path, '/'));
+            } else {
+                $backend_key = $path;
+            }
+
+            // Validate and perform permissions checks on the requested backend
+            if (!isset($backends[$backend_key])) {
+                return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+            }
+            //if (!Gollem::canAutoLogin($backend_key)) {
+            //    // FIXME: Is it possible to request secondary authentication
+            //    // credentials here for backends that require it?
+            //    return PEAR::raiseError(_("Additional authentication required."));
+            //}
+            if (!Gollem_Session::createSession($backend_key)) {
+                return PEAR::raiseError(_("Unable to create Gollem session"));
+            }
+            if (!Gollem::checkPermissions('backend', PERMS_READ)) {
+                return PEAR::raiseError(_("Permission denied to this backend."));
+            }
+
+            // Trim off the backend_key (and '/') to get the VFS relative path
+            $fullpath = substr($path, strlen($backend_key) + 1);
+
+            // Get the VFS-standard $name,$path pair
+            list($name, $path) = Gollem::getVFSPath($fullpath);
+
+            // Check to see if the request is a file or folder
+            if ($GLOBALS['gollem_vfs']->isFolder($path, $name)) {
+                // This is a folder request.  Return a directory listing.
+                $list = Gollem::listFolder($path . '/' . $name);
+                if (is_a($list, 'PEAR_Error')) {
+                    return $list;
+                }
+
+                // Iterate over the directory contents
+                if (is_array($list) && count($list)) {
+                    $index = 'gollem/' . $backend_key . '/' . $fullpath;
+                    foreach ($list as $key => $val) {
+                        $entry = Gollem::pathEncode($index . '/' . $val['name']);
+                        $results[$entry]['name'] = $val['name'];
+                        $results[$entry]['modified'] = $val['date'];
+                        if ($val['type'] == '**dir') {
+                            $results[$entry]['browseable'] = true;
+                        } else {
+                            $results[$entry]['browseable'] = false;
+                            $results[$entry]['contentlength'] = $val['size'];
+                        }
+                    }
+                }
+            } else {
+                // A file has been requested.  Return the contents of the file.
+
+                // Get the file meta-data
+                $list = Gollem::listFolder($path);
+                $i = false;
+                foreach ($list as $key => $file) {
+                    if ($file['name'] == $name) {
+                        $i = $key;
+                        break;
+                    }
+                }
+                if ($i === false) {
+                    // File not found
+                    return $i;
+                }
+
+                // Read the file contents
+                $data = $GLOBALS['gollem_vfs']->read($path, $name);
+                if (is_a($data, 'PEAR_Error')) {
+                    return false;
+                }
+
+                // Send the file
+                $results['name'] = $name;
+                $results['data'] = $data;
+                $results['contentlength'] = $list[$i]['size'];
+                $results['mtime'] = $list[$i]['date'];
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Accepts a file for storage into the VFS
+     *
+     * @param string $path           Path to store file
+     * @param string $content        Contents of file
+     * @param string $content_type   MIME type of file
+     *
+     * @return mixed                 True on success; PEAR_Error on failure
+     */
+    public function put($path, $content, $content_type)
+    {
+        // Gollem does not handle authentication
+        $GLOBALS['gollem_authentication'] = 'none';
+
+        // Include Gollem base libraries
+        require_once dirname(__FILE__) . '/base.php';
+        require GOLLEM_BASE . '/config/backends.php';
+        require GOLLEM_BASE . '/config/credentials.php';
+
+        // Clean off the irrelevant portions of the path
+        $path = Gollem::stripAPIPath($path);
+
+        if ($path == '') {
+            // We are at the root of gollem.  Any writes at this level are
+            // disallowed.
+            return PEAR::raiseError(_("Files must be written inside a VFS backend."));
+        } else {
+            // We must be inside one of the VFS areas.  Determine which one.
+            // Locate the backend_key in the path
+            if (strchr($path, '/')) {
+                $backend_key = substr($path, 0, strpos($path, '/'));
+            } else {
+                $backend_key = $path;
+            }
+
+            // Validate and perform permissions checks on the requested backend
+            if (!isset($backends[$backend_key])) {
+                return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+            }
+            //if (!Gollem::canAutoLogin($backend_key)) {
+            //    // FIXME: Is it possible to request secondary authentication
+            //    // credentials here for backends that require it?
+            //    return PEAR::raiseError(_("Additional authentication required."));
+            //}
+            if (!Gollem_Session::createSession($backend_key)) {
+                return PEAR::raiseError(_("Unable to create Gollem session"));
+            }
+            if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
+                return PEAR::raiseError(_("Permission denied to this backend."));
+            }
+
+            // Trim off the backend_key (and '/') to get the VFS relative path
+            $fullpath = substr($path, strlen($backend_key) + 1);
+
+            // Get the VFS-standard $name,$path pair
+            list($name, $path) = Gollem::getVFSPath($fullpath);
+
+            return $GLOBALS['gollem_vfs']->writeData($path, $name, $content);
+        }
+    }
+
+    /**
+     * Creates a directory ("collection" in WebDAV-speak) within the VFS
+     *
+     * @param string $path  Path of directory to create
+     *
+     * @return mixed  True on success; PEAR_Error on failure
+     */
+    public function mkcol($path)
+    {
+        // Gollem does not handle authentication
+        $GLOBALS['gollem_authentication'] = 'none';
+
+        // Include Gollem base libraries
+        require_once dirname(__FILE__) . '/base.php';
+        require GOLLEM_BASE . '/config/backends.php';
+        require GOLLEM_BASE . '/config/credentials.php';
+
+        // Clean off the irrelevant portions of the path
+        $path = Gollem::stripAPIPath($path);
+
+        if ($path == '') {
+            // We are at the root of gollem.  Any writes at this level are
+            // disallowed.
+            return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
+        } else {
+            // We must be inside one of the VFS areas.  Determine which one.
+            // Locate the backend_key in the path
+            if (!strchr($path, '/')) {
+                // Disallow attempts to create a share-level directory.
+                return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
+            } else {
+                $backend_key = substr($path, 0, strpos($path, '/'));
+            }
+
+            // Validate and perform permissions checks on the requested backend
+            if (!isset($backends[$backend_key])) {
+                return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+            }
+            //if (!Gollem::canAutoLogin($backend_key)) {
+            //    // FIXME: Is it possible to request secondary authentication
+            //    // credentials here for backends that require it?
+            //    return PEAR::raiseError(_("Additional authentication required."));
+            //}
+            if (!Gollem_Session::createSession($backend_key)) {
+                return PEAR::raiseError(_("Unable to create Gollem session"));
+            }
+            if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
+                return PEAR::raiseError(_("Permission denied to this backend."));
+            }
+
+            // Trim off the backend_key (and '/') to get the VFS relative path
+            $fullpath = substr($path, strlen($backend_key) + 1);
+
+            // Get the VFS-standard $name,$path pair
+            list($name, $path) = Gollem::getVFSPath($fullpath);
+
+            return $GLOBALS['gollem_vfs']->createFolder($path, $name);
+        }
+    }
+
+    /**
+     * Renames a file or directory
+     *
+     * @param string $path  Path to source object to be renamed
+     * @param string $dest  Path to new name
+     *
+     * @return mixed  True on success; PEAR_Error on failure
+     */
+    public function move($path, $dest)
+    {
+        // Gollem does not handle authentication
+        $GLOBALS['gollem_authentication'] = 'none';
+
+        // Include Gollem base libraries
+        require_once dirname(__FILE__) . '/base.php';
+        require GOLLEM_BASE . '/config/backends.php';
+        require GOLLEM_BASE . '/config/credentials.php';
+
+        // Clean off the irrelevant portions of the path
+        $path = Gollem::stripAPIPath($path);
+        $dest = Gollem::stripAPIPath($dest);
+
+        if ($path == '') {
+            // We are at the root of gollem.  Any writes at this level are
+            // disallowed.
+            return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
+        } else {
+            // We must be inside one of the VFS areas.  Determine which one.
+            // Locate the backend_key in the path
+            if (!strchr($path, '/')) {
+                // Disallow attempts to rename a share-level directory.
+                return PEAR::raiseError(_('Renaming of backends is not allowed.'));
+            } else {
+                $backend_key = substr($path, 0, strpos($path, '/'));
+            }
+
+            // Ensure that the destination is within the same backend
+            if (!strchr($dest, '/')) {
+                // Disallow attempts to rename a share-level directory.
+                return PEAR::raiseError(_('Renaming of backends is not allowed.'));
+            } else {
+                $dest_backend_key = substr($path, 0, strpos($path, '/'));
+                if ($dest_backend_key != $backend_key) {
+                    return PEAR::raiseError(_('Renaming across backends is not supported.'));
+                }
+            }
+
+            // Validate and perform permissions checks on the requested backend
+            if (!isset($backends[$backend_key])) {
+                return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+            }
+            //if (!Gollem::canAutoLogin($backend_key)) {
+            //    // FIXME: Is it possible to request secondary authentication
+            //    // credentials here for backends that require it?
+            //    return PEAR::raiseError(_("Additional authentication required."));
+            //}
+            if (!Gollem_Session::createSession($backend_key)) {
+                return PEAR::raiseError(_("Unable to create Gollem session"));
+            }
+            if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
+                return PEAR::raiseError(_("Permission denied to this backend."));
+            }
+
+            // Trim off the backend_key (and '/') to get the VFS relative path
+            $srcfullpath = substr($path, strlen($backend_key) + 1);
+            $dstfullpath = substr($dest, strlen($backend_key) + 1);
+
+            // Get the VFS-standard $name,$path pair
+            list($srcname, $srcpath) = Gollem::getVFSPath($srcfullpath);
+            list($dstname, $dstpath) = Gollem::getVFSPath($dstfullpath);
+
+            return $GLOBALS['gollem_vfs']->rename($srcpath, $srcname, $dstpath, $dstname);
+        }
+    }
+
+    /**
+     * Removes a file or folder from the VFS
+     *
+     * @param string $path  Path of file or folder to delete
+     *
+     * @return mixed  True on success; PEAR_Error on failure
+     */
+    public function path_delete($path)
+    {
+        // Gollem does not handle authentication
+        $GLOBALS['gollem_authentication'] = 'none';
+
+        // Include Gollem base libraries
+        require_once dirname(__FILE__) . '/base.php';
+        require GOLLEM_BASE . '/config/backends.php';
+        require GOLLEM_BASE . '/config/credentials.php';
+
+        // Clean off the irrelevant portions of the path
+        $path = Gollem::stripAPIPath($path);
+
+        if ($path == '') {
+            // We are at the root of gollem.  Any writes at this level are
+            // disallowed.
+            return PEAR::raiseError(_("The application folder can not be deleted."));
+        } else {
+            // We must be inside one of the VFS areas.  Determine which one.
+            // Locate the backend_key in the path
+            if (strchr($path, '/')) {
+                $backend_key = substr($path, 0, strpos($path, '/'));
+            } else {
+                $backend_key = $path;
+            }
+
+            // Validate and perform permissions checks on the requested backend
+            if (!isset($backends[$backend_key])) {
+                return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
+            }
+            //if (!Gollem::canAutoLogin($backend_key)) {
+            //    // FIXME: Is it possible to request secondary authentication
+            //    // credentials here for backends that require it?
+            //    return PEAR::raiseError(_("Additional authentication required."));
+            //}
+            if (!Gollem_Session::createSession($backend_key)) {
+                return PEAR::raiseError(_("Unable to create Gollem session"));
+            }
+            if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
+                return PEAR::raiseError(_("Permission denied to this backend."));
+            }
+
+            // Trim off the backend_key (and '/') to get the VFS relative path
+            $fullpath = substr($path, strlen($backend_key) + 1);
+
+            // Get the VFS-standard $name,$path pair
+            list($name, $path) = Gollem::getVFSPath($fullpath);
+
+            // Apparently Gollem::verifyDir() (called by deleteF* next) needs to
+            // see a path with a leading '/'
+            $path = $backends[$backend_key]['root'] . $path;
+            if ($GLOBALS['gollem_vfs']->isFolder($path, $name)) {
+                return Gollem::deleteFolder($path, $name);
+            } else {
+                return Gollem::deleteFile($path, $name);
+            }
+        }
+    }
+
+    public function perms()
+    {
+        static $perms = array();
+        if (!empty($perms)) {
+            return $perms;
+        }
+
+        require_once dirname(__FILE__) . '/base.load.php';
+        require GOLLEM_BASE . '/config/backends.php';
+
+        $perms['tree']['gollem']['backends'] = false;
+        $perms['title']['gollem:backends'] = _("Backends");
+
+        // Run through every backend.
+        foreach ($backends as $backend => $curBackend) {
+            $perms['tree']['gollem']['backends'][$backend] = false;
+            $perms['title']['gollem:backends:' . $backend] = $curBackend['name'];
+        }
+
+        return $perms;
+    }
+
+    /**
+     * Returns a link to the gollem file preview interface
+     *
+     * @param string $dir       File absolute path
+     * @param string $file      File basename
+     * @param string $backend   Backend key. Defaults to Gollem::getPreferredBackend()
+     *
+     * @return string  The URL string.
+     */
+    public function getViewLink($dir, $file, $backend = '')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (empty($backend)) {
+            $backend = Gollem::getPreferredBackend();
+        }
+
+        $url = Horde_Util::addParameter(
+            Horde::applicationUrl('view.php'),
+            array('actionID' => 'view_file',
+            'type' => substr($file, strrpos($file, '.') + 1),
+            'file' => $file,
+            'dir' => $dir,
+            'driver' => $_SESSION['gollem']['backends'][$backend]['driver']));
+
+        return $url;
+    }
+
+    /**
+     * Creates a link to the gollem file selection window.
+     *
+     * The file section window will return a cache ID value which should be used
+     * (along with the selectListResults and returnFromSelectList functions below)
+     * to obtain the data from a list of selected files.
+     *
+     * There MUST be a form field named 'selectlist_selectid' in the calling
+     * form. This field will be populated with the selection ID when the user
+     * completes file selection.
+     *
+     * There MUST be a form parameter named 'actionID' in the calling form.
+     * This form will be populated with the value 'selectlist_process' when
+     * the user completes file selection.  The calling form will be submitted
+     * after the window closes (i.e. the calling form must process the
+     * 'selectlist_process' actionID).
+     *
+     * @param string $link_text   The text to use in the link.
+     * @param string $link_style  The style to use for the link.
+     * @param string $formid      The formid of the calling script.
+     * @param boolean $icon       Create the link with an icon instead of text?
+     * @param string $selectid    Selection ID.
+     *
+     * @return string  The URL string.
+     */
+    public function selectlistLink($link_text, $link_style, $formid,
+        $icon = false, $selectid = '')
+    {
+        $link = Horde::link('#', $link_text, $link_style, '_blank', Horde::popupJs(Horde::applicationUrl('selectlist.php'), array('params' => array('formid' => $formid, 'cacheid' => $selectid), 'height' => 500, 'width' => 300, 'urlencode' => true)) . 'return false;');
+        if ($icon) {
+            $link_text = Horde::img('gollem.png', $link_text);
+        }
+        return '<script type="text/javascript">document.write(\''
+            . addslashes($link . $link_text) . '<\' + \'/a>\');</script>';
+    }
+
+    /**
+     * Returns the list of files selected by the user for a given selection ID.
+     *
+     * @param string $selectid  The selection ID.
+     *
+     * @param array  An array with each file entry stored in its own array, with
+     *               the key as the directory name and the value as the filename.
+     */
+    public function selectlistResults($selectid)
+    {
+        if (!isset($_SESSION['gollem']['selectlist'][$selectid]['files'])) {
+            return null;
+        } else {
+            $list = array();
+            foreach ($_SESSION['gollem']['selectlist'][$selectid]['files'] as $val) {
+                list($dir, $filename) = explode('|', $val);
+                $list[] = array($dir => $filename);
+            }
+            return $list;
+        }
+    }
+
+    /**
+     * Returns the data for a given selection ID and index.
+     *
+     * @param string $selectid  The selection ID.
+     * @param integer $index    The index of the file data to return.
+     *
+     * @return string  The file data.
+     */
+    public function returnFromSelectlist($selectid, $index)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!isset($_SESSION['gollem']['selectlist'][$selectid]['files'][$index])) {
+            return null;
+        }
+
+        list($dir, $filename) = explode('|', $_SESSION['gollem']['selectlist'][$selectid]['files'][$index]);
+        return $GLOBALS['gollem_vfs']->read($dir, $filename);
+    }
+
+    /**
+     * Sets the files selected for a given selection ID.
+     *
+     * @param string $selectid  The selection ID to use.
+     * @param array $files      An array with each file entry stored in its own
+     *                          array, with the key as the directory name and the
+     *                          value as the filename.
+     *
+     * @return string  The selection ID.
+     */
+    public function setSelectlist($selectid = '', $files = array())
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (empty($selectid)) {
+            $selectid = uniqid(mt_rand(), true);
+        }
+
+        if (count($files) > 0) {
+            $list = array();
+            foreach ($files as $file) {
+                $list[] = key($file) . '|' . current($file);
+            }
+            $_SESSION['gollem']['selectlist'][$selectid]['files'] = $list;
+        }
+
+        return $selectid;
+    }
+
+}
diff --git a/gollem/lib/api.php b/gollem/lib/api.php
deleted file mode 100644 (file)
index 286c58b..0000000
+++ /dev/null
@@ -1,605 +0,0 @@
-<?php
-/**
- * Gollem external API interface.
- *
- * This file defines Gollem's external API interface. Other
- * applications can interact with Gollem through this API.
- *
- * @author  Amith Varghese (amith@xalan.com)
- * @author  Michael Slusarz (slusarz@curecanti.org)
- * @author  Ben Klang (bklang@alkaloid.net)
- * @package Gollem
- */
-
-$_services['browse'] = array(
-    'args' => array('path' => 'string'),
-    'type' => '{urn:horde}hashHash',
-);
-
-$_services['put'] = array(
-    'args' => array('path' => 'string', 'content' => 'string', 'content_type' => 'string'),
-    'type' => 'int',
-);
-
-$_services['mkcol'] = array(
-    'args' => array('path' => 'string'),
-    'type' => 'int',
-);
-
-$_services['move'] = array(
-    'args' => array('path' => 'string', 'dest' => 'string'),
-    'type' => 'int',
-);
-
-$_services['path_delete'] = array(
-    'args' => array('path' => 'string'),
-    'type' => 'int',
-);
-
-$_services['perms'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray');
-
-$_services['selectlistLink'] = array(
-    'args' => array('link_text' => 'string', 'link_style' => 'string', 'formid' => 'string', 'icon' => 'boolean', 'selectid' => 'string'),
-    'type' => 'string');
-
-$_services['selectlistResults'] = array(
-    'args' => array('selectid' => 'string'),
-    'type' => 'array');
-
-$_services['returnFromSelectlist'] = array(
-    'args' => array('selectid' => 'string', 'index' => 'string'),
-    'type' => 'string');
-
-$_services['setSelectList'] = array(
-    'args' => array('selectid' => 'string', 'files' => 'array'),
-    'type' => 'string');
-
-$_services['getViewLink'] = array(
-    'args' => array('dir' => 'string', 'file' => 'string', 'backend' => 'string'),
-    'type' => 'string');
-
-/**
- * Browses through the VFS tree.
- *
- * Each VFS backend is listed as a directory at the top level.  No modify
- * operations are allowed outside any VFS area.
- *
- * @param string $path       The level of the tree to browse.
- * @param array $properties  The item properties to return. Defaults to 'name',
- *                           'icon', and 'browseable'.
- *
- * @return array  The contents of $path.
- */
-function _gollem_browse($path = '', $properties = array())
-{
-    $GLOBALS['gollem_authentication'] = 'none';
-    require_once dirname(__FILE__) . '/base.php';
-    require GOLLEM_BASE . '/config/backends.php';
-    require GOLLEM_BASE . '/config/credentials.php';
-
-    $path = Gollem::stripAPIPath($path);
-
-    // Default properties.
-    if (!$properties) {
-        $properties = array('name', 'icon', 'browseable');
-    }
-
-    $results = array();
-    if ($path == '') {
-        // We are at the root of gollem.  Return a set of folders, one for
-        // each backend available.
-        foreach ($backends as $backend => $curBackend) {
-            if (Gollem::checkPermissions('backend', PERMS_SHOW, $backend)) {
-                $results['gollem/' . $backend]['name'] = $curBackend['name'];
-                $results['gollem/' . $backend]['browseable'] = true;
-            }
-        }
-    } else {
-        // A file or directory has been requested.
-
-        // Locate the backend_key in the path.
-        if (strchr($path, '/')) {
-            $backend_key = substr($path, 0, strpos($path, '/'));
-        } else {
-            $backend_key = $path;
-        }
-
-        // Validate and perform permissions checks on the requested backend
-        if (!isset($backends[$backend_key])) {
-            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
-        }
-        //if (!Gollem::canAutoLogin($backend_key)) {
-        //    // FIXME: Is it possible to request secondary authentication
-        //    // credentials here for backends that require it?
-        //    return PEAR::raiseError(_("Additional authentication required."));
-        //}
-        if (!Gollem_Session::createSession($backend_key)) {
-            return PEAR::raiseError(_("Unable to create Gollem session"));
-        }
-        if (!Gollem::checkPermissions('backend', PERMS_READ)) {
-            return PEAR::raiseError(_("Permission denied to this backend."));
-        }
-
-        // Trim off the backend_key (and '/') to get the VFS relative path
-        $fullpath = substr($path, strlen($backend_key) + 1);
-
-        // Get the VFS-standard $name,$path pair
-        list($name, $path) = Gollem::getVFSPath($fullpath);
-
-        // Check to see if the request is a file or folder
-        if ($GLOBALS['gollem_vfs']->isFolder($path, $name)) {
-            // This is a folder request.  Return a directory listing.
-            $list = Gollem::listFolder($path . '/' . $name);
-            if (is_a($list, 'PEAR_Error')) {
-                return $list;
-            }
-
-            // Iterate over the directory contents
-            if (is_array($list) && count($list)) {
-                $index = 'gollem/' . $backend_key . '/' . $fullpath;
-                foreach ($list as $key => $val) {
-                    $entry = Gollem::pathEncode($index . '/' . $val['name']);
-                    $results[$entry]['name'] = $val['name'];
-                    $results[$entry]['modified'] = $val['date'];
-                    if ($val['type'] == '**dir') {
-                        $results[$entry]['browseable'] = true;
-                    } else {
-                        $results[$entry]['browseable'] = false;
-                        $results[$entry]['contentlength'] = $val['size'];
-                    }
-                }
-            }
-        } else {
-            // A file has been requested.  Return the contents of the file.
-
-            // Get the file meta-data
-            $list = Gollem::listFolder($path);
-            $i = false;
-            foreach ($list as $key => $file) {
-                if ($file['name'] == $name) {
-                    $i = $key;
-                    break;
-                }
-            }
-            if ($i === false) {
-                // File not found
-                return $i;
-            }
-
-            // Read the file contents
-            $data = $GLOBALS['gollem_vfs']->read($path, $name);
-            if (is_a($data, 'PEAR_Error')) {
-                return false;
-            }
-
-            // Send the file
-            $results['name'] = $name;
-            $results['data'] = $data;
-            $results['contentlength'] = $list[$i]['size'];
-            $results['mtime'] = $list[$i]['date'];
-        }
-    }
-
-    return $results;
-}
-
-/**
- * Accepts a file for storage into the VFS
- *
- * @param string $path           Path to store file
- * @param string $content        Contents of file
- * @param string $content_type   MIME type of file
- *
- * @return mixed                 True on success; PEAR_Error on failure
- */
-function _gollem_put($path, $content, $content_type)
-{
-    // Gollem does not handle authentication
-    $GLOBALS['gollem_authentication'] = 'none';
-
-    // Include Gollem base libraries
-    require_once dirname(__FILE__) . '/base.php';
-    require GOLLEM_BASE . '/config/backends.php';
-    require GOLLEM_BASE . '/config/credentials.php';
-
-    // Clean off the irrelevant portions of the path
-    $path = Gollem::stripAPIPath($path);
-
-    if ($path == '') {
-        // We are at the root of gollem.  Any writes at this level are
-        // disallowed.
-        return PEAR::raiseError(_("Files must be written inside a VFS backend."));
-    } else {
-        // We must be inside one of the VFS areas.  Determine which one.
-         // Locate the backend_key in the path
-        if (strchr($path, '/')) {
-            $backend_key = substr($path, 0, strpos($path, '/'));
-        } else {
-            $backend_key = $path;
-        }
-
-        // Validate and perform permissions checks on the requested backend
-        if (!isset($backends[$backend_key])) {
-            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
-        }
-        //if (!Gollem::canAutoLogin($backend_key)) {
-        //    // FIXME: Is it possible to request secondary authentication
-        //    // credentials here for backends that require it?
-        //    return PEAR::raiseError(_("Additional authentication required."));
-        //}
-        if (!Gollem_Session::createSession($backend_key)) {
-            return PEAR::raiseError(_("Unable to create Gollem session"));
-        }
-        if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
-            return PEAR::raiseError(_("Permission denied to this backend."));
-        }
-
-        // Trim off the backend_key (and '/') to get the VFS relative path
-        $fullpath = substr($path, strlen($backend_key) + 1);
-
-        // Get the VFS-standard $name,$path pair
-        list($name, $path) = Gollem::getVFSPath($fullpath);
-
-        return $GLOBALS['gollem_vfs']->writeData($path, $name, $content);
-    }
-}
-
-/**
- * Creates a directory ("collection" in WebDAV-speak) within the VFS
- *
- * @param string $path  Path of directory to create
- *
- * @return mixed  True on success; PEAR_Error on failure
- */
-function _gollem_mkcol($path)
-{
-    // Gollem does not handle authentication
-    $GLOBALS['gollem_authentication'] = 'none';
-
-    // Include Gollem base libraries
-    require_once dirname(__FILE__) . '/base.php';
-    require GOLLEM_BASE . '/config/backends.php';
-    require GOLLEM_BASE . '/config/credentials.php';
-
-    // Clean off the irrelevant portions of the path
-    $path = Gollem::stripAPIPath($path);
-
-    if ($path == '') {
-        // We are at the root of gollem.  Any writes at this level are
-        // disallowed.
-        return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
-    } else {
-        // We must be inside one of the VFS areas.  Determine which one.
-        // Locate the backend_key in the path
-        if (!strchr($path, '/')) {
-            // Disallow attempts to create a share-level directory.
-            return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
-        } else {
-            $backend_key = substr($path, 0, strpos($path, '/'));
-        }
-
-        // Validate and perform permissions checks on the requested backend
-        if (!isset($backends[$backend_key])) {
-            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
-        }
-        //if (!Gollem::canAutoLogin($backend_key)) {
-        //    // FIXME: Is it possible to request secondary authentication
-        //    // credentials here for backends that require it?
-        //    return PEAR::raiseError(_("Additional authentication required."));
-        //}
-        if (!Gollem_Session::createSession($backend_key)) {
-            return PEAR::raiseError(_("Unable to create Gollem session"));
-        }
-        if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
-            return PEAR::raiseError(_("Permission denied to this backend."));
-        }
-
-        // Trim off the backend_key (and '/') to get the VFS relative path
-        $fullpath = substr($path, strlen($backend_key) + 1);
-
-        // Get the VFS-standard $name,$path pair
-        list($name, $path) = Gollem::getVFSPath($fullpath);
-
-        return $GLOBALS['gollem_vfs']->createFolder($path, $name);
-    }
-}
-
-/**
- * Renames a file or directory
- *
- * @param string $path  Path to source object to be renamed
- * @param string $dest  Path to new name
- *
- * @return mixed  True on success; PEAR_Error on failure
- */
-function _gollem_move($path, $dest)
-{
-    // Gollem does not handle authentication
-    $GLOBALS['gollem_authentication'] = 'none';
-
-    // Include Gollem base libraries
-    require_once dirname(__FILE__) . '/base.php';
-    require GOLLEM_BASE . '/config/backends.php';
-    require GOLLEM_BASE . '/config/credentials.php';
-
-    // Clean off the irrelevant portions of the path
-    $path = Gollem::stripAPIPath($path);
-    $dest = Gollem::stripAPIPath($dest);
-
-    if ($path == '') {
-        // We are at the root of gollem.  Any writes at this level are
-        // disallowed.
-        return PEAR::raiseError(_('Folders must be created inside a VFS backend.'));
-    } else {
-        // We must be inside one of the VFS areas.  Determine which one.
-        // Locate the backend_key in the path
-        if (!strchr($path, '/')) {
-            // Disallow attempts to rename a share-level directory.
-            return PEAR::raiseError(_('Renaming of backends is not allowed.'));
-        } else {
-            $backend_key = substr($path, 0, strpos($path, '/'));
-        }
-
-        // Ensure that the destination is within the same backend
-        if (!strchr($dest, '/')) {
-            // Disallow attempts to rename a share-level directory.
-            return PEAR::raiseError(_('Renaming of backends is not allowed.'));
-        } else {
-            $dest_backend_key = substr($path, 0, strpos($path, '/'));
-            if ($dest_backend_key != $backend_key) {
-                return PEAR::raiseError(_('Renaming across backends is not supported.'));
-            }
-        }
-
-        // Validate and perform permissions checks on the requested backend
-        if (!isset($backends[$backend_key])) {
-            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
-        }
-        //if (!Gollem::canAutoLogin($backend_key)) {
-        //    // FIXME: Is it possible to request secondary authentication
-        //    // credentials here for backends that require it?
-        //    return PEAR::raiseError(_("Additional authentication required."));
-        //}
-        if (!Gollem_Session::createSession($backend_key)) {
-            return PEAR::raiseError(_("Unable to create Gollem session"));
-        }
-        if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
-            return PEAR::raiseError(_("Permission denied to this backend."));
-        }
-
-        // Trim off the backend_key (and '/') to get the VFS relative path
-        $srcfullpath = substr($path, strlen($backend_key) + 1);
-        $dstfullpath = substr($dest, strlen($backend_key) + 1);
-
-        // Get the VFS-standard $name,$path pair
-        list($srcname, $srcpath) = Gollem::getVFSPath($srcfullpath);
-        list($dstname, $dstpath) = Gollem::getVFSPath($dstfullpath);
-
-        return $GLOBALS['gollem_vfs']->rename($srcpath, $srcname, $dstpath, $dstname);
-    }
-}
-
-/**
- * Removes a file or folder from the VFS
- *
- * @param string $path  Path of file or folder to delete
- *
- * @return mixed  True on success; PEAR_Error on failure
- */
-function _gollem_path_delete($path)
-{
-    // Gollem does not handle authentication
-    $GLOBALS['gollem_authentication'] = 'none';
-
-    // Include Gollem base libraries
-    require_once dirname(__FILE__) . '/base.php';
-    require GOLLEM_BASE . '/config/backends.php';
-    require GOLLEM_BASE . '/config/credentials.php';
-
-    // Clean off the irrelevant portions of the path
-    $path = Gollem::stripAPIPath($path);
-
-    if ($path == '') {
-        // We are at the root of gollem.  Any writes at this level are
-        // disallowed.
-        return PEAR::raiseError(_("The application folder can not be deleted."));
-    } else {
-        // We must be inside one of the VFS areas.  Determine which one.
-        // Locate the backend_key in the path
-        if (strchr($path, '/')) {
-            $backend_key = substr($path, 0, strpos($path, '/'));
-        } else {
-            $backend_key = $path;
-        }
-
-        // Validate and perform permissions checks on the requested backend
-        if (!isset($backends[$backend_key])) {
-            return PEAR::raiseError(sprintf(_("Invalid backend requested: %s"), $backend_key));
-        }
-        //if (!Gollem::canAutoLogin($backend_key)) {
-        //    // FIXME: Is it possible to request secondary authentication
-        //    // credentials here for backends that require it?
-        //    return PEAR::raiseError(_("Additional authentication required."));
-        //}
-        if (!Gollem_Session::createSession($backend_key)) {
-            return PEAR::raiseError(_("Unable to create Gollem session"));
-        }
-        if (!Gollem::checkPermissions('backend', PERMS_EDIT)) {
-            return PEAR::raiseError(_("Permission denied to this backend."));
-        }
-
-        // Trim off the backend_key (and '/') to get the VFS relative path
-        $fullpath = substr($path, strlen($backend_key) + 1);
-
-        // Get the VFS-standard $name,$path pair
-        list($name, $path) = Gollem::getVFSPath($fullpath);
-
-        // Apparently Gollem::verifyDir() (called by deleteF* next) needs to
-        // see a path with a leading '/'
-        $path = $backends[$backend_key]['root'] . $path;
-        if ($GLOBALS['gollem_vfs']->isFolder($path, $name)) {
-            return Gollem::deleteFolder($path, $name);
-        } else {
-            return Gollem::deleteFile($path, $name);
-        }
-    }
-}
-
-function _gollem_perms()
-{
-    static $perms = array();
-    if (!empty($perms)) {
-        return $perms;
-    }
-
-    require_once dirname(__FILE__) . '/base.load.php';
-    require GOLLEM_BASE . '/config/backends.php';
-
-    $perms['tree']['gollem']['backends'] = false;
-    $perms['title']['gollem:backends'] = _("Backends");
-
-    // Run through every backend.
-    foreach ($backends as $backend => $curBackend) {
-        $perms['tree']['gollem']['backends'][$backend] = false;
-        $perms['title']['gollem:backends:' . $backend] = $curBackend['name'];
-    }
-
-    return $perms;
-}
-
-/**
- * Returns a link to the gollem file preview interface
- *
- * @param string $dir       File absolute path
- * @param string $file      File basename
- * @param string $backend   Backend key. Defaults to Gollem::getPreferredBackend()
- *
- * @return string  The URL string.
- */
-function _gollem_getViewLink($dir, $file, $backend = '')
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (empty($backend)) {
-        $backend = Gollem::getPreferredBackend();
-    }
-
-    $url = Horde_Util::addParameter(
-        Horde::applicationUrl('view.php'),
-        array('actionID' => 'view_file',
-              'type' => substr($file, strrpos($file, '.') + 1),
-              'file' => $file,
-              'dir' => $dir,
-              'driver' => $_SESSION['gollem']['backends'][$backend]['driver']));
-
-    return $url;
-}
-
-/**
- * Creates a link to the gollem file selection window.
- *
- * The file section window will return a cache ID value which should be used
- * (along with the selectListResults and returnFromSelectList functions below)
- * to obtain the data from a list of selected files.
- *
- * There MUST be a form field named 'selectlist_selectid' in the calling
- * form. This field will be populated with the selection ID when the user
- * completes file selection.
- *
- * There MUST be a form parameter named 'actionID' in the calling form.
- * This form will be populated with the value 'selectlist_process' when
- * the user completes file selection.  The calling form will be submitted
- * after the window closes (i.e. the calling form must process the
- * 'selectlist_process' actionID).
- *
- * @param string $link_text   The text to use in the link.
- * @param string $link_style  The style to use for the link.
- * @param string $formid      The formid of the calling script.
- * @param boolean $icon       Create the link with an icon instead of text?
- * @param string $selectid    Selection ID.
- *
- * @return string  The URL string.
- */
-function _gollem_selectlistLink($link_text, $link_style, $formid,
-                                $icon = false, $selectid = '')
-{
-    $link = Horde::link('#', $link_text, $link_style, '_blank', Horde::popupJs(Horde::applicationUrl('selectlist.php'), array('params' => array('formid' => $formid, 'cacheid' => $selectid), 'height' => 500, 'width' => 300, 'urlencode' => true)) . 'return false;');
-    if ($icon) {
-        $link_text = Horde::img('gollem.png', $link_text);
-    }
-    return '<script type="text/javascript">document.write(\''
-        . addslashes($link . $link_text) . '<\' + \'/a>\');</script>';
-}
-
-/**
- * Returns the list of files selected by the user for a given selection ID.
- *
- * @param string $selectid  The selection ID.
- *
- * @param array  An array with each file entry stored in its own array, with
- *               the key as the directory name and the value as the filename.
- */
-function _gollem_selectlistResults($selectid)
-{
-    if (!isset($_SESSION['gollem']['selectlist'][$selectid]['files'])) {
-        return null;
-    } else {
-        $list = array();
-        foreach ($_SESSION['gollem']['selectlist'][$selectid]['files'] as $val) {
-            list($dir, $filename) = explode('|', $val);
-            $list[] = array($dir => $filename);
-        }
-        return $list;
-    }
-}
-
-/**
- * Returns the data for a given selection ID and index.
- *
- * @param string $selectid  The selection ID.
- * @param integer $index    The index of the file data to return.
- *
- * @return string  The file data.
- */
-function _gollem_returnFromSelectlist($selectid, $index)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!isset($_SESSION['gollem']['selectlist'][$selectid]['files'][$index])) {
-        return null;
-    }
-
-    list($dir, $filename) = explode('|', $_SESSION['gollem']['selectlist'][$selectid]['files'][$index]);
-    return $GLOBALS['gollem_vfs']->read($dir, $filename);
-}
-
-/**
- * Sets the files selected for a given selection ID.
- *
- * @param string $selectid  The selection ID to use.
- * @param array $files      An array with each file entry stored in its own
- *                          array, with the key as the directory name and the
- *                          value as the filename.
- *
- * @return string  The selection ID.
- */
-function _gollem_setSelectlist($selectid = '', $files = array())
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (empty($selectid)) {
-        $selectid = uniqid(mt_rand(), true);
-    }
-
-    if (count($files) > 0) {
-        $list = array();
-        foreach ($files as $file) {
-            $list[] = key($file) . '|' . current($file);
-        }
-        $_SESSION['gollem']['selectlist'][$selectid]['files'] = $list;
-    }
-
-    return $selectid;
-}
diff --git a/gollem/lib/version.php b/gollem/lib/version.php
deleted file mode 100644 (file)
index 79f4a69..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('GOLLEM_VERSION', 'H4 (2.0-git)') ?>
index 5b4b8ed..dfb7ac6 100644 (file)
@@ -33,8 +33,9 @@ $horde_test = new Horde_Test();
 
 /* Gollem version. */
 $module = 'Gollem';
-require_once GOLLEM_BASE . '/lib/version.php';
-$module_version = GOLLEM_VERSION;
+require_once dirname(__FILE__) . '/lib/Api.php';
+$api = new Gollem_Api();
+$module_version = $api->version;
 
 /* Gollem configuration files. */
 $file_list = array(
index a755ba5..4e27e24 100644 (file)
@@ -172,10 +172,9 @@ $t->set('canedit', $canEdit);
 if (empty($_SESSION['imp']['admin'])) {
     $new_user_field = '<input id="new_user" type="text" name="new_user"/>';
 } else {
-    require_once IMP_BASE . '/lib/api.php';
     $current_users = array_keys($curr_acl);
     $new_user_field = '<select id="new_user" name="new_user">';
-    foreach (_imp_userList() as $user) {
+    foreach ($registry->callByPackage('userList', 'imp') as $user) {
         if (in_array($user, $current_users)) {
             continue;
         }
diff --git a/imp/lib/Api.php b/imp/lib/Api.php
new file mode 100644 (file)
index 0000000..841247e
--- /dev/null
@@ -0,0 +1,819 @@
+<?php
+/**
+ * IMP external API interface.
+ *
+ * This file defines IMP's external API interface. Other applications
+ * can interact with IMP through this API.
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @package IMP
+ */
+class IMP_Api extends Horde_Registry_Api
+{
+    /**
+     * Does this application support a mobile view?
+     *
+     * @var boolean
+     */
+    public $mobileView = true;
+
+    /**
+     * The application's version.
+     *
+     * @var string
+     */
+    public $version = 'H4 (5.0-git)';
+
+    /**
+     * The services provided by this application.
+     *
+     * @var array
+     */
+    public $services = array(
+        'perms' => array(
+            'args' => array(),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'authCredentials' => array(
+            'args' => array(),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'compose' => array(
+            'args' => array(
+                'args' => '{urn:horde}hash',
+                'extra' => '{urn:horde}hash'
+            ),
+            'type' => 'string'
+        ),
+
+        'batchCompose' => array(
+            'args' => array(
+                'args' => '{urn:horde}hash',
+                'extra' => '{urn:horde}hash'
+            ),
+            'type' => 'string'
+        ),
+
+        'folderlist' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'createFolder' => array(
+            'args' => array('folder' => 'string'),
+            'type' => 'string'
+        ),
+
+        'deleteMessages' => array(
+            'args' => array(
+                'mailbox' => 'string',
+                'indices' => '{urn:horde}integerArray'
+            ),
+            'type' => 'integer'
+        ),
+
+        'copyMessages' => array(
+            'args' => array(
+                'mailbox' => 'string',
+                'indices' => '{urn:horde}integerArray',
+                'target' => 'string'
+            ),
+            'type' => 'boolean'
+        ),
+
+        'moveMessages' => array(
+            'args' => array(
+                'mailbox' => 'string',
+                'indices' => '{urn:horde}integerArray',
+                'target' => 'string'
+            ),
+            'type' => 'boolean'
+        ),
+
+        'flagMessages' => array(
+            'args' => array(
+                'mailbox' => 'string',
+                'indices' => '{urn:horde}integerArray',
+                'flags' => '{urn:horde}stringArray',
+                'set' => 'boolean'
+            ),
+            'type' => 'boolean'
+        ),
+
+        'msgEnvelope' => array(
+            'args' => array(
+                'mailbox' => 'string',
+                'indices' => '{urn:horde}integerArray'
+            ),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'searchMailbox' => array(
+            'args' => array(
+                'mailbox' => 'string',
+                'query' => 'object'
+            ),
+            'type' => '{urn:horde}integerArray'
+        ),
+
+        'mailboxCacheId' => array(
+            'args' => array(
+                'mailbox' => 'string'
+            ),
+            'type' => 'string'
+        ),
+
+        'server' => array(
+            'args' => array(),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'favouriteRecipients' => array(
+            'args' => array(
+                'limit' => 'int'
+            ),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'changeLanguage' => array(
+            'args' => array(),
+            'type' => 'boolean'
+        ),
+
+        /* Cache display method. */
+        'cacheOutput' => array(
+            'args' => array(
+                '{urn:horde}hashHash'
+            ),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        /* Horde_Auth_Application methods. */
+        'authLoginParams' => array(
+            'args' => array(),
+            'checkperms' => false,
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'authAuthenticate' => array(
+            'args' => array(
+                'userID' => 'string',
+                'credentials' => '{urn:horde}hash',
+                'params' => '{urn:horde}hash'
+            ),
+            'checkperms' => false,
+            'type' => 'boolean'
+        ),
+
+        'authAuthenticateCallback' => array(
+            'args' => array(),
+            'checkperms' => false
+        ),
+
+        'authTransparent' => array(
+            'args' => array(),
+            'checkperms' => false,
+            'type' => 'boolean'
+        ),
+
+        'authAddUser' => array(
+            'args' => array(
+                'userId' => 'string',
+                'credentials' => '{urn:horde}stringArray'
+            )
+        ),
+        'authRemoveUser' => array(
+            'args' => array(
+                'userId' => 'string'
+            )
+        ),
+        'authUserList' => array(
+            'type' => '{urn:horde}stringArray'
+        )
+    );
+
+    /**
+     * Constructor.
+     */
+    public function __construct()
+    {
+        /* Only available if admin config is set for this server/login. */
+        if (empty($_SESSION['imp']['admin'])) {
+            unset($this->services['authAddUser'], $this->services['authRemoveUser'], $this->services['authUserList']);
+        }
+    }
+
+    /* Horde-defined functions. */
+
+    /**
+     * Returns a list of available permissions.
+     *
+     * @return array  The permissions list.
+     */
+    public function perms()
+    {
+        return array(
+            'tree' => array(
+                'imp' => array(
+                     'create_folders' => false,
+                     'max_folders' => false,
+                     'max_recipients' => false,
+                     'max_timelimit' => false,
+                 ),
+            ),
+            'title' => array(
+                'imp:create_folders' => _("Allow Folder Creation?"),
+                'imp:max_folders' => _("Maximum Number of Folders"),
+                'imp:max_recipients' => _("Maximum Number of Recipients per Message"),
+                'imp:max_timelimit' => _("Maximum Number of Recipients per Time Period"),
+            ),
+            'type' => array(
+                'imp:create_folders' => 'boolean',
+                'imp:max_folders' => 'int',
+                'imp:max_recipients' => 'int',
+                'imp:max_timelimit' => 'int',
+            )
+        );
+    }
+
+    /**
+     * Performs tasks necessary when the language is changed during the
+     * session.
+     */
+    public function changeLanguage()
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return;
+        }
+
+        $imp_folder = IMP_Folder::singleton();
+        $imp_folder->clearFlistCache();
+        $imaptree = IMP_Imap_Tree::singleton();
+        $imaptree->init();
+        $GLOBALS['imp_search']->initialize(true);
+    }
+
+    /**
+     * Returns a list of authentication credentials, i.e. server settings that
+     * can be specified by the user on the login screen.
+     *
+     * @return array  A hash with credentials, suited for the preferences
+     *                interface.
+     */
+    public function authCredentials()
+    {
+        $app_name = $GLOBALS['registry']->get('name');
+
+        $servers = IMP_Imap::loadServerConfig();
+        $server_list = array();
+        foreach ($servers as $key => $val) {
+            $server_list[$key] = $val['name'];
+        }
+        reset($server_list);
+
+        $credentials = array(
+            'username' => array(
+                'desc' => sprintf(_("%s for %s"), _("Username"), $app_name),
+                'type' => 'text'
+            ),
+            'password' => array(
+                'desc' => sprintf(_("%s for %s"), _("Password"), $app_name),
+                'type' => 'password'
+            ),
+            'server' => array(
+                'desc' => sprintf(_("%s for %s"), _("Server"), $app_name),
+                'type' => 'enum',
+                'enum' => $server_list,
+                'value' => key($server_list)
+            )
+        );
+
+        return $credentials;
+    }
+
+    /**
+     * Application-specific cache output driver.
+     *
+     * @param array $params  A list of params needed (USED: 'id').
+     *
+     * @return array  See Horde::getCacheUrl().
+     * @throws Horde_Exception
+     */
+    public function cacheOutput($params)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            throw new Horde_Exception('No cache data available');
+        }
+
+        switch ($params['id']) {
+        case 'fckeditor':
+            return array(
+                'data' =>
+                    'FCKConfig.ToolbarSets["ImpToolbar"] = ' . $GLOBALS['prefs']->getValue('fckeditor_buttons') . ";\n" .
+                    /* To more closely match "normal" textarea behavior, send
+                     * send <BR> on enter instead of <P>. */
+                    "FCKConfig.EnterMode = 'br';\n" .
+                    'FCKConfig.ShiftEnterMode = \'p\';',
+                'type' => 'text/javascript'
+            );
+        }
+    }
+
+    /* Horde_Auth_Application defined functions. */
+
+    /**
+     * Return login parameters used on the login page.
+     *
+     * @return array  TODO
+     */
+    public function authLoginParams()
+    {
+        $params = array();
+
+        if ($GLOBALS['conf']['server']['server_list'] == 'shown') {
+            $servers = IMP_Imap::loadServerConfig();
+            $server_list = array();
+            $selected = Horde_Util::getFormData('imp_server_key', IMP_Auth::getAutoLoginServer());
+            foreach ($servers as $key => $val) {
+                $server_list[$key] = array(
+                    'name' => $val['name'],
+                    'selected' => ($selected == $key)
+                );
+            }
+            $params['imp_server_key'] = array(
+                'label' => _("Server"),
+                'type' => 'select',
+                'value' => $server_list
+            );
+        }
+
+        /* Show selection of alternate views. */
+        if (!empty($GLOBALS['conf']['user']['select_view'])) {
+            $views = array();
+            if (!($view_cookie = Horde_Util::getFormData('imp_select_view'))) {
+                if (isset($_COOKIE['default_imp_view'])) {
+                    $view_cookie = $_COOKIE['default_imp_view'];
+                } else {
+                    $browser = Horde_Browser::singleton();
+                    $view_cookie = $browser->isMobile() ? 'mimp' : 'imp';
+                }
+            }
+
+            $params['imp_select_view'] = array(
+                'label' => _("Mode"),
+                'type' => 'select',
+                'value' => array(
+                    'imp' => array(
+                        'name' => _("Traditional"),
+                        'selected' => $view_cookie == 'imp'
+                    ),
+                    'dimp' => array(
+                        'hidden' => true,
+                        'name' => _("Dynamic")
+                        // Dimp selected is handled by javascript (dimp_sel)
+                    ),
+                    'mimp' => array(
+                        'name' => _("Minimalist"),
+                        'selected' => $view_cookie == 'mimp'
+                    )
+                )
+            );
+        }
+
+        return array(
+            'js_code' => array(
+                'ImpLogin.dimp_sel=' . intval($view_cookie == 'dimp'),
+                'ImpLogin.server_key_error=' . Horde_Serialize::serialize(_("Please choose a mail server."), Horde_Serialize::JSON)
+            ),
+            'js_files' => array(
+                array('login.js', 'imp')
+            ),
+            'params' => $params
+        );
+    }
+
+    /**
+     * Tries to authenticate with the mail server and create a mail session.
+     *
+     * @param string $userId      The username of the user.
+     * @param array $credentials  Credentials of the user. Allowed keys:
+     *                            'imp_select_view', 'imp_server_key',
+     *                            'password'.
+     *
+     * @throws Horde_Auth_Exception
+     */
+    public function authAuthenticate($userId, $credentials)
+    {
+        $GLOBALS['imp_authentication'] = 'none';
+        require_once dirname(__FILE__) . '/base.php';
+
+        $new_session = IMP_Auth::authenticate(array(
+            'password' => $credentials['password'],
+            'server' => empty($credentials['imp_server_key']) ? IMP_Auth::getAutoLoginServer() : $credentials['imp_server_key'],
+            'userId' => $userId
+        ));
+
+        if ($new_session) {
+            $_SESSION['imp']['cache']['select_view'] = empty($credentials['imp_select_view']) ? '' : $credentials['imp_select_view'];
+
+            /* Set the Horde ID, since it may have been altered by the 'realm'
+             * setting. */
+            $credentials['auth_ob']->setCredential('userId', $_SESSION['imp']['uniquser']);
+        }
+    }
+
+    /**
+     * Tries to transparently authenticate with the mail server and create a
+     * mail session.
+     *
+     * @return boolean  Whether transparent login is supported.
+     * @throws Horde_Auth_Exception
+     */
+    public function authTransparent()
+    {
+        /* Transparent auth is a bit goofy - we most likely have reached this
+         * code from the pushApp() call in base.php already. As such, some of
+         * the IMP init has not yet been done, so we need to do the necessary
+         * init here or else things will fail in IMP_Auth. */
+        $GLOBALS['imp_authentication'] = 'none';
+        require_once dirname(__FILE__) . '/base.php';
+        IMP::initialize();
+        return IMP_Auth::transparent();
+    }
+
+    /**
+     * Does necessary authentication tasks reliant on a full IMP environment.
+     *
+     * @throws Horde_Auth_Exception
+     */
+    public function authAuthenticateCallback()
+    {
+        if (Horde_Auth::getAuth()) {
+            require_once dirname(__FILE__) . '/base.php';
+            IMP_Auth::authenticateCallback();
+        }
+    }
+
+    /**
+     * Adds a user defined by authentication credentials.
+     *
+     * @param string $userId      The userId to add.
+     * @param array $credentials  An array of login credentials. For IMAP,
+     *                            this must contain a password entry.
+     *
+     * @throws Horde_Exception
+     */
+    public function authAddUser($userId, $credentials)
+    {
+        if (($params = $this->server()) === null) {
+            return;
+        }
+
+        $params = array_merge($params, $_SESSION['imp']['admin']['params']);
+        if (isset($params['admin_password'])) {
+            $params['admin_password'] = Horde_Secret::read(Horde_Secret::getKey('imp'), $params['admin_password']);
+        }
+        $auth = Horde_Auth::singleton('imap', $params);
+        $auth->addUser($userId, $credentials);
+    }
+
+    /**
+     * Deletes a user defined by authentication credentials.
+     *
+     * @param string $userId  The userId to delete.
+     *
+     * @throws Horde_Exception
+     */
+    public function authRemoveUser($userId)
+    {
+        if (($params = $this->server()) === null) {
+            return;
+        }
+
+        $params = array_merge($params, $_SESSION['imp']['admin']['params']);
+        if (isset($params['admin_password'])) {
+            $params['admin_password'] = Horde_Secret::read(Horde_Secret::getKey('imp'), $params['admin_password']);
+        }
+        $auth = Horde_Auth::singleton('imap', $params);
+        $auth->removeUser($userId);
+    }
+
+    /**
+     * Lists all users in the system.
+     *
+     * @return array  The array of userIds.
+     * @throws Horde_Exception
+     */
+    public function authUserList()
+    {
+        if (($params = $this->server()) === null) {
+            return;
+        }
+
+        $params = array_merge($params, $_SESSION['imp']['admin']['params']);
+        if (isset($params['admin_password'])) {
+            $params['admin_password'] = Horde_Secret::read(Horde_Secret::getKey('imp'), $params['admin_password']);
+        }
+        $auth = Horde_Auth::singleton('imap', $params);
+        return $auth->listUsers();
+    }
+
+    /* IMP-specific functions. */
+
+    /**
+     * Returns a compose window link.
+     *
+     * @param string|array $args  List of arguments to pass to compose.php.
+     *                            If this is passed in as a string, it will be
+     *                            parsed as a
+     *                            toaddress?subject=foo&cc=ccaddress
+     *                            (mailto-style) string.
+     * @param array $extra        Hash of extra, non-standard arguments to
+     *                            pass to compose.php.
+     *
+     * @return string  The link to the message composition screen.
+     */
+    public function compose($args = array(), $extra = array())
+    {
+        $link = $this->batchCompose(array($args), array($extra));
+        return $link[0];
+    }
+
+    /**
+     * Return a list of compose window links.
+     *
+     * @param string|array $args  List of arguments to pass to compose.php.
+     *                            If this is passed in as a string, it will be
+     *                            parsed as a
+     *                            toaddress?subject=foo&cc=ccaddress
+     *                            (mailto-style) string.
+     * @param array $extra        List of hashes of extra, non-standard
+     *                            arguments to pass to compose.php.
+     *
+     * @return string  The list of links to the message composition screen.
+     */
+    public function batchCompose($args = array(), $extra = array())
+    {
+        $GLOBALS['imp_authentication'] = 'none';
+        require_once dirname(__FILE__) . '/base.php';
+
+        $links = array();
+        foreach ($args as $i => $arg) {
+            $links[$i] = IMP::composeLink($arg, !empty($extra[$i]) ? $extra[$i] : array());
+        }
+
+        return $links;
+    }
+
+    /**
+     * Returns the list of folders.
+     *
+     * @return array  The list of IMAP folders or false if not available.
+     */
+    public function folderlist()
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        $imp_folder = IMP_Folder::singleton();
+        return $imp_folder->flist();
+    }
+
+    /**
+     * Creates a new folder.
+     *
+     * @param string $folder  The name of the folder to create (UTF7-IMAP).
+     *
+     * @return string  The full folder name created or false on failure.
+     */
+    public function createFolder($folder)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        $imp_folder = IMP_Folder::singleton();
+        return $imp_folder->create(IMP::appendNamespace($folder), $GLOBALS['prefs']->getValue('subscribe'));
+    }
+
+    /**
+     * Deletes messages from a mailbox.
+     *
+     * @param string $mailbox  The name of the mailbox (UTF7-IMAP).
+     * @param array $indices   The list of UIDs to delete.
+     *
+     * @return integer|boolean  The number of messages deleted if successful,
+     *                          false if not.
+     */
+    public function deleteMessages($mailbox, $indices)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        $imp_message = IMP_Message::singleton();
+        return $imp_message->delete(array($mailbox => $indices), array('nuke' => true));
+    }
+
+    /**
+     * Copies messages to a mailbox.
+     *
+     * @param string $mailbox  The name of the source mailbox (UTF7-IMAP).
+     * @param array $indices   The list of UIDs to copy.
+     * @param string $target   The name of the target mailbox (UTF7-IMAP).
+     *
+     * @return boolean  True if successful, false if not.
+     */
+    public function copyMessages($mailbox, $indices, $target)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        $imp_message = IMP_Message::singleton();
+        return $imp_message->copy($target, 'copy', array($mailbox => $indices), true);
+    }
+
+    /**
+     * Moves messages to a mailbox.
+     *
+     * @param string $mailbox  The name of the source mailbox (UTF7-IMAP).
+     * @param array $indices   The list of UIDs to move.
+     * @param string $target   The name of the target mailbox (UTF7-IMAP).
+     *
+     * @return boolean  True if successful, false if not.
+     */
+    public function moveMessages($mailbox, $indices, $target)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        $imp_message = IMP_Message::singleton();
+        return $imp_message->copy($target, 'move', array($mailbox => $indices), true);
+    }
+
+    /**
+     * Flag messages.
+     *
+     * @param string $mailbox  The name of the source mailbox (UTF7-IMAP).
+     * @param array $indices   The list of UIDs to flag.
+     * @param array $flags     The flags to set.
+     * @param boolean $set     True to set flags, false to clear flags.
+     *
+     * @return boolean  True if successful, false if not.
+     */
+    public function flagMessages($mailbox, $indices, $flags, $set)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        $imp_message = IMP_Message::singleton();
+        return $imp_message->flag($flags, 'move', array($mailbox => $indices), $set);
+    }
+
+    /**
+     * Return envelope information for the given list of indices.
+     *
+     * @param string $mailbox  The name of the mailbox (UTF7-IMAP).
+     * @param array $indices   The list of UIDs.
+     *
+     * @return array|boolean  TODO if successful, false if not.
+     */
+    public function msgEnvelope($mailbox, $indices)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        return $GLOBALS['imp_imap']->ob->fetch($mailbox, array(Horde_Imap_Client::FETCH_ENVELOPE => true), array('ids' => $indices));
+    }
+
+    /**
+     * Perform a search query on the remote IMAP server.
+     *
+     * @param string $mailbox                        The name of the source
+     *                                               mailbox (UTF7-IMAP).
+     * @param Horde_Imap_Client_Search_Query $query  The query object.
+     *
+     * @return array|boolean  The search results (UID list) or false.
+     */
+    public function searchMailbox($mailbox, $query)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        return $GLOBALS['imp_search']->runSearchQuery($query, $mailbox);
+    }
+
+    /**
+     * Returns the cache ID value for a mailbox
+     *
+     * @param string $mailbox  The name of the source mailbox (UTF7-IMAP).
+     *
+     * @return string|boolean  The cache ID value, or false if not
+     *                         authenticated.
+     */
+    public function mailboxCacheId($mailbox)
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return false;
+        }
+
+        return $GLOBALS['imp_imap']->ob->getCacheId($mailbox);
+    }
+
+    /**
+     * Returns information on the currently logged on IMAP server.
+     *
+     * @return mixed  Returns null if the user has not authenticated into IMP
+     *                yet Otherwise, an array with the following entries:
+     * <pre>
+     * 'hostspec' - (string) The server hostname.
+     * 'port' - (integer) The server port.
+     * 'protocol' - (string) Either 'imap' or 'pop'.
+     * 'secure' - (string) Either 'none', 'ssl', or 'tls'.
+     * </pre>
+     */
+    public function server()
+    {
+        try {
+            $GLOBALS['imp_authentication'] = 'throw';
+            require_once dirname(__FILE__) . '/base.php';
+        } catch (Horde_Exception $e) {
+            return null;
+        }
+
+        $imap_obj = unserialize($_SESSION['imp']['imap_ob']);
+        return array(
+            'hostspec' => $imap_obj->getParam('hostspec'),
+            'port' => $imap_obj->getParam('port'),
+            'protocol' => $_SESSION['imp']['protocol'],
+            'secure' => $imap_obj->getParam('secure')
+        );
+    }
+
+    /**
+     * Returns the list of favorite recipients.
+     *
+     * @param integer $limit  Return this number of recipients.
+     * @param array $filter   A list of messages types that should be returned.
+     *                        A value of null returns all message types.
+     *
+     * @return array  A list with the $limit most favourite recipients.
+     */
+    public function favouriteRecipients($limit,
+                                        $filter = array('new', 'forward', 'reply', 'redirect'))
+    {
+        $GLOBALS['imp_authentication'] = 'none';
+        require_once dirname(__FILE__) . '/base.php';
+
+        if ($GLOBALS['conf']['sentmail']['driver'] != 'none') {
+            $sentmail = IMP_Sentmail::factory();
+            return $sentmail->favouriteRecipients($limit, $filter);
+        }
+
+        return array();
+    }
+
+}
diff --git a/imp/lib/api.php b/imp/lib/api.php
deleted file mode 100644 (file)
index 8f8521a..0000000
+++ /dev/null
@@ -1,795 +0,0 @@
-<?php
-/**
- * IMP external API interface.
- *
- * This file defines IMP's external API interface. Other applications
- * can interact with IMP through this API.
- *
- * See the enclosed file COPYING for license information (GPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- *
- * @package IMP
- */
-
-$_services = array(
-    'perms' => array(
-        'args' => array(),
-        'type' => '{urn:horde}stringArray'
-    ),
-
-    'authCredentials' => array(
-        'args' => array(),
-        'type' => '{urn:horde}hashHash'
-    ),
-
-    'compose' => array(
-        'args' => array(
-            'args' => '{urn:horde}hash',
-            'extra' => '{urn:horde}hash'
-        ),
-        'type' => 'string'
-    ),
-
-    'batchCompose' => array(
-        'args' => array(
-            'args' => '{urn:horde}hash',
-            'extra' => '{urn:horde}hash'
-        ),
-        'type' => 'string'
-    ),
-
-    'folderlist' => array(
-        'args' => array(),
-        'type' => '{urn:horde}stringArray'
-    ),
-
-    'createFolder' => array(
-        'args' => array('folder' => 'string'),
-        'type' => 'string'
-    ),
-
-    'deleteMessages' => array(
-        'args' => array(
-            'mailbox' => 'string',
-            'indices' => '{urn:horde}integerArray'
-        ),
-        'type' => 'integer'
-    ),
-
-    'copyMessages' => array(
-        'args' => array(
-            'mailbox' => 'string',
-            'indices' => '{urn:horde}integerArray',
-            'target' => 'string'
-        ),
-        'type' => 'boolean'
-    ),
-
-    'moveMessages' => array(
-        'args' => array(
-            'mailbox' => 'string',
-            'indices' => '{urn:horde}integerArray',
-            'target' => 'string'
-        ),
-        'type' => 'boolean'
-    ),
-
-    'flagMessages' => array(
-        'args' => array(
-            'mailbox' => 'string',
-            'indices' => '{urn:horde}integerArray',
-            'flags' => '{urn:horde}stringArray',
-            'set' => 'boolean'
-        ),
-        'type' => 'boolean'
-    ),
-
-    'msgEnvelope' => array(
-        'args' => array(
-            'mailbox' => 'string',
-            'indices' => '{urn:horde}integerArray'
-        ),
-        'type' => '{urn:horde}hashHash'
-    ),
-
-    'searchMailbox' => array(
-        'args' => array(
-            'mailbox' => 'string',
-            'query' => 'object'
-        ),
-        'type' => '{urn:horde}integerArray'
-    ),
-
-    'mailboxCacheId' => array(
-        'args' => array(
-            'mailbox' => 'string'
-        ),
-        'type' => 'string'
-    ),
-
-    'server' => array(
-        'args' => array(),
-        'type' => '{urn:horde}hashHash'
-    ),
-
-    'favouriteRecipients' => array(
-        'args' => array(
-            'limit' => 'int'
-        ),
-        'type' => '{urn:horde}stringArray'
-    ),
-
-    'changeLanguage' => array(
-        'args' => array(),
-        'type' => 'boolean'
-    ),
-
-    /* Indicate that mobile view is available. */
-    'mobileView' => array(
-        'args' => array(),
-        'type' => 'boolean'
-    ),
-
-    /* Cache display method. */
-    'cacheOutput' => array(
-        'args' => array(
-            '{urn:horde}hashHash'
-        ),
-        'type' => '{urn:horde}hashHash'
-    ),
-
-    /* Horde_Auth_Application methods. */
-    'authLoginParams' => array(
-        'args' => array(),
-        'checkperms' => false,
-        'type' => '{urn:horde}hashHash'
-    ),
-
-    'authAuthenticate' => array(
-        'args' => array(
-            'userID' => 'string',
-            'credentials' => '{urn:horde}hash',
-            'params' => '{urn:horde}hash'
-        ),
-        'checkperms' => false,
-        'type' => 'boolean'
-    ),
-
-    'authAuthenticateCallback' => array(
-        'args' => array(),
-        'checkperms' => false
-    ),
-
-    'authTransparent' => array(
-        'args' => array(),
-        'checkperms' => false,
-        'type' => 'boolean'
-    )
-);
-
-/* Only available if admin config is set for this server/login. */
-if (!empty($_SESSION['imp']['admin'])) {
-    $_services = array_merge($_services, array(
-        'authAddUser' => array(
-            'args' => array(
-                'userId' => 'string',
-                'credentials' => '{urn:horde}stringArray'
-            )
-        ),
-        'authRemoveUser' => array(
-            'args' => array(
-                'userId' => 'string'
-            )
-        ),
-        'authUserList' => array(
-            'type' => '{urn:horde}stringArray'
-        )
-    ));
-}
-
-/**
- * Returns a list of available permissions.
- */
-function _imp_perms()
-{
-    return array(
-        'tree' => array(
-            'imp' => array(
-                 'create_folders' => false,
-                 'max_folders' => false,
-                 'max_recipients' => false,
-                 'max_timelimit' => false,
-             ),
-        ),
-        'title' => array(
-            'imp:create_folders' => _("Allow Folder Creation?"),
-            'imp:max_folders' => _("Maximum Number of Folders"),
-            'imp:max_recipients' => _("Maximum Number of Recipients per Message"),
-            'imp:max_timelimit' => _("Maximum Number of Recipients per Time Period"),
-        ),
-        'type' => array(
-            'imp:create_folders' => 'boolean',
-            'imp:max_folders' => 'int',
-            'imp:max_recipients' => 'int',
-            'imp:max_timelimit' => 'int',
-        )
-    );
-}
-
-/**
- * Returns a list of authentication credentials, i.e. server settings that can
- * be specified by the user on the login screen.
- *
- * @return array  A hash with credentials, suited for the preferences
- *                interface.
- */
-function _imp_authCredentials()
-{
-    $app_name = $GLOBALS['registry']->get('name');
-
-    $servers = IMP_Imap::loadServerConfig();
-    $server_list = array();
-    foreach ($servers as $key => $val) {
-        $server_list[$key] = $val['name'];
-    }
-    reset($server_list);
-
-    $credentials = array(
-        'username' => array(
-            'desc' => sprintf(_("%s for %s"), _("Username"), $app_name),
-            'type' => 'text'
-        ),
-        'password' => array(
-            'desc' => sprintf(_("%s for %s"), _("Password"), $app_name),
-            'type' => 'password'
-        ),
-        'server' => array(
-            'desc' => sprintf(_("%s for %s"), _("Server"), $app_name),
-            'type' => 'enum',
-            'enum' => $server_list,
-            'value' => key($server_list)
-        )
-    );
-
-    return $credentials;
-}
-
-/**
- * Returns a compose window link.
- *
- * @param string|array $args  List of arguments to pass to compose.php.
- *                            If this is passed in as a string, it will be
- *                            parsed as a toaddress?subject=foo&cc=ccaddress
- *                            (mailto-style) string.
- * @param array $extra        Hash of extra, non-standard arguments to pass to
- *                            compose.php.
- *
- * @return string  The link to the message composition screen.
- */
-function _imp_compose($args = array(), $extra = array())
-{
-    $link = _imp_batchCompose(array($args), array($extra));
-    return $link[0];
-}
-
-/**
- * Return a list of compose window links.
- *
- * @param mixed $args   List of lists of arguments to pass to compose.php. If
- *                      the lists are passed in as strings, they will be parsed
- *                      as toaddress?subject=foo&cc=ccaddress (mailto-style)
- *                      strings.
- * @param array $extra  List of hashes of extra, non-standard arguments to pass
- *                      to compose.php.
- *
- * @return string  The list of links to the message composition screen.
- */
-function _imp_batchCompose($args = array(), $extra = array())
-{
-    $GLOBALS['imp_authentication'] = 'none';
-    require_once dirname(__FILE__) . '/base.php';
-
-    $links = array();
-    foreach ($args as $i => $arg) {
-        $links[$i] = IMP::composeLink($arg, !empty($extra[$i]) ? $extra[$i] : array());
-    }
-
-    return $links;
-}
-
-/**
- * Returns the list of folders.
- *
- * @return array  The list of IMAP folders or false if not available.
- */
-function _imp_folderlist()
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    $imp_folder = IMP_Folder::singleton();
-    return $imp_folder->flist();
-}
-
-/**
- * Creates a new folder.
- *
- * @param string $folder  The name of the folder to create (UTF7-IMAP).
- *
- * @return string  The full folder name created or false on failure.
- */
-function _imp_createFolder($folder)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    $imp_folder = IMP_Folder::singleton();
-    return $imp_folder->create(IMP::appendNamespace($folder), $GLOBALS['prefs']->getValue('subscribe'));
-}
-
-/**
- * Deletes messages from a mailbox.
- *
- * @param string $mailbox  The name of the mailbox (UTF7-IMAP).
- * @param array $indices   The list of UIDs to delete.
- *
- * @return integer|boolean  The number of messages deleted if successful,
- *                          false if not.
- */
-function _imp_deleteMessages($mailbox, $indices)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    $imp_message = IMP_Message::singleton();
-    return $imp_message->delete(array($mailbox => $indices), array('nuke' => true));
-}
-
-/**
- * Copies messages to a mailbox.
- *
- * @param string $mailbox  The name of the source mailbox (UTF7-IMAP).
- * @param array $indices   The list of UIDs to copy.
- * @param string $target   The name of the target mailbox (UTF7-IMAP).
- *
- * @return boolean  True if successful, false if not.
- */
-function _imp_copyMessages($mailbox, $indices, $target)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    $imp_message = IMP_Message::singleton();
-    return $imp_message->copy($target, 'copy', array($mailbox => $indices), true);
-}
-
-/**
- * Moves messages to a mailbox.
- *
- * @param string $mailbox  The name of the source mailbox (UTF7-IMAP).
- * @param array $indices   The list of UIDs to move.
- * @param string $target   The name of the target mailbox (UTF7-IMAP).
- *
- * @return boolean  True if successful, false if not.
- */
-function _imp_moveMessages($mailbox, $indices, $target)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    $imp_message = IMP_Message::singleton();
-    return $imp_message->copy($target, 'move', array($mailbox => $indices), true);
-}
-
-/**
- * Flag messages.
- *
- * @param string $mailbox  The name of the source mailbox (UTF7-IMAP).
- * @param array $indices   The list of UIDs to flag.
- * @param array $flags     The flags to set.
- * @param boolean $set     True to set flags, false to clear flags.
- *
- * @return boolean  True if successful, false if not.
- */
-function _imp_flagMessages($mailbox, $indices, $flags, $set)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    $imp_message = IMP_Message::singleton();
-    return $imp_message->flag($flags, 'move', array($mailbox => $indices), $set);
-}
-
-/**
- * Return envelope information for the given list of indices.
- *
- * @param string $mailbox  The name of the mailbox (UTF7-IMAP).
- * @param array $indices   The list of UIDs.
- *
- * @return array|boolean  TODO if successful, false if not.
- */
-function _imp_msgEnvelope($mailbox, $indices)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    return $GLOBALS['imp_imap']->ob->fetch($mailbox, array(Horde_Imap_Client::FETCH_ENVELOPE => true), array('ids' => $indices));
-}
-
-/**
- * Perform a search query on the remote IMAP server.
- *
- * @param string $mailbox                        The name of the source mailbox
- *                                               (UTF7-IMAP).
- * @param Horde_Imap_Client_Search_Query $query  The query object.
- *
- * @return array|boolean  The search results (UID list) or false.
- */
-function _imp_searchMailbox($mailbox, $query)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    return $GLOBALS['imp_search']->runSearchQuery($query, $mailbox);
-}
-
-/**
- * Returns the cache ID value for a mailbox
- *
- * @param string $mailbox  The name of the source mailbox (UTF7-IMAP).
- *
- * @return string|boolean  The cache ID value, or false if not authenticated.
- */
-function _imp_mailboxCacheId($mailbox)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return false;
-    }
-
-    return $GLOBALS['imp_imap']->ob->getCacheId($mailbox);
-}
-
-/**
- * Returns information on the currently logged on IMAP server.
- *
- * @return mixed  Returns null if the user has not authenticated into IMP yet.
- *                Otherwise, an array with the following entries:
- * <pre>
- * 'hostspec' - (string) The server hostname.
- * 'port' - (integer) The server port.
- * 'protocol' - (string) Either 'imap' or 'pop'.
- * 'secure' - (string) Either 'none', 'ssl', or 'tls'.
- * </pre>
- */
-function _imp_server()
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return null;
-    }
-
-    $imap_obj = unserialize($_SESSION['imp']['imap_ob']);
-    return array(
-        'hostspec' => $imap_obj->getParam('hostspec'),
-        'port' => $imap_obj->getParam('port'),
-        'protocol' => $_SESSION['imp']['protocol'],
-        'secure' => $imap_obj->getParam('secure')
-    );
-}
-
-/**
- * Returns the list of favorite recipients.
- *
- * @param integer $limit  Return this number of recipients.
- * @param array $filter   A list of messages types that should be returned.
- *                        A value of null returns all message types.
- *
- * @return array  A list with the $limit most favourite recipients.
- */
-function _imp_favouriteRecipients($limit,
-                                  $filter = array('new', 'forward', 'reply', 'redirect'))
-{
-    $GLOBALS['imp_authentication'] = 'none';
-    require_once dirname(__FILE__) . '/base.php';
-
-    if ($GLOBALS['conf']['sentmail']['driver'] != 'none') {
-        $sentmail = IMP_Sentmail::factory();
-        return $sentmail->favouriteRecipients($limit, $filter);
-    }
-
-    return array();
-}
-
-/**
- * Performs tasks necessary when the language is changed during the session.
- */
-function _imp_changeLanguage()
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        return;
-    }
-
-    $imp_folder = IMP_Folder::singleton();
-    $imp_folder->clearFlistCache();
-    $imaptree = IMP_Imap_Tree::singleton();
-    $imaptree->init();
-    $GLOBALS['imp_search']->initialize(true);
-}
-
-/**
- * Application-specific cache output driver.
- *
- * @param array $params  A list of params needed (USED: 'id').
- *
- * @return array  See Horde::getCacheUrl().
- * @throws Horde_Exception
- */
-function _imp_cacheOutput($params)
-{
-    try {
-        $GLOBALS['imp_authentication'] = 'throw';
-        require_once dirname(__FILE__) . '/base.php';
-    } catch (Horde_Exception $e) {
-        throw new Horde_Exception('No cache data available');
-    }
-
-    switch ($params['id']) {
-    case 'fckeditor':
-        return array(
-            'data' =>
-                'FCKConfig.ToolbarSets["ImpToolbar"] = ' . $GLOBALS['prefs']->getValue('fckeditor_buttons') . ";\n" .
-                /* To more closely match "normal" textarea behavior, send
-                 * send <BR> on enter instead of <P>. */
-                "FCKConfig.EnterMode = 'br';\n" .
-                'FCKConfig.ShiftEnterMode = \'p\';',
-            'type' => 'text/javascript'
-        );
-    }
-}
-
-/**
- * Indicate that IMP supports a mobile view.
- *
- * @return boolean  True.
- */
-function _imp_mobileView()
-{
-    return true;
-}
-
-/**
- * Return login parameters used on the login page.
- *
- * @return array  TODO
- */
-function _imp_authLoginParams()
-{
-    $params = array();
-
-    if ($GLOBALS['conf']['server']['server_list'] == 'shown') {
-        $servers = IMP_Imap::loadServerConfig();
-        $server_list = array();
-        $selected = Horde_Util::getFormData('imp_server_key', IMP_Auth::getAutoLoginServer());
-        foreach ($servers as $key => $val) {
-            $server_list[$key] = array(
-                'name' => $val['name'],
-                'selected' => ($selected == $key)
-            );
-        }
-        $params['imp_server_key'] = array(
-            'label' => _("Server"),
-            'type' => 'select',
-            'value' => $server_list
-        );
-    }
-
-    /* If dimp/mimp are available, show selection of alternate views. */
-    if (!empty($GLOBALS['conf']['user']['select_view'])) {
-        $views = array();
-        if (!($view_cookie = Horde_Util::getFormData('imp_select_view'))) {
-            if (isset($_COOKIE['default_imp_view'])) {
-                $view_cookie = $_COOKIE['default_imp_view'];
-            } else {
-                $browser = Horde_Browser::singleton();
-                $view_cookie = $browser->isMobile() ? 'mimp' : 'imp';
-            }
-        }
-
-        $params['imp_select_view'] = array(
-            'label' => _("Mode"),
-            'type' => 'select',
-            'value' => array(
-                'imp' => array(
-                    'name' => _("Traditional"),
-                    'selected' => $view_cookie == 'imp'
-                ),
-                'dimp' => array(
-                    'hidden' => true,
-                    'name' => _("Dynamic")
-                    // Dimp selected is handled by javascript (dimp_sel)
-                ),
-                'mimp' => array(
-                    'name' => _("Minimalist"),
-                    'selected' => $view_cookie == 'mimp'
-                )
-            )
-        );
-    }
-
-    return array(
-        'js_code' => array(
-            'ImpLogin.dimp_sel=' . intval($view_cookie == 'dimp'),
-            'ImpLogin.server_key_error=' . Horde_Serialize::serialize(_("Please choose a mail server."), Horde_Serialize::JSON)
-        ),
-        'js_files' => array(
-            array('login.js', 'imp')
-        ),
-        'params' => $params
-    );
-}
-
-/**
- * Tries to authenticate with the mail server and create a mail session.
- *
- * @param string $userId      The username of the user.
- * @param array $credentials  Credentials of the user. Allowed keys:
- *                            'imp_select_view', 'imp_server_key',
- *                            'password'.
- *
- * @throws Horde_Auth_Exception
- */
-function _imp_authAuthenticate($userId, $credentials)
-{
-    $GLOBALS['imp_authentication'] = 'none';
-    require_once dirname(__FILE__) . '/base.php';
-
-    $new_session = IMP_Auth::authenticate(array(
-        'password' => $credentials['password'],
-        'server' => empty($credentials['imp_server_key']) ? IMP_Auth::getAutoLoginServer() : $credentials['imp_server_key'],
-        'userId' => $userId
-    ));
-
-    if ($new_session) {
-        $_SESSION['imp']['cache']['select_view'] = empty($credentials['imp_select_view']) ? '' : $credentials['imp_select_view'];
-
-        /* Set the Horde ID, since it may have been altered by the 'realm'
-         * setting. */
-        $credentials['auth_ob']->setCredential('userId', $_SESSION['imp']['uniquser']);
-    }
-}
-
-/**
- * Tries to transparently authenticate with the mail server and create a mail
- * session.
- *
- * @return boolean  Whether transparent login is supported.
- * @throws Horde_Auth_Exception
- */
-function _imp_authTransparent()
-{
-    /* Transparent auth is a bit goofy - we most likely have reached this
-     * code from the pushApp() call in base.php already. As such, some of the
-     * IMP init has not yet been done, so we need to do the necessary init
-     * here or else things will fail in IMP_Auth. */
-    $GLOBALS['imp_authentication'] = 'none';
-    require_once dirname(__FILE__) . '/base.php';
-    IMP::initialize();
-    return IMP_Auth::transparent();
-}
-
-/**
- * Does necessary authentication tasks reliant on a full IMP environment.
- *
- * @throws Horde_Auth_Exception
- */
-function _imp_authAuthenticateCallback()
-{
-    if (Horde_Auth::getAuth()) {
-        require_once dirname(__FILE__) . '/base.php';
-        IMP_Auth::authenticateCallback();
-    }
-}
-
-/**
- * Adds a user defined by authentication credentials.
- *
- * @param string $userId      The userId to add.
- * @param array $credentials  An array of login credentials. For IMAP,
- *                            this must contain a password entry.
- *
- * @throws Horde_Exception
- */
-function _imp_authAddUser($userId, $credentials)
-{
-    if (($params = _imp_server()) === null) {
-        return;
-    }
-
-    $params = array_merge($params, $_SESSION['imp']['admin']['params']);
-    if (isset($params['admin_password'])) {
-        $params['admin_password'] = Horde_Secret::read(Horde_Secret::getKey('imp'), $params['admin_password']);
-    }
-    $auth = Horde_Auth::singleton('imap', $params);
-    $auth->addUser($userId, $credentials);
-}
-
-/**
- * Deletes a user defined by authentication credentials.
- *
- * @param string $userId  The userId to delete.
- *
- * @throws Horde_Exception
- */
-function _imp_authRemoveUser($userId)
-{
-    if (($params = _imp_server()) === null) {
-        return;
-    }
-
-    $params = array_merge($params, $_SESSION['imp']['admin']['params']);
-    if (isset($params['admin_password'])) {
-        $params['admin_password'] = Horde_Secret::read(Horde_Secret::getKey('imp'), $params['admin_password']);
-    }
-    $auth = Horde_Auth::singleton('imap', $params);
-    $auth->removeUser($userId);
-}
-
-/**
- * Lists all users in the system.
- *
- * @return array  The array of userIds.
- * @throws Horde_Exception
- */
-function _imp_authUserList()
-{
-    if (($params = _imp_server()) === null) {
-        return;
-    }
-
-    $params = array_merge($params, $_SESSION['imp']['admin']['params']);
-    if (isset($params['admin_password'])) {
-        $params['admin_password'] = Horde_Secret::read(Horde_Secret::getKey('imp'), $params['admin_password']);
-    }
-    $auth = Horde_Auth::singleton('imap', $params);
-    return $auth->listUsers();
-}
diff --git a/imp/lib/version.php b/imp/lib/version.php
deleted file mode 100644 (file)
index 0d2ae05..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('IMP_VERSION', 'H4 (5.0-git)') ?>
index 4d7f817..671bcd2 100644 (file)
@@ -122,8 +122,9 @@ $horde_test = new Horde_Test;
 
 /* IMP version. */
 $module = 'IMP';
-require_once IMP_BASE .'/lib/version.php';
-$module_version = IMP_VERSION;
+require_once dirname(__FILE__) . '/lib/Api.php';
+$api = new IMP_Api();
+$module_version = $api->version;
 
 require TEST_TEMPLATES . 'header.inc';
 require TEST_TEMPLATES . 'version.inc';
diff --git a/ingo/lib/Api.php b/ingo/lib/Api.php
new file mode 100644 (file)
index 0000000..b78a329
--- /dev/null
@@ -0,0 +1,371 @@
+<?php
+/**
+ * Ingo external API interface.
+ *
+ * This file defines Ingo's external API interface. Other applications
+ * can interact with Ingo through this API.
+ *
+ * See the enclosed file LICENSE for license information (ASL).  If you
+ * did not receive this file, see http://www.horde.org/licenses/asl.php.
+ */
+class Ingo_Api extends Horde_Registry_Api
+{
+    /**
+     * The application's version.
+     *
+     * @var string
+     */
+    public $version = 'H4 (2.0-git)';
+
+    /**
+     * The services provided by this application.
+     * TODO: Describe structure.
+     *
+     * @var array
+     */
+    public $services = array(
+        'perms' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'removeUserData' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'blacklistFrom' => array(
+            'args' => array('addresses' => '{urn:horde}stringArray'),
+            'type' => 'boolean',
+        ),
+
+        'showBlacklist' => array(
+            'link' => '%application%/blacklist.php',
+        ),
+
+        'whitelistFrom' => array(
+            'args' => array('addresses' => '{urn:horde}stringArray'),
+            'type' => 'boolean',
+        ),
+
+        'showWhitelist' => array(
+            'link' => '%application%/whitelist.php',
+        ),
+
+        'canApplyFilters' => array(
+            'args' => array(),
+            'type' => 'boolean',
+        ),
+
+        'applyFilters' => array(
+            'args' => array('params' => '{urn:horde}stringArray'),
+            'type' => 'boolean',
+        ),
+
+        'showFilters' => array(
+            'link' => '%application%/filters.php',
+        ),
+
+        'showVacation' => array(
+            'link' => '%application%/vacation.php',
+        ),
+
+        'setVacation' => array(
+            'args' => array('info' => '{urn:horde}stringArray'),
+            'type' => 'boolean',
+        ),
+
+        'disableVacation' => array(
+            'args' => array(),
+            'type' => 'boolean',
+        ),
+    );
+
+    /**
+     * Returns a list of available permissions.
+     *
+     * @return array  An array describing all available permissions.
+     */
+    public function perms()
+    {
+        return array(
+            'title' => array(
+                'ingo:allow_rules' => _("Allow Rules"),
+                'ingo:max_rules' => _("Maximum Number of Rules")
+            ),
+            'tree' => array(
+                'ingo' => array(
+                    'allow_rules' => false,
+                    'max_rules' => false
+                )
+            ),
+            'type' => array(
+                'ingo:allow_rules' => 'boolean',
+                'ingo:max_rules' => 'int'
+            )
+        );
+    }
+
+    /**
+     * Removes user data.
+     *
+     * @param string $user  Name of user to remove data for.
+     *
+     * @return mixed  true on success | PEAR_Error on failure
+     */
+    public function removeUserData($user)
+    {
+        if (!Horde_Auth::isAdmin() && $user != Horde_Auth::getAuth()) {
+            return PEAR::raiseError(_("You are not allowed to remove user data."));
+        }
+
+        require_once dirname(__FILE__) . '/../lib/base.php';
+
+        /* Remove all filters/rules owned by the user. */
+        $result = $GLOBALS['ingo_storage']->removeUserData($user);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+
+        /* Now remove all shares owned by the user. */
+        if (!empty($GLOBALS['ingo_shares'])) {
+            /* Get the user's default share. */
+            $share = $GLOBALS['ingo_shares']->getShare($user);
+            if (is_a($share, 'PEAR_Error')) {
+                Horde::logMessage($share, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $share;
+            } else {
+                $result = $GLOBALS['ingo_shares']->removeShare($share);
+                if (is_a($result, 'PEAR_Error')) {
+                    Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    return $result;
+                }
+            }
+
+            /* Get a list of all shares this user has perms to and remove the
+             * perms. */
+            $shares = $GLOBALS['ingo_shares']->listShares($user);
+            if (is_a($shares, 'PEAR_Error')) {
+                Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+            foreach ($shares as $share) {
+                $share->removeUser($user);
+            }
+
+            /* Get a list of all shares this user owns and has perms to delete
+             * and remove them. */
+            $shares = $GLOBALS['ingo_shares']->listShares($user, PERMS_DELETE, $user);
+            if (is_a($shares, 'PEAR_Error')) {
+                Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $shares;
+            }
+            foreach ($shares as $share) {
+                $GLOBALS['ingo_shares']->removeShare($share);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Add addresses to the blacklist.
+     *
+     * @param string $addresses  The addresses to add to the blacklist.
+     */
+    public function blacklistFrom($addresses)
+    {
+        require_once dirname(__FILE__) . '/../lib/base.php';
+        if (!empty($GLOBALS['ingo_shares'])) {
+            $_SESSION['ingo']['current_share'] = $signature;
+        }
+
+        /* Check for '@' entries in $addresses - this would call all mail to
+         * be blacklisted which is most likely not what is desired. */
+        $addresses = array_unique($addresses);
+        $key = array_search('@', $addresses);
+        if ($key !== false) {
+            unset($addresses[$key]);
+        }
+
+        if (!empty($addresses)) {
+            $blacklist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_BLACKLIST);
+            $ret = $blacklist->setBlacklist(array_merge($blacklist->getBlacklist(), $addresses));
+            if (is_a($ret, 'PEAR_Error')) {
+                $GLOBALS['notification']->push($ret, $ret->getCode());
+            } else {
+                $GLOBALS['ingo_storage']->store($blacklist);
+                Ingo::updateScript();
+                foreach ($addresses as $from) {
+                    $GLOBALS['notification']->push(sprintf(_("The address \"%s\" has been added to your blacklist."), $from));
+                }
+            }
+        }
+    }
+
+    /**
+     * Add addresses to the whitelist.
+     *
+     * @param string $addresses  The addresses to add to the whitelist.
+     */
+    public function whitelistFrom($addresses)
+    {
+        require_once dirname(__FILE__) . '/../lib/base.php';
+        if (!empty($GLOBALS['ingo_shares'])) {
+            $_SESSION['ingo']['current_share'] = $signature;
+        }
+
+        $whitelist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_WHITELIST);
+        $ret = $whitelist->setWhitelist(array_merge($whitelist->getWhitelist(), $addresses));
+        if (is_a($ret, 'PEAR_Error')) {
+            $GLOBALS['notification']->push($ret, $ret->getCode());
+        } else {
+            $GLOBALS['ingo_storage']->store($whitelist);
+            Ingo::updateScript();
+            foreach ($addresses as $from) {
+                $GLOBALS['notification']->push(sprintf(_("The address \"%s\" has been added to your whitelist."), $from));
+            }
+        }
+    }
+
+    /**
+     * Can this driver perform on-demand filtering?
+     *
+     * @return boolean  True if perform() is available, false if not.
+     */
+    public function canApplyFilters()
+    {
+        require_once dirname(__FILE__) . '/../lib/base.php';
+
+        $ingo_script = Ingo::loadIngoScript();
+        return $ingo_script
+            ? $ingo_script->performAvailable()
+            : false;
+    }
+
+    /**
+     * Perform the filtering specified in the rules.
+     *
+     * @param array $params  The parameter array.
+     *
+     * @return boolean  True if filtering was performed, false if not.
+     */
+    public function applyFilters($params = array())
+    {
+        require_once dirname(__FILE__) . '/../lib/base.php';
+        if (!empty($GLOBALS['ingo_shares'])) {
+            $_SESSION['ingo']['current_share'] = $signature;
+        }
+
+        $ingo_script = Ingo::loadIngoScript();
+        return $ingo_script
+            ? $ingo_script->perform($params)
+            : false;
+    }
+
+    /**
+     * Set vacation
+     *
+     * @param array $info  Vacation details.
+     *
+     * @return boolean  True on success.
+     */
+    public function setVacation($info)
+    {
+        require_once dirname(__FILE__) . '/../lib/base.php';
+        if (!empty($GLOBALS['ingo_shares'])) {
+            $_SESSION['ingo']['current_share'] = $signature;
+        }
+
+        if (empty($info)) {
+            return true;
+        }
+
+        /* Get vacation filter. */
+        $filters = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FILTERS);
+        $vacation_rule_id = $filters->findRuleId(Ingo_Storage::ACTION_VACATION);
+
+        /* Set vacation object and rules. */
+        $vacation = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_VACATION);
+
+        /* Make sure we have at least one address. */
+        if (empty($info['addresses'])) {
+            require_once 'Horde/Identity.php';
+            $identity = Identity::singleton('none');
+            $info['addresses'] = implode("\n", $identity->getAll('from_addr'));
+            /* Remove empty lines. */
+            $info['addresses'] = preg_replace('/\n+/', "\n", $info['addresses']);
+            if (empty($addresses)) {
+                $info['addresses'] = Horde_Auth::getAuth();
+            }
+        }
+
+        $vacation->setVacationAddresses($addresses);
+
+        if (isset($info['days'])) {
+            $vacation->setVacationDays($info['days']);
+        }
+        if (isset($info['excludes'])) {
+            $vacation->setVacationExcludes($info['excludes']);
+        }
+        if (isset($info['ignorelist'])) {
+            $vacation->setVacationIgnorelist(($info['ignorelist'] == 'on'));
+        }
+        if (isset($info['reason'])) {
+            $vacation->setVacationReason($info['reason']);
+        }
+        if (isset($info['subject'])) {
+            $vacation->setVacationSubject($info['subject']);
+        }
+        if (isset($info['start'])) {
+            $vacation->setVacationStart($info['start']);
+        }
+        if (isset($info['end'])) {
+            $vacation->setVacationEnd($info['end']);
+        }
+
+        $filters->ruleEnable($vacation_rule_id);
+        $result = $GLOBALS['ingo_storage']->store($filters);
+        if (!is_a($result, 'PEAR_Error')) {
+            if ($GLOBALS['prefs']->getValue('auto_update')) {
+                Ingo::updateScript();
+            }
+
+            /* Update the timestamp for the rules. */
+            $_SESSION['ingo']['change'] = time();
+        }
+
+        return $result;
+    }
+
+    /**
+     * Disable vacation
+     *
+     * @return boolean  True on success.
+     */
+    public function disableVacation()
+    {
+        require_once dirname(__FILE__) . '/../lib/base.php';
+        if (!empty($GLOBALS['ingo_shares'])) {
+            $_SESSION['ingo']['current_share'] = $signature;
+        }
+
+        /* Get vacation filter. */
+        $filters = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FILTERS);
+        $vacation_rule_id = $filters->findRuleId(Ingo_Storage::ACTION_VACATION);
+
+        $filters->ruleDisable($vacation_rule_id);
+        $result = $GLOBALS['ingo_storage']->store($filters);
+        if (!is_a($result, 'PEAR_Error')) {
+            if ($GLOBALS['prefs']->getValue('auto_update')) {
+                Ingo::updateScript();
+            }
+
+            /* Update the timestamp for the rules. */
+            $_SESSION['ingo']['change'] = time();
+        }
+
+        return $result;
+    }
+
+}
diff --git a/ingo/lib/api.php b/ingo/lib/api.php
deleted file mode 100644 (file)
index 7027771..0000000
+++ /dev/null
@@ -1,352 +0,0 @@
-<?php
-/**
- * Ingo external API interface.
- *
- * This file defines Ingo's external API interface. Other applications
- * can interact with Ingo through this API.
- *
- * See the enclosed file LICENSE for license information (ASL).  If you
- * did not receive this file, see http://www.horde.org/licenses/asl.php.
- */
-
-$_services['perms'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray');
-
-$_services['removeUserData'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['blacklistFrom'] = array(
-    'args' => array('addresses' => '{urn:horde}stringArray'),
-    'type' => 'boolean',
-);
-
-$_services['showBlacklist'] = array(
-    'link' => '%application%/blacklist.php',
-);
-
-$_services['whitelistFrom'] = array(
-    'args' => array('addresses' => '{urn:horde}stringArray'),
-    'type' => 'boolean',
-);
-
-$_services['showWhitelist'] = array(
-    'link' => '%application%/whitelist.php',
-);
-
-$_services['canApplyFilters'] = array(
-    'args' => array(),
-    'type' => 'boolean',
-);
-
-$_services['applyFilters'] = array(
-    'args' => array('params' => '{urn:horde}stringArray'),
-    'type' => 'boolean',
-);
-
-$_services['showFilters'] = array(
-    'link' => '%application%/filters.php',
-);
-
-$_services['showVacation'] = array(
-    'link' => '%application%/vacation.php',
-);
-
-$_services['setVacation'] = array(
-    'args' => array('info' => '{urn:horde}stringArray'),
-    'type' => 'boolean',
-);
-
-$_services['disableVacation'] = array(
-    'args' => array(),
-    'type' => 'boolean',
-);
-
-/**
- * Returns a list of available permissions.
- *
- * @return array  An array describing all available permissions.
- */
-function _ingo_perms()
-{
-    return array(
-        'title' => array(
-            'ingo:allow_rules' => _("Allow Rules"),
-            'ingo:max_rules' => _("Maximum Number of Rules")
-        ),
-        'tree' => array(
-            'ingo' => array(
-                'allow_rules' => false,
-                'max_rules' => false
-            )
-        ),
-        'type' => array(
-            'ingo:allow_rules' => 'boolean',
-            'ingo:max_rules' => 'int'
-        )
-    );
-}
-
-/**
- * Removes user data.
- *
- * @param string $user  Name of user to remove data for.
- *
- * @return mixed  true on success | PEAR_Error on failure
- */
-function _ingo_removeUserData($user)
-{
-    if (!Horde_Auth::isAdmin() && $user != Horde_Auth::getAuth()) {
-        return PEAR::raiseError(_("You are not allowed to remove user data."));
-    }
-
-    require_once dirname(__FILE__) . '/../lib/base.php';
-
-    /* Remove all filters/rules owned by the user. */
-    $result = $GLOBALS['ingo_storage']->removeUserData($user);
-    if (is_a($result, 'PEAR_Error')) {
-        Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-        return $result;
-    }
-
-    /* Now remove all shares owned by the user. */
-    if (!empty($GLOBALS['ingo_shares'])) {
-        /* Get the user's default share. */
-        $share = $GLOBALS['ingo_shares']->getShare($user);
-        if (is_a($share, 'PEAR_Error')) {
-            Horde::logMessage($share, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $share;
-        } else {
-            $result = $GLOBALS['ingo_shares']->removeShare($share);
-            if (is_a($result, 'PEAR_Error')) {
-                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-                return $result;
-            }
-        }
-
-        /* Get a list of all shares this user has perms to and remove the
-         * perms. */
-        $shares = $GLOBALS['ingo_shares']->listShares($user);
-        if (is_a($shares, 'PEAR_Error')) {
-            Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
-        }
-        foreach ($shares as $share) {
-            $share->removeUser($user);
-        }
-
-        /* Get a list of all shares this user owns and has perms to delete and
-         * remove them. */
-        $shares = $GLOBALS['ingo_shares']->listShares($user, PERMS_DELETE, $user);
-        if (is_a($shares, 'PEAR_Error')) {
-            Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $shares;
-        }
-        foreach ($shares as $share) {
-            $GLOBALS['ingo_shares']->removeShare($share);
-        }
-    }
-
-    return true;
-}
-
-/**
- * Add addresses to the blacklist.
- *
- * @param string $addresses  The addresses to add to the blacklist.
- */
-function _ingo_blacklistFrom($addresses)
-{
-    require_once dirname(__FILE__) . '/../lib/base.php';
-    if (!empty($GLOBALS['ingo_shares'])) {
-        $_SESSION['ingo']['current_share'] = $signature;
-    }
-
-    /* Check for '@' entries in $addresses - this would call all mail to
-     * be blacklisted which is most likely not what is desired. */
-    $addresses = array_unique($addresses);
-    $key = array_search('@', $addresses);
-    if ($key !== false) {
-        unset($addresses[$key]);
-    }
-
-    if (!empty($addresses)) {
-        $blacklist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_BLACKLIST);
-        $ret = $blacklist->setBlacklist(array_merge($blacklist->getBlacklist(), $addresses));
-        if (is_a($ret, 'PEAR_Error')) {
-            $GLOBALS['notification']->push($ret, $ret->getCode());
-        } else {
-            $GLOBALS['ingo_storage']->store($blacklist);
-            Ingo::updateScript();
-            foreach ($addresses as $from) {
-                $GLOBALS['notification']->push(sprintf(_("The address \"%s\" has been added to your blacklist."), $from));
-            }
-        }
-    }
-}
-
-/**
- * Add addresses to the whitelist.
- *
- * @param string $addresses  The addresses to add to the whitelist.
- */
-function _ingo_whitelistFrom($addresses)
-{
-    require_once dirname(__FILE__) . '/../lib/base.php';
-    if (!empty($GLOBALS['ingo_shares'])) {
-        $_SESSION['ingo']['current_share'] = $signature;
-    }
-
-    $whitelist = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_WHITELIST);
-    $ret = $whitelist->setWhitelist(array_merge($whitelist->getWhitelist(), $addresses));
-    if (is_a($ret, 'PEAR_Error')) {
-        $GLOBALS['notification']->push($ret, $ret->getCode());
-    } else {
-        $GLOBALS['ingo_storage']->store($whitelist);
-        Ingo::updateScript();
-        foreach ($addresses as $from) {
-            $GLOBALS['notification']->push(sprintf(_("The address \"%s\" has been added to your whitelist."), $from));
-        }
-    }
-}
-
-/**
- * Can this driver perform on-demand filtering?
- *
- * @return boolean  True if perform() is available, false if not.
- */
-function _ingo_canApplyFilters()
-{
-    require_once dirname(__FILE__) . '/../lib/base.php';
-
-    $ingo_script = Ingo::loadIngoScript();
-    return $ingo_script
-        ? $ingo_script->performAvailable()
-        : false;
-}
-
-/**
- * Perform the filtering specified in the rules.
- *
- * @param array $params  The parameter array.
- *
- * @return boolean  True if filtering was performed, false if not.
- */
-function _ingo_applyFilters($params = array())
-{
-    require_once dirname(__FILE__) . '/../lib/base.php';
-    if (!empty($GLOBALS['ingo_shares'])) {
-        $_SESSION['ingo']['current_share'] = $signature;
-    }
-
-    $ingo_script = Ingo::loadIngoScript();
-    return $ingo_script
-        ? $ingo_script->perform($params)
-        : false;
-}
-
-/**
- * Set vacation
- *
- * @param array $info  Vacation details
- *
- * @return boolean  True on success.
- */
-function _ingo_setVacation($info)
-{
-    require_once dirname(__FILE__) . '/../lib/base.php';
-    if (!empty($GLOBALS['ingo_shares'])) {
-        $_SESSION['ingo']['current_share'] = $signature;
-    }
-
-    if (empty($info)) {
-        return true;
-    }
-
-    /* Get vacation filter. */
-    $filters = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FILTERS);
-    $vacation_rule_id = $filters->findRuleId(Ingo_Storage::ACTION_VACATION);
-
-    /* Set vacation object and rules. */
-    $vacation = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_VACATION);
-
-    /* Make sure we have at least one address. */
-    if (empty($info['addresses'])) {
-        require_once 'Horde/Identity.php';
-        $identity = &Identity::singleton('none');
-        $info['addresses'] = implode("\n", $identity->getAll('from_addr'));
-        /* Remove empty lines. */
-        $info['addresses'] = preg_replace('/\n+/', "\n", $info['addresses']);
-        if (empty($addresses)) {
-            $info['addresses'] = Horde_Auth::getAuth();
-        }
-    }
-
-    $vacation->setVacationAddresses($addresses);
-
-    if (isset($info['days'])) {
-        $vacation->setVacationDays($info['days']);
-    }
-    if (isset($info['excludes'])) {
-        $vacation->setVacationExcludes($info['excludes']);
-    }
-    if (isset($info['ignorelist'])) {
-        $vacation->setVacationIgnorelist(($info['ignorelist'] == 'on'));
-    }
-    if (isset($info['reason'])) {
-        $vacation->setVacationReason($info['reason']);
-    }
-    if (isset($info['subject'])) {
-        $vacation->setVacationSubject($info['subject']);
-    }
-    if (isset($info['start'])) {
-        $vacation->setVacationStart($info['start']);
-    }
-    if (isset($info['end'])) {
-        $vacation->setVacationEnd($info['end']);
-    }
-
-    $filters->ruleEnable($vacation_rule_id);
-    $result = $GLOBALS['ingo_storage']->store($filters);
-    if (!is_a($result, 'PEAR_Error')) {
-        if ($GLOBALS['prefs']->getValue('auto_update')) {
-            Ingo::updateScript();
-        }
-
-        /* Update the timestamp for the rules. */
-        $_SESSION['ingo']['change'] = time();
-    }
-
-    return $result;
-}
-
-/**
- * Disable vacation
- *
- * @return boolean  True on success.
- */
-function _ingo_disableVacation()
-{
-    require_once dirname(__FILE__) . '/../lib/base.php';
-    if (!empty($GLOBALS['ingo_shares'])) {
-        $_SESSION['ingo']['current_share'] = $signature;
-    }
-
-    /* Get vacation filter. */
-    $filters = &$GLOBALS['ingo_storage']->retrieve(Ingo_Storage::ACTION_FILTERS);
-    $vacation_rule_id = $filters->findRuleId(Ingo_Storage::ACTION_VACATION);
-
-    $filters->ruleDisable($vacation_rule_id);
-    $result = $GLOBALS['ingo_storage']->store($filters);
-    if (!is_a($result, 'PEAR_Error')) {
-        if ($GLOBALS['prefs']->getValue('auto_update')) {
-            Ingo::updateScript();
-        }
-
-        /* Update the timestamp for the rules. */
-        $_SESSION['ingo']['change'] = time();
-    }
-
-    return $result;
-}
diff --git a/ingo/lib/version.php b/ingo/lib/version.php
deleted file mode 100644 (file)
index d5aedd3..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('INGO_VERSION', 'H4 (2.0-git)') ?>
index 43fa34c..50408fe 100644 (file)
@@ -33,8 +33,9 @@ $horde_test = new Horde_Test;
 
 /* Ingo version. */
 $module = 'Ingo';
-require_once INGO_BASE . '/lib/version.php';
-$module_version = INGO_VERSION;
+require_once dirname(__FILE__) . '/lib/Api.php';
+$api = new Ingo_Api();
+$module_version = $api->version;
 
 require TEST_TEMPLATES . 'header.inc';
 require TEST_TEMPLATES . 'version.inc';
diff --git a/jeta/lib/Api.php b/jeta/lib/Api.php
new file mode 100644 (file)
index 0000000..77cc189
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+class Jeta_Api extends Horde_Registry_Api
+{
+    public $version = 'H4 (2.0-git)';
+}
diff --git a/jeta/lib/version.php b/jeta/lib/version.php
deleted file mode 100644 (file)
index 078f1ef..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('JETA_VERSION', 'H4 (2.0-git)') ?>
index 3a0f17a..a5b8725 100644 (file)
@@ -30,8 +30,9 @@ $horde_test = new Horde_Test();
 
 /* Jeta version. */
 $module = 'Jeta';
-require_once JETA_BASE . '/lib/version.php';
-$module_version = JETA_VERSION;
+require_once dirname(__FILE__) . '/lib/Api.php';
+$api = new Jeta_Api();
+$module_version = $api->version;
 
 /* Jeta configuration files. */
 $file_list = array(
diff --git a/kastalia/lib/Api.php b/kastalia/lib/Api.php
new file mode 100644 (file)
index 0000000..8a0d665
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+class Kastalia_Api extends Horde_Registry_Api
+{
+    public $version = '1.0.1';
+}
diff --git a/kastalia/lib/version.php b/kastalia/lib/version.php
deleted file mode 100755 (executable)
index 1977071..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('KASTALIA_VERSION', '1.0.1') ?>
\ No newline at end of file
diff --git a/kronolith/lib/Api.php b/kronolith/lib/Api.php
new file mode 100644 (file)
index 0000000..f7a5943
--- /dev/null
@@ -0,0 +1,1544 @@
+<?php
+/**
+ * Kronolith external API interface.
+ *
+ * This file defines Kronolith's external API interface. Other applications
+ * can interact with Kronolith through this API.
+ *
+ * @package Kronolith
+ */
+class Kronolith_Api extends Horde_Registry_Api
+{
+    public $version = 'H3 (3.0-git)';
+
+    public $services = array(
+        'show' => array(
+            'link' => '%application%/event.php?calendar=|calendar|&eventID=|event|&uid=|uid|'
+        ),
+
+        'perms' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'removeUserData' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'shareHelp' => array(
+            'args' => array(),
+            'type' => 'string'
+        ),
+
+        'modified' => array(
+            'args' => array('uid' => 'string'),
+            'type' => 'int',
+        ),
+
+        'browse' => array(
+            'args' => array('path' => 'string', 'properties' => '{urn:horde}stringArray'),
+            'type' => '{urn:horde}hashHash',
+        ),
+
+        'put' => array(
+            'args' => array('path' => 'string', 'content' => 'string', 'content_type' => 'string'),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'path_delete' => array(
+            'args' => array('path' => 'string'),
+            'type' => 'boolean',
+        ),
+
+        'getFreeBusy' => array(
+            'args' => array('startstamp' => 'int', 'endstamp' => 'int', 'calendar' => 'string'),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'listCalendars' => array(
+            'args' => array('owneronly' => 'boolean', 'permission' => 'int'),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'listEvents' => array(
+            'args' => array('startstamp' => 'int', 'endstamp' => 'int', 'calendar' => 'string', 'showRecurrence' => 'string', 'alarmsOnly' => 'boolean'),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'listAlarms' => array(
+            'args' => array('time' => 'int', 'user' => 'string'),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'list' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'listBy' => array(
+            'args' => array('action' => 'string', 'timestamp' => 'int'),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'getActionTimestamp' => array(
+            'args' => array('uid' => 'string', 'action' => 'string', 'calendar' => 'string'),
+            'type' => 'int',
+        ),
+
+        'import' => array(
+            'args' => array('content' => 'string', 'contentType' => 'string', 'calendar' => 'string'),
+            'type' => 'string'
+        ),
+
+        'export' => array(
+            'args' => array('uid' => 'string', 'contentType' => 'string'),
+            'type' => 'string'
+        ),
+
+        'exportCalendar' => array(
+            'args' => array('calendar' => 'string', 'contentType' => 'string'),
+            'type' => 'string'
+        ),
+
+        'delete' => array(
+            'args' => array('uid' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'replace' => array(
+            'args' => array('uid' => 'string', 'content' => 'string', 'contentType' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        // FIXME: create complex type definition for SOAP calls.
+        'eventFromUID' => array(
+            'args' => array('uid' => 'string'),
+            'type' => 'object'
+        ),
+
+        // FIXME: create complex type definition for SOAP calls.
+        'updateAttendee' => array(
+            'args' => array('response' => 'object', 'sender' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'subscribe' => array(
+            'args' => array('calendar' => '{urn:horde}stringArray'),
+            'type' => 'boolean',
+        ),
+
+        'unsubscribe' => array(
+            'args' => array('calendar' => '{urn:horde}stringArray'),
+            'type' => 'boolean',
+        ),
+
+        'lock' => array(
+            'args' => array('calendar' => 'string', 'event' => 'string'),
+            'type' => 'string'
+        ),
+
+        'unlock' => array(
+            'args' => array('calendar' => 'string', 'lockid' => 'string'),
+            'type' => 'string'
+        ),
+
+        'checkLocks' => array(
+            'args' => array('calendar' => 'string', 'event' => 'string'),
+            'type' => 'string'
+        ),
+
+        'getFbCalendars' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray'
+        )
+    );
+
+    /**
+     * Returns a list of available permissions.
+     *
+     * @return array  An array describing all available permissions.
+     */
+    public function perms()
+    {
+        $perms = array();
+        $perms['tree']['kronolith']['max_events'] = false;
+        $perms['title']['kronolith:max_events'] = _("Maximum Number of Events");
+        $perms['type']['kronolith:max_events'] = 'int';
+
+        return $perms;
+    }
+
+    /**
+     * Removes user data.
+     *
+     * @param string $user  Name of user to remove data for.
+     *
+     * @return mixed  true on success | PEAR_Error on failure
+     */
+    public function removeUserData($user)
+    {
+        if (!Horde_Auth::isAdmin() && $user != Horde_Auth::getAuth()) {
+            return PEAR::raiseError(_("You are not allowed to remove user data."));
+        }
+
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        /* Remove all events owned by the user in all calendars. */
+        $result = Kronolith::getDriver()->removeUserData($user);
+
+        /* Now delete history as well. */
+        $history = &Horde_History::singleton();
+        if (method_exists($history, 'removeByParent')) {
+            $histories = $history->removeByParent('kronolith:' . $user);
+        } else {
+            /* Remove entries 100 at a time. */
+            $all = $history->getByTimestamp('>', 0, array(), 'kronolith:' . $user);
+            if (is_a($all, 'PEAR_Error')) {
+                Horde::logMessage($all, __FILE__, __LINE__, PEAR_LOG_ERR);
+            } else {
+                $all = array_keys($all);
+                while (count($d = array_splice($all, 0, 100)) > 0) {
+                    $history->removebyNames($d);
+                }
+            }
+        }
+
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Get the user's default share */
+        $share = $GLOBALS['kronolith_shares']->getShare($user);
+        if (is_a($share, 'PEAR_Error')) {
+            Horde::logMessage($share, __FILE__, __LINE__, PEAR_LOG_ERR);
+        } else {
+            $result = $GLOBALS['kronolith_shares']->removeShare($share);
+            if (is_a($result, 'PEAR_Error')) {
+                $hasError = true;
+                Horde::logMessage($result->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        }
+
+        /* Get a list of all shares this user has perms to and remove the perms */
+        $shares = $GLOBALS['kronolith_shares']->listShares($user);
+        if (is_a($shares, 'PEAR_Error')) {
+            Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+        foreach ($shares as $share) {
+            $share->removeUser($user);
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the share helper prefix
+     *
+     * @return string
+     */
+    public function shareHelp()
+    {
+        return 'shares';
+    }
+
+    /**
+     * Returns the last modification timestamp for the given uid.
+     *
+     * @param string $uid      The uid to look for.
+     *
+     * @return integer  The timestamp for the last modification of $uid.
+     */
+    public function modified($uid)
+    {
+        $modified = $this->getActionTimestamp($uid, 'modify');
+        if (empty($modified)) {
+            $modified = $this->getActionTimestamp($uid, 'add');
+        }
+        return $modified;
+    }
+
+    /**
+     * Browse through Kronolith's object tree.
+     *
+     * @param string $path       The level of the tree to browse.
+     * @param array $properties  The item properties to return. Defaults to 'name',
+     *                           'icon', and 'browseable'.
+     *
+     * @return array  The contents of $path
+     */
+    public function browse($path = '', $properties = array())
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+        global $registry;
+
+        // Default properties.
+        if (!$properties) {
+            $properties = array('name', 'icon', 'browseable');
+        }
+
+        if (substr($path, 0, 9) == 'kronolith') {
+            $path = substr($path, 9);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        if (empty($path)) {
+            //
+            // This request is for a list of all users who have calendars visible
+            // to the requesting user.
+            //
+            $calendars = Kronolith::listCalendars(false, PERMS_READ);
+            $owners = array();
+            foreach ($calendars as $calendar) {
+                $owners[$calendar->get('owner')] = true;
+            }
+
+            $results = array();
+            foreach (array_keys($owners) as $owner) {
+                $path = 'kronolith/' . $owner;
+                if (in_array('name', $properties)) {
+                    $results[$path]['name'] = $owner;
+                }
+                if (in_array('icon', $properties)) {
+                    $results[$path]['icon'] =
+                        $registry->getImageDir('horde') . '/user.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results[$path]['browseable'] = true;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results[$path]['contenttype'] =
+                        'httpd/unix-directory';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results[$path]['contentlength'] = 0;
+                }
+                if (in_array('modified', $properties)) {
+                    $results[$path]['modified'] =
+                        $_SERVER['REQUEST_TIME'];
+                }
+                if (in_array('created', $properties)) {
+                    $results[$path]['created'] = 0;
+                }
+
+                // CalDAV Properties from RFC 4791 and
+                // draft-desruisseaux-caldav-sched-03
+                $caldavns = 'urn:ietf:params:xml:ns:caldav';
+                $kronolith_rpc_base = $GLOBALS['registry']->get('webroot', 'horde') . '/rpc/kronolith/';
+                if (in_array($caldavns . ':calendar-home-set', $properties)) {
+                    $results[$path][$caldavns . ':calendar-home-set'] =  Horde::url($kronolith_rpc_base . urlencode($owner), true);
+                }
+
+                if (in_array($caldavns . ':calendar-user-address-set', $properties)) {
+                    // FIXME: Add the calendar owner's email address from their Horde Identity
+                }
+            }
+            return $results;
+
+        } elseif (count($parts) == 1) {
+            //
+            // This request is for all calendars owned by the requested user
+            //
+            $calendars = $GLOBALS['kronolith_shares']->listShares(Horde_Auth::getAuth(),
+                PERMS_SHOW,
+                $parts[0]);
+            $results = array();
+            foreach ($calendars as $calendarId => $calendar) {
+                $retpath = 'kronolith/' . $parts[0] . '/' . $calendarId;
+                if (in_array('name', $properties)) {
+                    $results[$retpath]['name'] = sprintf(_("Events from %s"), $calendar->get('name'));
+                    $results[$retpath . '.ics']['name'] = $calendar->get('name');
+                }
+                if (in_array('displayname', $properties)) {
+                    $results[$retpath]['displayname'] = rawurlencode($calendar->get('name'));
+                    $results[$retpath . '.ics']['displayname'] = rawurlencode($calendar->get('name')) . '.ics';
+                }
+                if (in_array('icon', $properties)) {
+                    $results[$retpath]['icon'] = $registry->getImageDir() . '/kronolith.png';
+                    $results[$retpath . '.ics']['icon'] = $registry->getImageDir() . '/mime/icalendar.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results[$retpath]['browseable'] = $calendar->hasPermission(Horde_Auth::getAuth(), PERMS_READ);
+                    $results[$retpath . '.ics']['browseable'] = false;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results[$retpath]['contenttype'] = 'httpd/unix-directory';
+                    $results[$retpath . '.ics']['contenttype'] = 'text/calendar';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results[$retpath]['contentlength'] = 0;
+                    // FIXME:  This is a hack.  If the content length is longer
+                    // than the actual data then some WebDAV clients will report
+                    // an error when the file EOF is received.  Ideally we should
+                    // determine the actual size of the .ics and report it here, but
+                    // the performance hit may be prohibitive.  This requires
+                    // further investigation.
+                    $results[$retpath . '.ics']['contentlength'] = 1;
+                }
+                if (in_array('modified', $properties)) {
+                    $results[$retpath]['modified'] = $_SERVER['REQUEST_TIME'];
+                    $results[$retpath . '.ics']['modified'] = $_SERVER['REQUEST_TIME'];
+                }
+                if (in_array('created', $properties)) {
+                    $results[$retpath]['created'] = 0;
+                    $results[$retpath . '.ics']['created'] = 0;
+                }
+            }
+            return $results;
+
+        } elseif (count($parts) == 2 &&
+            array_key_exists($parts[1],
+                Kronolith::listCalendars(false, PERMS_READ))) {
+                    //
+                    // This request is browsing into a specific calendar.  Generate the list
+                    // of items and represent them as files within the directory.
+                    //
+                    $kronolith_driver = Kronolith::getDriver(null, $parts[1]);
+                    $events = $kronolith_driver->listEvents();
+                    if (is_a($events, 'PEAR_Error')) {
+                        return $events;
+                    }
+
+                    $icon = $registry->getImageDir('horde') . '/mime/icalendar.png';
+                    $results = array();
+                    foreach ($events as $dayevents) {
+                        foreach ($dayevents as $event) {
+                            $key = 'kronolith/' . $path . '/' . $event->getId();
+                            if (in_array('name', $properties)) {
+                                $results[$key]['name'] = $event->getTitle();
+                            }
+                            if (in_array('icon', $properties)) {
+                                $results[$key]['icon'] = $icon;
+                            }
+                            if (in_array('browseable', $properties)) {
+                                $results[$key]['browseable'] = false;
+                            }
+                            if (in_array('contenttype', $properties)) {
+                                $results[$key]['contenttype'] = 'text/calendar';
+                            }
+                            if (in_array('contentlength', $properties)) {
+                                // FIXME:  This is a hack.  If the content length is longer
+                                // than the actual data then some WebDAV clients will report
+                                // an error when the file EOF is received.  Ideally we should
+                                // determine the actual size of the data and report it here, but
+                                // the performance hit may be prohibitive.  This requires
+                                // further investigation.
+                                $results[$key]['contentlength'] = 1;
+                            }
+                            if (in_array('modified', $properties)) {
+                                $results[$key]['modified'] = $this->modified($event->getUID());
+                            }
+                            if (in_array('created', $properties)) {
+                                $results[$key]['created'] = $this->getActionTimestamp($event->getUID(), 'add');
+                            }
+                        }
+                    }
+                    return $results;
+                } else {
+                    //
+                    // The only valid request left is for either a specific event
+                    // or for the entire calendar.
+                    //
+                    if (count($parts) == 3 &&
+                        array_key_exists($parts[1],
+                            Kronolith::listCalendars(false, PERMS_READ))) {
+                                //
+                                // This request is for a specific item within a given calendar.
+                                //
+                                $event = Kronolith::getCalendar(null, $parts[1])->getEvent($parts[2]);
+                                if (is_a($event, 'PEAR_Error')) {
+                                    return $event;
+                                }
+
+                                $result = array(
+                                    'data' => $this->export($event->getUID(), 'text/calendar'),
+                                    'mimetype' => 'text/calendar');
+                                $modified = $this->modified($event->getUID());
+                                if (!empty($modified)) {
+                                    $result['mtime'] = $modified;
+                                }
+                                return $result;
+                            } elseif (count($parts) == 2 &&
+                                substr($parts[1], -4, 4) == '.ics' &&
+                                array_key_exists(
+                                    substr($parts[1], 0, -4),
+                                    Kronolith::listCalendars(false, PERMS_READ))) {
+                                        //
+                                        // This request is for an entire calendar (calendar.ics).
+                                        //
+                                        $ical_data = $this->exportCalendar(substr($parts[1], 0, -4), 'text/calendar');
+                                        $result = array('data'          => $ical_data,
+                                            'mimetype'      => 'text/calendar',
+                                            'contentlength' => strlen($ical_data),
+                                            'mtime'         => $_SERVER['REQUEST_TIME']);
+
+                                        return $result;
+                                    } else {
+                                        //
+                                        // All other requests are a 404: Not Found
+                                        //
+                                        return false;
+                                    }
+                }
+    }
+
+    /**
+     * Saves a file into the Kronolith tree.
+     *
+     * @param string $path          The path where to PUT the file.
+     * @param string $content       The file content.
+     * @param string $content_type  The file's content type.
+     *
+     * @return array  The event UIDs, or a PEAR_Error on failure.
+     */
+    public function put($path, $content, $content_type)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (substr($path, 0, 9) == 'kronolith') {
+            $path = substr($path, 9);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        if (count($parts) == 2 &&
+            substr($parts[1], -4) == '.ics') {
+
+                // Workaround for WebDAV clients that are not smart enough to send
+                // the right content type.  Assume text/calendar.
+                if ($content_type == 'application/octet-stream') {
+                    $content_type = 'text/calendar';
+                }
+                $calendar = substr($parts[1], 0, -4);
+            } elseif (count($parts) == 3) {
+                $calendar = $parts[1];
+
+                // Workaround for WebDAV clients that are not smart enough to send
+                // the right content type.  Assume text/calendar.
+                if ($content_type == 'application/octet-stream') {
+                    $content_type = 'text/calendar';
+                }
+            } else {
+                return PEAR::raiseError("Invalid calendar data supplied.");
+            }
+
+        if (!array_key_exists($calendar, Kronolith::listCalendars(false, PERMS_EDIT))) {
+            // FIXME: Should we attempt to create a calendar based on the filename
+            // in the case that the requested calendar does not exist?
+            return PEAR::raiseError("Calendar does not exist or no permission to edit");
+        }
+
+        // Store all currently existings UIDs. Use this info to delete UIDs not
+        // present in $content after processing.
+        $ids = array();
+        $uids_remove = array_flip($this->listUids($calendar));
+
+        switch ($content_type) {
+        case 'text/calendar':
+        case 'text/x-vcalendar':
+            $iCal = new Horde_iCalendar();
+            if (!is_a($content, 'Horde_iCalendar_vevent')) {
+                if (!$iCal->parsevCalendar($content)) {
+                    return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+                }
+            } else {
+                $iCal->addComponent($content);
+            }
+
+            $kronolith_driver = Kronolith::getDriver();
+            foreach ($iCal->getComponents() as $content) {
+                if (is_a($content, 'Horde_iCalendar_vevent')) {
+                    $event = $kronolith_driver->getEvent();
+                    $event->fromiCalendar($content);
+                    $event->setCalendar($calendar);
+                    $uid = $event->getUID();
+                    // Remove from uids_remove list so we won't delete in
+                    // the end.
+                    if (isset($uids_remove[$uid])) {
+                        unset($uids_remove[$uid]);
+                    }
+                    $existing_event = $kronolith_driver->getByUID($uid, array($calendar));
+                    if (!is_a($existing_event, 'PEAR_Error')) {
+                        // Check if our event is newer then the existing - get the
+                        // event's history.
+                        $history = &Horde_History::singleton();
+                        $created = $modified = null;
+                        $log = $history->getHistory('kronolith:' . $calendar . ':'
+                            . $uid);
+                        if ($log && !is_a($log, 'PEAR_Error')) {
+                            foreach ($log->getData() as $entry) {
+                                switch ($entry['action']) {
+                                case 'add':
+                                    $created = $entry['ts'];
+                                    break;
+
+                                case 'modify':
+                                    $modified = $entry['ts'];
+                                    break;
+                                }
+                            }
+                        }
+                        if (empty($modified) && !empty($created)) {
+                            $modified = $created;
+                        }
+                        if (!empty($modified) &&
+                            $modified >= $content->getAttribute('LAST-MODIFIED')) {
+                                // LAST-MODIFIED timestamp of existing entry is newer:
+                                // don't replace it.
+                                continue;
+                            }
+
+                        // Don't change creator/owner.
+                        $event->setCreatorId($existing_event->getCreatorId());
+                    }
+
+                    // Save entry.
+                    $saved = $event->save();
+                    if (is_a($saved, 'PEAR_Error')) {
+                        return $saved;
+                    }
+                    $ids[] = $event->getUID();
+                }
+            }
+            break;
+
+        default:
+            return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $content_type));
+        }
+
+        if (array_key_exists($calendar, Kronolith::listCalendars(false, PERMS_DELETE))) {
+            foreach (array_keys($uids_remove) as $uid) {
+                $this->delete($uid);
+            }
+        }
+
+        return $ids;
+    }
+
+    /**
+     * Deletes a file from the Kronolith tree.
+     *
+     * @param string $path  The path to the file.
+     *
+     * @return mixed  The event's UID, or a PEAR_Error on failure.
+     */
+    public function path_delete($path)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (substr($path, 0, 9) == 'kronolith') {
+            $path = substr($path, 9);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        if (substr($parts[1], -4) == '.ics') {
+            $calendarId = substr($parts[1], 0, -4);
+        } else {
+            $calendarId = $parts[1];
+        }
+
+        if (!(count($parts) == 2 || count($parts) == 3) ||
+            !array_key_exists($calendarId, Kronolith::listCalendars(false, PERMS_DELETE))) {
+                return PEAR::raiseError("Calendar does not exist or no permission to delete");
+            }
+
+        if (count($parts) == 3) {
+            // Delete just a single entry
+            return Kronolith::getDriver(null, $calendarId)->deleteEvent($parts[2]);
+        } else {
+            // Delete the entire calendar
+            $result = Kronolith::getDriver()->delete($calendarId);
+            if (is_a($result, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Unable to delete calendar \"%s\": %s"), $calendarId, $result->getMessage()));
+            } else {
+                // Remove share and all groups/permissions.
+                $share = $GLOBALS['kronolith_shares']->getShare($calendarId);
+                $result = $GLOBALS['kronolith_shares']->removeShare($share);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns all calendars a user has access to, according to several
+     * parameters/permission levels.
+     *
+     * @param boolean $owneronly   Only return calenders that this user owns?
+     *                             Defaults to false.
+     * @param integer $permission  The permission to filter calendars by.
+     *
+     * @return array  The calendar list.
+     */
+    public function listCalendars($owneronly = false, $permission = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+        if (is_null($permission)) {
+            $permission = PERMS_SHOW;
+        }
+        return array_keys(Kronolith::listCalendars($owneronly, $permission));
+    }
+
+    /**
+     * Returns the ids of all the events that happen within a time period.
+     *
+     * @param string $calendar      The calendar to check for events.
+     * @param object $startstamp    The start of the time range.
+     * @param object $endstamp      The end of the time range.
+     *
+     * @return array  The event ids happening in this time period.
+     */
+    public function listUids($calendar = null, $startstamp = 0, $endstamp = 0)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (empty($calendar)) {
+            $calendar = Kronolith::getDefaultCalendar();
+        }
+        if (!array_key_exists($calendar,
+            Kronolith::listCalendars(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $driver = Kronolith::getDriver(null, $calendar);
+        if (is_a($driver, 'PEAR_Error')) {
+            return $driver;
+        }
+        $events = $driver->listEvents(new Horde_Date($startstamp),
+            new Horde_Date($endstamp));
+        if (is_a($events, 'PEAR_Error')) {
+            return $events;
+        }
+
+        $uids = array();
+        foreach ($events as $dayevents) {
+            foreach ($dayevents as $event) {
+                $uids[] = $event->getUID();
+            }
+        }
+
+        return $uids;
+    }
+
+    /**
+     * Returns an array of UIDs for events that have had $action happen since
+     * $timestamp.
+     *
+     * @param string  $action     The action to check for - add, modify, or delete.
+     * @param integer $timestamp  The time to start the search.
+     * @param string  $calendar   The calendar to search in.
+     *
+     * @return array  An array of UIDs matching the action and time criteria.
+     */
+    public function listBy($action, $timestamp, $calendar = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (empty($calendar)) {
+            $calendar = Kronolith::getDefaultCalendar();
+        }
+
+        if (!array_key_exists($calendar,
+            Kronolith::listCalendars(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $history = &Horde_History::singleton();
+        $histories = $history->getByTimestamp('>', $timestamp, array(array('op' => '=', 'field' => 'action', 'value' => $action)), 'kronolith:' . $calendar);
+        if (is_a($histories, 'PEAR_Error')) {
+            return $histories;
+        }
+
+        // Strip leading kronolith:username:.
+        return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
+    }
+
+    /**
+     * Returns the timestamp of an operation for a given uid an action
+     *
+     * @param string $uid      The uid to look for.
+     * @param string $action   The action to check for - add, modify, or delete.
+     * @param string $calendar The calendar to search in.
+     *
+     * @return integer  The timestamp for this action.
+     */
+    public function getActionTimestamp($uid, $action, $calendar = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (empty($calendar)) {
+            $calendar = Kronolith::getDefaultCalendar();
+        }
+
+        if (!array_key_exists($calendar,
+            Kronolith::listCalendars(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $history = &Horde_History::singleton();
+        return $history->getActionTimestamp('kronolith:' . $calendar . ':' .
+            $uid, $action);
+    }
+
+    /**
+     * Imports an event represented in the specified content type.
+     *
+     * @param string $content      The content of the event.
+     * @param string $contentType  What format is the data in? Currently supports:
+     *                             <pre>
+     *                             text/calendar
+     *                             text/x-vcalendar
+     *                             </pre>
+     * @param string $calendar     What calendar should the event be added to?
+     *
+     * @return mixed  The event's UID, or a PEAR_Error on failure.
+     */
+    public function import($content, $contentType, $calendar = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!isset($calendar)) {
+            $calendar = Kronolith::getDefaultCalendar(PERMS_EDIT);
+        }
+        if (!array_key_exists($calendar,
+            Kronolith::listCalendars(false, PERMS_EDIT))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        switch ($contentType) {
+        case 'text/calendar':
+        case 'text/x-vcalendar':
+            $iCal = new Horde_iCalendar();
+            if (!is_a($content, 'Horde_iCalendar_vevent')) {
+                if (!$iCal->parsevCalendar($content)) {
+                    return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+                }
+            } else {
+                $iCal->addComponent($content);
+            }
+
+            $components = $iCal->getComponents();
+            if (count($components) == 0) {
+                return PEAR::raiseError(_("No iCalendar data was found."));
+            }
+
+            $kronolith_driver = Kronolith::getDriver(null, $calendar);
+            $ids = array();
+            foreach ($components as $content) {
+                if (is_a($content, 'Horde_iCalendar_vevent')) {
+                    $event = $kronolith_driver->getEvent();
+                    $event->fromiCalendar($content);
+                    $event->setCalendar($calendar);
+                    // Check if the entry already exists in the data source, first
+                    // by UID.
+                    $uid = $event->getUID();
+                    $existing_event = $kronolith_driver->getByUID($uid, array($calendar));
+                    if (!is_a($existing_event, 'PEAR_Error')) {
+                        return PEAR::raiseError(_("Already Exists"),
+                            'horde.message', null, null, $uid);
+                    }
+                    $result = $kronolith_driver->search($event);
+                    // Check if the match really is an exact match:
+                    if (is_array($result) && count($result) > 0) {
+                        foreach($result as $match) {
+                            if ($match->start == $event->start &&
+                                $match->end == $event->end &&
+                                $match->title == $event->title &&
+                                $match->location == $event->location &&
+                                $match->hasPermission(PERMS_EDIT)) {
+                                    return PEAR::raiseError(_("Already Exists"), 'horde.message', null, null, $match->getUID());
+                                }
+                        }
+                    }
+
+                    $eventId = $event->save();
+                    if (is_a($eventId, 'PEAR_Error')) {
+                        return $eventId;
+                    }
+                    $ids[] = $event->getUID();
+                }
+            }
+            if (count($ids) == 0) {
+                return PEAR::raiseError(_("No iCalendar data was found."));
+            } else if (count($ids) == 1) {
+                return $ids[0];
+            }
+            return $ids;
+        }
+
+        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+    }
+
+    /**
+     * Exports an event, identified by UID, in the requested content type.
+     *
+     * @param string $uid         Identify the event to export.
+     * @param string $contentType  What format should the data be in?
+     *                            A string with one of:
+     *                            <pre>
+     *                             text/calendar (VCALENDAR 2.0. Recommended as
+     *                                            this is specified in rfc2445)
+     *                             text/x-vcalendar (old VCALENDAR 1.0 format.
+     *                                              Still in wide use)
+     *                            </pre>
+     *
+     * @return string  The requested data.
+     */
+    public function export($uid, $contentType)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+        global $kronolith_shares;
+
+        $event = Kronolith::getDriver()->getByUID($uid);
+        if (is_a($event, 'PEAR_Error')) {
+            return $event;
+        }
+        if (!$event->hasPermission(PERMS_READ)) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        $version = '2.0';
+        switch ($contentType) {
+        case 'text/x-vcalendar':
+            $version = '1.0';
+        case 'text/calendar':
+            $share = &$kronolith_shares->getShare($event->getCalendar());
+
+            $iCal = new Horde_iCalendar($version);
+            $iCal->setAttribute('X-WR-CALNAME', Horde_String::convertCharset($share->get('name'), Horde_Nls::getCharset(), 'utf-8'));
+
+            // Create a new vEvent.
+            $vEvent = &$event->toiCalendar($iCal);
+            $iCal->addComponent($vEvent);
+
+            return $iCal->exportvCalendar();
+
+        }
+
+        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+    }
+
+    /**
+     * Exports a calendar in the requested content type.
+     *
+     * @param string $calendar    The calendar to export.
+     * @param string $contentType  What format should the data be in?
+     *                             A string with one of:
+     *                             <pre>
+     *                             text/calendar (VCALENDAR 2.0. Recommended as
+     *                                            this is specified in rfc2445)
+     *                             text/x-vcalendar (old VCALENDAR 1.0 format.
+     *                                              Still in wide use)
+     *                             </pre>
+     *
+     * @return string  The iCalendar representation of the calendar.
+     */
+    public function exportCalendar($calendar, $contentType)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+        global $kronolith_shares;
+
+        if (!array_key_exists($calendar,
+            Kronolith::listCalendars(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $kronolith_driver = Kronolith::getDriver(null, $calendar);
+        $events = $kronolith_driver->listEvents();
+
+        $version = '2.0';
+        switch ($contentType) {
+        case 'text/x-vcalendar':
+            $version = '1.0';
+        case 'text/calendar':
+            $share = &$kronolith_shares->getShare($calendar);
+
+            $iCal = new Horde_iCalendar($version);
+            $iCal->setAttribute('X-WR-CALNAME', Horde_String::convertCharset($share->get('name'), Horde_Nls::getCharset(), 'utf-8'));
+
+            foreach ($events as $dayevents) {
+                foreach ($dayevents as $event) {
+                    $vEvent = &$event->toiCalendar($iCal);
+                    $iCal->addComponent($vEvent);
+                }
+            }
+
+            return $iCal->exportvCalendar();
+        }
+
+        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+    }
+
+    /**
+     * Deletes an event identified by UID.
+     *
+     * @param string|array $uid  A single UID or an array identifying the event(s)
+     *                           to delete.
+     *
+     * @return boolean  Success or failure.
+     */
+    public function delete($uid)
+    {
+        // Handle an array of UIDs for convenience of deleting multiple events at
+        // once.
+        if (is_array($uid)) {
+            foreach ($uid as $g) {
+                $result = $this->delete($g);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+
+            return true;
+        }
+
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        $kronolith_driver = Kronolith::getDriver();
+        $events = $kronolith_driver->getByUID($uid, null, true);
+        if (is_a($events, 'PEAR_Error')) {
+            return $events;
+        }
+
+        $event = null;
+        if (Horde_Auth::isAdmin()) {
+            $event = $events[0];
+        }
+
+        // First try the user's own calendars.
+        if (empty($event)) {
+            $ownerCalendars = Kronolith::listCalendars(true, PERMS_DELETE);
+            foreach ($events as $ev) {
+                if (Horde_Auth::isAdmin() || isset($ownerCalendars[$ev->getCalendar()])) {
+                    $event = $ev;
+                    break;
+                }
+            }
+        }
+
+        // If not successful, try all calendars the user has access to.
+        if (empty($event)) {
+            $deletableCalendars = Kronolith::listCalendars(false, PERMS_DELETE);
+            foreach ($events as $ev) {
+                if (isset($deletableCalendars[$ev->getCalendar()])) {
+                    $kronolith_driver->open($ev->getCalendar());
+                    $event = $ev;
+                    break;
+                }
+            }
+        }
+
+        if (empty($event)) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        return $kronolith_driver->deleteEvent($event->getId());
+    }
+
+    /**
+     * Replaces the event identified by UID with the content represented in the
+     * specified contentType.
+     *
+     * @param string $uid          Idenfity the event to replace.
+     * @param mixed  $content      The content of the event. String or
+     *                             Horde_iCalendar_vevent
+     * @param string $contentType  What format is the data in? Currently supports:
+     *                             text/calendar
+     *                             text/x-vcalendar
+     *                             (Ignored if content is Horde_iCalendar_vevent)
+     *
+     * @return mixed  True on success, PEAR_Error otherwise.
+     */
+    public function replace($uid, $content, $contentType)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        $event = Kronolith::getDriver()->getByUID($uid);
+        if (is_a($event, 'PEAR_Error')) {
+            return $event;
+        }
+
+        if (!$event->hasPermission(PERMS_EDIT) ||
+            ($event->isPrivate() && $event->getCreatorId() != Horde_Auth::getAuth())) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        if (is_a($content, 'Horde_iCalendar_vevent')) {
+            $component = $content;
+        } else {
+            switch ($contentType) {
+            case 'text/calendar':
+            case 'text/x-vcalendar':
+                if (!is_a($content, 'Horde_iCalendar_vevent')) {
+                    $iCal = new Horde_iCalendar();
+                    if (!$iCal->parsevCalendar($content)) {
+                        return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+                    }
+
+                    $components = $iCal->getComponents();
+                    $component = null;
+                    foreach ($components as $content) {
+                        if (is_a($content, 'Horde_iCalendar_vevent')) {
+                            if ($component !== null) {
+                                return PEAR::raiseError(_("Multiple iCalendar components found; only one vEvent is supported."));
+                            }
+                            $component = $content;
+                        }
+
+                    }
+                    if ($component === null) {
+                        return PEAR::raiseError(_("No iCalendar data was found."));
+                    }
+                }
+                break;
+
+            default:
+                return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+            }
+        }
+
+        $event->fromiCalendar($component);
+        // Ensure we keep the original UID, even when content does not
+        // contain one and fromiCalendar creates a new one.
+        $event->setUID($uid);
+        $eventId = $event->save();
+
+        return is_a($eventId, 'PEAR_Error') ? $eventId : true;
+    }
+
+    /**
+     * Generates free/busy information for a given time period.
+     *
+     * @param integer $startstamp  The start of the time period to retrieve.
+     * @param integer $endstamp    The end of the time period to retrieve.
+     * @param string $calendar     The calendar to view free/busy slots for.
+     *                             Defaults to the user's default calendar.
+     *
+     * @return Horde_iCalendar_vfreebusy  A freebusy object that covers the
+     *                                    specified time period.
+     */
+    public function getFreeBusy($startstamp = null, $endstamp = null,
+                                $calendar = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (is_null($calendar)) {
+            $calendar = Kronolith::getDefaultCalendar();
+        }
+        // Free/Busy information is globally available; no permission
+        // check is needed.
+
+        return Kronolith_FreeBusy::generate($calendar, $startstamp, $endstamp, true);
+    }
+
+    /**
+     * Retrieves a Kronolith_Event object, given an event UID.
+     *
+     * @param string $uid  The event's UID.
+     *
+     * @return Kronolith_Event  A valid Kronolith_Event on success, or a PEAR_Error
+     *                          on failure.
+     */
+    public function eventFromUID($uid)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        $event = Kronolith::getDriver()->getByUID($uid);
+        if (is_a($event, 'PEAR_Error')) {
+            return $event;
+        }
+        if (!$event->hasPermission(PERMS_SHOW)) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        return $event;
+    }
+
+    /**
+     * Updates an attendee's response status for a specified event.
+     *
+     * @param Horde_iCalender_vevent $response  A Horde_iCalender_vevent object,
+     *                                          with a valid UID attribute that
+     *                                          points to an existing event.
+     *                                          This is typically the vEvent
+     *                                          portion of an iTip meeting-request
+     *                                          response, with the attendee's
+     *                                          response in an ATTENDEE parameter.
+     * @param string $sender                    The email address of the person
+     *                                          initiating the update. Attendees
+     *                                          are only updated if this address
+     *                                          matches.
+     *
+     * @return mixed  True on success, PEAR_Error on failure.
+     */
+    public function updateAttendee($response, $sender = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        $uid = $response->getAttribute('UID');
+        if (is_a($uid, 'PEAR_Error')) {
+            return $uid;
+        }
+
+        $events = Kronolith::getDriver()->getByUID($uid, null, true);
+        if (is_a($events, 'PEAR_Error')) {
+            return $events;
+        }
+
+        /* First try the user's own calendars. */
+        $ownerCalendars = Kronolith::listCalendars(true, PERMS_EDIT);
+        $event = null;
+        foreach ($events as $ev) {
+            if (isset($ownerCalendars[$ev->getCalendar()])) {
+                $event = $ev;
+                break;
+            }
+        }
+
+        /* If not successful, try all calendars the user has access to. */
+        if (empty($event)) {
+            $editableCalendars = Kronolith::listCalendars(false, PERMS_EDIT);
+            foreach ($events as $ev) {
+                if (isset($editableCalendars[$ev->getCalendar()])) {
+                    $event = $ev;
+                    break;
+                }
+            }
+        }
+
+        if (empty($event) ||
+            ($event->isPrivate() && $event->getCreatorId() != Horde_Auth::getAuth())) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $atnames = $response->getAttribute('ATTENDEE');
+        if (!is_array($atnames)) {
+            $atnames = array($atnames);
+        }
+        $atparms = $response->getAttribute('ATTENDEE', true);
+
+        $found = false;
+        $error = _("No attendees have been updated because none of the provided email addresses have been found in the event's attendees list.");
+        $sender_lcase = Horde_String::lower($sender);
+        foreach ($atnames as $index => $attendee) {
+            $attendee = str_replace('mailto:', '', Horde_String::lower($attendee));
+            $name = isset($atparms[$index]['CN']) ? $atparms[$index]['CN'] : null;
+            if ($event->hasAttendee($attendee)) {
+                if (is_null($sender) || $sender_lcase == $attendee) {
+                    $event->addAttendee($attendee, Kronolith::PART_IGNORE, Kronolith::responseFromICal($atparms[$index]['PARTSTAT']), $name);
+                    $found = true;
+                } else {
+                    $error = _("The attendee hasn't been updated because the update was not sent from the attendee.");
+                }
+            }
+        }
+
+        $result = $event->save();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (!$found) {
+            return PEAR::raiseError($error);
+        }
+
+        return true;
+    }
+
+    /**
+     * Lists events for a given time period.
+     *
+     * @param integer $startstamp      The start of the time period to retrieve.
+     * @param integer $endstamp        The end of the time period to retrieve.
+     * @param array   $calendars       The calendars to view events from.
+     *                                 Defaults to the user's default calendar.
+     * @param boolean $showRecurrence  Return every instance of a recurring event?
+     *                                 If false, will only return recurring events
+     *                                 once inside the $startDate - $endDate range.
+     * @param boolean $alarmsOnly      Filter results for events with alarms.
+     *                                 Defaults to false.
+     *
+     * @return array  A list of event hashes.
+     */
+    public function listEvents($startstamp = null, $endstamp = null,
+        $calendars = null, $showRecurrence = true,
+        $alarmsOnly = false)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!isset($calendars)) {
+            $calendars = array($GLOBALS['prefs']->getValue('default_share'));
+        } elseif (!is_array($calendars)) {
+            $calendars = array($calendars);
+        }
+        $allowed_calendars = Kronolith::listCalendars(false, PERMS_READ);
+        foreach ($calendars as $calendar) {
+            if (!array_key_exists($calendar, $allowed_calendars)) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+        }
+
+        return Kronolith::listEvents(new Horde_Date($startstamp),
+            new Horde_Date($endstamp),
+            $calendars, $showRecurrence, $alarmsOnly);
+    }
+
+    /**
+     * Lists alarms for a given moment.
+     *
+     * @param integer $time  The time to retrieve alarms for.
+     * @param string $user   The user to retrieve alarms for. All users if null.
+     *
+     * @return array  An array of UIDs
+     */
+    public function listAlarms($time, $user = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+        require_once 'Horde/Group.php';
+
+        $current_user = Horde_Auth::getAuth();
+        if ((empty($user) || $user != $current_user) && !Horde_Auth::isAdmin()) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        $group = &Group::singleton();
+        $alarm_list = array();
+        $time = new Horde_Date($time);
+        $calendars = is_null($user) ? array_keys($GLOBALS['kronolith_shares']->listAllShares()) : $GLOBALS['display_calendars'];
+        $alarms = Kronolith::listAlarms($time, $calendars, true);
+        if (is_a($alarms, 'PEAR_Error')) {
+            return $alarms;
+        }
+        foreach ($alarms as $calendar => $cal_alarms) {
+            $share = $GLOBALS['kronolith_shares']->getShare($calendar);
+            if (is_a($share, 'PEAR_Error')) {
+                continue;
+            }
+            if (empty($user)) {
+                $users = $share->listUsers(PERMS_READ);
+                $groups = $share->listGroups(PERMS_READ);
+                foreach ($groups as $gid) {
+                    $group_users = $group->listUsers($gid);
+                    if (!is_a($group_users, 'PEAR_Error')) {
+                        $users = array_merge($users, $group_users);
+                    }
+                }
+                $users = array_unique($users);
+            } else {
+                $users = array($user);
+            }
+            $owner = $share->get('owner');
+            foreach ($cal_alarms as $event) {
+                foreach ($users as $alarm_user) {
+                    if ($alarm_user == $current_user) {
+                        $prefs = &$GLOBALS['prefs'];
+                    } else {
+                        $prefs = &Prefs::singleton($GLOBALS['conf']['prefs']['driver'],
+                            'kronolith', $alarm_user, null,
+                            null, false);
+                    }
+                    $shown_calendars = unserialize($prefs->getValue('display_cals'));
+                    $reminder = $prefs->getValue('event_reminder');
+                    if (($reminder == 'owner' && $alarm_user == $owner) ||
+                        ($reminder == 'show' && in_array($calendar, $shown_calendars)) ||
+                        $reminder == 'read') {
+                            Horde_Nls::setLanguageEnvironment($prefs->getValue('language'));
+                            $alarm = $event->toAlarm($time, $alarm_user, $prefs);
+                            if ($alarm) {
+                                $alarm_list[] = $alarm;
+                            }
+                        }
+                }
+            }
+        }
+
+        return $alarm_list;
+    }
+
+    /**
+     * Subscribe to a calendar.
+     *
+     * @param array $calendar  Calendar description hash, with required 'type'
+     *                         parameter. Currently supports 'http' and 'webcal'
+     *                         for remote calendars.
+     */
+    public function subscribe($calendar)
+    {
+        if (!isset($calendar['type'])) {
+            return PEAR::raiseError(_("Unknown calendar protocol"));
+        }
+
+        switch ($calendar['type']) {
+        case 'http':
+        case 'webcal':
+            $username = isset($calendar['username']) ? $calendar['username'] : null;
+            $password = isset($calendar['password']) ? $calendar['password'] : null;
+
+            $cals = unserialize($GLOBALS['prefs']->getValue('remote_cals'));
+            if (!is_array($cals)) {
+                $cals = array();
+            }
+            $array_key = count($cals);
+            foreach ($cals as $key => $cal) {
+                if ($cal['url'] == $calendar['url']) {
+                    $array_key = $key;
+                    break;
+                }
+            }
+
+            $cals[$array_key] = array('name' => $calendar['name'],
+                'url'  => $calendar['url'],
+                'user' => $username,
+                'password' => $password);
+            $GLOBALS['prefs']->setValue('remote_cals', serialize($cals));
+            break;
+
+        case 'external':
+            $cals = unserialize($GLOBALS['prefs']->getValue('display_external_cals'));
+            if (array_search($calendar['name'], $cals) === false) {
+                $cals[] = $calendar['name'];
+                $GLOBALS['prefs']->setValue('display_external_cals', serialize($cals));
+            }
+
+        default:
+            return PEAR::raiseError(_("Unknown calendar protocol"));
+        }
+    }
+
+    /**
+     * Unsubscribe from a calendar.
+     *
+     * @param array $calendar  Calendar description array, with required 'type'
+     *                         parameter. Currently supports 'http' and 'webcal'
+     *                         for remote calendars.
+     */
+    public function unsubscribe($calendar)
+    {
+        if (!isset($calendar['type'])) {
+            return PEAR::raiseError('Unknown calendar specification');
+        }
+
+        switch ($calendar['type']) {
+        case 'http':
+        case 'webcal':
+            $cals = unserialize($GLOBALS['prefs']->getValue('remote_cals'));
+            foreach ($cals as $key => $cal) {
+                if ($cal['url'] == $calendar['url']) {
+                    unset($cals[$key]);
+                    break;
+                }
+            }
+
+            $GLOBALS['prefs']->setValue('remote_cals', serialize($cals));
+            break;
+
+        case 'external':
+            $cals = unserialize($GLOBALS['prefs']->getValue('display_external_cals'));
+            if (($key = array_search($calendar['name'], $cals)) !== false) {
+                unset($cals[$key]);
+                $GLOBALS['prefs']->setValue('display_external_cals', serialize($cals));
+            }
+
+        default:
+            return PEAR::raiseError('Unknown calendar specification');
+        }
+    }
+
+
+    /**
+     * Places an exclusive lock for a calendar or an event.
+     *
+     * @param array $calendar  The calendar to lock
+     * @param array $event     The event to lock
+     *
+     * @return mixed   A lock ID on success, PEAR_Error on failure, false if:
+     *                   - The calendar is already locked
+     *                   - The event is already locked
+     *                   - A calendar lock was requested and an event is already
+     *                     locked in the calendar
+     */
+    public function lock($calendar, $event = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!array_key_exists($calendar,
+            Kronolith::listCalendars(false, PERMS_EDIT))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+        $share = &$GLOBALS['kronolith_shares']->getShare($calendar);
+
+        return $share->lock($calendar, $event);
+    }
+
+    /**
+     * Releases a lock.
+     *
+     * @param array $calendar  The event to lock.
+     * @param array $lockid    The lock id to unlock.
+     */
+    public function unlock($calendar, $lockid)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!array_key_exists($calendar,
+            Kronolith::listCalendars(false, PERMS_EDIT))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+        $share = &$GLOBALS['kronolith_shares']->getShare($calendar);
+
+        return $share->unlock($lockid);
+    }
+
+    /**
+     * Check for existing calendar or event locks.
+     *
+     * @param array $calendar  The calendar to check locks for.
+     * @param array $event     The event to check locks for.
+     */
+    public function checkLocks($calendar, $event = null)
+    {
+        $no_maint = true;
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!array_key_exists($calendar,
+            Kronolith::listCalendars(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+        $share = &$GLOBALS['kronolith_shares']->getShare($calendar);
+
+        return $share->checkLocks($event);
+    }
+
+    /**
+     *
+     * @return array  A list of calendars used to display free/busy information
+     */
+    public function getFbCalendars()
+    {
+        return (unserialize($GLOBALS['prefs']->getValue('fb_cals')));
+    }
+
+}
diff --git a/kronolith/lib/api.php b/kronolith/lib/api.php
deleted file mode 100644 (file)
index f388683..0000000
+++ /dev/null
@@ -1,1536 +0,0 @@
-<?php
-/**
- * Kronolith external API interface.
- *
- * This file defines Kronolith's external API interface. Other applications
- * can interact with Kronolith through this API.
- *
- * @package Kronolith
- */
-
-$_services['show'] = array(
-    'link' => '%application%/event.php?calendar=|calendar|' .
-              '&eventID=|event|&uid=|uid|'
-);
-
-$_services['perms'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['removeUserData'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['shareHelp'] = array(
-    'args' => array(),
-    'type' => 'string');
-
-$_services['modified'] = array(
-    'args' => array('uid' => 'string'),
-    'type' => 'int',
-);
-
-$_services['browse'] = array(
-    'args' => array('path' => 'string', 'properties' => '{urn:horde}stringArray'),
-    'type' => '{urn:horde}hashHash',
-);
-
-$_services['put'] = array(
-    'args' => array('path' => 'string', 'content' => 'string', 'content_type' => 'string'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['path_delete'] = array(
-    'args' => array('path' => 'string'),
-    'type' => 'boolean',
-);
-
-$_services['getFreeBusy'] = array(
-    'args' => array('startstamp' => 'int', 'endstamp' => 'int', 'calendar' => 'string'),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['listCalendars'] = array(
-    'args' => array('owneronly' => 'boolean', 'permission' => 'int'),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['listEvents'] = array(
-    'args' => array('startstamp' => 'int', 'endstamp' => 'int', 'calendar' => 'string', 'showRecurrence' => 'string', 'alarmsOnly' => 'boolean'),
-    'type' => '{urn:horde}hashHash'
-);
-
-$_services['listAlarms'] = array(
-    'args' => array('time' => 'int', 'user' => 'string'),
-    'type' => '{urn:horde}hashHash'
-);
-
-$_services['list'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['listBy'] = array(
-    'args' => array('action' => 'string', 'timestamp' => 'int'),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['getActionTimestamp'] = array(
-    'args' => array('uid' => 'string', 'action' => 'string', 'calendar' => 'string'),
-    'type' => 'int',
-);
-
-$_services['import'] = array(
-    'args' => array('content' => 'string', 'contentType' => 'string', 'calendar' => 'string'),
-    'type' => 'string'
-);
-
-$_services['export'] = array(
-    'args' => array('uid' => 'string', 'contentType' => 'string'),
-    'type' => 'string'
-);
-
-$_services['exportCalendar'] = array(
-    'args' => array('calendar' => 'string', 'contentType' => 'string'),
-    'type' => 'string'
-);
-
-$_services['delete'] = array(
-    'args' => array('uid' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['replace'] = array(
-    'args' => array('uid' => 'string', 'content' => 'string', 'contentType' => 'string'),
-    'type' => 'boolean'
-);
-
-// FIXME: create complex type definition for SOAP calls.
-$_services['eventFromUID'] = array(
-    'args' => array('uid' => 'string'),
-    'type' => 'object'
-);
-
-// FIXME: create complex type definition for SOAP calls.
-$_services['updateAttendee'] = array(
-    'args' => array('response' => 'object', 'sender' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['subscribe'] = array(
-    'args' => array('calendar' => '{urn:horde}stringArray'),
-    'type' => 'boolean',
-);
-
-$_services['unsubscribe'] = array(
-    'args' => array('calendar' => '{urn:horde}stringArray'),
-    'type' => 'boolean',
-);
-
-$_services['lock'] = array(
-    'args' => array('calendar' => 'string', 'event' => 'string'),
-    'type' => 'string'
-);
-
-$_services['unlock'] = array(
-    'args' => array('calendar' => 'string', 'lockid' => 'string'),
-    'type' => 'string'
-);
-
-$_services['checkLocks'] = array(
-    'args' => array('calendar' => 'string', 'event' => 'string'),
-    'type' => 'string'
-);
-
-$_services['getFbCalendars'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray'
-);
-
-/**
- * Returns a list of available permissions.
- *
- * @return array  An array describing all available permissions.
- */
-function _kronolith_perms()
-{
-    $perms = array();
-    $perms['tree']['kronolith']['max_events'] = false;
-    $perms['title']['kronolith:max_events'] = _("Maximum Number of Events");
-    $perms['type']['kronolith:max_events'] = 'int';
-
-    return $perms;
-}
-
-/**
- * Removes user data.
- *
- * @param string $user  Name of user to remove data for.
- *
- * @return mixed  true on success | PEAR_Error on failure
- */
-function _kronolith_removeUserData($user)
-{
-    if (!Horde_Auth::isAdmin() && $user != Horde_Auth::getAuth()) {
-        return PEAR::raiseError(_("You are not allowed to remove user data."));
-    }
-
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* Remove all events owned by the user in all calendars. */
-    $result = Kronolith::getDriver()->removeUserData($user);
-
-    /* Now delete history as well. */
-    $history = &Horde_History::singleton();
-    if (method_exists($history, 'removeByParent')) {
-        $histories = $history->removeByParent('kronolith:' . $user);
-    } else {
-        /* Remove entries 100 at a time. */
-        $all = $history->getByTimestamp('>', 0, array(), 'kronolith:' . $user);
-        if (is_a($all, 'PEAR_Error')) {
-            Horde::logMessage($all, __FILE__, __LINE__, PEAR_LOG_ERR);
-        } else {
-            $all = array_keys($all);
-            while (count($d = array_splice($all, 0, 100)) > 0) {
-                $history->removebyNames($d);
-            }
-        }
-    }
-
-    if (is_a($result, 'PEAR_Error')) {
-        return $result;
-    }
-
-    /* Get the user's default share */
-    $share = $GLOBALS['kronolith_shares']->getShare($user);
-    if (is_a($share, 'PEAR_Error')) {
-        Horde::logMessage($share, __FILE__, __LINE__, PEAR_LOG_ERR);
-    } else {
-        $result = $GLOBALS['kronolith_shares']->removeShare($share);
-        if (is_a($result, 'PEAR_Error')) {
-            $hasError = true;
-            Horde::logMessage($result->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
-        }
-    }
-
-    /* Get a list of all shares this user has perms to and remove the perms */
-    $shares = $GLOBALS['kronolith_shares']->listShares($user);
-    if (is_a($shares, 'PEAR_Error')) {
-        Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
-    }
-    foreach ($shares as $share) {
-        $share->removeUser($user);
-    }
-
-    return true;
-}
-
-/**
- * Returns the share helper prefix
- *
- * @return string
- */
-function _kronolith_shareHelp()
-{
-    return 'shares';
-}
-
-/**
- * Returns the last modification timestamp for the given uid.
- *
- * @param string $uid      The uid to look for.
- *
- * @return integer  The timestamp for the last modification of $uid.
- */
-function _kronolith_modified($uid)
-{
-    $modified = _kronolith_getActionTimestamp($uid, 'modify');
-    if (empty($modified)) {
-        $modified = _kronolith_getActionTimestamp($uid, 'add');
-    }
-    return $modified;
-}
-
-/**
- * Browse through Kronolith's object tree.
- *
- * @param string $path       The level of the tree to browse.
- * @param array $properties  The item properties to return. Defaults to 'name',
- *                           'icon', and 'browseable'.
- *
- * @return array  The contents of $path
- */
-function _kronolith_browse($path = '', $properties = array())
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-    global $registry;
-
-    // Default properties.
-    if (!$properties) {
-        $properties = array('name', 'icon', 'browseable');
-    }
-
-    if (substr($path, 0, 9) == 'kronolith') {
-        $path = substr($path, 9);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    if (empty($path)) {
-        //
-        // This request is for a list of all users who have calendars visible
-        // to the requesting user.
-        //
-        $calendars = Kronolith::listCalendars(false, PERMS_READ);
-        $owners = array();
-        foreach ($calendars as $calendar) {
-            $owners[$calendar->get('owner')] = true;
-        }
-
-        $results = array();
-        foreach (array_keys($owners) as $owner) {
-            $path = 'kronolith/' . $owner;
-            if (in_array('name', $properties)) {
-                $results[$path]['name'] = $owner;
-            }
-            if (in_array('icon', $properties)) {
-                $results[$path]['icon'] =
-                    $registry->getImageDir('horde') . '/user.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results[$path]['browseable'] = true;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results[$path]['contenttype'] =
-                    'httpd/unix-directory';
-            }
-            if (in_array('contentlength', $properties)) {
-                $results[$path]['contentlength'] = 0;
-            }
-            if (in_array('modified', $properties)) {
-                $results[$path]['modified'] =
-                    $_SERVER['REQUEST_TIME'];
-            }
-            if (in_array('created', $properties)) {
-                $results[$path]['created'] = 0;
-            }
-
-            // CalDAV Properties from RFC 4791 and
-            // draft-desruisseaux-caldav-sched-03
-            $caldavns = 'urn:ietf:params:xml:ns:caldav';
-            $kronolith_rpc_base = $GLOBALS['registry']->get('webroot', 'horde') . '/rpc/kronolith/';
-            if (in_array($caldavns . ':calendar-home-set', $properties)) {
-                $results[$path][$caldavns . ':calendar-home-set'] =  Horde::url($kronolith_rpc_base . urlencode($owner), true);
-            }
-
-            if (in_array($caldavns . ':calendar-user-address-set', $properties)) {
-                // FIXME: Add the calendar owner's email address from their Horde Identity
-            }
-        }
-        return $results;
-
-    } elseif (count($parts) == 1) {
-        //
-        // This request is for all calendars owned by the requested user
-        //
-        $calendars = $GLOBALS['kronolith_shares']->listShares(Horde_Auth::getAuth(),
-                                                              PERMS_SHOW,
-                                                              $parts[0]);
-        $results = array();
-        foreach ($calendars as $calendarId => $calendar) {
-            $retpath = 'kronolith/' . $parts[0] . '/' . $calendarId;
-            if (in_array('name', $properties)) {
-                $results[$retpath]['name'] = sprintf(_("Events from %s"), $calendar->get('name'));
-                $results[$retpath . '.ics']['name'] = $calendar->get('name');
-            }
-            if (in_array('displayname', $properties)) {
-                $results[$retpath]['displayname'] = rawurlencode($calendar->get('name'));
-                $results[$retpath . '.ics']['displayname'] = rawurlencode($calendar->get('name')) . '.ics';
-            }
-            if (in_array('icon', $properties)) {
-                $results[$retpath]['icon'] = $registry->getImageDir() . '/kronolith.png';
-                $results[$retpath . '.ics']['icon'] = $registry->getImageDir() . '/mime/icalendar.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results[$retpath]['browseable'] = $calendar->hasPermission(Horde_Auth::getAuth(), PERMS_READ);
-                $results[$retpath . '.ics']['browseable'] = false;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results[$retpath]['contenttype'] = 'httpd/unix-directory';
-                $results[$retpath . '.ics']['contenttype'] = 'text/calendar';
-            }
-            if (in_array('contentlength', $properties)) {
-                $results[$retpath]['contentlength'] = 0;
-                // FIXME:  This is a hack.  If the content length is longer
-                // than the actual data then some WebDAV clients will report
-                // an error when the file EOF is received.  Ideally we should
-                // determine the actual size of the .ics and report it here, but
-                // the performance hit may be prohibitive.  This requires
-                // further investigation.
-                $results[$retpath . '.ics']['contentlength'] = 1;
-            }
-            if (in_array('modified', $properties)) {
-                $results[$retpath]['modified'] = $_SERVER['REQUEST_TIME'];
-                $results[$retpath . '.ics']['modified'] = $_SERVER['REQUEST_TIME'];
-            }
-            if (in_array('created', $properties)) {
-                $results[$retpath]['created'] = 0;
-                $results[$retpath . '.ics']['created'] = 0;
-            }
-        }
-        return $results;
-
-    } elseif (count($parts) == 2 &&
-              array_key_exists($parts[1],
-                               Kronolith::listCalendars(false, PERMS_READ))) {
-        //
-        // This request is browsing into a specific calendar.  Generate the list
-        // of items and represent them as files within the directory.
-        //
-        $kronolith_driver = Kronolith::getDriver(null, $parts[1]);
-        $events = $kronolith_driver->listEvents();
-        if (is_a($events, 'PEAR_Error')) {
-            return $events;
-        }
-
-        $icon = $registry->getImageDir('horde') . '/mime/icalendar.png';
-        $results = array();
-        foreach ($events as $dayevents) {
-            foreach ($dayevents as $event) {
-                $key = 'kronolith/' . $path . '/' . $event->getId();
-                if (in_array('name', $properties)) {
-                    $results[$key]['name'] = $event->getTitle();
-                }
-                if (in_array('icon', $properties)) {
-                    $results[$key]['icon'] = $icon;
-                }
-                if (in_array('browseable', $properties)) {
-                    $results[$key]['browseable'] = false;
-                }
-                if (in_array('contenttype', $properties)) {
-                    $results[$key]['contenttype'] = 'text/calendar';
-                }
-                if (in_array('contentlength', $properties)) {
-                    // FIXME:  This is a hack.  If the content length is longer
-                    // than the actual data then some WebDAV clients will report
-                    // an error when the file EOF is received.  Ideally we should
-                    // determine the actual size of the data and report it here, but
-                    // the performance hit may be prohibitive.  This requires
-                    // further investigation.
-                    $results[$key]['contentlength'] = 1;
-                }
-                if (in_array('modified', $properties)) {
-                    $results[$key]['modified'] = _kronolith_modified($event->getUID());
-                }
-                if (in_array('created', $properties)) {
-                    $results[$key]['created'] = _kronolith_getActionTimestamp($event->getUID(), 'add');
-                }
-            }
-        }
-        return $results;
-    } else {
-        //
-        // The only valid request left is for either a specific event
-        // or for the entire calendar.
-        //
-        if (count($parts) == 3 &&
-            array_key_exists($parts[1],
-                             Kronolith::listCalendars(false, PERMS_READ))) {
-            //
-            // This request is for a specific item within a given calendar.
-            //
-            $event = Kronolith::getCalendar(null, $parts[1])->getEvent($parts[2]);
-            if (is_a($event, 'PEAR_Error')) {
-                return $event;
-            }
-
-            $result = array(
-                'data' => _kronolith_export($event->getUID(), 'text/calendar'),
-                'mimetype' => 'text/calendar');
-            $modified = _kronolith_modified($event->getUID());
-            if (!empty($modified)) {
-                $result['mtime'] = $modified;
-            }
-            return $result;
-        } elseif (count($parts) == 2 &&
-                  substr($parts[1], -4, 4) == '.ics' &&
-                  array_key_exists(
-                      substr($parts[1], 0, -4),
-                      Kronolith::listCalendars(false, PERMS_READ))) {
-            //
-            // This request is for an entire calendar (calendar.ics).
-            //
-            $ical_data = _kronolith_exportCalendar(substr($parts[1], 0, -4), 'text/calendar');
-            $result = array('data'          => $ical_data,
-                            'mimetype'      => 'text/calendar',
-                            'contentlength' => strlen($ical_data),
-                            'mtime'         => $_SERVER['REQUEST_TIME']);
-
-            return $result;
-        } else {
-            //
-            // All other requests are a 404: Not Found
-            //
-            return false;
-        }
-    }
-}
-
-/**
- * Saves a file into the Kronolith tree.
- *
- * @param string $path          The path where to PUT the file.
- * @param string $content       The file content.
- * @param string $content_type  The file's content type.
- *
- * @return array  The event UIDs, or a PEAR_Error on failure.
- */
-function _kronolith_put($path, $content, $content_type)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (substr($path, 0, 9) == 'kronolith') {
-        $path = substr($path, 9);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    if (count($parts) == 2 &&
-        substr($parts[1], -4) == '.ics') {
-
-        // Workaround for WebDAV clients that are not smart enough to send
-        // the right content type.  Assume text/calendar.
-        if ($content_type == 'application/octet-stream') {
-            $content_type = 'text/calendar';
-        }
-        $calendar = substr($parts[1], 0, -4);
-    } elseif (count($parts) == 3) {
-        $calendar = $parts[1];
-
-        // Workaround for WebDAV clients that are not smart enough to send
-        // the right content type.  Assume text/calendar.
-        if ($content_type == 'application/octet-stream') {
-            $content_type = 'text/calendar';
-        }
-    } else {
-        return PEAR::raiseError("Invalid calendar data supplied.");
-    }
-
-    if (!array_key_exists($calendar, Kronolith::listCalendars(false, PERMS_EDIT))) {
-        // FIXME: Should we attempt to create a calendar based on the filename
-        // in the case that the requested calendar does not exist?
-        return PEAR::raiseError("Calendar does not exist or no permission to edit");
-    }
-
-    // Store all currently existings UIDs. Use this info to delete UIDs not
-    // present in $content after processing.
-    $ids = array();
-    $uids_remove = array_flip(_kronolith_list($calendar));
-
-    switch ($content_type) {
-    case 'text/calendar':
-    case 'text/x-vcalendar':
-        $iCal = new Horde_iCalendar();
-        if (!is_a($content, 'Horde_iCalendar_vevent')) {
-            if (!$iCal->parsevCalendar($content)) {
-                return PEAR::raiseError(_("There was an error importing the iCalendar data."));
-            }
-        } else {
-            $iCal->addComponent($content);
-        }
-
-        $kronolith_driver = Kronolith::getDriver();
-        foreach ($iCal->getComponents() as $content) {
-            if (is_a($content, 'Horde_iCalendar_vevent')) {
-                $event = $kronolith_driver->getEvent();
-                $event->fromiCalendar($content);
-                $event->setCalendar($calendar);
-                $uid = $event->getUID();
-                // Remove from uids_remove list so we won't delete in
-                // the end.
-                if (isset($uids_remove[$uid])) {
-                    unset($uids_remove[$uid]);
-                }
-                $existing_event = $kronolith_driver->getByUID($uid, array($calendar));
-                if (!is_a($existing_event, 'PEAR_Error')) {
-                    // Check if our event is newer then the existing - get the
-                    // event's history.
-                    $history = &Horde_History::singleton();
-                    $created = $modified = null;
-                    $log = $history->getHistory('kronolith:' . $calendar . ':'
-                                                . $uid);
-                    if ($log && !is_a($log, 'PEAR_Error')) {
-                        foreach ($log->getData() as $entry) {
-                            switch ($entry['action']) {
-                            case 'add':
-                                $created = $entry['ts'];
-                                break;
-
-                            case 'modify':
-                                $modified = $entry['ts'];
-                                break;
-                            }
-                        }
-                    }
-                    if (empty($modified) && !empty($created)) {
-                        $modified = $created;
-                    }
-                    if (!empty($modified) &&
-                        $modified >= $content->getAttribute('LAST-MODIFIED')) {
-                        // LAST-MODIFIED timestamp of existing entry is newer:
-                        // don't replace it.
-                        continue;
-                    }
-
-                    // Don't change creator/owner.
-                    $event->setCreatorId($existing_event->getCreatorId());
-                }
-
-                // Save entry.
-                $saved = $event->save();
-                if (is_a($saved, 'PEAR_Error')) {
-                    return $saved;
-                }
-                $ids[] = $event->getUID();
-            }
-        }
-        break;
-
-    default:
-        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $content_type));
-    }
-
-    if (array_key_exists($calendar, Kronolith::listCalendars(false, PERMS_DELETE))) {
-        foreach (array_keys($uids_remove) as $uid) {
-            _kronolith_delete($uid);
-        }
-    }
-
-    return $ids;
-}
-
-/**
- * Deletes a file from the Kronolith tree.
- *
- * @param string $path  The path to the file.
- *
- * @return mixed  The event's UID, or a PEAR_Error on failure.
- */
-function _kronolith_path_delete($path)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (substr($path, 0, 9) == 'kronolith') {
-        $path = substr($path, 9);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    if (substr($parts[1], -4) == '.ics') {
-        $calendarId = substr($parts[1], 0, -4);
-    } else {
-        $calendarId = $parts[1];
-    }
-
-    if (!(count($parts) == 2 || count($parts) == 3) ||
-        !array_key_exists($calendarId, Kronolith::listCalendars(false, PERMS_DELETE))) {
-        return PEAR::raiseError("Calendar does not exist or no permission to delete");
-    }
-
-    if (count($parts) == 3) {
-        // Delete just a single entry
-        return Kronolith::getDriver(null, $calendarId)->deleteEvent($parts[2]);
-    } else {
-        // Delete the entire calendar
-        $result = Kronolith::getDriver()->delete($calendarId);
-        if (is_a($result, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Unable to delete calendar \"%s\": %s"), $calendarId, $result->getMessage()));
-        } else {
-            // Remove share and all groups/permissions.
-            $share = $GLOBALS['kronolith_shares']->getShare($calendarId);
-            $result = $GLOBALS['kronolith_shares']->removeShare($share);
-            if (is_a($result, 'PEAR_Error')) {
-                return $result;
-            }
-        }
-    }
-}
-
-/**
- * Returns all calendars a user has access to, according to several
- * parameters/permission levels.
- *
- * @param boolean $owneronly   Only return calenders that this user owns?
- *                             Defaults to false.
- * @param integer $permission  The permission to filter calendars by.
- *
- * @return array  The calendar list.
- */
-function _kronolith_listCalendars($owneronly = false, $permission = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-    if (is_null($permission)) {
-        $permission = PERMS_SHOW;
-    }
-    return array_keys(Kronolith::listCalendars($owneronly, $permission));
-}
-
-/**
- * Returns the ids of all the events that happen within a time period.
- *
- * @param string $calendar      The calendar to check for events.
- * @param object $startstamp    The start of the time range.
- * @param object $endstamp      The end of the time range.
- *
- * @return array  The event ids happening in this time period.
- */
-function _kronolith_list($calendar = null, $startstamp = 0, $endstamp = 0)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (empty($calendar)) {
-        $calendar = Kronolith::getDefaultCalendar();
-    }
-    if (!array_key_exists($calendar,
-                          Kronolith::listCalendars(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $driver = Kronolith::getDriver(null, $calendar);
-    if (is_a($driver, 'PEAR_Error')) {
-        return $driver;
-    }
-    $events = $driver->listEvents(new Horde_Date($startstamp),
-                                  new Horde_Date($endstamp));
-    if (is_a($events, 'PEAR_Error')) {
-        return $events;
-    }
-
-    $uids = array();
-    foreach ($events as $dayevents) {
-        foreach ($dayevents as $event) {
-            $uids[] = $event->getUID();
-        }
-    }
-
-    return $uids;
-}
-
-/**
- * Returns an array of UIDs for events that have had $action happen since
- * $timestamp.
- *
- * @param string  $action     The action to check for - add, modify, or delete.
- * @param integer $timestamp  The time to start the search.
- * @param string  $calendar   The calendar to search in.
- *
- * @return array  An array of UIDs matching the action and time criteria.
- */
-function _kronolith_listBy($action, $timestamp, $calendar = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (empty($calendar)) {
-        $calendar = Kronolith::getDefaultCalendar();
-    }
-
-    if (!array_key_exists($calendar,
-                          Kronolith::listCalendars(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $history = &Horde_History::singleton();
-    $histories = $history->getByTimestamp('>', $timestamp, array(array('op' => '=', 'field' => 'action', 'value' => $action)), 'kronolith:' . $calendar);
-    if (is_a($histories, 'PEAR_Error')) {
-        return $histories;
-    }
-
-    // Strip leading kronolith:username:.
-    return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
-}
-
-/**
- * Returns the timestamp of an operation for a given uid an action
- *
- * @param string $uid      The uid to look for.
- * @param string $action   The action to check for - add, modify, or delete.
- * @param string $calendar The calendar to search in.
- *
- * @return integer  The timestamp for this action.
- */
-function _kronolith_getActionTimestamp($uid, $action, $calendar = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (empty($calendar)) {
-        $calendar = Kronolith::getDefaultCalendar();
-    }
-
-    if (!array_key_exists($calendar,
-                          Kronolith::listCalendars(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $history = &Horde_History::singleton();
-    return $history->getActionTimestamp('kronolith:' . $calendar . ':' .
-                                        $uid, $action);
-}
-
-/**
- * Imports an event represented in the specified content type.
- *
- * @param string $content      The content of the event.
- * @param string $contentType  What format is the data in? Currently supports:
- *                             <pre>
- *                             text/calendar
- *                             text/x-vcalendar
- *                             </pre>
- * @param string $calendar     What calendar should the event be added to?
- *
- * @return mixed  The event's UID, or a PEAR_Error on failure.
- */
-function _kronolith_import($content, $contentType, $calendar = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!isset($calendar)) {
-        $calendar = Kronolith::getDefaultCalendar(PERMS_EDIT);
-    }
-    if (!array_key_exists($calendar,
-                          Kronolith::listCalendars(false, PERMS_EDIT))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    switch ($contentType) {
-    case 'text/calendar':
-    case 'text/x-vcalendar':
-        $iCal = new Horde_iCalendar();
-        if (!is_a($content, 'Horde_iCalendar_vevent')) {
-            if (!$iCal->parsevCalendar($content)) {
-                return PEAR::raiseError(_("There was an error importing the iCalendar data."));
-            }
-        } else {
-            $iCal->addComponent($content);
-        }
-
-        $components = $iCal->getComponents();
-        if (count($components) == 0) {
-            return PEAR::raiseError(_("No iCalendar data was found."));
-        }
-
-        $kronolith_driver = Kronolith::getDriver(null, $calendar);
-        $ids = array();
-        foreach ($components as $content) {
-            if (is_a($content, 'Horde_iCalendar_vevent')) {
-                $event = $kronolith_driver->getEvent();
-                $event->fromiCalendar($content);
-                $event->setCalendar($calendar);
-                // Check if the entry already exists in the data source, first
-                // by UID.
-                $uid = $event->getUID();
-                $existing_event = $kronolith_driver->getByUID($uid, array($calendar));
-                if (!is_a($existing_event, 'PEAR_Error')) {
-                    return PEAR::raiseError(_("Already Exists"),
-                                            'horde.message', null, null, $uid);
-                }
-                $result = $kronolith_driver->search($event);
-                // Check if the match really is an exact match:
-                if (is_array($result) && count($result) > 0) {
-                    foreach($result as $match) {
-                        if ($match->start == $event->start &&
-                            $match->end == $event->end &&
-                            $match->title == $event->title &&
-                            $match->location == $event->location &&
-                            $match->hasPermission(PERMS_EDIT)) {
-                            return PEAR::raiseError(_("Already Exists"), 'horde.message', null, null, $match->getUID());
-                        }
-                    }
-                }
-
-                $eventId = $event->save();
-                if (is_a($eventId, 'PEAR_Error')) {
-                    return $eventId;
-                }
-                $ids[] = $event->getUID();
-            }
-        }
-        if (count($ids) == 0) {
-            return PEAR::raiseError(_("No iCalendar data was found."));
-        } else if (count($ids) == 1) {
-            return $ids[0];
-        }
-        return $ids;
-    }
-
-    return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-}
-
-/**
- * Exports an event, identified by UID, in the requested content type.
- *
- * @param string $uid         Identify the event to export.
- * @param string $contentType  What format should the data be in?
- *                            A string with one of:
- *                            <pre>
- *                             text/calendar (VCALENDAR 2.0. Recommended as
- *                                            this is specified in rfc2445)
- *                             text/x-vcalendar (old VCALENDAR 1.0 format.
- *                                              Still in wide use)
- *                            </pre>
- *
- * @return string  The requested data.
- */
-function _kronolith_export($uid, $contentType)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-    global $kronolith_shares;
-
-    $event = Kronolith::getDriver()->getByUID($uid);
-    if (is_a($event, 'PEAR_Error')) {
-        return $event;
-    }
-    if (!$event->hasPermission(PERMS_READ)) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $version = '2.0';
-    switch ($contentType) {
-    case 'text/x-vcalendar':
-        $version = '1.0';
-    case 'text/calendar':
-        $share = &$kronolith_shares->getShare($event->getCalendar());
-
-        $iCal = new Horde_iCalendar($version);
-        $iCal->setAttribute('X-WR-CALNAME', Horde_String::convertCharset($share->get('name'), Horde_Nls::getCharset(), 'utf-8'));
-
-        // Create a new vEvent.
-        $vEvent = &$event->toiCalendar($iCal);
-        $iCal->addComponent($vEvent);
-
-        return $iCal->exportvCalendar();
-
-    }
-
-    return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));;
-}
-
-/**
- * Exports a calendar in the requested content type.
- *
- * @param string $calendar    The calendar to export.
- * @param string $contentType  What format should the data be in?
- *                             A string with one of:
- *                             <pre>
- *                             text/calendar (VCALENDAR 2.0. Recommended as
- *                                            this is specified in rfc2445)
- *                             text/x-vcalendar (old VCALENDAR 1.0 format.
- *                                              Still in wide use)
- *                             </pre>
- *
- * @return string  The iCalendar representation of the calendar.
- */
-function _kronolith_exportCalendar($calendar, $contentType)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-    global $kronolith_shares;
-
-    if (!array_key_exists($calendar,
-                          Kronolith::listCalendars(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $kronolith_driver = Kronolith::getDriver(null, $calendar);
-    $events = $kronolith_driver->listEvents();
-
-    $version = '2.0';
-    switch ($contentType) {
-    case 'text/x-vcalendar':
-        $version = '1.0';
-    case 'text/calendar':
-        $share = &$kronolith_shares->getShare($calendar);
-
-        $iCal = new Horde_iCalendar($version);
-        $iCal->setAttribute('X-WR-CALNAME', Horde_String::convertCharset($share->get('name'), Horde_Nls::getCharset(), 'utf-8'));
-
-        foreach ($events as $dayevents) {
-            foreach ($dayevents as $event) {
-                $vEvent = &$event->toiCalendar($iCal);
-                $iCal->addComponent($vEvent);
-            }
-        }
-
-        return $iCal->exportvCalendar();
-    }
-
-    return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-}
-
-/**
- * Deletes an event identified by UID.
- *
- * @param string|array $uid  A single UID or an array identifying the event(s)
- *                           to delete.
- *
- * @return boolean  Success or failure.
- */
-function _kronolith_delete($uid)
-{
-    // Handle an array of UIDs for convenience of deleting multiple events at
-    // once.
-    if (is_array($uid)) {
-        foreach ($uid as $g) {
-            $result = _kronolith_delete($g);
-            if (is_a($result, 'PEAR_Error')) {
-                return $result;
-            }
-        }
-
-        return true;
-    }
-
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    $kronolith_driver = Kronolith::getDriver();
-    $events = $kronolith_driver->getByUID($uid, null, true);
-    if (is_a($events, 'PEAR_Error')) {
-        return $events;
-    }
-
-    $event = null;
-    if (Horde_Auth::isAdmin()) {
-        $event = $events[0];
-    }
-
-    // First try the user's own calendars.
-    if (empty($event)) {
-        $ownerCalendars = Kronolith::listCalendars(true, PERMS_DELETE);
-        foreach ($events as $ev) {
-            if (Horde_Auth::isAdmin() || isset($ownerCalendars[$ev->getCalendar()])) {
-                $event = $ev;
-                break;
-            }
-        }
-    }
-
-    // If not successful, try all calendars the user has access to.
-    if (empty($event)) {
-        $deletableCalendars = Kronolith::listCalendars(false, PERMS_DELETE);
-        foreach ($events as $ev) {
-            if (isset($deletableCalendars[$ev->getCalendar()])) {
-                $kronolith_driver->open($ev->getCalendar());
-                $event = $ev;
-                break;
-            }
-        }
-    }
-
-    if (empty($event)) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    return $kronolith_driver->deleteEvent($event->getId());
-}
-
-/**
- * Replaces the event identified by UID with the content represented in the
- * specified contentType.
- *
- * @param string $uid          Idenfity the event to replace.
- * @param mixed  $content      The content of the event. String or
- *                             Horde_iCalendar_vevent
- * @param string $contentType  What format is the data in? Currently supports:
- *                             text/calendar
- *                             text/x-vcalendar
- *                             (Ignored if content is Horde_iCalendar_vevent)
- *
- * @return mixed  True on success, PEAR_Error otherwise.
- */
-function _kronolith_replace($uid, $content, $contentType)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    $event = Kronolith::getDriver()->getByUID($uid);
-    if (is_a($event, 'PEAR_Error')) {
-        return $event;
-    }
-
-    if (!$event->hasPermission(PERMS_EDIT) ||
-        ($event->isPrivate() && $event->getCreatorId() != Horde_Auth::getAuth())) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    if (is_a($content, 'Horde_iCalendar_vevent')) {
-        $component = $content;
-    } else {
-        switch ($contentType) {
-        case 'text/calendar':
-        case 'text/x-vcalendar':
-            if (!is_a($content, 'Horde_iCalendar_vevent')) {
-                $iCal = new Horde_iCalendar();
-                if (!$iCal->parsevCalendar($content)) {
-                    return PEAR::raiseError(_("There was an error importing the iCalendar data."));
-                }
-
-                $components = $iCal->getComponents();
-                $component = null;
-                foreach ($components as $content) {
-                    if (is_a($content, 'Horde_iCalendar_vevent')) {
-                        if ($component !== null) {
-                            return PEAR::raiseError(_("Multiple iCalendar components found; only one vEvent is supported."));
-                        }
-                        $component = $content;
-                    }
-
-                }
-                if ($component === null) {
-                    return PEAR::raiseError(_("No iCalendar data was found."));
-                }
-            }
-            break;
-        default:
-            return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-        }
-    }
-
-    $event->fromiCalendar($component);
-    // Ensure we keep the original UID, even when content does not
-    // contain one and fromiCalendar creates a new one.
-    $event->setUID($uid);
-    $eventId = $event->save();
-
-    return is_a($eventId, 'PEAR_Error') ? $eventId : true;
-}
-
-/**
- * Generates free/busy information for a given time period.
- *
- * @param integer $startstamp  The start of the time period to retrieve.
- * @param integer $endstamp    The end of the time period to retrieve.
- * @param string $calendar     The calendar to view free/busy slots for.
- *                             Defaults to the user's default calendar.
- *
- * @return Horde_iCalendar_vfreebusy  A freebusy object that covers the
- *                                    specified time period.
- */
-function _kronolith_getFreeBusy($startstamp = null, $endstamp = null,
-                                $calendar = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (is_null($calendar)) {
-        $calendar = Kronolith::getDefaultCalendar();
-    }
-    // Free/Busy information is globally available; no permission
-    // check is needed.
-
-    return Kronolith_FreeBusy::generate($calendar, $startstamp, $endstamp, true);
-}
-
-/**
- * Retrieves a Kronolith_Event object, given an event UID.
- *
- * @param string $uid  The event's UID.
- *
- * @return Kronolith_Event  A valid Kronolith_Event on success, or a PEAR_Error
- *                          on failure.
- */
-function &_kronolith_eventFromUID($uid)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    $event = Kronolith::getDriver()->getByUID($uid);
-    if (is_a($event, 'PEAR_Error')) {
-        return $event;
-    }
-    if (!$event->hasPermission(PERMS_SHOW)) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    return $event;
-}
-
-/**
- * Updates an attendee's response status for a specified event.
- *
- * @param Horde_iCalender_vevent $response  A Horde_iCalender_vevent object,
- *                                          with a valid UID attribute that
- *                                          points to an existing event.
- *                                          This is typically the vEvent
- *                                          portion of an iTip meeting-request
- *                                          response, with the attendee's
- *                                          response in an ATTENDEE parameter.
- * @param string $sender                    The email address of the person
- *                                          initiating the update. Attendees
- *                                          are only updated if this address
- *                                          matches.
- *
- * @return mixed  True on success, PEAR_Error on failure.
- */
-function _kronolith_updateAttendee($response, $sender = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    $uid = $response->getAttribute('UID');
-    if (is_a($uid, 'PEAR_Error')) {
-        return $uid;
-    }
-
-    $events = Kronolith::getDriver()->getByUID($uid, null, true);
-    if (is_a($events, 'PEAR_Error')) {
-        return $events;
-    }
-
-    /* First try the user's own calendars. */
-    $ownerCalendars = Kronolith::listCalendars(true, PERMS_EDIT);
-    $event = null;
-    foreach ($events as $ev) {
-        if (isset($ownerCalendars[$ev->getCalendar()])) {
-            $event = $ev;
-            break;
-        }
-    }
-
-    /* If not successful, try all calendars the user has access to. */
-    if (empty($event)) {
-        $editableCalendars = Kronolith::listCalendars(false, PERMS_EDIT);
-        foreach ($events as $ev) {
-            if (isset($editableCalendars[$ev->getCalendar()])) {
-                $event = $ev;
-                break;
-            }
-        }
-    }
-
-    if (empty($event) ||
-        ($event->isPrivate() && $event->getCreatorId() != Horde_Auth::getAuth())) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $atnames = $response->getAttribute('ATTENDEE');
-    if (!is_array($atnames)) {
-        $atnames = array($atnames);
-    }
-    $atparms = $response->getAttribute('ATTENDEE', true);
-
-    $found = false;
-    $error = _("No attendees have been updated because none of the provided email addresses have been found in the event's attendees list.");
-    $sender_lcase = Horde_String::lower($sender);
-    foreach ($atnames as $index => $attendee) {
-        $attendee = str_replace('mailto:', '', Horde_String::lower($attendee));
-        $name = isset($atparms[$index]['CN']) ? $atparms[$index]['CN'] : null;
-        if ($event->hasAttendee($attendee)) {
-            if (is_null($sender) || $sender_lcase == $attendee) {
-                $event->addAttendee($attendee, Kronolith::PART_IGNORE, Kronolith::responseFromICal($atparms[$index]['PARTSTAT']), $name);
-                $found = true;
-            } else {
-                $error = _("The attendee hasn't been updated because the update was not sent from the attendee.");
-            }
-        }
-    }
-
-    $result = $event->save();
-    if (is_a($result, 'PEAR_Error')) {
-        return $result;
-    }
-
-    if (!$found) {
-        return PEAR::raiseError($error);
-    }
-
-    return true;
-}
-
-/**
- * Lists events for a given time period.
- *
- * @param integer $startstamp      The start of the time period to retrieve.
- * @param integer $endstamp        The end of the time period to retrieve.
- * @param array   $calendars       The calendars to view events from.
- *                                 Defaults to the user's default calendar.
- * @param boolean $showRecurrence  Return every instance of a recurring event?
- *                                 If false, will only return recurring events
- *                                 once inside the $startDate - $endDate range.
- * @param boolean $alarmsOnly      Filter results for events with alarms.
- *                                 Defaults to false.
- *
- * @return array  A list of event hashes.
- */
-function _kronolith_listEvents($startstamp = null, $endstamp = null,
-                               $calendars = null, $showRecurrence = true,
-                               $alarmsOnly = false)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!isset($calendars)) {
-        $calendars = array($GLOBALS['prefs']->getValue('default_share'));
-    } elseif (!is_array($calendars)) {
-        $calendars = array($calendars);
-    }
-    $allowed_calendars = Kronolith::listCalendars(false, PERMS_READ);
-    foreach ($calendars as $calendar) {
-        if (!array_key_exists($calendar, $allowed_calendars)) {
-            return PEAR::raiseError(_("Permission Denied"));
-        }
-    }
-
-    return Kronolith::listEvents(new Horde_Date($startstamp),
-                                 new Horde_Date($endstamp),
-                                 $calendars, $showRecurrence, $alarmsOnly);
-}
-
-/**
- * Lists alarms for a given moment.
- *
- * @param integer $time  The time to retrieve alarms for.
- * @param string $user   The user to retrieve alarms for. All users if null.
- *
- * @return array  An array of UIDs
- */
-function _kronolith_listAlarms($time, $user = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-    require_once 'Horde/Group.php';
-
-    $current_user = Horde_Auth::getAuth();
-    if ((empty($user) || $user != $current_user) && !Horde_Auth::isAdmin()) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $group = &Group::singleton();
-    $alarm_list = array();
-    $time = new Horde_Date($time);
-    $calendars = is_null($user) ? array_keys($GLOBALS['kronolith_shares']->listAllShares()) : $GLOBALS['display_calendars'];
-    $alarms = Kronolith::listAlarms($time, $calendars, true);
-    if (is_a($alarms, 'PEAR_Error')) {
-        return $alarms;
-    }
-    foreach ($alarms as $calendar => $cal_alarms) {
-        $share = $GLOBALS['kronolith_shares']->getShare($calendar);
-        if (is_a($share, 'PEAR_Error')) {
-            continue;
-        }
-        if (empty($user)) {
-            $users = $share->listUsers(PERMS_READ);
-            $groups = $share->listGroups(PERMS_READ);
-            foreach ($groups as $gid) {
-                $group_users = $group->listUsers($gid);
-                if (!is_a($group_users, 'PEAR_Error')) {
-                    $users = array_merge($users, $group_users);
-                }
-            }
-            $users = array_unique($users);
-        } else {
-            $users = array($user);
-        }
-        $owner = $share->get('owner');
-        foreach ($cal_alarms as $event) {
-            foreach ($users as $alarm_user) {
-                if ($alarm_user == $current_user) {
-                    $prefs = &$GLOBALS['prefs'];
-                } else {
-                    $prefs = &Prefs::singleton($GLOBALS['conf']['prefs']['driver'],
-                                               'kronolith', $alarm_user, null,
-                                               null, false);
-                }
-                $shown_calendars = unserialize($prefs->getValue('display_cals'));
-                $reminder = $prefs->getValue('event_reminder');
-                if (($reminder == 'owner' && $alarm_user == $owner) ||
-                    ($reminder == 'show' && in_array($calendar, $shown_calendars)) ||
-                    $reminder == 'read') {
-                    Horde_Nls::setLanguageEnvironment($prefs->getValue('language'));
-                    $alarm = $event->toAlarm($time, $alarm_user, $prefs);
-                    if ($alarm) {
-                        $alarm_list[] = $alarm;
-                    }
-                }
-            }
-        }
-    }
-
-    return $alarm_list;
-}
-
-/**
- * Subscribe to a calendar.
- *
- * @param array $calendar  Calendar description hash, with required 'type'
- *                         parameter. Currently supports 'http' and 'webcal'
- *                         for remote calendars.
- */
-function _kronolith_subscribe($calendar)
-{
-    if (!isset($calendar['type'])) {
-        return PEAR::raiseError(_("Unknown calendar protocol"));
-    }
-
-    switch ($calendar['type']) {
-    case 'http':
-    case 'webcal':
-        $username = isset($calendar['username']) ? $calendar['username'] : null;
-        $password = isset($calendar['password']) ? $calendar['password'] : null;
-
-        $cals = unserialize($GLOBALS['prefs']->getValue('remote_cals'));
-        if (!is_array($cals)) {
-            $cals = array();
-        }
-        $array_key = count($cals);
-        foreach ($cals as $key => $cal) {
-            if ($cal['url'] == $calendar['url']) {
-                $array_key = $key;
-                break;
-            }
-        }
-
-        $cals[$array_key] = array('name' => $calendar['name'],
-                                  'url'  => $calendar['url'],
-                                  'user' => $username,
-                                  'password' => $password);
-        $GLOBALS['prefs']->setValue('remote_cals', serialize($cals));
-        break;
-
-    case 'external':
-        $cals = unserialize($GLOBALS['prefs']->getValue('display_external_cals'));
-        if (array_search($calendar['name'], $cals) === false) {
-            $cals[] = $calendar['name'];
-            $GLOBALS['prefs']->setValue('display_external_cals', serialize($cals));
-        }
-
-    default:
-        return PEAR::raiseError(_("Unknown calendar protocol"));
-    }
-}
-
-/**
- * Unsubscribe from a calendar.
- *
- * @param array $calendar  Calendar description array, with required 'type'
- *                         parameter. Currently supports 'http' and 'webcal'
- *                         for remote calendars.
- */
-function _kronolith_unsubscribe($calendar)
-{
-    if (!isset($calendar['type'])) {
-        return PEAR::raiseError('Unknown calendar specification');
-    }
-
-    switch ($calendar['type']) {
-    case 'http':
-    case 'webcal':
-        $cals = unserialize($GLOBALS['prefs']->getValue('remote_cals'));
-        foreach ($cals as $key => $cal) {
-            if ($cal['url'] == $calendar['url']) {
-                unset($cals[$key]);
-                break;
-            }
-        }
-
-        $GLOBALS['prefs']->setValue('remote_cals', serialize($cals));
-        break;
-
-    case 'external':
-        $cals = unserialize($GLOBALS['prefs']->getValue('display_external_cals'));
-        if (($key = array_search($calendar['name'], $cals)) !== false) {
-            unset($cals[$key]);
-            $GLOBALS['prefs']->setValue('display_external_cals', serialize($cals));
-        }
-
-    default:
-        return PEAR::raiseError('Unknown calendar specification');
-    }
-}
-
-
-/**
- * Places an exclusive lock for a calendar or an event.
- *
- * @param array $calendar  The calendar to lock
- * @param array $event     The event to lock
- *
- * @return mixed   A lock ID on success, PEAR_Error on failure, false if:
- *                   - The calendar is already locked
- *                   - The event is already locked
- *                   - A calendar lock was requested and an event is already
- *                     locked in the calendar
- */
-function _kronolith_lock($calendar, $event = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!array_key_exists($calendar,
-                          Kronolith::listCalendars(false, PERMS_EDIT))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-    $share = &$GLOBALS['kronolith_shares']->getShare($calendar);
-
-    return $share->lock($calendar, $event);
-}
-
-/**
- * Releases a lock.
- *
- * @param array $calendar  The event to lock.
- * @param array $lockid    The lock id to unlock.
- */
-function _kronolith_unlock($calendar, $lockid)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!array_key_exists($calendar,
-                          Kronolith::listCalendars(false, PERMS_EDIT))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-    $share = &$GLOBALS['kronolith_shares']->getShare($calendar);
-
-    return $share->unlock($lockid);
-}
-
-/**
- * Check for existing calendar or event locks.
- *
- * @param array $calendar  The calendar to check locks for.
- * @param array $event     The event to check locks for.
- */
-function _kronolith_checkLocks($calendar, $event = null)
-{
-    $no_maint = true;
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!array_key_exists($calendar,
-                          Kronolith::listCalendars(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-    $share = &$GLOBALS['kronolith_shares']->getShare($calendar);
-
-    return $share->checkLocks($event);
-}
-
-/**
- *
- * @return array  A list of calendars used to display free/busy information
- */
-function _kronolith_getFbCalendars()
-{
-    return (unserialize($GLOBALS['prefs']->getValue('fb_cals')));
-}
diff --git a/kronolith/lib/version.php b/kronolith/lib/version.php
deleted file mode 100644 (file)
index cee7a01..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('KRONOLITH_VERSION', 'H3 (3.0-git)') ?>
index 1d51f97..db93d1c 100644 (file)
@@ -20,8 +20,9 @@ require_once HORDE_BASE . '/lib/Test.php';
 $horde_test = new Horde_Test;
 
 $module = 'Kronolith';
-require_once KRONOLITH_BASE . '/lib/version.php';
-$module_version = KRONOLITH_VERSION;
+require_once dirname(__FILE__) . '/lib/Api.php';
+$api = new Kronolith_Api();
+$module_version = $api->version;
 
 require TEST_TEMPLATES . 'header.inc';
 require TEST_TEMPLATES . 'version.inc';
diff --git a/nag/lib/Api.php b/nag/lib/Api.php
new file mode 100644 (file)
index 0000000..68f9b9c
--- /dev/null
@@ -0,0 +1,1528 @@
+<?php
+/**
+ * Nag external API interface.
+ *
+ * This file defines Nag's external API interface. Other applications can
+ * interact with Nag through this API.
+ *
+ * @package Nag
+ */
+class Nag_Api extends Horde_Registry_Api
+{
+    /**
+     * The application's version.
+     *
+     * @var string
+     */
+    public $version = 'H4 (3.0-git)';
+
+    /**
+     * The services provided by this application.
+     *
+     * @var array
+     */
+    public $services = array(
+        'perms' => array(
+            'args' => array(),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'removeUserData' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'show' => array(
+            'link' => '%application%/view.php?tasklist=|tasklist|&task=|task|&uid=|uid|',
+        ),
+
+        'browse' => array(
+            'args' => array('path' => 'string'),
+            'type' => '{urn:horde}hashHash',
+        ),
+
+        'put' => array(
+            'args' => array(
+                'path' => 'string',
+                'content' => 'string',
+                'content_type' => 'string'
+            ),
+            'type' => 'int',
+        ),
+
+        'path_delete' => array(
+            'args' => array('path' => 'string'),
+            'type' => 'boolean',
+        ),
+
+        'addTasklist' => array(
+            'args' => array(
+                'name' => 'string',
+                'description' => 'string'
+            ),
+            'type' => 'string',
+        ),
+
+        'listTasklists' => array(
+            'args' => array(
+                'owneronly' => 'boolean',
+                'permission' => 'int'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'listTasks' => array(
+            'args' => array(
+                'sortby' => 'string',
+                'sortdir' => 'int',
+                'altsortby' => 'string',
+                'tasklists' => '{urn:horde}stringArray',
+                'completed' => 'int',
+                'json' => 'boolean'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'listAlarms' => array(
+            'args' => array(
+                'time' => 'int',
+                'user' => 'string'
+            ),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'list' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'listBy' => array(
+            'args' => array(
+                'action' => 'string',
+                'timestamp' => 'int'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'getActionTimestamp' => array(
+            'args' => array(
+                'uid' => 'string',
+                'action' => 'string',
+                'tasklist' => 'string'
+            ),
+            'type' => 'int',
+        ),
+
+        'import' => array(
+            'args' => array(
+                'content' => 'string',
+                'contentType' => 'string',
+                'tasklist' => 'string'
+            ),
+            'type' => 'string',
+        ),
+
+        'quickAdd' => array(
+            'args' => array(
+                'content' => 'string',
+                'tasklist' => 'string'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'export' => array(
+            'args' => array(
+                'uid' => 'string',
+                'contentType' => '{urn:horde}stringArray'
+            ),
+            'type' => 'string',
+        ),
+
+        'exportTasklist' => array(
+            'args' => array(
+                'tasklist' => 'string',
+                'contentType' => 'string'
+            ),
+            'type' => 'string'
+        ),
+
+        'delete' => array(
+            'args' => array('uid' => '{urn:horde}stringArray'),
+            'type' => 'boolean',
+        ),
+
+        'replace' => array(
+            'args' => array(
+                'uid' => 'string',
+                'content' => 'string',
+                'contentType' => 'string'
+            ),
+            'type' => 'boolean',
+        ),
+
+        'listCostObjects' => array(
+            'args' => array('criteria' => '{urn:horde}hash'),
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'listTimeObjectCategories' => array(
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'listTimeObjects' => array(
+            'args' => array(
+                'start' => 'int',
+                'end' => 'int'
+            ),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'toggleCompletion' => array(
+            'args' => array(
+                'task_id' => 'string',
+                'tasklist_id' => 'string'
+            ),
+            'type' => 'boolean'
+        )
+    );
+
+    /**
+     * Returns a list of available permissions.
+     *
+     * @return array  An array describing all available permissions.
+     */
+    public function perms()
+    {
+        $perms = array();
+        $perms['tree']['nag']['max_tasks'] = false;
+        $perms['title']['nag:max_tasks'] = _("Maximum Number of Tasks");
+        $perms['type']['nag:max_tasks'] = 'int';
+
+        return $perms;
+    }
+
+    /**
+     * Removes user data.
+     *
+     * @param string $user  Name of user to remove data for.
+     *
+     * @return mixed  true on success | PEAR_Error on failure
+     */
+    public function removeUserData($user)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!Horde_Auth::isAdmin() && $user != Horde_Auth::getAuth()) {
+            return PEAR::raiseError(_("You are not allowed to remove user data."));
+        }
+
+        /* Error flag */
+        $hasError = false;
+
+        /* Get the share for later deletion */
+        $share = $GLOBALS['nag_shares']->getShare($user);
+        if(is_a($share, 'PEAR_Error')) {
+            Horde::logMessage($share->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
+            unset($share);
+        } else {
+            /* Get the list of all tasks */
+            $tasks = Nag::listTasks(null, null, null, $user, 1);
+            if (is_a($tasks, 'PEAR_Error')) {
+                $hasError = true;
+                Horde::logMessage($share->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
+            } else {
+                $uids = array();
+                $tasks->reset();
+                while ($task = $tasks->each()) {
+                    $uids[] = $task->uid;
+                }
+
+                /* ... and delete them. */
+                foreach ($uids as $uid) {
+                    $this->delete($uid);
+                }
+            }
+        }
+
+        /* Now delete history as well. */
+        $history = Horde_History::singleton();
+        if (method_exists($history, 'removeByParent')) {
+            $histories = $history->removeByParent('nag:' . $user);
+        } else {
+            /* Remove entries 100 at a time. */
+            $all = $history->getByTimestamp('>', 0, array(), 'nag:' . $user);
+            if (is_a($all, 'PEAR_Error')) {
+                Horde::logMessage($all, __FILE__, __LINE__, PEAR_LOG_ERR);
+            } else {
+                $all = array_keys($all);
+                while (count($d = array_splice($all, 0, 100)) > 0) {
+                    $history->removebyNames($d);
+                }
+            }
+        }
+
+        /* ...and finally, delete the actual share */
+        if (!empty($share)) {
+            $result = $GLOBALS['nag_shares']->removeShare($share);
+            if (is_a($result, 'PEAR_Error')) {
+                $hasError = true;
+                Horde::logMessage($result->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        }
+
+        /* Now remove perms for this user from all other shares */
+        $shares = $GLOBALS['nag_shares']->listShares($user);
+        if (is_a($shares, 'PEAR_Error')) {
+            $hasError = true;
+            Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+        foreach ($shares as $share) {
+            $share->removeUser($user);
+        }
+
+        if ($hasError) {
+            return PEAR::raiseError(sprintf(_("There was an error removing tasks for %s. Details have been logged."), $user));
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Retrieves the current user's task list from storage.
+     *
+     * This function will also sort the resulting list, if requested.
+     *
+     * @param string $sortby        The field by which to sort
+     *                              (NAG_SORT_PRIORITY, NAG_SORT_NAME
+     *                              NAG_SORT_DUE, NAG_SORT_COMPLETION).
+     * @param integer $sortdir      The direction by which to sort
+     * @param string $altsortby     The secondary sort field.
+     * @param array $tasklists      An array of tasklist to display or
+     *                              null/empty to display taskslists
+     *                              $GLOBALS['display_tasklists'].
+     * @param integer $completed    Which tasks to retrieve (1 = all tasks,
+     *                              0 = incomplete tasks, 2 = complete tasks,
+     *                              3 = future tasks, 4 = future and incomplete
+     *                              tasks).
+     * @param boolean $json         Retrieve the results of the tasks in
+     *                              'json format'
+     *
+     * @return Nag_Task  A list of the requested tasks.
+     */
+    public function listTasks($sortby = null, $sortdir = null, $altsortby = null,
+        $tasklists = null, $completed = null, $json = false)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!isset($sortby)) {
+            $sortby = $GLOBALS['prefs']->getValue('sortby');
+        }
+        if (!isset($sortdir)) {
+            $sortdir = $GLOBALS['prefs']->getValue('sortdir');
+        }
+        if (is_null($altsortby)) {
+            $altsortby =  $GLOBALS['prefs']->getValue('altsortby');
+        }
+        if (is_null($tasklists)) {
+            $tasklists = $GLOBALS['display_tasklists'];
+        }
+        if (is_null($completed)) {
+            $completed = $GLOBALS['prefs']->getValue('show_completed');
+        }
+
+        $tasks = Nag::listTasks($sortby, $sortdir, $altsortby, $tasklists);
+        $tasks->reset();
+        $list = array();
+        while ($task = $tasks->each()) {
+            $list[$task->id] = $json ? $task->toJson() : $task->toHash();
+        }
+
+        return $list;
+    }
+
+    /**
+     * Add a new task list
+     *
+     * @param string $name        Task list name
+     * @param string $description Task list description
+     *
+     * @return integer  The new tasklist's id.
+     */
+    public function addTasklist($name, $description = '')
+    {
+        if (!Horde_Auth::getAuth()) {
+            return PEAR::raiseError(_("Permission denied"));
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+        global $nag_shares;
+
+        $tasklistId = md5(microtime());
+        $tasklist = $nag_shares->newShare($tasklistId);
+
+        if (is_a($tasklist, 'PEAR_Error')) {
+            return $tasklist;
+        }
+
+        $tasklist->set('name', $name, false);
+        $tasklist->set('desc', $description, false);
+        $result = $nag_shares->addShare($tasklist);
+
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return $tasklistId;
+    }
+
+    /**
+     * Returns the last modification timestamp of a given uid.
+     *
+     * @param string $uid      The uid to look for.
+     * @param string $tasklist The tasklist to look in.
+     *
+     * @return integer  The timestamp for the last modification of $uid.
+     */
+    public function modified($uid, $tasklist = null)
+    {
+        $modified = $this->getActionTimestamp($uid, 'modify', $tasklist);
+        if (empty($modified)) {
+            $modified = $this->getActionTimestamp($uid, 'add', $tasklist);
+        }
+        return $modified;
+    }
+
+    /**
+     * Browse through Nag's object tree.
+     *
+     * @param string $path       The level of the tree to browse.
+     * @param array $properties  The item properties to return. Defaults to 'name',
+     *                           'icon', and 'browseable'.
+     *
+     * @return array  The contents of $path
+     */
+    public function browse($path = '', $properties = array())
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $registry;
+
+        function _getTasklistSize($tasklistID)
+        {
+            // This ugly and performance-heavy hack is required to set the content
+            // length.  Some clients (at least OS X) respect the content-length
+            // header a little too exactly.  If we send a content-length that is
+            // longer than the actual data it will complain that the connection
+            // broke.  If we specify one that is too short it will truncate the
+            // downlaoded file.  To make matters worse it seems to respect the
+            // content-length from the PROPFIND request used to enumerate objects
+            // rather than the actual content-length sent at the time the file is
+            // downloaded.  Way to go, Apple.
+            return strlen($thisd->exportTasklist($tasklistID, 'text/calendar'));
+        }
+
+        // Default properties.
+        if (!$properties) {
+            $properties = array('name', 'icon', 'browseable');
+        }
+
+        if (substr($path, 0, 3) == 'nag') {
+            $path = substr($path, 3);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        if (empty($path)) {
+            //
+            // This request is for a list of all users who have tasklists visible
+            // to the requesting user.
+            //
+            $tasklists = Nag::listTasklists(false, PERMS_READ);
+            $owners = array();
+            foreach ($tasklists as $tasklist) {
+                $owners[$tasklist->get('owner')] = true;
+            }
+
+            $results = array();
+            foreach (array_keys($owners) as $owner) {
+                if (in_array('name', $properties)) {
+                    $results['nag/' . $owner]['name'] = $owner;
+                }
+                if (in_array('icon', $properties)) {
+                    $results['nag/' . $owner]['icon'] =
+                        $registry->getImageDir('horde') . '/user.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results['nag/' . $owner]['browseable'] = true;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results['nag/' . $owner]['contenttype'] =
+                        'httpd/unix-directory';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results['nag/' . $owner]['contentlength'] = 0;
+                }
+                if (in_array('modified', $properties)) {
+                    $results['nag/' . $owner]['modified'] =
+                        $_SERVER['REQUEST_TIME'];
+                }
+                if (in_array('created', $properties)) {
+                    $results['nag/' . $owner]['created'] = 0;
+                }
+            }
+            return $results;
+
+        } elseif (count($parts) == 1) {
+            //
+            // This request is for all tasklists owned by the requested user
+            //
+            $tasklists = $GLOBALS['nag_shares']->listShares($parts[0],
+                PERMS_SHOW,
+                $parts[0]);
+
+            // The last check returns all addressbooks for the requested user,
+            // but that does not mean the requesting user has access to them.
+            // Filter out those address books for which the requesting user has
+            // no access.
+            $tasklists = Nag::permissionsFilter($tasklists);
+
+            $results = array();
+            foreach ($tasklists as $tasklistId => $tasklist) {
+                $retpath = 'nag/' . $parts[0] . '/' . $tasklistId;
+                if (in_array('name', $properties)) {
+                    $results[$retpath]['name'] = sprintf(_("Tasks from %s"), $tasklist->get('name'));
+                    $results[$retpath . '.ics']['name'] = $tasklist->get('name');
+                }
+                if (in_array('icon', $properties)) {
+                    $results[$retpath]['icon'] = $registry->getImageDir() . '/nag.png';
+                    $results[$retpath . '.ics']['icon'] = $registry->getImageDir() . '/mime/icalendar.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results[$retpath]['browseable'] = $tasklist->hasPermission(Horde_Auth::getAuth(), PERMS_READ);
+                    $results[$retpath . '.ics']['browseable'] = false;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results[$retpath]['contenttype'] = 'httpd/unix-directory';
+                    $results[$retpath . '.ics']['contenttype'] = 'text/calendar';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results[$retpath]['contentlength'] = 0;
+                    $results[$retpath . '.ics']['contentlength'] = _getTasklistSize($tasklistId);
+                }
+                if (in_array('modified', $properties)) {
+                    // @TODO Find a way to get the actual modification times
+                    $results[$retpath]['modified'] = $_SERVER['REQUEST_TIME'];
+                    $results[$retpath . '.ics']['modified'] = $_SERVER['REQUEST_TIME'];
+                }
+                if (in_array('created', $properties)) {
+                    // @TODO Find a way to get the actual creation times
+                    $results[$retpath]['created'] = 0;
+                    $results[$retpath . '.ics']['created'] = 0;
+                }
+            }
+            return $results;
+
+        } elseif (count($parts) == 2 && substr($parts[1], -4) == '.ics') {
+            //
+            // This is a request for the entire tasklist in iCalendar format.
+            //
+            $tasklist = substr($parts[1], 0, -4);
+            if (!array_key_exists($tasklist, Nag::listTasklists(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Invalid tasklist file requested."), 404);
+            }
+            $ical_data = $this->exportTasklist($tasklist, 'text/calendar');
+            $result = array('data'          => $ical_data,
+                'mimetype'      => 'text/calendar',
+                'contentlength' => strlen($ical_data),
+                'mtime'         => $_SERVER['REQUEST_TIME']);
+
+            return $result;
+
+        } elseif (count($parts) == 2) {
+            //
+            // This request is browsing into a specific tasklist.  Generate the list
+            // of items and represent them as files within the directory.
+            //
+            if (!array_key_exists($parts[1], Nag::listTasklists(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Invalid tasklist requested."), 404);
+            }
+            $storage = Nag_Driver::singleton($parts[1]);
+            $result = $storage->retrieve();
+            if (is_a($result, 'PEAR_Error')) {
+                $result->code = 500;
+                return $result;
+            }
+
+            $icon = $registry->getImageDir() . '/nag.png';
+            $results = array();
+            $storage->tasks->reset();
+            while ($task = $storage->tasks->each()) {
+                $key = 'nag/' . $parts[0] . '/' . $parts[1] . '/' . $task->id;
+                if (in_array('name', $properties)) {
+                    $results[$key]['name'] = $task->name;
+                }
+                if (in_array('icon', $properties)) {
+                    $results[$key]['icon'] = $icon;
+                }
+                if (in_array('browseable', $properties)) {
+                    $results[$key]['browseable'] = false;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results[$key]['contenttype'] = 'text/calendar';
+                }
+                if (in_array('contentlength', $properties)) {
+                    // FIXME:  This is a hack.  If the content length is longer
+                    // than the actual data then some WebDAV clients will report
+                    // an error when the file EOF is received.  Ideally we should
+                    // determine the actual size of the data and report it here, but
+                    // the performance hit may be prohibitive.  This requires
+                    // further investigation.
+                    $results[$key]['contentlength'] = 1;
+                }
+                if (in_array('modified', $properties)) {
+                    $results[$key]['modified'] = $this->modified($task->uid, $path);
+                }
+                if (in_array('created', $properties)) {
+                    $results[$key]['created'] = $this->getActionTimestamp($task->uid, 'add', $path);
+                }
+            }
+            return $results;
+        } else {
+            //
+            // The only valid request left is for either a specific task item.
+            //
+            if (count($parts) == 3 &&
+                array_key_exists($parts[1], Nag::listTasklists(false,
+                    PERMS_READ))) {
+                        //
+                        // This request is for a specific item within a given task list.
+                        //
+                        /* Create a Nag storage instance. */
+                        $storage = Nag_Driver::singleton($parts[1]);
+                        if (is_a($storage, 'PEAR_Error')) {
+                            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $storage->getMessage()));
+                        }
+                        $storage->retrieve();
+
+                        $task = $storage->get($parts[2]);
+                        if (is_a($task, 'PEAR_Error')) {
+                            $task->code = 500;
+                            return $task;
+                        }
+
+                        $result = array('data' => $this->export($task->uid, 'text/calendar'),
+                            'mimetype' => 'text/calendar');
+                        $modified = $this->modified($task->uid, $parts[1]);
+                        if (!empty($modified)) {
+                            $result['mtime'] = $modified;
+                        }
+                        return $result;
+                    } elseif (count($parts) == 2 &&
+                        substr($parts[1], -4) == '.ics' &&
+                        array_key_exists(substr($parts[1], 0, -4), Nag::listTasklists(false, PERMS_READ))) {
+                        } else {
+                            //
+                            // All other requests are a 404: Not Found
+                            //
+                            return false;
+                        }
+        }
+    }
+
+    /**
+     * Saves a file into the Nag tree.
+     *
+     * @param string $path          The path where to PUT the file.
+     * @param string $content       The file content.
+     * @param string $content_type  The file's content type.
+     *
+     * @return array  The event UIDs, or a PEAR_Error on failure.
+     */
+    public function put($path, $content, $content_type)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (substr($path, 0, 3) == 'nag') {
+            $path = substr($path, 3);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        if (count($parts) == 2 &&
+            substr($parts[1], -4) == '.ics') {
+
+                // Workaround for WebDAV clients that are not smart enough to send
+                // the right content type.  Assume text/calendar.
+                if ($content_type == 'application/octet-stream') {
+                    $content_type = 'text/calendar';
+                }
+                $tasklist = substr($parts[1], 0, -4);
+            } elseif (count($parts) == 3) {
+                $tasklist = $parts[1];
+
+                // Workaround for WebDAV clients that are not smart enough to send
+                // the right content type.  Assume the same format we send individual
+                // tasklist items: text/calendar
+                if ($content_type == 'application/octet-stream') {
+                    $content_type = 'text/calendar';
+                }
+            } else {
+                return PEAR::raiseError(_("Invalid tasklist name supplied."), 403);
+            }
+
+        if (!array_key_exists($tasklist, Nag::listTasklists(false, PERMS_EDIT))) {
+            // FIXME: Should we attempt to create a tasklist based on the filename
+            // in the case that the requested tasklist does not exist?
+            return PEAR::raiseError(_("Tasklist does not exist or no permission to edit"), 403);
+        }
+
+        // Store all currently existings UIDs. Use this info to delete UIDs not
+        // present in $content after processing.
+        $ids = array();
+        $uids_remove = array_flip($this->listTaskUids($tasklist));
+
+        $storage = Nag_Driver::singleton($tasklist);
+
+        switch ($content_type) {
+        case 'text/calendar':
+        case 'text/x-vcalendar':
+            $iCal = new Horde_iCalendar();
+            if (!is_a($content, 'Horde_iCalendar_vtodo')) {
+                if (!$iCal->parsevCalendar($content)) {
+                    return PEAR::raiseError(_("There was an error importing the iCalendar data."), 400);
+                }
+            } else {
+                $iCal->addComponent($content);
+            }
+
+            foreach ($iCal->getComponents() as $content) {
+                if (is_a($content, 'Horde_iCalendar_vtodo')) {
+                    $task = new Nag_Task();
+                    $task->fromiCalendar($content);
+                    $task->tasklist = $tasklist;
+                    if (isset($task->uid) &&
+                        !is_a(($existing = $storage->getByUID($task->uid)), 'PEAR_Error')) {
+                        // Entry exists, remove from uids_remove list so we won't
+                        // delete in the end.
+                        if (isset($uids_remove[$task->uid])) {
+                            unset($uids_remove[$task->uid]);
+                        }
+                        if ($existing->private &&
+                            $existing->owner != Horde_Auth::getAuth()) {
+                                continue;
+                            }
+                        // Check if our task is newer then the existing - get the
+                        // task's history.
+                        $history = Horde_History::singleton();
+                        $created = $modified = null;
+                        $log = $history->getHistory('nag:' . $tasklist . ':' . $task->uid);
+                        if ($log && !is_a($log, 'PEAR_Error')) {
+                            foreach ($log->getData() as $entry) {
+                                switch ($entry['action']) {
+                                case 'add':
+                                    $created = $entry['ts'];
+                                    break;
+
+                                case 'modify':
+                                    $modified = $entry['ts'];
+                                    break;
+                                }
+                            }
+                        }
+                        if (empty($modified) && !empty($add)) {
+                            $modified = $add;
+                        }
+                        if (!empty($modified) &&
+                            $modified >= $content->getAttribute('LAST-MODIFIED')) {
+                                // LAST-MODIFIED timestamp of existing entry is newer:
+                                // don't replace it.
+                                continue;
+                            }
+
+                        // Don't change creator/owner.
+                        $owner = $existing->owner;
+                        $taskId = $existing->id;
+                        $result = $storage->modify(
+                            $taskId,
+                            isset($task->name) ? $task->name : $existing->name,
+                            isset($task->desc) ? $task->desc : $existing->desc,
+                            isset($task->start) ? $task->start : $existing->start,
+                            isset($task->due) ? $task->due : $existing->due,
+                            isset($task->priority) ? $task->priority : $existing->priority,
+                            isset($task->estimate) ? $task->estimate : 0,
+                            isset($task->completed) ? (int)$task->completed : $existing->completed,
+                            isset($task->category) ? $task->category : $existing->category,
+                            isset($task->alarm) ? $task->alarm : $existing->alarm,
+                            isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
+                            isset($task->private) ? $task->private : $existing->private,
+                            $owner,
+                            isset($task->assignee) ? $task->assignee : $existing->assignee);
+
+                        if (is_a($result, 'PEAR_Error')) {
+                            $result->code = 500;
+                            return $result;
+                        }
+                        $ids[] = $task->uid;
+                    } else {
+                        $newTask = $storage->add(
+                            isset($task->name) ? $task->name : '',
+                            isset($task->desc) ? $task->desc : '',
+                            isset($task->start) ? $task->start : 0,
+                            isset($task->due) ? $task->due : 0,
+                            isset($task->priority) ? $task->priority : 3,
+                            isset($task->estimate) ? $task->estimate : 0,
+                            !empty($task->completed),
+                            isset($task->category) ? $task->category : '',
+                            isset($task->alarm) ? $task->alarm : 0,
+                            isset($task->uid) ? $task->uid : null,
+                            isset($task->parent_id) ? $task->parent_id : '',
+                            !empty($task->private),
+                            Horde_Auth::getAuth(),
+                            isset($task->assignee) ? $task->assignee : null);
+                        if (is_a($newTask, 'PEAR_Error')) {
+                            $newtask->code = 500;
+                            return $newTask;
+                        }
+                        // use UID rather than ID
+                        $ids[] = $newTask[1];
+                    }
+                }
+            }
+            break;
+
+        default:
+                return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $content_type), 400);
+        }
+
+        if (array_key_exists($tasklist, Nag::listTasklists(false, PERMS_DELETE))) {
+            foreach (array_keys($uids_remove) as $uid) {
+                $this->delete($uid);
+            }
+        }
+
+        return $ids;
+    }
+
+    /**
+     * Deletes a file from the Nag tree.
+     *
+     * @param string $path  The path to the file.
+     *
+     * @return mixed  The event's UID, or a PEAR_Error on failure.
+     */
+    public function path_delete($path)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (substr($path, 0, 3) == 'nag') {
+            $path = substr($path, 3);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        if (count($parts) == 2) {
+            // @TODO Deny deleting of the entire tasklist for now.
+            // Allow users to delete tasklists but not create them via WebDAV will
+            // be more confusing than helpful.  They are, however, still able to
+            // delete individual task items within the tasklist folder.
+            return PEAR::raiseError(_("Deleting entire tasklists is not supported."), 403);
+            // To re-enable the functionality just remove this if {} block.
+        }
+
+        if (substr($parts[1], -4) == '.ics') {
+            $tasklistID = substr($parts[1], 0, -4);
+        } else {
+            $tasklistID = $parts[1];
+        }
+
+        if (!(count($parts) == 2 || count($parts) == 3) ||
+            !array_key_exists($tasklistID, Nag::listTasklists(false, PERMS_DELETE))) {
+                return PEAR::raiseError(_("Tasklist does not exist or no permission to delete"), 403);
+            }
+
+        /* Create a Nag storage instance. */
+        $storage = Nag_Driver::singleton($tasklistID);
+        if (is_a($storage, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $storage->getMessage()), 500);
+        }
+        $retrieved = $storage->retrieve();
+        if (is_a($retrieved, 'PEAR_Error')) {
+            $retrieved->code = 500;
+            return $retrieved;
+        }
+
+        if (count($parts) == 3) {
+            // Delete just a single entry
+            return $storage->delete($parts[2]);
+        } else {
+            // Delete the entire task list
+            $result = $storage->deleteAll();
+            if (is_a($result, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Unable to delete tasklist \"%s\": %s"), $tasklistID, $result->getMessage()), 500);
+            } else {
+                // Remove share and all groups/permissions.
+                $share = $GLOBALS['nag_shares']->getShare($tasklistID);
+                $result = $GLOBALS['nag_shares']->removeShare($share);
+                if (is_a($result, 'PEAR_Error')) {
+                    $result->code = 500;
+                    return $result;
+                }
+            }
+        }
+    }
+
+    /**
+     * @param boolean $owneronly   Only return tasklists that this user owns?
+     *                             Defaults to false.
+     * @param integer $permission  The permission to filter tasklists by.
+     *
+     * @return array  The task lists.
+     */
+    public function listTasklists($owneronly, $permission)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return Nag::listTasklists($owneronly, $permission);
+    }
+
+    /**
+     * Returns an array of UIDs for all tasks that the current user is authorized
+     * to see.
+     *
+     * @param variant $tasklist  The tasklist or an array of taskslists to list.
+     *
+     * @return array             An array of UIDs for all tasks
+     *                           the user can access.
+     */
+    public function listTaskUids($tasklist = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!isset($GLOBALS['conf']['storage']['driver'])) {
+            return PEAR::raiseError(_("Not configured"));
+        }
+
+        if ($tasklist === null) {
+            $tasklist = Nag::getDefaultTasklist(PERMS_READ);
+        }
+
+        if (!array_key_exists($tasklist,
+            Nag::listTasklists(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $tasks = Nag::listTasks(null, null, null, $tasklist, 1);
+        if (is_a($tasks, 'PEAR_Error')) {
+            return $tasks;
+        }
+
+        $uids = array();
+        $tasks->reset();
+        while ($task = $tasks->each()) {
+            $uids[] = $task->uid;
+        }
+
+        return $uids;
+    }
+
+    /**
+     * Returns an array of UIDs for tasks that have had $action happen since
+     * $timestamp.
+     *
+     * @param string  $action     The action to check for - add, modify, or delete.
+     * @param integer $timestamp  The time to start the search.
+     * @param string  $tasklist   The tasklist to be used. If 'null', the
+     *                            user's default tasklist will be used.
+     *
+     * @return array  An array of UIDs matching the action and time criteria.
+     */
+    public function listBy($action, $timestamp, $tasklist = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if ($tasklist === null) {
+            $tasklist = Nag::getDefaultTasklist(PERMS_READ);
+        }
+
+        if (!array_key_exists($tasklist,
+            Nag::listTasklists(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $history = Horde_History::singleton();
+        $histories = $history->getByTimestamp('>', $timestamp, array(array('op' => '=', 'field' => 'action', 'value' => $action)), 'nag:' . $tasklist);
+        if (is_a($histories, 'PEAR_Error')) {
+            return $histories;
+        }
+
+        // Strip leading nag:username:.
+        return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
+    }
+
+    /**
+     * Returns the timestamp of an operation for a given uid an action.
+     *
+     * @param string $uid      The uid to look for.
+     * @param string $action   The action to check for - add, modify, or delete.
+     * @param string $tasklist The tasklist to be used. If 'null', the
+     *                         user's default tasklist will be used.
+     *
+     * @return integer  The timestamp for this action.
+     */
+    public function getActionTimestamp($uid, $action, $tasklist = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if ($tasklist === null) {
+            $tasklist = Nag::getDefaultTasklist(PERMS_READ);
+        }
+
+        if (!array_key_exists($tasklist,
+            Nag::listTasklists(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $history = Horde_History::singleton();
+        return $history->getActionTimestamp('nag:' . $tasklist . ':' . $uid, $action);
+    }
+
+    /**
+     * Imports one or more tasks represented in the specified content type.
+     *
+     * If a UID is present in the content and the task is already in the
+     * database, a replace is performed rather than an add.
+     *
+     * @param string $content      The content of the task.
+     * @param string $contentType  What format is the data in? Currently supports:
+     *                             text/calendar
+     *                             text/x-vcalendar
+     * @param string $tasklist     The tasklist into which the task will be
+     *                             imported.  If 'null', the user's default
+     *                             tasklist will be used.
+     *
+     * @return string  The new UID on one import, an array of UIDs on multiple imports,
+     *                 or PEAR_Error on failure.
+     */
+    public function import($content, $contentType, $tasklist = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if ($tasklist === null) {
+            $tasklist = Nag::getDefaultTasklist(PERMS_EDIT);
+        }
+
+        if (!array_key_exists($tasklist, Nag::listTasklists(false, PERMS_EDIT))) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        /* Create a Nag_Driver instance. */
+        $storage = Nag_Driver::singleton($tasklist);
+
+        switch ($contentType) {
+        case 'text/x-vcalendar':
+        case 'text/calendar':
+        case 'text/x-vtodo':
+            $iCal = new Horde_iCalendar();
+            if (!is_a($content, 'Horde_iCalendar_vtodo')) {
+                if (!$iCal->parsevCalendar($content)) {
+                    return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+                }
+            } else {
+                $iCal->addComponent($content);
+            }
+
+            $components = $iCal->getComponents();
+            if (count($components) == 0) {
+                return PEAR::raiseError(_("No iCalendar data was found."));
+            }
+
+            $ids = array();
+            foreach ($components as $content) {
+                if (is_a($content, 'Horde_iCalendar_vtodo')) {
+                    $task = new Nag_Task();
+                    $task->fromiCalendar($content);
+                    if (isset($task->uid) &&
+                        !is_a(($existing = $storage->getByUID($task->uid)), 'PEAR_Error')) {
+                            $taskId = $existing->id;
+                            $result = $storage->modify(
+                                $taskId,
+                                isset($task->name) ? $task->name : $existing->name,
+                                isset($task->desc) ? $task->desc : $existing->desc,
+                                isset($task->start) ? $task->start : $existing->start,
+                                isset($task->due) ? $task->due : $existing->due,
+                                isset($task->priority) ? $task->priority : $existing->priority,
+                                isset($task->estimate) ? $task->estimate : 0,
+                                isset($task->completed) ? (int)$task->completed : $existing->completed,
+                                isset($task->category) ? $task->category : $existing->category,
+                                isset($task->alarm) ? $task->alarm : $existing->alarm,
+                                isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
+                                isset($task->private) ? $task->private : $existing->private,
+                                isset($task->owner) ? $task->owner : $existing->owner,
+                                isset($task->assignee) ? $task->assignee : $existing->assignee);
+
+                            if (is_a($result, 'PEAR_Error')) {
+                                return $result;
+                            }
+                            $ids[] = $task->uid;
+                        } else {
+                            $newTask = $storage->add(
+                                isset($task->name) ? $task->name : '',
+                                isset($task->desc) ? $task->desc : '',
+                                isset($task->start) ? $task->start : 0,
+                                isset($task->due) ? $task->due : 0,
+                                isset($task->priority) ? $task->priority : 3,
+                                isset($task->estimate) ? $task->estimate : 0,
+                                !empty($task->completed),
+                                isset($task->category) ? $task->category : '',
+                                isset($task->alarm) ? $task->alarm : 0,
+                                isset($task->methods) ? $task->methods : null,
+                                isset($task->uid) ? $task->uid : null,
+                                isset($task->parent_id) ? $task->parent_id : '',
+                                !empty($task->private),
+                                Horde_Auth::getAuth(),
+                                isset($task->assignee) ? $task->assignee : null);
+                            if (is_a($newTask, 'PEAR_Error')) {
+                                return $newTask;
+                            }
+                            // use UID rather than ID
+                            $ids[] = $newTask[1];
+                        }
+                }
+            }
+            if (count($ids) == 0) {
+                return PEAR::raiseError(_("No iCalendar data was found."));
+            } else if (count($ids) == 1) {
+                return $ids[0];
+            }
+            return $ids;
+        }
+
+        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+    }
+
+    /**
+     * Imports one or more tasks parsed from a string.
+     *
+     * @param string $text      The text to parse into
+     * @param string $tasklist  The tasklist into which the task will be
+     *                          imported.  If 'null', the user's default
+     *                          tasklist will be used.
+     *
+     * @return array  The UIDs of all tasks that were added.
+     */
+    public function quickAdd($text, $tasklist = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if ($tasklist === null) {
+            $tasklist = Nag::getDefaultTasklist(PERMS_EDIT);
+        }
+        if (!array_key_exists($tasklist, Nag::listTasklists(false, PERMS_EDIT))) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        return Nag::createTasksFromText($text, $tasklist);
+    }
+
+    /**
+     * Toggles the task completion flag.
+     *
+     * @param string $task_id      The task ID.
+     * @param string $tasklist_id  The tasklist that contains the task.
+     */
+    public function toggleCompletion($task_id, $tasklist_id)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!array_key_exists($tasklist_id,
+            Nag::listTasklists(false, PERMS_EDIT))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $share = $GLOBALS['nag_shares']->getShare($tasklist_id);
+        if (is_a($share, 'PEAR_Error')) {
+            return $share;
+        }
+
+        $task = Nag::getTask($tasklist_id, $task_id);
+        if (is_a($task, 'PEAR_Error')) {
+            return $task;
+        }
+
+        $task->completed = !$task->completed;
+        if ($task->completed) {
+            $task->completed_date = time();
+        } else {
+            $task->completed_date = null;
+        }
+
+        return $task->save();
+    }
+
+    /**
+     * Exports a task, identified by UID, in the requested content type.
+     *
+     * @param string $uid          Identify the task to export.
+     * @param string $contentType  What format should the data be in?
+     *                             A string with one of:
+     * <pre>
+     * text/calendar    - (VCALENDAR 2.0. Recommended as this is specified in
+     *                    rfc2445)
+     * text/x-vcalendar - (old VCALENDAR 1.0 format. Still in wide use)
+     * </pre>
+     *
+     * @return string  The requested data.
+     */
+    public function export($uid, $contentType)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $storage = Nag_Driver::singleton();
+        $task = $storage->getByUID($uid);
+        if (is_a($task, 'PEAR_Error')) {
+            return $task;
+        }
+
+        if (!array_key_exists($task->tasklist, Nag::listTasklists(false, PERMS_READ))) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        $version = '2.0';
+        switch ($contentType) {
+        case 'text/x-vcalendar':
+            $version = '1.0';
+        case 'text/calendar':
+            // Create the new iCalendar container.
+            $iCal = new Horde_iCalendar($version);
+            $iCal->setAttribute('PRODID', '-//The Horde Project//Nag ' . $GLOBALS['registry']->getVersion() . '//EN');
+            $iCal->setAttribute('METHOD', 'PUBLISH');
+
+            // Create new vTodo object.
+            $vTodo = $task->toiCalendar($iCal);
+            $vTodo->setAttribute('VERSION', $version);
+
+            $iCal->addComponent($vTodo);
+
+            return $iCal->exportvCalendar();
+
+        default:
+            return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+        }
+    }
+
+    /**
+     * Exports a tasklist in the requested content type.
+     *
+     * @param string $tasklist     The tasklist to export.
+     * @param string $contentType  What format should the data be in?
+     *                             A string with one of:
+     *                             <pre>
+     *                             text/calendar (VCALENDAR 2.0. Recommended as
+     *                                            this is specified in rfc2445)
+     *                             text/x-vcalendar (old VCALENDAR 1.0 format.
+     *                                              Still in wide use)
+     *                             </pre>
+     *
+     * @return string  The iCalendar representation of the tasklist.
+     */
+    public function exportTasklist($tasklist, $contentType)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!array_key_exists($tasklist,
+            Nag::listTasklists(false, PERMS_READ))) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+
+        $tasks = Nag::listTasks(null, null, null, array($tasklist), 1);
+
+        $version = '2.0';
+        switch ($contentType) {
+        case 'text/x-vcalendar':
+            $version = '1.0';
+        case 'text/calendar':
+            $share = $GLOBALS['nag_shares']->getShare($tasklist);
+
+            $iCal = new Horde_iCalendar($version);
+            $iCal->setAttribute('X-WR-CALNAME', Horde_String::convertCharset($share->get('name'), Horde_Nls::getCharset(), 'utf-8'));
+
+            $tasks->reset();
+            while ($task = $tasks->each()) {
+                $iCal->addComponent($task->toiCalendar($iCal));
+            }
+
+            return $iCal->exportvCalendar();
+        }
+
+        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+
+    }
+
+    /**
+     * Deletes a task identified by UID.
+     *
+     * @param string|array $uid  Identify the task to delete, either a single UID
+     *                           or an array.
+     *
+     * @return boolean  Success or failure.
+     */
+    public function delete($uid)
+    {
+        // Handle an arrray of UIDs for convenience of deleting multiple tasks at
+        // once.
+        if (is_array($uid)) {
+            foreach ($uid as $g) {
+                $result = $this->delete($g);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+
+            return true;
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+
+        $storage = Nag_Driver::singleton();
+        $task = $storage->getByUID($uid);
+        if (is_a($task, 'PEAR_Error')) {
+            return $task;
+        }
+
+        if (!Horde_Auth::isAdmin() &&
+            !array_key_exists($task->tasklist,
+                Nag::listTasklists(false, PERMS_DELETE))) {
+                    return PEAR::raiseError(_("Permission Denied"));
+                }
+
+        return $storage->delete($task->id);
+    }
+
+    /**
+     * Replaces the task identified by UID with the content represented in the
+     * specified content type.
+     *
+     * If you want to replace multiple tasks with the UID specified in the
+     * VCALENDAR data, you may use $this->import instead. This automatically does a
+     * replace if existings UIDs are found.
+     *
+     *
+     * @param string $uid          Identify the task to replace.
+     * @param string $content      The content of the task.
+     * @param string $contentType  What format is the data in? Currently supports:
+     *                             - text/x-vcalendar
+     *                             - text/calendar
+     *
+     * @return boolean  Success or failure.
+     */
+    public function replace($uid, $content, $contentType)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $storage = Nag_Driver::singleton();
+        $existing = $storage->getByUID($uid);
+        if (is_a($existing, 'PEAR_Error')) {
+            return $existing;
+        }
+        $taskId = $existing->id;
+
+        if (!array_key_exists($existing->tasklist, Nag::listTasklists(false, PERMS_EDIT))) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        switch ($contentType) {
+        case 'text/calendar':
+        case 'text/x-vcalendar':
+            if (!is_a($content, 'Horde_iCalendar_vtodo')) {
+                $iCal = new Horde_iCalendar();
+                if (!$iCal->parsevCalendar($content)) {
+                    return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+                }
+
+                $components = $iCal->getComponents();
+                $component = null;
+                foreach ($components as $content) {
+                    if (is_a($content, 'Horde_iCalendar_vtodo')) {
+                        if ($component !== null) {
+                            return PEAR::raiseError(_("Multiple iCalendar components found; only one vTodo is supported."));
+                        }
+                        $component = $content;
+                    }
+
+                }
+                if ($component === null) {
+                    return PEAR::raiseError(_("No iCalendar data was found."));
+                }
+            }
+
+            $task = new Nag_Task();
+            $task->fromiCalendar($content);
+            $result = $storage->modify(
+                $taskId,
+                isset($task->name) ? $task->name : $existing->name,
+                isset($task->desc) ? $task->desc : $existing->desc,
+                isset($task->start) ? $task->start : $existing->start,
+                isset($task->due) ? $task->due : $existing->due,
+                isset($task->priority) ? $task->priority : $existing->priority,
+                isset($task->estimate) ? $task->estimate : 0,
+                isset($task->completed) ? (int)$task->completed : $existing->completed,
+                isset($task->category) ? $task->category : $existing->category,
+                isset($task->alarm) ? $task->alarm : $existing->alarm,
+                isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
+                isset($task->private) ? $task->private : $existing->private,
+                isset($task->owner) ? $task->owner : $existing->owner,
+                isset($task->assignee) ? $task->assignee : $existing->assignee);
+
+            break;
+
+        default:
+            return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+        }
+
+        return $result;
+    }
+
+    /**
+     * Lists active tasks as cost objects.
+     *
+     * @todo Implement $criteria parameter.
+     *
+     * @param array $criteria   Filter attributes
+     */
+    public function listCostObjects($criteria)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $tasks = Nag::listTasks(null, null, null, null, 1);
+        $result = array();
+        $tasks->reset();
+        while ($task = $tasks->each()) {
+            $result[$task->id] = array('id' => $task->id,
+                'active' => !$task->completed,
+                'name' => $task->name);
+            if (!empty($task->estimate)) {
+                $result[$task->id]['estimate'] = $task->estimate;
+            }
+        }
+
+        if (count($result) == 0) {
+            return array();
+        } else {
+            return array(array('category' => _("Tasks"),
+                'objects'  => array_values($result)));
+        }
+    }
+
+    public function listTimeObjectCategories()
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $categories = array();
+        $tasklists = Nag::listTasklists(false, PERMS_SHOW | PERMS_READ);
+        foreach ($tasklists as $tasklistId => $tasklist) {
+            $categories[$tasklistId] = $tasklist->get('name');
+        }
+        return $categories;
+    }
+
+    /**
+     * Lists active tasks as time objects.
+     *
+     * @param array $categories  The time categories (from listTimeObjectCategories) to list.
+     * @param mixed $start       The start date of the period.
+     * @param mixed $end         The end date of the period.
+     */
+    public function listTimeObjects($categories, $start, $end)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $allowed_tasklists = Nag::listTasklists(false, PERMS_READ);
+        foreach ($categories as $tasklist) {
+            if (!array_key_exists($tasklist, $allowed_tasklists)) {
+                return PEAR::raiseError(_("Permission Denied"));
+            }
+        }
+
+        $timeobjects = array();
+        $start = new Horde_Date($start);
+        $start_ts = $start->timestamp();
+        $end = new Horde_Date($end);
+        $end_ts = $end->timestamp();
+
+        // List incomplete tasks.
+        $tasks = Nag::listTasks(null, null, null, $categories, 0);
+        $tasks->reset();
+        while ($task = $tasks->each()) {
+            // If there's no due date, it's not a time object.
+            if (!$task->due || $task->due + 1 < $start_ts || $task->due > $end_ts) {
+                continue;
+            }
+            $due_date = date('Y-m-d\TH:i:s', $task->due);
+            $timeobjects[$task->id] = array(
+                'id' => $task->id,
+                'title' => $task->name,
+                'description' => $task->desc,
+                'start' => $due_date,
+                'end' => $due_date,
+                'category' => $task->category,
+                'params' => array('task' => $task->id,
+                'tasklist' => $task->tasklist),
+                'link' => Horde_Util::addParameter(Horde::applicationUrl('view.php', true), array('tasklist' => $task->tasklist, 'task' => $task->id)));
+        }
+
+        return $timeobjects;
+    }
+
+    /**
+     * Lists alarms for a given moment.
+     *
+     * @param integer $time  The time to retrieve alarms for.
+     * @param string $user   The user to retreive alarms for. All users if null.
+     *
+     * @return array  An array of UIDs
+     */
+    public function listAlarms($time, $user = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        require_once 'Horde/Group.php';
+
+        if ((empty($user) || $user != Horde_Auth::getAuth()) && !Horde_Auth::isAdmin()) {
+            return PEAR::raiseError(_("Permission Denied"));
+        }
+
+        $storage = Nag_Driver::singleton();
+        $group = Group::singleton();
+        $alarm_list = array();
+        $tasklists = is_null($user) ? array_keys($GLOBALS['nag_shares']->listAllShares()) :  $GLOBALS['display_tasklists'];
+
+        $alarms = Nag::listAlarms($time, $tasklists);
+        if (is_a($alarms, 'PEAR_Error')) {
+            return $alarms;
+        }
+
+        foreach ($alarms as $alarm) {
+            $share = $GLOBALS['nag_shares']->getShare($alarm->tasklist);
+            if (is_a($share, 'PEAR_Error')) {
+                continue;
+            }
+            if (empty($user)) {
+                $users = $share->listUsers(PERMS_READ);
+                $groups = $share->listGroups(PERMS_READ);
+                foreach ($groups as $gid) {
+                    $users = array_merge($users, $group->listUsers($gid));
+                }
+                $users = array_unique($users);
+            } else {
+                $users = array($user);
+            }
+            foreach ($users as $alarm_user) {
+                $prefs = Prefs::singleton($GLOBALS['conf']['prefs']['driver'],
+                    'nag', $alarm_user, null, null, false);
+                Horde_Nls::setLanguageEnvironment($prefs->getValue('language'));
+                $alarm_list[] = $alarm->toAlarm($alarm_user, $prefs);
+            }
+        }
+
+        return $alarm_list;
+    }
+
+}
diff --git a/nag/lib/api.php b/nag/lib/api.php
deleted file mode 100644 (file)
index 8950d7c..0000000
+++ /dev/null
@@ -1,1461 +0,0 @@
-<?php
-/**
- * Nag external API interface.
- *
- * This file defines Nag's external API interface. Other applications can
- * interact with Nag through this API.
- *
- * @package Nag
- */
-
-$_services['perms'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}hashHash'
-);
-
-$_services['removeUserData'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['show'] = array(
-    'link' => '%application%/view.php?tasklist=|tasklist|&task=|task|&uid=|uid|',
-);
-
-$_services['browse'] = array(
-    'args' => array('path' => 'string'),
-    'type' => '{urn:horde}hashHash',
-);
-
-$_services['put'] = array(
-    'args' => array('path' => 'string', 'content' => 'string', 'content_type' => 'string'),
-    'type' => 'int',
-);
-
-$_services['path_delete'] = array(
-    'args' => array('path' => 'string'),
-    'type' => 'boolean',
-);
-
-$_services['addTasklist'] = array(
-    'args' => array('name' => 'string', 'description' => 'string'),
-    'type' => 'string',
-);
-
-$_services['listTasklists'] = array(
-    'args' => array('owneronly' => 'boolean', 'permission' => 'int'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['listTasks'] = array(
-    'args' => array('sortby' => 'string', 'sortdir' => 'int', 'altsortby' => 'string', 'tasklists' => '{urn:horde}stringArray', 'completed' => 'int', 'json' => 'boolean'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['listAlarms'] = array(
-    'args' => array('time' => 'int', 'user' => 'string'),
-    'type' => '{urn:horde}hashHash'
-);
-
-$_services['list'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['listBy'] = array(
-    'args' => array('action' => 'string', 'timestamp' => 'int'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['getActionTimestamp'] = array(
-    'args' => array('uid' => 'string', 'action' => 'string', 'tasklist' => 'string'),
-    'type' => 'int',
-);
-
-$_services['import'] = array(
-    'args' => array('content' => 'string', 'contentType' => 'string', 'tasklist' => 'string'),
-    'type' => 'string',
-);
-
-$_services['quickAdd'] = array(
-    'args' => array('content' => 'string', 'tasklist' => 'string'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['export'] = array(
-    'args' => array('uid' => 'string', 'contentType' => '{urn:horde}stringArray'),
-    'type' => 'string',
-);
-
-$_services['exportTasklist'] = array(
-    'args' => array('tasklist' => 'string', 'contentType' => 'string'),
-    'type' => 'string'
-);
-
-$_services['delete'] = array(
-    'args' => array('uid' => '{urn:horde}stringArray'),
-    'type' => 'boolean',
-);
-
-$_services['replace'] = array(
-    'args' => array('uid' => 'string', 'content' => 'string', 'contentType' => 'string'),
-    'type' => 'boolean',
-);
-
-$_services['listCostObjects'] = array(
-    'args' => array('criteria' => '{urn:horde}hash'),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['listTimeObjectCategories'] = array(
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['listTimeObjects'] = array(
-    'args' => array('start' => 'int', 'end' => 'int'),
-    'type' => '{urn:horde}hashHash'
-);
-
-$_services['toggleCompletion'] = array(
-    'args' => array('task_id' => 'string', 'tasklist_id' => 'string'),
-    'type' => 'boolean'
-);
-
-/**
- * Returns a list of available permissions.
- *
- * @return array  An array describing all available permissions.
- */
-function _nag_perms()
-{
-    $perms = array();
-    $perms['tree']['nag']['max_tasks'] = false;
-    $perms['title']['nag:max_tasks'] = _("Maximum Number of Tasks");
-    $perms['type']['nag:max_tasks'] = 'int';
-
-    return $perms;
-}
-
-/**
- * Removes user data.
- *
- * @param string $user  Name of user to remove data for.
- *
- * @return mixed  true on success | PEAR_Error on failure
- */
-function _nag_removeUserData($user)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!Horde_Auth::isAdmin() && $user != Horde_Auth::getAuth()) {
-        return PEAR::raiseError(_("You are not allowed to remove user data."));
-    }
-
-    /* Error flag */
-    $hasError = false;
-
-    /* Get the share for later deletion */
-    $share = $GLOBALS['nag_shares']->getShare($user);
-    if(is_a($share, 'PEAR_Error')) {
-        Horde::logMessage($share->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
-        unset($share);
-    } else {
-        /* Get the list of all tasks */
-        $tasks = Nag::listTasks(null, null, null, $user, 1);
-        if (is_a($tasks, 'PEAR_Error')) {
-            $hasError = true;
-            Horde::logMessage($share->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
-        } else {
-            $uids = array();
-            $tasks->reset();
-            while ($task = $tasks->each()) {
-                $uids[] = $task->uid;
-            }
-
-            /* ... and delete them. */
-            foreach ($uids as $uid) {
-                _nag_delete($uid);
-            }
-        }
-    }
-
-    /* Now delete history as well. */
-    $history = Horde_History::singleton();
-    if (method_exists($history, 'removeByParent')) {
-        $histories = $history->removeByParent('nag:' . $user);
-    } else {
-        /* Remove entries 100 at a time. */
-        $all = $history->getByTimestamp('>', 0, array(), 'nag:' . $user);
-        if (is_a($all, 'PEAR_Error')) {
-            Horde::logMessage($all, __FILE__, __LINE__, PEAR_LOG_ERR);
-        } else {
-            $all = array_keys($all);
-            while (count($d = array_splice($all, 0, 100)) > 0) {
-                $history->removebyNames($d);
-            }
-        }
-    }
-
-    /* ...and finally, delete the actual share */
-    if (!empty($share)) {
-        $result = $GLOBALS['nag_shares']->removeShare($share);
-        if (is_a($result, 'PEAR_Error')) {
-            $hasError = true;
-            Horde::logMessage($result->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
-        }
-    }
-
-    /* Now remove perms for this user from all other shares */
-    $shares = $GLOBALS['nag_shares']->listShares($user);
-    if (is_a($shares, 'PEAR_Error')) {
-        $hasError = true;
-        Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
-    }
-    foreach ($shares as $share) {
-        $share->removeUser($user);
-    }
-
-    if ($hasError) {
-        return PEAR::raiseError(sprintf(_("There was an error removing tasks for %s. Details have been logged."), $user));
-    } else {
-        return true;
-    }
-}
-
-/**
- * Retrieves the current user's task list from storage.
- *
- * This function will also sort the resulting list, if requested.
- *
- * @param string $sortby        The field by which to sort
- *                              (NAG_SORT_PRIORITY, NAG_SORT_NAME
- *                              NAG_SORT_DUE, NAG_SORT_COMPLETION).
- * @param integer $sortdir      The direction by which to sort
- * @param string $altsortby     The secondary sort field.
- * @param array $tasklists      An array of tasklist to display or
- *                              null/empty to display taskslists
- *                              $GLOBALS['display_tasklists'].
- * @param integer $completed    Which tasks to retrieve (1 = all tasks,
- *                              0 = incomplete tasks, 2 = complete tasks,
- *                              3 = future tasks, 4 = future and incomplete
- *                              tasks).
- * @param boolean $json         Retrieve the results of the tasks in
- *                              'json format'
- *
- * @return Nag_Task  A list of the requested tasks.
- */
-function _nag_listTasks($sortby = null, $sortdir = null, $altsortby = null,
-                        $tasklists = null, $completed = null, $json = false)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!isset($sortby)) {
-        $sortby = $GLOBALS['prefs']->getValue('sortby');
-    }
-    if (!isset($sortdir)) {
-        $sortdir = $GLOBALS['prefs']->getValue('sortdir');
-    }
-    if (is_null($altsortby)) {
-        $altsortby =  $GLOBALS['prefs']->getValue('altsortby');
-    }
-    if (is_null($tasklists)) {
-        $tasklists = $GLOBALS['display_tasklists'];
-    }
-    if (is_null($completed)) {
-        $completed = $GLOBALS['prefs']->getValue('show_completed');
-    }
-
-    $tasks = Nag::listTasks($sortby, $sortdir, $altsortby, $tasklists);
-    $tasks->reset();
-    $list = array();
-    while ($task = $tasks->each()) {
-        $list[$task->id] = $json ? $task->toJson() : $task->toHash();
-    }
-
-    return $list;
-}
-
-/**
- * Add a new task list
- *
- * @param string $name        Task list name
- * @param string $description Task list description
- *
- * @return integer  The new tasklist's id.
- */
-function _nag_addTasklist($name, $description = '')
-{
-    if (!Horde_Auth::getAuth()) {
-        return PEAR::raiseError(_("Permission denied"));
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-    global $nag_shares;
-
-    $tasklistId = md5(microtime());
-    $tasklist = $nag_shares->newShare($tasklistId);
-
-    if (is_a($tasklist, 'PEAR_Error')) {
-        return $tasklist;
-    }
-
-    $tasklist->set('name', $name, false);
-    $tasklist->set('desc', $description, false);
-    $result = $nag_shares->addShare($tasklist);
-
-    if (is_a($result, 'PEAR_Error')) {
-        return $result;
-    }
-
-    return $tasklistId;
-}
-
-/**
- * Returns the last modification timestamp of a given uid.
- *
- * @param string $uid      The uid to look for.
- * @param string $tasklist The tasklist to look in.
- *
- * @return integer  The timestamp for the last modification of $uid.
- */
-function __nag_modified($uid, $tasklist = null)
-{
-    $modified = _nag_getActionTimestamp($uid, 'modify', $tasklist);
-    if (empty($modified)) {
-        $modified = _nag_getActionTimestamp($uid, 'add', $tasklist);
-    }
-    return $modified;
-}
-
-/**
- * Browse through Nag's object tree.
- *
- * @param string $path       The level of the tree to browse.
- * @param array $properties  The item properties to return. Defaults to 'name',
- *                           'icon', and 'browseable'.
- *
- * @return array  The contents of $path
- */
-function _nag_browse($path = '', $properties = array())
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $registry;
-
-    function _getTasklistSize($tasklistID)
-    {
-        // This ugly and performance-heavy hack is required to set the content
-        // length.  Some clients (at least OS X) respect the content-length
-        // header a little too exactly.  If we send a content-length that is
-        // longer than the actual data it will complain that the connection
-        // broke.  If we specify one that is too short it will truncate the
-        // downlaoded file.  To make matters worse it seems to respect the
-        // content-length from the PROPFIND request used to enumerate objects
-        // rather than the actual content-length sent at the time the file is
-        // downloaded.  Way to go, Apple.
-        return strlen(_nag_exportTasklist($tasklistID, 'text/calendar'));
-    }
-
-    // Default properties.
-    if (!$properties) {
-        $properties = array('name', 'icon', 'browseable');
-    }
-
-    if (substr($path, 0, 3) == 'nag') {
-        $path = substr($path, 3);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    if (empty($path)) {
-        //
-        // This request is for a list of all users who have tasklists visible
-        // to the requesting user.
-        //
-        $tasklists = Nag::listTasklists(false, PERMS_READ);
-        $owners = array();
-        foreach ($tasklists as $tasklist) {
-            $owners[$tasklist->get('owner')] = true;
-        }
-
-        $results = array();
-        foreach (array_keys($owners) as $owner) {
-            if (in_array('name', $properties)) {
-                $results['nag/' . $owner]['name'] = $owner;
-            }
-            if (in_array('icon', $properties)) {
-                $results['nag/' . $owner]['icon'] =
-                    $registry->getImageDir('horde') . '/user.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results['nag/' . $owner]['browseable'] = true;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results['nag/' . $owner]['contenttype'] =
-                    'httpd/unix-directory';
-            }
-            if (in_array('contentlength', $properties)) {
-                $results['nag/' . $owner]['contentlength'] = 0;
-            }
-            if (in_array('modified', $properties)) {
-                $results['nag/' . $owner]['modified'] =
-                    $_SERVER['REQUEST_TIME'];
-            }
-            if (in_array('created', $properties)) {
-                $results['nag/' . $owner]['created'] = 0;
-            }
-        }
-        return $results;
-
-    } elseif (count($parts) == 1) {
-        //
-        // This request is for all tasklists owned by the requested user
-        //
-        $tasklists = $GLOBALS['nag_shares']->listShares($parts[0],
-                                                        PERMS_SHOW,
-                                                        $parts[0]);
-
-        // The last check returns all addressbooks for the requested user,
-        // but that does not mean the requesting user has access to them.
-        // Filter out those address books for which the requesting user has
-        // no access.
-        $tasklists = Nag::permissionsFilter($tasklists);
-
-        $results = array();
-        foreach ($tasklists as $tasklistId => $tasklist) {
-            $retpath = 'nag/' . $parts[0] . '/' . $tasklistId;
-            if (in_array('name', $properties)) {
-                $results[$retpath]['name'] = sprintf(_("Tasks from %s"), $tasklist->get('name'));
-                $results[$retpath . '.ics']['name'] = $tasklist->get('name');
-            }
-            if (in_array('icon', $properties)) {
-                $results[$retpath]['icon'] = $registry->getImageDir() . '/nag.png';
-                $results[$retpath . '.ics']['icon'] = $registry->getImageDir() . '/mime/icalendar.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results[$retpath]['browseable'] = $tasklist->hasPermission(Horde_Auth::getAuth(), PERMS_READ);
-                $results[$retpath . '.ics']['browseable'] = false;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results[$retpath]['contenttype'] = 'httpd/unix-directory';
-                $results[$retpath . '.ics']['contenttype'] = 'text/calendar';
-            }
-            if (in_array('contentlength', $properties)) {
-                $results[$retpath]['contentlength'] = 0;
-                $results[$retpath . '.ics']['contentlength'] = _getTasklistSize($tasklistId);
-            }
-            if (in_array('modified', $properties)) {
-                // @TODO Find a way to get the actual modification times
-                $results[$retpath]['modified'] = $_SERVER['REQUEST_TIME'];
-                $results[$retpath . '.ics']['modified'] = $_SERVER['REQUEST_TIME'];
-            }
-            if (in_array('created', $properties)) {
-                // @TODO Find a way to get the actual creation times
-                $results[$retpath]['created'] = 0;
-                $results[$retpath . '.ics']['created'] = 0;
-            }
-        }
-        return $results;
-
-    } elseif (count($parts) == 2 && substr($parts[1], -4) == '.ics') {
-        //
-        // This is a request for the entire tasklist in iCalendar format.
-        //
-        $tasklist = substr($parts[1], 0, -4);
-        if (!array_key_exists($tasklist, Nag::listTasklists(false, PERMS_READ))) {
-            return PEAR::raiseError(_("Invalid tasklist file requested."), 404);
-        }
-        $ical_data = _nag_exportTasklist($tasklist, 'text/calendar');
-        $result = array('data'          => $ical_data,
-                        'mimetype'      => 'text/calendar',
-                        'contentlength' => strlen($ical_data),
-                        'mtime'         => $_SERVER['REQUEST_TIME']);
-
-        return $result;
-
-    } elseif (count($parts) == 2) {
-        //
-        // This request is browsing into a specific tasklist.  Generate the list
-        // of items and represent them as files within the directory.
-        //
-        if (!array_key_exists($parts[1], Nag::listTasklists(false, PERMS_READ))) {
-            return PEAR::raiseError(_("Invalid tasklist requested."), 404);
-        }
-        $storage = Nag_Driver::singleton($parts[1]);
-        $result = $storage->retrieve();
-        if (is_a($result, 'PEAR_Error')) {
-            $result->code = 500;
-            return $result;
-        }
-
-        $icon = $registry->getImageDir() . '/nag.png';
-        $results = array();
-        $storage->tasks->reset();
-        while ($task = $storage->tasks->each()) {
-            $key = 'nag/' . $parts[0] . '/' . $parts[1] . '/' . $task->id;
-            if (in_array('name', $properties)) {
-                $results[$key]['name'] = $task->name;
-            }
-            if (in_array('icon', $properties)) {
-                $results[$key]['icon'] = $icon;
-            }
-            if (in_array('browseable', $properties)) {
-                $results[$key]['browseable'] = false;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results[$key]['contenttype'] = 'text/calendar';
-            }
-            if (in_array('contentlength', $properties)) {
-                // FIXME:  This is a hack.  If the content length is longer
-                // than the actual data then some WebDAV clients will report
-                // an error when the file EOF is received.  Ideally we should
-                // determine the actual size of the data and report it here, but
-                // the performance hit may be prohibitive.  This requires
-                // further investigation.
-                $results[$key]['contentlength'] = 1;
-            }
-            if (in_array('modified', $properties)) {
-                $results[$key]['modified'] = __nag_modified($task->uid, $path);
-            }
-            if (in_array('created', $properties)) {
-                $results[$key]['created'] = _nag_getActionTimestamp($task->uid, 'add', $path);
-            }
-        }
-        return $results;
-    } else {
-        //
-        // The only valid request left is for either a specific task item.
-        //
-        if (count($parts) == 3 &&
-            array_key_exists($parts[1], Nag::listTasklists(false,
-                             PERMS_READ))) {
-            //
-            // This request is for a specific item within a given task list.
-            //
-            /* Create a Nag storage instance. */
-            $storage = Nag_Driver::singleton($parts[1]);
-            if (is_a($storage, 'PEAR_Error')) {
-                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $storage->getMessage()));
-            }
-            $storage->retrieve();
-
-            $task = $storage->get($parts[2]);
-            if (is_a($task, 'PEAR_Error')) {
-                $task->code = 500;
-                return $task;
-            }
-
-            $result = array('data' => _nag_export($task->uid, 'text/calendar'),
-                            'mimetype' => 'text/calendar');
-            $modified = __nag_modified($task->uid, $parts[1]);
-            if (!empty($modified)) {
-                $result['mtime'] = $modified;
-            }
-            return $result;
-        } elseif (count($parts) == 2 &&
-                  substr($parts[1], -4) == '.ics' &&
-                  array_key_exists(substr($parts[1], 0, -4), Nag::listTasklists(false, PERMS_READ))) {
-        } else {
-            //
-            // All other requests are a 404: Not Found
-            //
-            return false;
-        }
-    }
-}
-
-/**
- * Saves a file into the Nag tree.
- *
- * @param string $path          The path where to PUT the file.
- * @param string $content       The file content.
- * @param string $content_type  The file's content type.
- *
- * @return array  The event UIDs, or a PEAR_Error on failure.
- */
-function _nag_put($path, $content, $content_type)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (substr($path, 0, 3) == 'nag') {
-        $path = substr($path, 3);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    if (count($parts) == 2 &&
-        substr($parts[1], -4) == '.ics') {
-
-        // Workaround for WebDAV clients that are not smart enough to send
-        // the right content type.  Assume text/calendar.
-        if ($content_type == 'application/octet-stream') {
-            $content_type = 'text/calendar';
-        }
-        $tasklist = substr($parts[1], 0, -4);
-    } elseif (count($parts) == 3) {
-        $tasklist = $parts[1];
-
-        // Workaround for WebDAV clients that are not smart enough to send
-        // the right content type.  Assume the same format we send individual
-        // tasklist items: text/calendar
-        if ($content_type == 'application/octet-stream') {
-            $content_type = 'text/calendar';
-        }
-    } else {
-        return PEAR::raiseError(_("Invalid tasklist name supplied."), 403);
-    }
-
-    if (!array_key_exists($tasklist, Nag::listTasklists(false, PERMS_EDIT))) {
-        // FIXME: Should we attempt to create a tasklist based on the filename
-        // in the case that the requested tasklist does not exist?
-        return PEAR::raiseError(_("Tasklist does not exist or no permission to edit"), 403);
-    }
-
-    // Store all currently existings UIDs. Use this info to delete UIDs not
-    // present in $content after processing.
-    $ids = array();
-    $uids_remove = array_flip(_nag_list($tasklist));
-
-    $storage = Nag_Driver::singleton($tasklist);
-
-    switch ($content_type) {
-    case 'text/calendar':
-    case 'text/x-vcalendar':
-        $iCal = new Horde_iCalendar();
-        if (!is_a($content, 'Horde_iCalendar_vtodo')) {
-            if (!$iCal->parsevCalendar($content)) {
-                return PEAR::raiseError(_("There was an error importing the iCalendar data."), 400);
-            }
-        } else {
-            $iCal->addComponent($content);
-        }
-
-        foreach ($iCal->getComponents() as $content) {
-            if (is_a($content, 'Horde_iCalendar_vtodo')) {
-                $task = new Nag_Task();
-                $task->fromiCalendar($content);
-                $task->tasklist = $tasklist;
-                if (isset($task->uid) &&
-                    !is_a(($existing = $storage->getByUID($task->uid)), 'PEAR_Error')) {
-                    // Entry exists, remove from uids_remove list so we won't
-                    // delete in the end.
-                    if (isset($uids_remove[$task->uid])) {
-                        unset($uids_remove[$task->uid]);
-                    }
-                    if ($existing->private &&
-                        $existing->owner != Horde_Auth::getAuth()) {
-                        continue;
-                    }
-                    // Check if our task is newer then the existing - get the
-                    // task's history.
-                    $history = Horde_History::singleton();
-                    $created = $modified = null;
-                    $log = $history->getHistory('nag:' . $tasklist . ':' . $task->uid);
-                    if ($log && !is_a($log, 'PEAR_Error')) {
-                        foreach ($log->getData() as $entry) {
-                            switch ($entry['action']) {
-                            case 'add':
-                                $created = $entry['ts'];
-                                break;
-
-                            case 'modify':
-                                $modified = $entry['ts'];
-                                break;
-                            }
-                        }
-                    }
-                    if (empty($modified) && !empty($add)) {
-                        $modified = $add;
-                    }
-                    if (!empty($modified) &&
-                        $modified >= $content->getAttribute('LAST-MODIFIED')) {
-                        // LAST-MODIFIED timestamp of existing entry is newer:
-                        // don't replace it.
-                        continue;
-                    }
-
-                    // Don't change creator/owner.
-                    $owner = $existing->owner;
-                    $taskId = $existing->id;
-                    $result = $storage->modify(
-                        $taskId,
-                        isset($task->name) ? $task->name : $existing->name,
-                        isset($task->desc) ? $task->desc : $existing->desc,
-                        isset($task->start) ? $task->start : $existing->start,
-                        isset($task->due) ? $task->due : $existing->due,
-                        isset($task->priority) ? $task->priority : $existing->priority,
-                        isset($task->estimate) ? $task->estimate : 0,
-                        isset($task->completed) ? (int)$task->completed : $existing->completed,
-                        isset($task->category) ? $task->category : $existing->category,
-                        isset($task->alarm) ? $task->alarm : $existing->alarm,
-                        isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
-                        isset($task->private) ? $task->private : $existing->private,
-                        $owner,
-                        isset($task->assignee) ? $task->assignee : $existing->assignee);
-
-                    if (is_a($result, 'PEAR_Error')) {
-                        $result->code = 500;
-                        return $result;
-                    }
-                    $ids[] = $task->uid;
-                } else {
-                    $newTask = $storage->add(
-                        isset($task->name) ? $task->name : '',
-                        isset($task->desc) ? $task->desc : '',
-                        isset($task->start) ? $task->start : 0,
-                        isset($task->due) ? $task->due : 0,
-                        isset($task->priority) ? $task->priority : 3,
-                        isset($task->estimate) ? $task->estimate : 0,
-                        !empty($task->completed),
-                        isset($task->category) ? $task->category : '',
-                        isset($task->alarm) ? $task->alarm : 0,
-                        isset($task->uid) ? $task->uid : null,
-                        isset($task->parent_id) ? $task->parent_id : '',
-                        !empty($task->private),
-                        Horde_Auth::getAuth(),
-                        isset($task->assignee) ? $task->assignee : null);
-                    if (is_a($newTask, 'PEAR_Error')) {
-                        $newtask->code = 500;
-                        return $newTask;
-                    }
-                    // use UID rather than ID
-                    $ids[] = $newTask[1];
-                }
-            }
-        }
-        break;
-
-    default:
-        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $content_type), 400);
-    }
-
-    if (array_key_exists($tasklist, Nag::listTasklists(false, PERMS_DELETE))) {
-        foreach (array_keys($uids_remove) as $uid) {
-            _nag_delete($uid);
-        }
-    }
-
-    return $ids;
-}
-
-/**
- * Deletes a file from the Nag tree.
- *
- * @param string $path  The path to the file.
- *
- * @return mixed  The event's UID, or a PEAR_Error on failure.
- */
-function _nag_path_delete($path)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (substr($path, 0, 3) == 'nag') {
-        $path = substr($path, 3);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    if (count($parts) == 2) {
-        // @TODO Deny deleting of the entire tasklist for now.
-        // Allow users to delete tasklists but not create them via WebDAV will
-        // be more confusing than helpful.  They are, however, still able to
-        // delete individual task items within the tasklist folder.
-        return PEAR::raiseError(_("Deleting entire tasklists is not supported."), 403);
-        // To re-enable the functionality just remove this if {} block.
-    }
-
-    if (substr($parts[1], -4) == '.ics') {
-        $tasklistID = substr($parts[1], 0, -4);
-    } else {
-        $tasklistID = $parts[1];
-    }
-
-    if (!(count($parts) == 2 || count($parts) == 3) ||
-        !array_key_exists($tasklistID, Nag::listTasklists(false, PERMS_DELETE))) {
-        return PEAR::raiseError(_("Tasklist does not exist or no permission to delete"), 403);
-    }
-
-    /* Create a Nag storage instance. */
-    $storage = Nag_Driver::singleton($tasklistID);
-    if (is_a($storage, 'PEAR_Error')) {
-        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $storage->getMessage()), 500);
-    }
-    $retrieved = $storage->retrieve();
-    if (is_a($retrieved, 'PEAR_Error')) {
-        $retrieved->code = 500;
-        return $retrieved;
-    }
-
-    if (count($parts) == 3) {
-        // Delete just a single entry
-        return $storage->delete($parts[2]);
-    } else {
-        // Delete the entire task list
-        $result = $storage->deleteAll();
-        if (is_a($result, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Unable to delete tasklist \"%s\": %s"), $tasklistID, $result->getMessage()), 500);
-        } else {
-            // Remove share and all groups/permissions.
-            $share = $GLOBALS['nag_shares']->getShare($tasklistID);
-            $result = $GLOBALS['nag_shares']->removeShare($share);
-            if (is_a($result, 'PEAR_Error')) {
-                $result->code = 500;
-                return $result;
-            }
-        }
-    }
-}
-
-/**
- * @param boolean $owneronly   Only return tasklists that this user owns?
- *                             Defaults to false.
- * @param integer $permission  The permission to filter tasklists by.
- *
- * @return array  The task lists.
- */
-function _nag_listTasklists($owneronly, $permission)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return Nag::listTasklists($owneronly, $permission);
-}
-
-/**
- * Returns an array of UIDs for all tasks that the current user is authorized
- * to see.
- *
- * @param variant $tasklist  The tasklist or an array of taskslists to list.
- *
- * @return array             An array of UIDs for all tasks
- *                           the user can access.
- */
-function _nag_list($tasklist = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!isset($GLOBALS['conf']['storage']['driver'])) {
-        return PEAR::raiseError(_("Not configured"));
-    }
-
-    if ($tasklist === null) {
-        $tasklist = Nag::getDefaultTasklist(PERMS_READ);
-    }
-
-    if (!array_key_exists($tasklist,
-                          Nag::listTasklists(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $tasks = Nag::listTasks(null, null, null, $tasklist, 1);
-    if (is_a($tasks, 'PEAR_Error')) {
-        return $tasks;
-    }
-
-    $uids = array();
-    $tasks->reset();
-    while ($task = $tasks->each()) {
-        $uids[] = $task->uid;
-    }
-
-    return $uids;
-}
-
-/**
- * Returns an array of UIDs for tasks that have had $action happen since
- * $timestamp.
- *
- * @param string  $action     The action to check for - add, modify, or delete.
- * @param integer $timestamp  The time to start the search.
- * @param string  $tasklist   The tasklist to be used. If 'null', the
- *                            user's default tasklist will be used.
- *
- * @return array  An array of UIDs matching the action and time criteria.
- */
-function _nag_listBy($action, $timestamp, $tasklist = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if ($tasklist === null) {
-        $tasklist = Nag::getDefaultTasklist(PERMS_READ);
-    }
-
-    if (!array_key_exists($tasklist,
-                          Nag::listTasklists(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $history = Horde_History::singleton();
-    $histories = $history->getByTimestamp('>', $timestamp, array(array('op' => '=', 'field' => 'action', 'value' => $action)), 'nag:' . $tasklist);
-    if (is_a($histories, 'PEAR_Error')) {
-        return $histories;
-    }
-
-    // Strip leading nag:username:.
-    return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
-}
-
-/**
- * Returns the timestamp of an operation for a given uid an action.
- *
- * @param string $uid      The uid to look for.
- * @param string $action   The action to check for - add, modify, or delete.
- * @param string $tasklist The tasklist to be used. If 'null', the
- *                         user's default tasklist will be used.
- *
- * @return integer  The timestamp for this action.
- */
-function _nag_getActionTimestamp($uid, $action, $tasklist = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if ($tasklist === null) {
-        $tasklist = Nag::getDefaultTasklist(PERMS_READ);
-    }
-
-    if (!array_key_exists($tasklist,
-                          Nag::listTasklists(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $history = Horde_History::singleton();
-    return $history->getActionTimestamp('nag:' . $tasklist . ':' . $uid, $action);
-}
-
-/**
- * Imports one or more tasks represented in the specified content type.
- *
- * If a UID is present in the content and the task is already in the
- * database, a replace is performed rather than an add.
- *
- * @param string $content      The content of the task.
- * @param string $contentType  What format is the data in? Currently supports:
- *                             text/calendar
- *                             text/x-vcalendar
- * @param string $tasklist     The tasklist into which the task will be
- *                             imported.  If 'null', the user's default
- *                             tasklist will be used.
- *
- * @return string  The new UID on one import, an array of UIDs on multiple imports,
- *                 or PEAR_Error on failure.
- */
-function _nag_import($content, $contentType, $tasklist = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if ($tasklist === null) {
-        $tasklist = Nag::getDefaultTasklist(PERMS_EDIT);
-    }
-
-    if (!array_key_exists($tasklist, Nag::listTasklists(false, PERMS_EDIT))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    /* Create a Nag_Driver instance. */
-    $storage = Nag_Driver::singleton($tasklist);
-
-    switch ($contentType) {
-    case 'text/x-vcalendar':
-    case 'text/calendar':
-    case 'text/x-vtodo':
-        $iCal = new Horde_iCalendar();
-        if (!is_a($content, 'Horde_iCalendar_vtodo')) {
-            if (!$iCal->parsevCalendar($content)) {
-                return PEAR::raiseError(_("There was an error importing the iCalendar data."));
-            }
-        } else {
-            $iCal->addComponent($content);
-        }
-
-        $components = $iCal->getComponents();
-        if (count($components) == 0) {
-            return PEAR::raiseError(_("No iCalendar data was found."));
-        }
-
-        $ids = array();
-        foreach ($components as $content) {
-            if (is_a($content, 'Horde_iCalendar_vtodo')) {
-                $task = new Nag_Task();
-                $task->fromiCalendar($content);
-                if (isset($task->uid) &&
-                    !is_a(($existing = $storage->getByUID($task->uid)), 'PEAR_Error')) {
-                    $taskId = $existing->id;
-                    $result = $storage->modify(
-                        $taskId,
-                        isset($task->name) ? $task->name : $existing->name,
-                        isset($task->desc) ? $task->desc : $existing->desc,
-                        isset($task->start) ? $task->start : $existing->start,
-                        isset($task->due) ? $task->due : $existing->due,
-                        isset($task->priority) ? $task->priority : $existing->priority,
-                        isset($task->estimate) ? $task->estimate : 0,
-                        isset($task->completed) ? (int)$task->completed : $existing->completed,
-                        isset($task->category) ? $task->category : $existing->category,
-                        isset($task->alarm) ? $task->alarm : $existing->alarm,
-                        isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
-                        isset($task->private) ? $task->private : $existing->private,
-                        isset($task->owner) ? $task->owner : $existing->owner,
-                        isset($task->assignee) ? $task->assignee : $existing->assignee);
-
-                    if (is_a($result, 'PEAR_Error')) {
-                        return $result;
-                    }
-                    $ids[] = $task->uid;
-                } else {
-                    $newTask = $storage->add(
-                        isset($task->name) ? $task->name : '',
-                        isset($task->desc) ? $task->desc : '',
-                        isset($task->start) ? $task->start : 0,
-                        isset($task->due) ? $task->due : 0,
-                        isset($task->priority) ? $task->priority : 3,
-                        isset($task->estimate) ? $task->estimate : 0,
-                        !empty($task->completed),
-                        isset($task->category) ? $task->category : '',
-                        isset($task->alarm) ? $task->alarm : 0,
-                        isset($task->methods) ? $task->methods : null,
-                        isset($task->uid) ? $task->uid : null,
-                        isset($task->parent_id) ? $task->parent_id : '',
-                        !empty($task->private),
-                        Horde_Auth::getAuth(),
-                        isset($task->assignee) ? $task->assignee : null);
-                    if (is_a($newTask, 'PEAR_Error')) {
-                        return $newTask;
-                    }
-                    // use UID rather than ID
-                    $ids[] = $newTask[1];
-                }
-            }
-        }
-        if (count($ids) == 0) {
-            return PEAR::raiseError(_("No iCalendar data was found."));
-        } else if (count($ids) == 1) {
-            return $ids[0];
-        }
-        return $ids;
-    }
-
-    return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-}
-
-/**
- * Imports one or more tasks parsed from a string.
- *
- * @param string $text      The text to parse into
- * @param string $tasklist  The tasklist into which the task will be
- *                          imported.  If 'null', the user's default
- *                          tasklist will be used.
- *
- * @return array  The UIDs of all tasks that were added.
- */
-function _nag_quickAdd($text, $tasklist = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if ($tasklist === null) {
-        $tasklist = Nag::getDefaultTasklist(PERMS_EDIT);
-    }
-    if (!array_key_exists($tasklist, Nag::listTasklists(false, PERMS_EDIT))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    return Nag::createTasksFromText($text, $tasklist);
-}
-
-/**
- * Toggles the task completion flag.
- *
- * @param string $task_id      The task ID.
- * @param string $tasklist_id  The tasklist that contains the task.
- */
-function _nag_toggleCompletion($task_id, $tasklist_id)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!array_key_exists($tasklist_id,
-                          Nag::listTasklists(false, PERMS_EDIT))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $share = $GLOBALS['nag_shares']->getShare($tasklist_id);
-    if (is_a($share, 'PEAR_Error')) {
-        return $share;
-    }
-
-    $task = Nag::getTask($tasklist_id, $task_id);
-    if (is_a($task, 'PEAR_Error')) {
-        return $task;
-    }
-
-    $task->completed = !$task->completed;
-    if ($task->completed) {
-        $task->completed_date = time();
-    } else {
-        $task->completed_date = null;
-    }
-
-    return $task->save();
-}
-
-/**
- * Exports a task, identified by UID, in the requested content type.
- *
- * @param string $uid          Identify the task to export.
- * @param string $contentType  What format should the data be in?
- *                             A string with one of:
- * <pre>
- * text/calendar    - (VCALENDAR 2.0. Recommended as this is specified in
- *                    rfc2445)
- * text/x-vcalendar - (old VCALENDAR 1.0 format. Still in wide use)
- * </pre>
- *
- * @return string  The requested data.
- */
-function _nag_export($uid, $contentType)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $storage = Nag_Driver::singleton();
-    $task = $storage->getByUID($uid);
-    if (is_a($task, 'PEAR_Error')) {
-        return $task;
-    }
-
-    if (!array_key_exists($task->tasklist, Nag::listTasklists(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $version = '2.0';
-    switch ($contentType) {
-    case 'text/x-vcalendar':
-        $version = '1.0';
-    case 'text/calendar':
-        // Create the new iCalendar container.
-        $iCal = new Horde_iCalendar($version);
-        $iCal->setAttribute('PRODID', '-//The Horde Project//Nag ' . $GLOBALS['registry']->getVersion() . '//EN');
-        $iCal->setAttribute('METHOD', 'PUBLISH');
-
-        // Create new vTodo object.
-        $vTodo = $task->toiCalendar($iCal);
-        $vTodo->setAttribute('VERSION', $version);
-
-        $iCal->addComponent($vTodo);
-
-        return $iCal->exportvCalendar();
-
-    default:
-        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-    }
-}
-
-/**
- * Exports a tasklist in the requested content type.
- *
- * @param string $tasklist     The tasklist to export.
- * @param string $contentType  What format should the data be in?
- *                             A string with one of:
- *                             <pre>
- *                             text/calendar (VCALENDAR 2.0. Recommended as
- *                                            this is specified in rfc2445)
- *                             text/x-vcalendar (old VCALENDAR 1.0 format.
- *                                              Still in wide use)
- *                             </pre>
- *
- * @return string  The iCalendar representation of the tasklist.
- */
-function _nag_exportTasklist($tasklist, $contentType)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!array_key_exists($tasklist,
-                          Nag::listTasklists(false, PERMS_READ))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $tasks = Nag::listTasks(null, null, null, array($tasklist), 1);
-
-    $version = '2.0';
-    switch ($contentType) {
-    case 'text/x-vcalendar':
-        $version = '1.0';
-    case 'text/calendar':
-        $share = $GLOBALS['nag_shares']->getShare($tasklist);
-
-        $iCal = new Horde_iCalendar($version);
-        $iCal->setAttribute('X-WR-CALNAME', Horde_String::convertCharset($share->get('name'), Horde_Nls::getCharset(), 'utf-8'));
-
-        $tasks->reset();
-        while ($task = $tasks->each()) {
-            $iCal->addComponent($task->toiCalendar($iCal));
-        }
-
-        return $iCal->exportvCalendar();
-    }
-
-    return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-
-}
-
-/**
- * Deletes a task identified by UID.
- *
- * @param string|array $uid  Identify the task to delete, either a single UID
- *                           or an array.
- *
- * @return boolean  Success or failure.
- */
-function _nag_delete($uid)
-{
-    // Handle an arrray of UIDs for convenience of deleting multiple tasks at
-    // once.
-    if (is_array($uid)) {
-        foreach ($uid as $g) {
-            $result = _nag_delete($g);
-            if (is_a($result, 'PEAR_Error')) {
-                return $result;
-            }
-        }
-
-        return true;
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-
-    $storage = Nag_Driver::singleton();
-    $task = $storage->getByUID($uid);
-    if (is_a($task, 'PEAR_Error')) {
-        return $task;
-    }
-
-    if (!Horde_Auth::isAdmin() &&
-        !array_key_exists($task->tasklist,
-                          Nag::listTasklists(false, PERMS_DELETE))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    return $storage->delete($task->id);
-}
-
-/**
- * Replaces the task identified by UID with the content represented in the
- * specified content type.
- *
- * If you want to replace multiple tasks with the UID specified in the
- * VCALENDAR data, you may use _nag_import instead. This automatically does a
- * replace if existings UIDs are found.
- *
- *
- * @param string $uid          Identify the task to replace.
- * @param string $content      The content of the task.
- * @param string $contentType  What format is the data in? Currently supports:
- *                             - text/x-vcalendar
- *                             - text/calendar
- *
- * @return boolean  Success or failure.
- */
-function _nag_replace($uid, $content, $contentType)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $storage = Nag_Driver::singleton();
-    $existing = $storage->getByUID($uid);
-    if (is_a($existing, 'PEAR_Error')) {
-        return $existing;
-    }
-    $taskId = $existing->id;
-
-    if (!array_key_exists($existing->tasklist, Nag::listTasklists(false, PERMS_EDIT))) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    switch ($contentType) {
-    case 'text/calendar':
-    case 'text/x-vcalendar':
-        if (!is_a($content, 'Horde_iCalendar_vtodo')) {
-            $iCal = new Horde_iCalendar();
-            if (!$iCal->parsevCalendar($content)) {
-                return PEAR::raiseError(_("There was an error importing the iCalendar data."));
-            }
-
-            $components = $iCal->getComponents();
-            $component = null;
-            foreach ($components as $content) {
-                if (is_a($content, 'Horde_iCalendar_vtodo')) {
-                    if ($component !== null) {
-                        return PEAR::raiseError(_("Multiple iCalendar components found; only one vTodo is supported."));
-                    }
-                    $component = $content;
-                }
-
-            }
-            if ($component === null) {
-                return PEAR::raiseError(_("No iCalendar data was found."));
-            }
-        }
-
-        $task = new Nag_Task();
-        $task->fromiCalendar($content);
-        $result = $storage->modify(
-            $taskId,
-            isset($task->name) ? $task->name : $existing->name,
-            isset($task->desc) ? $task->desc : $existing->desc,
-            isset($task->start) ? $task->start : $existing->start,
-            isset($task->due) ? $task->due : $existing->due,
-            isset($task->priority) ? $task->priority : $existing->priority,
-            isset($task->estimate) ? $task->estimate : 0,
-            isset($task->completed) ? (int)$task->completed : $existing->completed,
-            isset($task->category) ? $task->category : $existing->category,
-            isset($task->alarm) ? $task->alarm : $existing->alarm,
-            isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
-            isset($task->private) ? $task->private : $existing->private,
-            isset($task->owner) ? $task->owner : $existing->owner,
-            isset($task->assignee) ? $task->assignee : $existing->assignee);
-
-        break;
-
-    default:
-        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-    }
-
-    return $result;
-}
-
-/**
- * Lists active tasks as cost objects.
- *
- * @todo Implement $criteria parameter.
- *
- * @param array $criteria   Filter attributes
- */
-function _nag_listCostObjects($criteria)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $tasks = Nag::listTasks(null, null, null, null, 1);
-    $result = array();
-    $tasks->reset();
-    while ($task = $tasks->each()) {
-        $result[$task->id] = array('id' => $task->id,
-                                   'active' => !$task->completed,
-                                   'name' => $task->name);
-        if (!empty($task->estimate)) {
-            $result[$task->id]['estimate'] = $task->estimate;
-        }
-    }
-
-    if (count($result) == 0) {
-        return array();
-    } else {
-        return array(array('category' => _("Tasks"),
-                           'objects'  => array_values($result)));
-    }
-}
-
-function _nag_listTimeObjectCategories()
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $categories = array();
-    $tasklists = Nag::listTasklists(false, PERMS_SHOW | PERMS_READ);
-    foreach ($tasklists as $tasklistId => $tasklist) {
-        $categories[$tasklistId] = $tasklist->get('name');
-    }
-    return $categories;
-}
-
-/**
- * Lists active tasks as time objects.
- *
- * @param array $categories  The time categories (from listTimeObjectCategories) to list.
- * @param mixed $start       The start date of the period.
- * @param mixed $end         The end date of the period.
- */
-function _nag_listTimeObjects($categories, $start, $end)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $allowed_tasklists = Nag::listTasklists(false, PERMS_READ);
-    foreach ($categories as $tasklist) {
-        if (!array_key_exists($tasklist, $allowed_tasklists)) {
-            return PEAR::raiseError(_("Permission Denied"));
-        }
-    }
-
-    $timeobjects = array();
-    $start = new Horde_Date($start);
-    $start_ts = $start->timestamp();
-    $end = new Horde_Date($end);
-    $end_ts = $end->timestamp();
-
-    // List incomplete tasks.
-    $tasks = Nag::listTasks(null, null, null, $categories, 0);
-    $tasks->reset();
-    while ($task = $tasks->each()) {
-        // If there's no due date, it's not a time object.
-        if (!$task->due || $task->due + 1 < $start_ts || $task->due > $end_ts) {
-            continue;
-        }
-        $due_date = date('Y-m-d\TH:i:s', $task->due);
-        $timeobjects[$task->id] = array(
-            'id' => $task->id,
-            'title' => $task->name,
-            'description' => $task->desc,
-            'start' => $due_date,
-            'end' => $due_date,
-            'category' => $task->category,
-            'params' => array('task' => $task->id,
-                              'tasklist' => $task->tasklist),
-            'link' => Horde_Util::addParameter(Horde::applicationUrl('view.php', true), array('tasklist' => $task->tasklist, 'task' => $task->id)));
-    }
-
-    return $timeobjects;
-}
-
-/**
- * Lists alarms for a given moment.
- *
- * @param integer $time  The time to retrieve alarms for.
- * @param string $user   The user to retreive alarms for. All users if null.
- *
- * @return array  An array of UIDs
- */
-function _nag_listAlarms($time, $user = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    require_once 'Horde/Group.php';
-
-    if ((empty($user) || $user != Horde_Auth::getAuth()) && !Horde_Auth::isAdmin()) {
-        return PEAR::raiseError(_("Permission Denied"));
-    }
-
-    $storage = Nag_Driver::singleton();
-    $group = Group::singleton();
-    $alarm_list = array();
-    $tasklists = is_null($user) ? array_keys($GLOBALS['nag_shares']->listAllShares()) :  $GLOBALS['display_tasklists'];
-
-    $alarms = Nag::listAlarms($time, $tasklists);
-    if (is_a($alarms, 'PEAR_Error')) {
-        return $alarms;
-    }
-
-    foreach ($alarms as $alarm) {
-        $share = $GLOBALS['nag_shares']->getShare($alarm->tasklist);
-        if (is_a($share, 'PEAR_Error')) {
-            continue;
-        }
-        if (empty($user)) {
-            $users = $share->listUsers(PERMS_READ);
-            $groups = $share->listGroups(PERMS_READ);
-            foreach ($groups as $gid) {
-                $users = array_merge($users, $group->listUsers($gid));
-            }
-            $users = array_unique($users);
-        } else {
-            $users = array($user);
-        }
-        foreach ($users as $alarm_user) {
-            $prefs = Prefs::singleton($GLOBALS['conf']['prefs']['driver'],
-                                      'nag', $alarm_user, null, null, false);
-            Horde_Nls::setLanguageEnvironment($prefs->getValue('language'));
-            $alarm_list[] = $alarm->toAlarm($alarm_user, $prefs);
-        }
-    }
-
-    return $alarm_list;
-}
diff --git a/nag/lib/version.php b/nag/lib/version.php
deleted file mode 100644 (file)
index 75c690e..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('NAG_VERSION', 'H3 (3.0-git)') ?>
diff --git a/news/lib/Api.php b/news/lib/Api.php
new file mode 100644 (file)
index 0000000..75ae010
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+/**
+ * News api
+ *
+ * $Id: api.php 1260 2009-02-01 23:15:50Z duck $
+ *
+ * Copyright 2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Duck <duck@obala.net>
+ * @package News
+ */
+class News_Api extends Horde_Registry_Api
+{
+    public $version = 'H4 (0.1-git)';
+
+    public $services = array(
+        'perms' => array(
+            'args' => array(),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'commentCallback' => array(
+            'args' => array('id' => 'string'),
+            'type' => 'string'
+        ),
+
+        'hasComments' => array(
+            'args' => array(),
+            'type' => 'boolean'
+        ),
+
+        'listNews' => array(
+            'args' => array('perms' => 'string', 'criteria' => 'array', 'from' => 'int', 'count' => 'int'),
+            'type' => 'array'
+        ),
+
+        'countNews' => array(
+            'args' => array('perms' => 'string', 'criteria' => 'array'),
+            'type' => 'int'
+        )
+    );
+
+    /**
+     * Categories/Permissions
+     */
+    public function perms()
+    {
+        static $perms = array();
+        if (!empty($perms)) {
+            return $perms;
+        }
+
+        $perms['tree']['news']['admin'] = true;
+        $perms['title']['news:admin'] = _("Admin");
+
+        $perms['tree']['news']['editors'] = true;
+        $perms['title']['news:editors'] = _("Editors");
+
+        require_once dirname(__FILE__) . '/base.php';
+        $tree = $GLOBALS['news_cat']->getEnum();
+
+        $perms['title']['news:categories'] = _("Categories");
+        foreach ($tree as $cat_id => $cat_name) {
+            $perms['tree']['news']['categories'][$cat_id] = false;
+            $perms['title']['news:categories:' . $cat_id] = $cat_name;
+        }
+
+        return $perms;
+    }
+
+    /**
+     * Callback for comment API
+     *
+     * @param int $id                Internal data identifier
+     * @param string $type      Type of data to retreive (title, owner...)
+     * @param array $params Additional parameters
+     */
+    public function commentCallback($id, $type = 'title', $params = null)
+    {
+        static $info;
+
+        if (!empty($info[$id][$type])) {
+            return $info[$id][$type];
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+
+        $news = $GLOBALS['news']->get($id);
+        if ($news instanceof PEAR_Error) {
+            return $news;
+        }
+
+        switch ($type) {
+
+        case 'owner':
+            return $news['user'];
+
+        case 'link':
+            return News::getUrlFor('news', $id, true, -1);
+
+        case 'messages':
+            $GLOBALS['news']->updateComments($id, $params);
+
+            if ($GLOBALS['registry']->hasMethod('logActivity', 'folks')) {
+                $link = '<a href="' . News::getUrlFor('news', $id) . '">' . $news['title'] . '</a>';
+                $message = sprintf(_("Has commented news \"%s\""), $link);
+                $GLOBALS['registry']->callByPackage('folks', 'logActivity', array($message, 'news'));
+            }
+
+            return true;
+
+        default:
+            $info[$id][$type] = $news['title'];
+            return $news['title'];
+        }
+    }
+
+    /**
+     * Returns if applications allows comments
+     */
+    public function hasComments()
+    {
+        return $GLOBALS['conf']['comments']['allow'];
+    }
+
+    /**
+     * List news
+     *
+     * @param array $criteria  Array of news attributes match
+     * @param intiger $from    Start fetching from news
+     * @param intiger $count  The number of news to fetch
+     * @param intiger $perms News permission access type
+     *
+     * @return array | PEAR_Error  True on success, PEAR_Error on failure.
+     */
+    public function listNews($criteria = array(), $from = 0, $count = 0, $perms = PERMS_READ)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['news']->listNews($criteria, $from, $count, $perms);
+    }
+
+    /**
+     * Count news
+     *
+     * @param array $criteria Array of news attributes match
+     * @param integer $perms Permisson level
+     *
+     * @return integer | PEAR_Error  True on success, PEAR_Error on failure.
+     */
+    public function countNews($criteria = array(), $perms = PERMS_READ)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        return $GLOBALS['news']->countNews($criteria, $perms);
+    }
+
+}
diff --git a/news/lib/api.php b/news/lib/api.php
deleted file mode 100644 (file)
index 101db35..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php
-/**
- * News api
- *
- * $Id: api.php 1260 2009-02-01 23:15:50Z duck $
- *
- * Copyright 2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (GPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- *
- * @author Duck <duck@obala.net>
- * @package News
- */
-
-$_services['perms'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['commentCallback'] = array(
-    'args' => array('id' => 'string'),
-    'type' => 'string'
-);
-
-$_services['hasComments'] = array(
-    'args' => array(),
-    'type' => 'boolean'
-);
-
-$_services['listNews'] = array(
-    'args' => array('perms' => 'string', 'criteria' => 'array', 'from' => 'int', 'count' => 'int'),
-    'type' => 'array'
-);
-
-$_services['countNews'] = array(
-    'args' => array('perms' => 'string', 'criteria' => 'array'),
-    'type' => 'int'
-);
-
-/**
- * Categories/Permissions
- */
-function _news_perms()
-{
-    static $perms = array();
-    if (!empty($perms)) {
-        return $perms;
-    }
-
-    $perms['tree']['news']['admin'] = true;
-    $perms['title']['news:admin'] = _("Admin");
-
-    $perms['tree']['news']['editors'] = true;
-    $perms['title']['news:editors'] = _("Editors");
-
-    require_once dirname(__FILE__) . '/base.php';
-    $tree = $GLOBALS['news_cat']->getEnum();
-
-    $perms['title']['news:categories'] = _("Categories");
-    foreach ($tree as $cat_id => $cat_name) {
-        $perms['tree']['news']['categories'][$cat_id] = false;
-        $perms['title']['news:categories:' . $cat_id] = $cat_name;
-    }
-
-    return $perms;
-}
-
-/**
- * Callback for comment API
- *
- * @param int $id                Internal data identifier
- * @param string $type      Type of data to retreive (title, owner...)
- * @param array $params Additional parameters
- */
-function _news_commentCallback($id, $type = 'title', $params = null)
-{
-    static $info;
-
-    if (!empty($info[$id][$type])) {
-        return $info[$id][$type];
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-
-    $news = $GLOBALS['news']->get($id);
-    if ($news instanceof PEAR_Error) {
-        return $news;
-    }
-
-    switch ($type) {
-
-    case 'owner':
-        return $news['user'];
-
-    case 'link':
-        return News::getUrlFor('news', $id, true, -1);
-
-    case 'messages':
-        $GLOBALS['news']->updateComments($id, $params);
-
-        if ($GLOBALS['registry']->hasMethod('logActivity', 'folks')) {
-            $link = '<a href="' . News::getUrlFor('news', $id) . '">' . $news['title'] . '</a>';
-            $message = sprintf(_("Has commented news \"%s\""), $link);
-            $GLOBALS['registry']->callByPackage('folks', 'logActivity', array($message, 'news'));
-        }
-
-        return true;
-
-    default:
-        $info[$id][$type] = $news['title'];
-        return $news['title'];
-    }
-}
-
-/**
- * Returns if applications allows comments
- */
-function _news_hasComments()
-{
-    return $GLOBALS['conf']['comments']['allow'];
-}
-
-/**
- * List news
- *
- * @param array $criteria  Array of news attributes match
- * @param intiger $from    Start fetching from news
- * @param intiger $count  The number of news to fetch
- * @param intiger $perms News permission access type
- *
- * @return array | PEAR_Error  True on success, PEAR_Error on failure.
- */
-function _news_listNews($criteria = array(), $from = 0, $count = 0, $perms = PERMS_READ)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['news']->listNews($criteria, $from, $count, $perms);
-}
-
-/**
- * Count news
- *
- * @param array $criteria Array of news attributes match
- * @param integer $perms Permisson level
- *
- * @return integer | PEAR_Error  True on success, PEAR_Error on failure.
- */
-function _news_countNews($criteria = array(), $perms = PERMS_READ)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    return $GLOBALS['news']->countNews($criteria, $perms);
-}
-
diff --git a/news/lib/version.php b/news/lib/version.php
deleted file mode 100755 (executable)
index acdca34..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('NEWS_VERSION', 'H4 (0.1-git)') ?>
\ No newline at end of file
diff --git a/skeleton/lib/Api.php b/skeleton/lib/Api.php
new file mode 100644 (file)
index 0000000..8477a2f
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+class Skeleton_Api extends Horde_Registry_Api
+{
+    public $version = 'H4 (0.1-git)';
+}
diff --git a/skeleton/lib/version.php b/skeleton/lib/version.php
deleted file mode 100644 (file)
index e1fddce..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('SKELETON_VERSION', '0.1-git') ?>
diff --git a/skoli/lib/Api.php b/skoli/lib/Api.php
new file mode 100644 (file)
index 0000000..e43684d
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+class Skoli_Api extends Horde_Registry_Api
+{
+    public $version = 'H4 (0.1-git)';
+}
diff --git a/skoli/lib/version.php b/skoli/lib/version.php
deleted file mode 100644 (file)
index 5e6d331..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('SKOLI_VERSION', '0.1-cvs') ?>
\ No newline at end of file
diff --git a/timeobjects/lib/Api.php b/timeobjects/lib/Api.php
new file mode 100644 (file)
index 0000000..5573d96
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+/**
+ * API methods for exposing various bits of data via the listTimeObjects API
+ */
+class Timeobjects_Api extends Horde_Registry_Api
+{
+    /**
+     * The services provided by this application.
+     *
+     * @var array
+     */
+    public $services = array(
+        'listTimeObjectCategories' => array(
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'listTimeObjects' => array(
+            'args' => array(
+                'categories' => '{urn:horde}stringArray',
+                'start' => 'integer',
+                'end' => 'integer'
+            ),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        // @TODO: Probably implement a URL endpoint or something so we can link
+        //        to the correct external site depending on what time object category
+        //        we are referring to.
+        'show' => array(
+            'link' => '#',
+        )
+    );
+
+    /**
+     * Returns the available categories we provide.
+     *
+     * Right now, only providing weather data.
+     *
+     * @return array
+     */
+    public function listTimeObjectCategories()
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        // @TODO: Probably want to iterate the driver directory
+        //        and dynamically build this list and/or maybe provide
+        //        a $conf[] setting to explicitly disable certain drivers?
+        $drivers = array();
+
+        $drv = TimeObjects_Driver::factory('Weatherdotcom');
+        if ($drv->ensure()) {
+            $drivers['Weatherdotcom'] = _("Weather");
+        }
+
+        $drv = TimeObjects_Driver::factory('FacebookEvents');
+        if ($drv->ensure()) {
+            $drivers['FacebookEvents'] = _("Facebook Events");
+        }
+
+        return $drivers;
+    }
+
+    /**
+     * Obtain the timeObjects for the requested category
+     *
+     * @param array $time_categories  An array of categories to list
+     * @param mixed $start            The start of the time period to list for
+     * @param mixed $end              The end of the time period to list for
+     *
+     * @return An array of timeobject arrays.
+     */
+    public function listTimeObjects($time_categories, $start, $end)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $return = array();
+        foreach ($time_categories as $category) {
+            $drv = TimeObjects_Driver::factory($category);
+
+            try {
+                $new = $drv->listTimeObjects($start, $end);
+            } catch (TimeObjects_Exception $e) {
+                //@TODO: Log the error,  but return an empty array.
+                $new = array();
+            }
+            $return = array_merge($return, $new);
+        }
+
+        return $return;
+    }
+
+}
diff --git a/timeobjects/lib/api.php b/timeobjects/lib/api.php
deleted file mode 100644 (file)
index 203df4a..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-/**
- * API methods for exposing various bits of data via the listTimeObjects API
- */
-$_services['listTimeObjectCategories'] = array(
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['listTimeObjects'] = array(
-    'args' => array('categories' => '{urn:horde}stringArray', 'start' => 'int', 'end' => 'int'),
-    'type' => '{urn:horde}hashHash'
-);
-
-// @TODO: Probably implement a URL endpoint or something so we can link
-//        to the correct external site depending on what time object category
-//        we are referring to.
-$_services['show'] = array(
-    'link' => '#',
-);
-
-/**
- * Returns the available categories we provide.
- *
- * Right now, only providing weather data.
- *
- * @return array
- */
-function _timeobjects_listTimeObjectCategories()
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    // @TODO: Probably want to iterate the driver directory
-    //        and dynamically build this list and/or maybe provide
-    //        a $conf[] setting to explicitly disable certain drivers?
-    $drivers = array();
-
-    $drv = TimeObjects_Driver::factory('Weatherdotcom');
-    if ($drv->ensure()) {
-        $drivers['Weatherdotcom'] = _("Weather");
-    }
-
-    $drv = TimeObjects_Driver::factory('FacebookEvents');
-    if ($drv->ensure()) {
-        $drivers['FacebookEvents'] = _("Facebook Events");
-    }
-
-    return $drivers;
-}
-
-/**
- * Obtain the timeObjects for the requested category
- *
- * @param array $time_categories  An array of categories to list
- * @param mixed $start            The start of the time period to list for
- * @param mixed $end              The end of the time period to list for
- *
- * @return An array of timeobject arrays.
- */
-function _timeobjects_listTimeObjects($time_categories, $start, $end)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $return = array();
-    foreach ($time_categories as $category) {
-        $drv = TimeObjects_Driver::factory($category);
-
-        try {
-            $new = $drv->listTimeObjects($start, $end);
-        } catch (TimeObjects_Exception $e) {
-            //@TODO: Log the error,  but return an empty array.
-            $new = array();
-        }
-        $return = array_merge($return, $new);
-    }
-
-    return $return;
-}
\ No newline at end of file
diff --git a/turba/lib/Api.php b/turba/lib/Api.php
new file mode 100644 (file)
index 0000000..7fba437
--- /dev/null
@@ -0,0 +1,2132 @@
+<?php
+/**
+ * Turba external API interface.
+ *
+ * This file defines Turba's external API interface. Other applications can
+ * interact with Turba through this API.
+ *
+ * @package Turba
+ */
+class Turba_Api extends Horde_Registry_Api
+{
+    /**
+     * The application's version.
+     *
+     * @var string
+     */
+    public $version = 'H3 (3.0-git)';
+
+    /**
+     * The services provided by this application.
+     * TODO: Describe structure.
+     *
+     * @var array
+     */
+    public $services = array(
+        'perms' => array(
+            'args' => array(),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'removeUserData' => array(
+            'args' => array('user' => 'string'),
+            'type' => 'boolean'
+        ),
+
+        'show' => array(
+            'link' => '%application%/contact.php?source=|source|&key=|key|&uid=|uid|',
+        ),
+
+        'browse' => array(
+            'args' => array(
+                'path' => 'string',
+                'properties' => '{urn:horde}stringArray'
+            ),
+            'type' => '{urn:horde}hashHash',
+        ),
+
+        'path_delete' => array(
+            'args' => array('path' => 'string'),
+            'type' => 'boolean',
+        ),
+
+        'sources' => array(
+            'args' => array('writeable' => 'boolean'),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'fields' => array(
+            'args' => array('source' => '{urn:horde}stringArray'),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'list' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'listBy' => array(
+            'args' => array(
+                'action' => 'string',
+                'timestamp' => 'int'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'getActionTimestamp' => array(
+            'args' => array(
+                'uid' => 'string',
+                'action' => 'string',
+                'sources' => '{urn:horde}stringArray'
+            ),
+            'type' => 'int',
+        ),
+
+        'import' => array(
+            'args' => array(
+                'content' => 'string',
+                'contentType' => 'string',
+                'source' => 'string'
+            ),
+            'type' => 'string',
+        ),
+
+        'export' => array(
+            'args' => array(
+                'uid' => 'string',
+                'contentType' => 'string'
+            ),
+            'type' => 'string',
+        ),
+
+        'ownVCard' => array(
+            'args' => array(),
+            'type' => 'string',
+        ),
+
+        'ownContact' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'delete' => array(
+            'args' => array('uid' => 'string'),
+            'type' => 'boolean',
+        ),
+
+        'replace' => array(
+            'args' => array(
+                'uid' => 'string',
+                'content' => 'string',
+                'contentType' => 'string'
+            ),
+            'type' => 'boolean',
+        ),
+
+        'search' => array(
+            'args' => array(
+                'names' => '{urn:horde}stringArray',
+                'sources' => '{urn:horde}stringArray',
+                'fields' => '{urn:horde}stringArray',
+                'matchBegin' => 'boolean',
+                'forceSource' => 'boolean'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'getContact' => array(
+            'args' => array(
+                'source' => 'string',
+                'objectId' => 'string'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'getContacts' => array(
+            'args' => array(
+                'source' => 'string',
+                'objectIds' => '{urn:horde}stringArray'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'addField' => array(
+            'args' => array(
+                'address' => 'string',
+                'name' => 'string',
+                'field' => 'string',
+                'value' => 'string',
+                'source' => 'string'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'deleteField' => array(
+            'args' => array(
+                'address' => 'string',
+                'field' => 'string',
+                'sources' => '{urn:horde}stringArray'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'getField' => array(
+            'args' => array(
+                'address' => 'string',
+                'field' => 'string',
+                'sources' => '{urn:horde}stringArray',
+                'strict' => 'boolean',
+                'multiple' => 'boolean'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'getAllAttributeValues' => array(
+            'args' => array(
+                'field' => 'string',
+                'sources' => '{urn:horde}stringArray'
+            ),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'listTimeObjectCategories' => array(
+            'type' => '{urn:horde}stringArray'
+        ),
+
+        'listTimeObjects' => array(
+            'args' => array(
+                'categories' => '{urn:horde}stringArray',
+                'start' => 'int',
+                'end' => 'int'
+            ),
+            'type' => '{urn:horde}hashHash'
+        ),
+
+        'getClientSource' => array(
+            'checkperms' => false,
+            'args' => array(),
+            'type' => 'string',
+        ),
+
+        'clientFields' => array(
+            'args' => array(),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'getClient' => array(
+            'checkperms' => false,
+            'args' => array('objectId' => 'string'),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'getClients' => array(
+            'checkperms' => false,
+            'args' => array('objectIds' => '{urn:horde}stringArray'),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'addClient' => array(
+            'args' => array('attributes' => '{urn:horde}stringArray'),
+            'type' => 'string',
+        ),
+
+        'updateClient' => array(
+            'args' => array(
+                'objectId' => 'string',
+                'attributes' => '{urn:horde}stringArray'
+            ),
+            'type' => 'string',
+        ),
+
+        'deleteClient' => array(
+            'args' => array('objectId' => 'string'),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'searchClients' => array(
+            'checkperms' => false,
+            'args' => array('names' => '{urn:horde}stringArray',
+            'fields' => '{urn:horde}stringArray',
+            'matchBegin' => 'boolean'),
+            'type' => '{urn:horde}stringArray',
+        ),
+
+        'commentCallback' => array(
+            'args' => array('id' => 'string'),
+            'type' => 'string'
+        ),
+
+        'hasComments' => array(
+            'args' => array(),
+            'type' => 'boolean'
+        ),
+
+        'getDefaultShare' => array(
+            'args' => array(),
+            'type' => 'string'
+        )
+    );
+
+    /**
+     * Removes user data.
+     *
+     * @param string $user  Name of user to remove data for.
+     *
+     * @return mixed  true on success | PEAR_Error on failure
+     */
+    public function removeUserData($user)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        if (!Horde_Auth::isAdmin() && $user != Horde_Auth::getAuth()) {
+            return PEAR::raiseError(_("You are not allowed to remove user data."));
+        }
+
+        /* We need a clean copy of the $cfgSources array here.*/
+        require TURBA_BASE . '/config/sources.php';
+        $hasError = false;
+
+        foreach ($cfgSources as $source) {
+            if (empty($source['use_shares'])) {
+                // Shares not enabled for this source
+                $driver = Turba_Driver::singleton($source);
+                if (is_a($driver, 'PEAR_Error')) {
+                    Horde::logMessage($driver, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    $hasError = true;
+                } else {
+                    $result = $driver->removeUserData($user);
+                    if (is_a($result, 'PEAR_Error')) {
+                        Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    }
+                }
+            }
+        }
+
+        /* Only attempt share removal if we have shares configured */
+        if (!empty($_SESSION['turba']['has_share'])) {
+            $shares = &$GLOBALS['turba_shares']->listShares(
+                $user, PERMS_EDIT, $user);
+
+            /* Look for the deleted user's default share and remove it */
+            foreach ($shares as $share) {
+                $params = @unserialize($share->get('params'));
+                /* Only attempt to delete the user's default share */
+                if (!empty($params['default'])) {
+                    $config = Turba::getSourceFromShare($share);
+                    $driver = Turba_Driver::singleton($config);
+                    $result = $driver->removeUserData($user);
+                    if (is_a($result, 'PEAR_Error')) {
+                        Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                        $hasError = true;
+                    }
+                }
+            }
+
+            /* Get a list of all shares this user has perms to and remove the perms */
+            $shares = $GLOBALS['turba_shares']->listShares($user);
+            if (is_a($shares, 'PEAR_Error')) {
+                Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+            foreach ($shares as $share) {
+                $share->removeUser($user);
+            }
+
+        }
+
+        if ($hasError) {
+            return PEAR::raiseError(sprintf(_("There was an error removing an address book for %s"), $user));
+        }
+
+        return true;
+    }
+
+    /**
+     * Callback for comment API
+     *
+     * @param integer $id  Internal data identifier
+     *
+     * @return mixed  Name of object on success | false on failure
+     */
+    public function commentCallback($id)
+    {
+        if (!$GLOBALS['conf']['comments']['allow']) {
+            return false;
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+
+        @list($source, $key) = explode('.', $id, 2);
+        if (isset($cfgSources[$source]) && $key) {
+            $driver = Turba_Driver::singleton($source);
+            if (!is_a($driver, 'PEAR_Error')) {
+                $object = $driver->getObject($key);
+                if (!is_a($object, 'PEAR_Error')) {
+                    return $object->getValue('name');
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns if applications allows comments
+     *
+     * @return boolean
+     */
+    public function hasComments()
+    {
+        return $GLOBALS['conf']['comments']['allow'];
+    }
+
+    /**
+     * Returns a list of available permissions.
+     *
+     * @return array  An array describing all available permissions.
+     */
+    public function perms()
+    {
+        static $perms = array();
+        if (!empty($perms)) {
+            return $perms;
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+        require TURBA_BASE . '/config/sources.php';
+
+        $perms['tree']['turba']['sources'] = false;
+        $perms['title']['turba:sources'] = _("Sources");
+
+        // Run through every contact source.
+        foreach ($cfgSources as $source => $curSource) {
+            $perms['tree']['turba']['sources'][$source] = false;
+            $perms['title']['turba:sources:' . $source] = $curSource['title'];
+            $perms['tree']['turba']['sources'][$source]['max_contacts'] = false;
+            $perms['title']['turba:sources:' . $source . ':max_contacts'] = _("Maximum Number of Contacts");
+            $perms['type']['turba:sources:' . $source . ':max_contacts'] = 'int';
+        }
+
+        return $perms;
+    }
+
+    /**
+     * Returns a list of available sources.
+     *
+     * @param boolean $writeable  Set to true to limit to writeable sources.
+     *
+     * @return array  An array of the available sources.
+     */
+    public function sources($writeable = false)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        $addressbooks = Turba::getAddressBooks($writeable ? PERMS_EDIT : PERMS_READ);
+        foreach ($addressbooks as $addressbook => $config) {
+            $addressbooks[$addressbook] = $config['title'];
+        }
+
+        return $addressbooks;
+    }
+
+    /**
+     * Returns a list of fields avaiable in a source.
+     *
+     * @param string $source  The name of the source
+     *
+     * @return mixed  An array describing the fields | PEAR_Error
+     */
+    public function fields($source = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources, $attributes;
+
+        if (empty($source) || !isset($cfgSources[$source])) {
+            return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
+        }
+
+        $fields = array();
+        foreach ($cfgSources[$source]['map'] as $field_name => $null) {
+            if (substr($field_name, 0, 2) != '__') {
+                $fields[$field_name] = array('name' => $field_name,
+                    'type' => $attributes[$field_name]['type'],
+                    'label' => $attributes[$field_name]['label'],
+                    'search' => in_array($field_name, $cfgSources[$source]['search']));
+            }
+        }
+
+        return $fields;
+    }
+
+    /**
+     * Retrieve the UID for the current user's default Turba share.
+     */
+    public function getDefaultShare()
+    {
+        global $prefs;
+
+        // Bring in turba's base and a clean copy of sources.
+        require_once dirname(__FILE__) . '/base.php';
+        require TURBA_BASE . '/config/sources.php';
+
+        if (!empty($_SESSION['turba']['has_share'])) {
+            $shares = Turba::listShares(true);
+            if (is_a($shares, 'PEAR_Error')) {
+                return false;
+            }
+            foreach ($shares as $uid => $share) {
+                $params = @unserialize($share->get('params'));
+                if (empty($params['source'])) {
+                    continue;
+                }
+                $driver = Turba_Driver::factory($params['source'], $cfgSources[$params['source']]);
+                if (is_a($driver, 'PEAR_Error')) {
+                    continue;
+                }
+                if ($driver->checkDefaultShare($share, $cfgSources[$params['source']])) {
+                    return $uid;
+                }
+            }
+        }
+
+        // Return Turba's default_dir as default
+        return $prefs->getValue('default_dir');
+    }
+
+    /**
+     * Browses through Turba's object tree.
+     *
+     * @param string $path       The path of the tree to browse.
+     * @param array $properties  The item properties to return. Defaults to 'name',
+     *                           'icon', and 'browseable'.
+     *
+     * @return array  Content of the specified path.
+     */
+    public function browse($path = '', $properties = array())
+    {
+        function _modified($uid)
+        {
+            $modified = $this->getActionTimestamp($uid, 'modify');
+            if (empty($modified)) {
+                $modified = $this->getActionTimestamp($uid, 'add');
+            }
+            return $modified;
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+        global $registry, $cfgSources;
+
+        // Default properties.
+        if (!$properties) {
+            $properties = array('name', 'icon', 'browseable');
+        }
+
+        // Strip off the application name if present
+        if (substr($path, 0, 5) == 'turba') {
+            $path = substr($path, 5);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        $now = time();
+        $results = array();
+        if (empty($path)) {
+            // We always provide the "global" folder which contains address book
+            // sources that are shared among all users.  Per-user shares are shown
+            // in a folder for each respective user.
+            $results = array();
+            $shares = Turba::listShares();
+            $owners = array('global' => true);
+            foreach ($shares as $share) {
+                $owners[$share->get('owner')] = true;
+            }
+
+            foreach (array_keys($owners) as $owner) {
+                if (in_array('name', $properties)) {
+                    $results['turba/' . $owner]['name'] = $owner;
+                }
+                if (in_array('icon', $properties)) {
+                    $results['turba/' . $owner]['icon'] = $registry->getImageDir() . '/turba.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results['turba/' . $owner]['browseable'] = true;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results['turba/' . $owner]['contenttype'] = 'httpd/unix-directory';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results['turba/' . $owner]['contentlength'] = 0;
+                }
+                if (in_array('modified', $properties)) {
+                    // @TODO: Get a real modification date
+                    $results['turba/' . $owner]['modified'] = $now;
+                }
+                if (in_array('created', $properties)) {
+                    // @TODO Get a real creation date
+                    $results['turba/' . $owner]['created'] = 0;
+                }
+            }
+            return $results;
+        } elseif (count($parts) == 1) {
+            //
+            // We should either have the username that is a valid share owner or
+            // 'global'
+            //
+            if (empty($parts[0])) {
+                // We need either 'global' or a valid username with shares
+                return array();
+            }
+
+            if ($parts[0] == 'global') {
+                // The client is requesting a list of global address books.
+                $addressbooks = Turba::getAddressBooks();
+                foreach ($addressbooks as $addressbook => $info) {
+                    if ($info['type'] == 'share') {
+                        // Ignore address book shares in the 'global' folder
+                        unset($addressbooks[$addressbook]);
+                    }
+                }
+            } else {
+                // Assume $parts[0] is a valid username and we need to list their
+                // shared addressbooks.
+                if (empty($_SESSION['turba']['has_share'])) {
+                    // No backends are configured to provide shares
+                    return array();
+                }
+                $addressbooks = $GLOBALS['turba_shares']->listShares($parts[0],
+                    PERMS_READ,
+                    $parts[0]);
+                // The last check returns all addressbooks for the requested user,
+                // but that does not mean the requesting user has access to them.
+                // Filter out those address books for which the requesting user has
+                // no access.
+                $addressbooks = Turba::permissionsFilter($addressbooks);
+            }
+
+            $curpath = 'turba/' . $parts[0] . '/';
+            foreach ($addressbooks as $addressbook => $info) {
+                if (in_array('name', $properties)) {
+                    if (is_a($info, 'Horde_Share_Object')) {
+                        $name = $info->get('title');
+                    } else {
+                        $name = $info['title'];
+                    }
+                    $results[$curpath . $addressbook]['name'] = $name;
+                }
+                if (in_array('icon', $properties)) {
+                    $results[$curpath . $addressbook]['icon'] = $registry->getImageDir() . '/turba.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results[$curpath . $addressbook]['browseable'] = true;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results[$curpath . $addressbook]['contenttype'] = 'httpd/unix-directory';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $results[$curpath . $addressbook]['contentlength'] = 0;
+                }
+                if (in_array('modified', $properties)) {
+                    // @TODO: Get a real modification date
+                    $results[$curpath . $addressbook]['modified'] = $now;
+                }
+                if (in_array('created', $properties)) {
+                    // @TODO Get a real creation date
+                    $results[$curpath . $addressbook]['created'] = 0;
+                }
+            }
+            return $results;
+
+        } elseif (count($parts) == 2) {
+            //
+            // The client is requesting all contacts from a given addressbook
+            //
+            if (empty($parts[0]) || empty($parts[1])) {
+                // $parts[0] must be either 'global' or a valid user with shares
+                // $parts[1] must be an address book ID
+                return array();
+            }
+
+            $addressbooks = Turba::getAddressBooks();
+            if (!isset($addressbooks[$parts[1]])) {
+                // We must have a valid addressbook to continue.
+                return array();
+            }
+
+            // Load the Turba driver.
+            $driver = Turba_Driver::singleton($parts[1]);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $cfgSources[$parts[1]]);
+            }
+
+            $contacts = $driver->search(array());
+            if (is_a($contacts, 'PEAR_Error')) {
+                return $contacts;
+            }
+
+            $contacts->reset();
+            $curpath = 'turba/' . $parts[0] . '/' . $parts[1] . '/';
+            while ($contact = $contacts->next()) {
+                $key = $curpath . $contact->getValue('__key');
+                if (in_array('name', $properties)) {
+                    $results[$key]['name'] = Turba::formatName($contact);
+                }
+                if (in_array('icon', $properties)) {
+                    $results[$key]['icon'] = $registry->getImageDir('horde') . '/mime/vcard.png';
+                }
+                if (in_array('browseable', $properties)) {
+                    $results[$key]['browseable'] = false;
+                }
+                if (in_array('contenttype', $properties)) {
+                    $results[$key]['contenttype'] = 'text/x-vcard';
+                }
+                if (in_array('contentlength', $properties)) {
+                    $data = $this->export($contact->getValue('__uid'), 'text/x-vcard', $contact->getSource());
+                    if (is_a($data, 'PEAR_Error')) {
+                        $data = '';
+                    }
+                    $results[$key]['contentlength'] = strlen($data);
+                }
+                if (in_array('modified', $properties)) {
+                    $results[$key]['modified'] = _modified($contact->getValue('__uid'));
+                }
+                if (in_array('created', $properties)) {
+                    $results[$key]['created'] = $this->getActionTimestamp($contact->getValue('__uid'), 'add');
+                }
+            }
+
+            return $results;
+
+        } elseif (count($parts) == 3) {
+            //
+            // The client is requesting an individual contact
+            //
+            $addressbooks = Turba::getAddressBooks();
+            if (!isset($addressbooks[$parts[1]])) {
+                // We must have a valid addressbook to continue.
+                return array();
+            }
+
+            // Load the Turba driver.
+            $driver = Turba_Driver::singleton($parts[1]);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $cfgSources[$parts[1]]);
+            }
+
+            $contact = &$driver->getObject($parts[2]);
+            if (is_a($contact, 'PEAR_Error')) {
+                return $contact;
+            }
+
+            $result = array('data' => $this->export($contact->getValue('__uid'), 'text/x-vcard', $contact->getSource()),
+                'mimetype' => 'text/x-vcard');
+            $modified = _modified($contact->getValue('__uid'));
+            if (!empty($modified)) {
+                $result['mtime'] = $modified;
+            }
+            return $result;
+        } else {
+            return PEAR::raiseError(_("Malformed request."));
+        }
+    }
+
+    /**
+     * Deletes a file from the Turba tree.
+     *
+     * @param string $path  The path to the file.
+     *
+     * @return mixed  The event's UID, or a PEAR_Error on failure.
+     */
+    public function path_delete($path)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $registry, $cfgSources;
+
+        // Strip off the application name if present
+        if (substr($path, 0, 5) == 'turba') {
+            $path = substr($path, 5);
+        }
+        $path = trim($path, '/');
+        $parts = explode('/', $path);
+
+        $now = time();
+        $results = array();
+
+        if (count($parts) < 3) {
+            // Deletes must be on individual contacts
+            return PEAR::raiseError(_("Delete denied."), 403);
+        }
+        if (!array_key_exists($parts[1], Turba::getAddressBooks())) {
+            return PEAR::raiseError("Address book does not exist", 404);
+        }
+
+        // Load the Turba driver.
+        $driver = Turba_Driver::singleton($parts[1]);
+        if (is_a($driver, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 500, null, null, $cfgSources[$parts]);
+        }
+
+        $ret = $driver->delete($parts[2]);
+        if (is_a($ret, 'PEAR_Error')) {
+            // A deeper error occurred.  Make sure the code is a valid HTTP response
+            $ret->code = 500;
+            return $ret;
+        }
+    }
+
+    /**
+     * Returns an array of UIDs for all contacts that the current user is
+     * authorized to see.
+     *
+     * @param string|array $sources  The name(s) of the source(s) to return
+     *                               contacts of. If left empty, the current user's
+     *                               sync sources or default source are used.
+     *
+     * @return array  An array of UIDs for all contacts the user can access.
+     */
+    public function listContacts($sources = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+
+        global $cfgSources, $prefs;
+
+        /* Get default address book from user preferences. */
+        if (empty($sources)) {
+            $sources = @unserialize($prefs->getValue('sync_books'));
+        } elseif (!is_array($sources)) {
+            $sources = array($sources);
+        }
+        if (empty($sources)) {
+            $sources = array(Turba::getDefaultAddressbook());
+        }
+        if (empty($sources)) {
+            return PEAR::raiseError(_("No address book specified"), 'horde.error');
+        }
+
+        $uids = array();
+        foreach ($sources as $source) {
+            if (empty($source) || !isset($cfgSources[$source])) {
+                return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
+            }
+
+            $storage = Turba_Driver::singleton($source);
+            if (is_a($storage, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $storage->getMessage()), 'horde.error', null, null, $source);
+            }
+
+            $results = $storage->search(array());
+
+            if (is_a($results, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Error searching the address book: %s"), $results->getMessage()), 'horde.error', null, null, $source);
+            }
+
+            foreach ($results->objects as $o) {
+                $uids[] = $o->getValue('__uid');
+            }
+        }
+
+        return $uids;
+    }
+
+    /**
+     * Returns an array of UIDs for contacts that have had $action happen since
+     * $timestamp.
+     *
+     * @param string  $action        The action to check for - add, modify, or
+     *                               delete.
+     * @param integer $timestamp     The time to start the search.
+     * @param string|array $sources  The source(s) for which to retrieve the
+     *                               history.
+     *
+     * @return array  An array of UIDs matching the action and time criteria.
+     */
+    public function listBy($action, $timestamp, $sources = null)
+    {
+        global $prefs, $cfgSources;
+        require_once dirname(__FILE__) . '/base.php';
+
+        /* Get default address book from user preferences. */
+        if (empty($sources)) {
+            $sources = @unserialize($prefs->getValue('sync_books'));
+        } elseif (!is_array($sources)) {
+            $sources = array($sources);
+        }
+        if (empty($sources)) {
+            $sources = array(Turba::getDefaultAddressbook());
+        }
+        if (empty($sources)) {
+            return PEAR::raiseError(_("No address book specified"), 'horde.error');
+        }
+
+        $uids = array();
+        foreach ($sources as $source) {
+            if (empty($source) || !isset($cfgSources[$source])) {
+                return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
+            }
+
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
+            }
+
+            $history = Horde_History::singleton();
+            $histories = $history->getByTimestamp('>', $timestamp,
+                array(array('op' => '=',
+                'field' => 'action',
+                'value' => $action)),
+                'turba:' . $driver->getName());
+            if (is_a($histories, 'PEAR_Error')) {
+                return $histories;
+            }
+
+            // Strip leading turba:addressbook:.
+            $uids = array_merge($uids,
+                str_replace('turba:' . $driver->getName() . ':',
+                '',
+                array_keys($histories)));
+        }
+
+        return $uids;
+    }
+
+    /**
+     * Returns the timestamp of an operation for a given uid an action.
+     *
+     * @param string $uid            The uid to look for.
+     * @param string $action         The action to check for - add, modify, or
+     *                               delete.
+     * @param string|array $sources  The source(s) for which to retrieve the
+     *                               history.
+     *
+     * @return integer  The timestamp for this action.
+     */
+    public function getActionTimestamp($uid, $action, $sources = null)
+    {
+        global $prefs, $cfgSources;
+        require_once dirname(__FILE__) . '/base.php';
+
+        /* Get default address book from user preferences. */
+        if (empty($sources)) {
+            $sources = @unserialize($prefs->getValue('sync_books'));
+        } elseif (!is_array($sources)) {
+            $sources = array($sources);
+        }
+        if (empty($sources)) {
+            $sources = array(Turba::getDefaultAddressbook());
+        }
+        if (empty($sources)) {
+            return PEAR::raiseError(_("No address book specified"), 'horde.error');
+        }
+
+        foreach ($sources as $source) {
+            if (empty($source) || !isset($cfgSources[$source])) {
+                return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
+            }
+
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
+            }
+
+            $history = Horde_History::singleton();
+            $ts = $history->getActionTimestamp('turba:' . $driver->getName()
+                . ':' . $uid,
+                $action);
+            if (!empty($ts)) {
+                return $ts;
+            }
+        }
+
+        return 0;
+    }
+
+    /**
+     * Import a contact represented in the specified contentType.
+     *
+     * @param string $content      The content of the contact.
+     * @param string $contentType  What format is the data in? Currently supports
+     *                             array, text/directory, text/vcard and
+     *                             text/x-vcard.
+     * @param string $source       The source into which the contact will be
+     *                             imported.
+     *
+     * @return string  The new UID, or false on failure.
+     */
+    public function import($content, $contentType = 'array',
+                           $import_source = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources, $prefs;
+
+        /* Get default address book from user preferences. */
+        if (empty($import_source)) {
+            $import_source = $prefs->getValue('default_dir');
+            /* On new installations default_dir is not set, use first source
+             * instead. */
+            if (empty($import_source)) {
+                $import_source = key(Turba::getAddressBooks(PERMS_EDIT));
+            }
+        }
+
+        // Check existance of and permissions on the specified source.
+        if (!isset($cfgSources[$import_source])) {
+            return PEAR::raiseError(sprintf(_("Invalid address book: %s"),
+                $import_source),
+            'horde.warning');
+        }
+
+        $driver = Turba_Driver::singleton($import_source);
+        if (is_a($driver, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $import_source);
+        }
+
+        if (!$driver->hasPermission(PERMS_EDIT)) {
+            return PEAR::raiseError(_("Permission denied"), 'horde.error', null, null, $import_source);
+        }
+
+        /* Create a category manager. */
+        require_once 'Horde/Prefs/CategoryManager.php';
+        $cManager = new Prefs_CategoryManager();
+        $categories = $cManager->get();
+
+        if (!is_a($content, 'Horde_iCalendar_vcard')) {
+            switch ($contentType) {
+            case 'array':
+                break;
+
+            case 'text/x-vcard':
+            case 'text/vcard':
+            case 'text/directory':
+                $iCal = new Horde_iCalendar();
+                if (!$iCal->parsevCalendar($content)) {
+                    return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+                }
+                switch ($iCal->getComponentCount()) {
+                case 0:
+                    return PEAR::raiseError(_("No vCard data was found."));
+
+                case 1:
+                    $content = $iCal->getComponent(0);
+                    break;
+
+                default:
+                    $ids = array();
+                    foreach ($iCal->getComponents() as $c) {
+                        if (is_a($c, 'Horde_iCalendar_vcard')) {
+                            $content = $driver->toHash($c);
+                            $result = $driver->search($content);
+                            if (is_a($result, 'PEAR_Error')) {
+                                return $result;
+                            } elseif ($result->count() > 0) {
+                                continue;
+                            }
+                            $result = $driver->add($content);
+                            if (is_a($result, 'PEAR_Error')) {
+                                return $result;
+                            }
+                            if (!empty($content['category']) &&
+                                !in_array($content['category'], $categories)) {
+                                    $cManager->add($content['category']);
+                                    $categories[] = $content['category'];
+                                }
+                            $ids[] = $result;
+                        }
+                    }
+                    return $ids;
+                }
+                break;
+
+            default:
+                return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+            }
+        }
+
+        if (is_a($content, 'Horde_iCalendar_vcard')) {
+            $content = $driver->toHash($content);
+        }
+
+        // Check if the entry already exists in the data source:
+        $result = $driver->search($content);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        } elseif ($result->count() > 0) {
+            $o = $result->objects[0];
+            return PEAR::raiseError(_("Already Exists"), 'horde.message', null, null, $o->getValue('__uid'));
+        }
+
+        $result = $driver->add($content);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (!empty($content['category']) &&
+            !in_array($content['category'], $categories)) {
+                $cManager->add($content['category']);
+            }
+
+        $object = &$driver->getObject($result);
+        return is_a($object, 'PEAR_Error') ? $object : $object->getValue('__uid');
+    }
+
+    /**
+     * Export a contact, identified by UID, in the requested contentType.
+     *
+     * @param string $uid            Identify the contact to export.
+     * @param mixed $contentType     What format should the data be in?
+     *                               Either a string with one of:
+     *                               - text/directory
+     *                               - text/vcard
+     *                               - text/x-vcard
+     *                               The first two produce a vcard3.0 (rfc2426),
+     *                               the second produces a vcard in old 2.1 format
+     *                               defined by imc.org
+     * @param string|array $sources  The source(s) from which the contact will be
+     *                               exported.
+     *
+     * @return mixed  The requested data | PEAR_Error
+     */
+    public function export($uid, $contentType, $sources = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources, $prefs;
+
+        /* Get default address book from user preferences. */
+        if (empty($sources)) {
+            $sources = @unserialize($prefs->getValue('sync_books'));
+        } elseif (!is_array($sources)) {
+            $sources = array($sources);
+        }
+        if (empty($sources)) {
+            $sources = array(Turba::getDefaultAddressbook());
+        }
+        if (empty($sources)) {
+            return PEAR::raiseError(_("No address book specified"), 'horde.error');
+        }
+
+        foreach ($sources as $source) {
+            if (empty($source) || !isset($cfgSources[$source])) {
+                return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
+            }
+
+            if (empty($uid)) {
+                return PEAR::raiseError(_("Invalid ID"), 'horde.error', null, null, $source);
+            }
+
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
+            }
+
+            if (!$driver->hasPermission(PERMS_READ)) {
+                continue;
+            }
+
+            $result = $driver->search(array('__uid' => $uid));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            } elseif ($result->count() == 0) {
+                continue;
+            } elseif ($result->count() > 1) {
+                return PEAR::raiseError("Internal Horde Error: multiple turba objects with same objectId.", 'horde.error', null, null, $source);
+            }
+
+            $version = '3.0';
+            list($contentType,) = explode(';', $contentType);
+            switch ($contentType) {
+            case 'text/x-vcard':
+                $version = '2.1';
+
+            case 'text/vcard':
+            case 'text/directory':
+                $export = '';
+                foreach ($result->objects as $obj) {
+                    $vcard = $driver->tovCard($obj, $version);
+                    /* vCards are not enclosed in BEGIN:VCALENDAR..END:VCALENDAR.
+                     * Export the individual cards instead. */
+                    $export .= $vcard->exportvCalendar();
+                }
+                return $export;
+            }
+
+            return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+        }
+
+        return PEAR::raiseError(_("Object not found"));
+    }
+
+    /**
+     * Exports the user's own contact as a vCard string.
+     *
+     * @return string  The requested vCard data or PEAR_Error.
+     */
+    public function ownVCard()
+    {
+        $contact = $this->getOwnContactObject();
+        if (is_a($contact, 'PEAR_Error')) {
+            return $contact;
+        }
+        $driver = Turba_Driver::singleton($contact['source']);
+        if (is_a($driver, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()));
+        }
+        $vcard = $driver->tovCard($contact['contact'], '3.0');
+        $vcard->setAttribute('VERSION', '3.0');
+
+        return $vcard->exportvCalendar();
+    }
+
+    /**
+     * Export the user's own contact as a hash
+     *
+     * @return Array  The contact hash or PEAR_Error
+     */
+    public function ownContact()
+    {
+        $contact = $this->getOwnContactObject();
+        if (is_a($contact, 'PEAR_Error')) {
+            return $contact;
+        }
+
+        return $contact['contact']->getAttributes();
+    }
+
+    /**
+     * Helper function to  return the user's own contact object
+     *
+     * @return Array  A hash containing the Turba_Object representing the user's
+     *                own contact and the source that it is from or PEAR_Error.
+     */
+    public function getOwnContactObject()
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+
+        $own_contact = $GLOBALS['prefs']->getValue('own_contact');
+        if (empty($own_contact)) {
+            return PEAR::raiseError(_("You didn't mark a contact as your own yet."));
+        }
+        @list($source, $id) = explode(';', $own_contact);
+
+        if (!isset($cfgSources[$source])) {
+            return PEAR::raiseError(_("The address book with your own contact doesn't exist anymore."));
+        }
+
+        $driver = Turba_Driver::singleton($source);
+        if (is_a($driver, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()));
+        }
+
+        if (!$driver->hasPermission(PERMS_READ)) {
+            return PEAR::raiseError(_("You don't have sufficient permissions to read the address book that contains your own contact."));
+        }
+
+        $contact = $driver->getObject($id);
+        if (is_a($contact, 'PEAR_Error')) {
+            return PEAR::raiseError(_("Your own contact cannot be found in the address book."));
+        }
+
+        $return = array('contact' => $contact,
+            'source'=> $source);
+
+        return $return;
+    }
+
+    /**
+     * Deletes a contact identified by UID.
+     *
+     * @param string|array $uid      Identify the contact to delete, either a
+     *                               single UID or an array.
+     * @param string|array $sources  The source(s) from which the contact will be
+     *                               deleted.
+     *
+     * @return boolean  Success or failure.
+     */
+    public function delete($uid, $sources = null)
+    {
+        // Handle an array of UIDs for convenience of deleting multiple contacts
+        // at once.
+        if (is_array($uid)) {
+            foreach ($uid as $g) {
+                $result = $this->delete($uid, $source);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+
+            return true;
+        }
+
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources, $prefs;
+
+        /* Get default address book from user preferences. */
+        if (empty($sources)) {
+            $sources = @unserialize($prefs->getValue('sync_books'));
+        } elseif (!is_array($sources)) {
+            $sources = array($sources);
+        }
+        if (empty($sources)) {
+            $sources = array(Turba::getDefaultAddressbook());
+        }
+        if (empty($sources)) {
+            return PEAR::raiseError(_("No address book specified"), 'horde.error');
+        }
+
+        foreach ($sources as $source) {
+            if (empty($source) || !isset($cfgSources[$source])) {
+                return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
+            }
+
+            if (empty($uid)) {
+                return PEAR::raiseError(_("Invalid ID"), 'horde.error', null, null, $source);
+            }
+
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
+            }
+
+            if (!Horde_Auth::isAdmin() && !$driver->hasPermission(PERMS_DELETE)) {
+                continue;
+            }
+
+            // If the objectId isn't in $source in the first place, just return
+            // true. Otherwise, try to delete it and return success or failure.
+            $result = $driver->search(array('__uid' => $uid));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            } elseif ($result->count() == 0) {
+                continue;
+            } else {
+                $r = $result->objects[0];
+                return $driver->delete($r->getValue('__key'));
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Replaces the contact identified by UID with the content represented in the
+     * specified contentType.
+     *
+     * @param string $uid            Idenfity the contact to replace.
+     * @param string $content        The content of the contact.
+     * @param string $contentType    What format is the data in? Currently supports
+     *                               array, text/directory, text/vcard and
+     *                               text/x-vcard.
+     * @param string|array $sources  The source(s) where the contact will be
+     *                               replaced.
+     *
+     * @return boolean  Success or failure.
+     */
+    public function replace($uid, $content, $contentType, $sources = null)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources, $prefs;
+
+        /* Get default address book from user preferences. */
+        if (empty($sources)) {
+            $sources = @unserialize($prefs->getValue('sync_books'));
+        } elseif (!is_array($sources)) {
+            $sources = array($sources);
+        }
+        if (empty($sources)) {
+            $sources = array(Turba::getDefaultAddressbook());
+        }
+        if (empty($sources)) {
+            return PEAR::raiseError(_("No address book specified"), 'horde.error');
+        }
+
+        foreach ($sources as $source) {
+            if (empty($source) || !isset($cfgSources[$source])) {
+                return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
+            }
+
+            if (empty($uid)) {
+                return PEAR::raiseError(_("Invalid contact unique ID"), 'horde.error', null, null, $source);
+            }
+
+            // Check permissions.
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
+            }
+            if (!$driver->hasPermission(PERMS_EDIT)) {
+                continue;
+            }
+            $result = $driver->search(array('__uid' => $uid));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            } elseif (!$result->count()) {
+                continue;
+            } elseif ($result->count() > 1) {
+                return PEAR::raiseError("Multiple contacts found with same unique ID.", 'horde.error', null, null, $source);
+            }
+
+            $object = $result->objects[0];
+
+            switch ($contentType) {
+            case 'array':
+                break;
+
+            case 'text/x-vcard':
+            case 'text/vcard':
+            case 'text/directory':
+                $iCal = new Horde_iCalendar();
+                if (!$iCal->parsevCalendar($content)) {
+                    return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+                }
+
+                switch ($iCal->getComponentCount()) {
+                case 0:
+                    return PEAR::raiseError(_("No vCard data was found."));
+
+                case 1:
+                    $content = $iCal->getComponent(0);
+                    $content = $driver->toHash($content);
+                    break;
+
+                default:
+                    return PEAR::raiseError(_("Only one vcard supported."));
+                }
+                break;
+
+            default:
+                return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
+            }
+
+            foreach ($content as $attribute => $value) {
+                if ($attribute != '__key') {
+                    $object->setValue($attribute, $value);
+                }
+            }
+
+            return $object->store();
+        }
+
+        return PEAR::raiseError(_("Object not found"));
+    }
+
+    /**
+     * Returns a contact search result.
+     *
+     * @param array $names          The search filter values
+     * @param array $sources        The sources to serach in
+     * @param array $fields         The fields to serach on
+     * @param boolean $matchBegin   Match word boundaries only
+     * @param boolean $forceSource  Whether to use the specified sources, even if
+     *                              they have been disabled in the preferences.
+     *
+     * @return array  Hash containing the search results.
+     */
+    public function search($names = array(), $sources = array(),
+                           $fields = array(),
+        $matchBegin = false, $forceSource = false)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources, $attributes, $prefs;
+
+        if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
+            return array();
+        }
+
+        if (!is_array($names)) {
+            $names = is_null($names) ? array() : array($names);
+        }
+
+        if (!$forceSource) {
+            // Make sure the selected source is activated in Turba.
+            $addressbooks = array_keys(Turba::getAddressBooks());
+            foreach (array_keys($sources) as $id) {
+                if (!in_array($sources[$id], $addressbooks)) {
+                    unset($sources[$id]);
+                }
+            }
+        }
+
+        // ...and ensure the default source is used as a default.
+        if (!count($sources)) {
+            $sources = array(Turba::getDefaultAddressBook());
+        }
+
+        // Read the columns to display from the preferences.
+        $sort_columns = Turba::getColumns();
+
+        $results = array();
+        $seen = array();
+        foreach ($sources as $source) {
+            // Skip invalid sources.
+            if (!isset($cfgSources[$source])) {
+                continue;
+            }
+
+            // Skip sources that aren't browseable if the search is empty.
+            if (empty($cfgSources[$source]['browse']) &&
+                (!count($names) || (count($names) == 1 && empty($names[0])))) {
+                    continue;
+                }
+
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
+            }
+
+            // Determine the name of the column to sort by.
+            $columns = isset($sort_columns[$source])
+                ? $sort_columns[$source] : array();
+
+            foreach ($names as $name) {
+                $criteria = array();
+                if (isset($fields[$source])) {
+                    foreach ($fields[$source] as $field) {
+                        $criteria[$field] = trim($name);
+                    }
+                }
+                if (count($criteria) == 0) {
+                    $criteria['name'] = trim($name);
+                }
+
+                $search = $driver->search($criteria, Turba::getPreferredSortOrder(), 'OR', array(), array(), $matchBegin);
+                if (!is_a($search, 'Turba_List')) {
+                    continue;
+                }
+
+                while ($ob = $search->next()) {
+                    if (!$ob->isGroup()) {
+                        /* Not a group. */
+                        $att = array('__key' => $ob->getValue('__key'));
+                        foreach ($ob->driver->getCriteria() as $info_key => $info_val) {
+                            $att[$info_key] = $ob->getValue($info_key);
+                        }
+                        $email = array();
+                        foreach (array_keys($att) as $key) {
+                            if (!$ob->getValue($key) ||
+                                !isset($attributes[$key]) ||
+                                $attributes[$key]['type'] != 'email') {
+                                    continue;
+                                }
+                            $email_val = $ob->getValue($key);
+
+                            // Multiple addresses support
+                            if (isset($attributes[$key]['params'])
+                                && is_array($attributes[$key]['params'])
+                                && !empty($attributes[$key]['params']['allow_multi'])) {
+                                    $addrs = Horde_Mime_Address::explode($email_val);
+                                } else {
+                                    $addrs = array($email_val);
+                                }
+
+                            foreach ($addrs as $addr) {
+                                $email[] = trim($addr);
+                            }
+                        }
+
+                        if ($ob->hasValue('name') ||
+                            !isset($ob->driver->alternativeName)) {
+                                $display_name = Turba::formatName($ob);
+                            } else {
+                                $display_name = $ob->getValue($ob->driver->alternativeName);
+                            }
+                        if (count($email)) {
+                            for ($i = 0; $i < count($email); $i++) {
+                                $seen_key = trim(Horde_String::lower($display_name)) . '/' . trim(Horde_String::lower($email[$i]));
+                                if (!empty($seen[$seen_key])) {
+                                    continue;
+                                }
+                                $seen[$seen_key] = true;
+                                if (!isset($results[$name])) {
+                                    $results[$name] = array();
+                                }
+                                $results[$name][] = array_merge($att,
+                                    array('id' => $att['__key'],
+                                    'name' => $display_name,
+                                    'email' => $email[$i],
+                                    '__type' => 'Object',
+                                    'source' => $source));
+                            }
+                        } else {
+                            if (!isset($results[$name])) {
+                                $results[$name] = array();
+                            }
+                            $results[$name][] = array_merge($att,
+                                array('id' => $att['__key'],
+                                'name' => $display_name,
+                                'email' => null,
+                                '__type' => 'Object',
+                                'source' => $source));
+                        }
+                    } else {
+                        /* Is a distribution list. */
+                        $listatt = $ob->getAttributes();
+                        $seeninlist = array();
+                        $members = $ob->listMembers();
+                        $listName = $ob->getValue('name');
+                        if (!is_a($members, 'Turba_List')) {
+                            continue;
+                        }
+                        if ($members->count() > 0) {
+                            if (!isset($results[$name])) {
+                                $results[$name] = array();
+                            }
+                            $emails = array();
+                            while ($ob = $members->next()) {
+                                $att = $ob->getAttributes();
+                                foreach (array_keys($att) as $key) {
+                                    $value = $ob->getValue($key);
+                                    if (empty($value)) {
+                                        continue;
+                                    }
+                                    if (!is_array($value)) {
+                                        $seen_key = trim(Horde_String::lower($ob->getValue('name')))
+                                            . trim(Horde_String::lower($value));
+                                    } else {
+                                        $seen_key = trim(Horde_String::lower($ob->getValue('name')))
+                                            . trim(Horde_String::lower($value['load']['file']));
+                                    }
+                                    if (isset($attributes[$key]) &&
+                                        $attributes[$key]['type'] == 'email' &&
+                                        empty($seeninlist[$seen_key])) {
+                                            $emails[] = $value;
+                                            $seeninlist[$seen_key] = true;
+                                        }
+                                }
+                            }
+                            $results[$name][] = array('name' => $listName,
+                                'email' => implode(', ', $emails),
+                                'id' => $listatt['__key'],
+                                'source' => $source);
+                        }
+                    }
+                }
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Retrieves a contact.
+     *
+     * @param string $source    The source name where the contact is stored
+     * @param string $objectId  The unique id of the contact to retrieve
+     *
+     * @return array  The retrieved contact.
+     */
+    public function getContact($source = null, $objectId = '')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+
+        if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
+            return array();
+        }
+
+        if (isset($cfgSources[$source])) {
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                return $driver;
+            }
+
+            $object = $driver->getObject($objectId);
+            if (is_a($object, 'PEAR_Error')) {
+                return $object;
+            }
+
+            $attributes = array();
+            foreach ($cfgSources[$source]['map'] as $field => $map) {
+                $attributes[$field] = $object->getValue($field);
+            }
+            return $attributes;
+        }
+
+        return array();
+    }
+
+    /**
+     * Retrieves a set of contacts from a single source.
+     *
+     * @param string $source    The source name where the contact is stored
+     * @param array $objectIds  The unique ids of the contact to retrieve.
+     *
+     * @return mixed  The retrieved contact | PEAR_Error
+     */
+    public function getContacts($source = '', $objectIds = array())
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+        $results = array();
+        if (!is_array($objectIds)) {
+            $objectIds = array($objectIds);
+        }
+
+        if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
+            return array();
+        }
+
+        if (isset($cfgSources[$source])) {
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                return $driver;
+            }
+
+            $objects = $driver->getObjects($objectIds);
+            if (is_a($objects, 'PEAR_Error')) {
+                return $objects;
+            }
+
+            foreach ($objects as $object) {
+                $attributes = array();
+                foreach ($cfgSources[$source]['map'] as $field => $map) {
+                    $attributes[$field] = $object->getValue($field);
+                }
+                $results[] = $attributes;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Retrieves a list of all possible values of a field in specified source(s).
+     *
+     * @param string $field   Field name to check
+     * @param array $sources  Array containing the sources to look in
+     *
+     * @return mixed  An array of fields and possible values | PEAR_Error
+     */
+    public function getAllAttributeValues($field = '', $sources = array())
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+
+        if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
+            return array();
+        }
+
+        if (!count($sources)) {
+            $sources = array(Turba::getDefaultAddressBook());
+        }
+
+        $results = array();
+        foreach ($sources as $source) {
+            if (isset($cfgSources[$source])) {
+                $driver = Turba_Driver::singleton($source);
+                if (is_a($driver, 'PEAR_Error')) {
+                    return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
+                }
+
+                $res = $driver->search(array());
+                if (!is_a($res, 'Turba_List')) {
+                    return PEAR::raiseError(_("Search failed"), 'horde.error', null, null, $source);
+                }
+
+                while ($ob = $res->next()) {
+                    if ($ob->hasValue($field)) {
+                        $results[$source . ':' . $ob->getValue('__key')] = array(
+                            'name' => $ob->getValue('name'),
+                            'email' => $ob->getValue('email'),
+                            $field => $ob->getValue($field));
+                    }
+                }
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Retrieves a list of available time objects categories
+     *
+     * @return array  An array of all configured time object categories.
+     */
+    public function listTimeObjectCategories()
+    {
+        include dirname(__FILE__) . '/../config/attributes.php';
+        include dirname(__FILE__) . '/../config/sources.php';
+        $categories = array();
+        foreach ($attributes as $key => $attribute) {
+            if ($attribute['type'] == 'monthdayyear' &&
+                !empty($attribute['time_object_label'])) {
+
+                    foreach ($cfgSources as $source) {
+                        if (!empty($source['map'][$key])) {
+                            $categories[$key] = $attribute['time_object_label'];
+                            break;
+                        }
+                    }
+                }
+        }
+
+
+
+        return $categories;
+    }
+
+    /**
+     * Lists birthdays and/or anniversaries as time objects.
+     *
+     * @param array $time_categories  The time categories (from
+     *                                listTimeObjectCategories) to list.
+     * @param mixed $start            The start date of the period.
+     * @param mixed $end              The end date of the period.
+     *
+     * @return mixed  An array of timeObject results || PEAR_Error
+     */
+    public function listTimeObjects($time_categories, $start, $end)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+
+        $start = new Horde_Date($start);
+        $end = new Horde_Date($end);
+
+        if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
+            return array();
+        }
+
+        $objects = array();
+        foreach ($cfgSources as $name => $source) {
+            // Check if we even have to load the driver.
+            $check = array();
+            foreach ($time_categories as $category) {
+                if (!empty($source['map'][$category])) {
+                    $check[] = $category;
+                }
+            }
+            if (!count($check)) {
+                continue;
+            }
+            $driver = Turba_Driver::singleton($name);
+            if (is_a($driver, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Connection failed: %s"),
+                    $driver->getMessage()), 'horde.error',
+                    null, null, $name);
+            }
+            foreach ($check as $category) {
+                $new_objects = $driver->listTimeObjects($start, $end, $category);
+                if (is_a($new_objects, 'PEAR_Error')) {
+                    return $new_objects;
+                }
+                $objects = array_merge($objects, $new_objects);
+            }
+        }
+
+        return $objects;
+    }
+
+    /**
+     * Returns the client source name
+     *
+     * @return string  The name of the source to use with the clients api.
+     */
+    public function getClientSource()
+    {
+        return !empty($GLOBALS['conf']['client']['addressbook']) ? $GLOBALS['conf']['client']['addressbook'] : false;
+    }
+
+    /**
+     * Returns the availabble client fields
+     *
+     * @return mixed  An array describing the fields | PEAR_Error
+     */
+    public function clientFields()
+    {
+        return $this->fields($GLOBALS['conf']['client']['addressbook']);
+    }
+
+    /**
+     * Returns a contact from the client source.
+     *
+     * @param string $objectId  Client unique ID
+     *
+     * @return mixed  Array of client data | PEAR_Error
+     */
+    public function getClient($objectId = '')
+    {
+        return $this->getContact($GLOBALS['conf']['client']['addressbook'], $objectId);
+    }
+
+    /**
+     * Returns mulitple contacts from the client source
+     *
+     * @param array $objectIds  client unique ids
+     *
+     * @return mixed  An array of clients data | PEAR_Error
+     */
+    public function getClients($objectIds = array())
+    {
+        return $this->getContacts($GLOBALS['conf']['client']['addressbook'], $objectIds);
+    }
+
+    /**
+     * Adds a client to the client source
+     *
+     * @param array $attributes  Array containing the client attributes
+     */
+    public function addClient($attributes = array())
+    {
+        return $this->import($attributes, 'array', $this->getClientSource());
+    }
+
+    /**
+     * Updates client data
+     *
+     * @param string $objectId   The unique id of the client
+     * @param array $attributes  An array of client attributes
+     *
+     * @return boolean
+     */
+    public function updateClient($objectId = '', $attributes = array())
+    {
+        return $this->replace($this->getClientSource() . ':' . $objectId, $attributes, 'array');
+    }
+
+    /**
+     * Deletes a client
+     *
+     * @param string $objectId  The unique id of the client
+     *
+     * @return boolean
+     */
+    public function deleteClient($objectId = '')
+    {
+        return $this->delete($this->getClientSource() . ':' . $objectId);
+    }
+
+    /**
+     * Search for clients
+     *
+     * @param array $names         The search filter values
+     * @param array $fields        The fields to serach in
+     * @param boolean $matchBegin  Match word boundaries only
+     *
+     * @return mixed  A hash containing the search results | PEAR_Error
+     */
+    public function searchClients($names = array(), $fields = array(),
+                                  $matchBegin = false)
+    {
+        return $this->search(
+            $names,
+            array($GLOBALS['conf']['client']['addressbook']),
+            array($GLOBALS['conf']['client']['addressbook'] => $fields),
+            $matchBegin,
+            true);
+    }
+
+    /**
+     * Sets the value of the specified attribute of a contact
+     *
+     * @param string $address  Contact email address
+     * @param string $name     Contact name
+     * @param string $field    Field to update
+     * @param string $value    Field value to set
+     * @param string $source   Contact source
+     *
+     * @return mixed  The new __key value on success | PEAR_Error on failure
+     */
+    public function addField($address = '', $name = '', $field = '',
+                             $value = '',
+        $source = '')
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+
+        if (empty($source) || !isset($cfgSources[$source])) {
+            return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
+        }
+
+        if (empty($address)) {
+            return PEAR::raiseError(_("Invalid email"), 'horde.error', null, null, $source);
+        }
+
+        if (empty($name)) {
+            return PEAR::raiseError(_("Invalid name"), 'horde.error', null, null, $source);
+        }
+
+        if (empty($value)) {
+            return PEAR::raiseError(_("Invalid entry"), 'horde.error', null, null, $source);
+        }
+
+        $driver = Turba_Driver::singleton($source);
+        if (is_a($driver, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
+        }
+
+        if (!$driver->hasPermission(PERMS_EDIT)) {
+            return PEAR::raiseError(_("Permission denied"), 'horde.error', null, null, $source);
+        }
+
+        $res = $driver->search(array('email' => trim($address)), null, 'AND');
+        if (is_a($res, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf(_("Search failed: %s"), $res->getMessage()), 'horde.message', null, null, $source);
+        }
+
+        if ($res->count() > 1) {
+            $res2 = $driver->search(array('email' => trim($address), 'name' => trim($name)), null, 'AND');
+            if (is_a($res2, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Search failed: %s"), $res2->getMessage()), 'horde.message', null, null, $source);
+            }
+
+            if (!$res2->count()) {
+                return PEAR::raiseError(sprintf(_("Multiple persons with address [%s], but none with name [%s] already exist"), trim($address), trim($name)), 'horde.message', null, null, $source);
+            }
+
+            $res3 = $driver->search(array('email' => $address, 'name' => $name, $field => $value));
+            if (is_a($res3, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Search failed: %s"), $res3->getMessage()), 'horde.message', null, null, $source);
+            }
+
+            if ($res3->count()) {
+                return PEAR::raiseError(sprintf(_("This person already has a %s entry in the address book"), $field), 'horde.message', null, null, $source);
+            }
+
+            $ob = $res2->next();
+            $ob->setValue($field, $value);
+            $ob->store();
+        } elseif ($res->count() == 1) {
+            $res4 = $driver->search(array('email' => $address, $field => $value));
+            if (is_a($res4, 'PEAR_Error')) {
+                return PEAR::raiseError(sprintf(_("Search failed: %s"), $res4->getMessage()), 'horde.message', null, null, $source);
+            }
+
+            if ($res4->count()) {
+                return PEAR::raiseError(sprintf(_("This person already has a %s entry in the address book"), $field), 'horde.message', null, null, $source);
+            }
+
+            $ob = $res->next();
+            $ob->setValue($field, $value);
+            $ob->store();
+        } else {
+            return $driver->add(array('email' => $address, 'name' => $name, $field => $value, '__owner' => Horde_Auth::getAuth()));
+        }
+
+        return;
+    }
+
+    /**
+     * Returns a field value
+     *
+     * @param string $address    Contact email address
+     * @param string $field      Field to get
+     * @param array $sources     Sources to check
+     * @param boolean $strict    Match the email address strictly
+     * @param boolean $multiple  Return more than one entry if found and true,
+     *                           return an error if this is false.
+     *
+     * @return mixed  An array of field value(s) | PEAR_Error on failure.
+     */
+    public function getField($address = '', $field = '', $sources = array(),
+                             $strict = false, $multiple = false)
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+
+        if (empty($address)) {
+            return PEAR::raiseError(_("Invalid email"), 'horde.error');
+        }
+
+        if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
+            return array();
+        }
+
+        if (!count($sources)) {
+            $sources = array(Turba::getDefaultAddressbook());
+        }
+
+        $result = array();
+        foreach ($sources as $source) {
+            if (!isset($cfgSources[$source])) {
+                continue;
+            }
+
+            $driver = Turba_Driver::singleton($source);
+            if (is_a($driver, 'PEAR_Error')) {
+                continue;
+            }
+
+            $list = $driver->search(array('email' => $address), null, 'AND', array(), $strict ? array('email') : array());
+            if (!is_a($list, 'Turba_List')) {
+                continue;
+            }
+
+            while ($ob = $list->next()) {
+                if ($ob->hasValue($field)) {
+                    $result[] = $ob->getValue($field);
+                }
+            }
+        }
+
+        if (count($result) > 1) {
+            if ($multiple) {
+                return $result;
+            } else {
+                return PEAR::raiseError(_("More than 1 entry found"), 'horde.warning', null, null, $source);
+            }
+        } elseif (empty($result)) {
+            return PEAR::raiseError(sprintf(_("No %s entry found for %s"), $field, $address), 'horde.warning', null, null, $source);
+        }
+        return reset($result);
+    }
+
+    /**
+     * Deletes a field value
+     *
+     * @param string $address Contact email address
+     * @param string $field   Field to delete value for
+     * @param array $sources  Sources to delete value from
+     *
+     * @return boolean
+     */
+    public function deleteField($address = '', $field = '', $sources = array())
+    {
+        require_once dirname(__FILE__) . '/base.php';
+        global $cfgSources;
+
+        if (empty($address)) {
+            return PEAR::raiseError(_("Invalid email"), 'horde.error');
+        }
+
+        if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
+            return array();
+        }
+
+        if (count($sources) == 0) {
+            $sources = array(Turba::getDefaultAddressbook());
+        }
+
+        $success = false;
+
+        foreach ($sources as $source) {
+            if (isset($cfgSources[$source])) {
+                $driver = Turba_Driver::singleton($source);
+                if (is_a($driver, 'PEAR_Error')) {
+                    continue;
+                }
+                if (!$driver->hasPermission(PERMS_EDIT)) {
+                    continue;
+                }
+
+                $res = $driver->search(array('email' => $address));
+                if (is_a($res, 'Turba_List')) {
+                    if ($res->count() > 1) {
+                        continue;
+                    }
+
+                    $ob = $res->next();
+                    if (is_object($ob) && $ob->hasValue($field)) {
+                        $ob->setValue($field, '');
+                        $ob->store();
+                        $success = true;
+                    }
+                }
+            }
+        }
+
+        if (!$success) {
+            return PEAR::raiseError(sprintf(_("No %s entry found for %s"), $field, $address), 'horde.error');
+        }
+
+        return;
+    }
+
+}
diff --git a/turba/lib/api.php b/turba/lib/api.php
deleted file mode 100644 (file)
index 36d3fca..0000000
+++ /dev/null
@@ -1,2064 +0,0 @@
-<?php
-/**
- * Turba external API interface.
- *
- * This file defines Turba's external API interface. Other applications can
- * interact with Turba through this API.
- *
- * @package Turba
- */
-
-$_services['perms'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}hashHash'
-);
-
-$_services['removeUserData'] = array(
-    'args' => array('user' => 'string'),
-    'type' => 'boolean'
-);
-
-$_services['show'] = array(
-    'link' => '%application%/contact.php?source=|source|&key=|key|&uid=|uid|',
-);
-
-$_services['browse'] = array(
-    'args' => array('path' => 'string', 'properties' => '{urn:horde}stringArray'),
-    'type' => '{urn:horde}hashHash',
-);
-
-$_services['path_delete'] = array(
-    'args' => array('path' => 'string'),
-    'type' => 'boolean',
-);
-
-$_services['sources'] = array(
-    'args' => array('writeable' => 'boolean'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['fields'] = array(
-    'args' => array('source' => '{urn:horde}stringArray'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['list'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['listBy'] = array(
-    'args' => array('action' => 'string', 'timestamp' => 'int'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['getActionTimestamp'] = array(
-    'args' => array('uid' => 'string', 'action' => 'string', 'sources' => '{urn:horde}stringArray'),
-    'type' => 'int',
-);
-
-$_services['import'] = array(
-    'args' => array('content' => 'string', 'contentType' => 'string', 'source' => 'string'),
-    'type' => 'string',
-);
-
-$_services['export'] = array(
-    'args' => array('uid' => 'string', 'contentType' => 'string'),
-    'type' => 'string',
-);
-
-$_services['ownVCard'] = array(
-    'args' => array(),
-    'type' => 'string',
-);
-
-$_services['ownContact'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['delete'] = array(
-    'args' => array('uid' => 'string'),
-    'type' => 'boolean',
-);
-
-$_services['replace'] = array(
-    'args' => array('uid' => 'string', 'content' => 'string', 'contentType' => 'string'),
-    'type' => 'boolean',
-);
-
-$_services['search'] = array(
-    'args' => array('names' => '{urn:horde}stringArray',
-                    'sources' => '{urn:horde}stringArray',
-                    'fields' => '{urn:horde}stringArray',
-                    'matchBegin' => 'boolean',
-                    'forceSource' => 'boolean'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['getContact'] = array(
-    'args' => array('source' => 'string', 'objectId' => 'string'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['getContacts'] = array(
-    'args' => array('source' => 'string', 'objectIds' => '{urn:horde}stringArray'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['addField'] = array(
-    'args' => array('address' => 'string', 'name' => 'string', 'field' => 'string', 'value' => 'string', 'source' => 'string'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['deleteField'] = array(
-    'args' => array('address' => 'string', 'field' => 'string', 'sources' => '{urn:horde}stringArray'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['getField'] = array(
-    'args' => array('address' => 'string', 'field' => 'string', 'sources' => '{urn:horde}stringArray', 'strict' => 'boolean', 'multiple' => 'boolean'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['getAllAttributeValues'] = array(
-    'args' => array('field' => 'string', 'sources' => '{urn:horde}stringArray'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['listTimeObjectCategories'] = array(
-    'type' => '{urn:horde}stringArray'
-);
-
-$_services['listTimeObjects'] = array(
-    'args' => array('categories' => '{urn:horde}stringArray', 'start' => 'int', 'end' => 'int'),
-    'type' => '{urn:horde}hashHash'
-);
-
-$_services['getClientSource'] = array(
-    'checkperms' => false,
-    'args' => array(),
-    'type' => 'string',
-);
-
-$_services['clientFields'] = array(
-    'args' => array(),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['getClient'] = array(
-    'checkperms' => false,
-    'args' => array('objectId' => 'string'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['getClients'] = array(
-    'checkperms' => false,
-    'args' => array('objectIds' => '{urn:horde}stringArray'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['addClient'] = array(
-    'args' => array('attributes' => '{urn:horde}stringArray'),
-    'type' => 'string',
-);
-
-$_services['updateClient'] = array(
-    'args' => array('objectId' => 'string', 'attributes' => '{urn:horde}stringArray'),
-    'type' => 'string',
-);
-
-$_services['deleteClient'] = array(
-    'args' => array('objectId' => 'string'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['searchClients'] = array(
-    'checkperms' => false,
-    'args' => array('names' => '{urn:horde}stringArray',
-                    'fields' => '{urn:horde}stringArray',
-                    'matchBegin' => 'boolean'),
-    'type' => '{urn:horde}stringArray',
-);
-
-$_services['commentCallback'] = array(
-    'args' => array('id' => 'string'),
-    'type' => 'string'
-);
-
-$_services['hasComments'] = array(
-    'args' => array(),
-    'type' => 'boolean'
-);
-
-$_services['getDefaultShare'] = array(
-    'args' => array(),
-    'type' => 'string'
-);
-
-/**
- * Removes user data.
- *
- * @param string $user  Name of user to remove data for.
- *
- * @return mixed  true on success | PEAR_Error on failure
- */
-function _turba_removeUserData($user)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    if (!Horde_Auth::isAdmin() && $user != Horde_Auth::getAuth()) {
-        return PEAR::raiseError(_("You are not allowed to remove user data."));
-    }
-
-    /* We need a clean copy of the $cfgSources array here.*/
-    require TURBA_BASE . '/config/sources.php';
-    $hasError = false;
-
-    foreach ($cfgSources as $source) {
-        if (empty($source['use_shares'])) {
-            // Shares not enabled for this source
-            $driver = &Turba_Driver::singleton($source);
-            if (is_a($driver, 'PEAR_Error')) {
-                Horde::logMessage($driver, __FILE__, __LINE__, PEAR_LOG_ERR);
-                $hasError = true;
-            } else {
-                $result = $driver->removeUserData($user);
-                if (is_a($result, 'PEAR_Error')) {
-                    Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-                }
-            }
-        }
-    }
-
-    /* Only attempt share removal if we have shares configured */
-    if (!empty($_SESSION['turba']['has_share'])) {
-        $shares = &$GLOBALS['turba_shares']->listShares(
-            $user, PERMS_EDIT, $user);
-
-        /* Look for the deleted user's default share and remove it */
-        foreach ($shares as $share) {
-            $params = @unserialize($share->get('params'));
-            /* Only attempt to delete the user's default share */
-            if (!empty($params['default'])) {
-                $config = Turba::getSourceFromShare($share);
-                $driver = &Turba_Driver::singleton($config);
-                $result = $driver->removeUserData($user);
-                if (is_a($result, 'PEAR_Error')) {
-                    Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-                    $hasError = true;
-                }
-            }
-        }
-
-        /* Get a list of all shares this user has perms to and remove the perms */
-        $shares = $GLOBALS['turba_shares']->listShares($user);
-        if (is_a($shares, 'PEAR_Error')) {
-            Horde::logMessage($shares, __FILE__, __LINE__, PEAR_LOG_ERR);
-        }
-        foreach ($shares as $share) {
-            $share->removeUser($user);
-        }
-
-    }
-
-    if ($hasError) {
-        return PEAR::raiseError(sprintf(_("There was an error removing an address book for %s"), $user));
-    }
-
-    return true;
-}
-
-/**
- * Callback for comment API
- *
- * @param integer $id  Internal data identifier
- *
- * @return mixed  Name of object on success | false on failure
- */
-function _turba_commentCallback($id)
-{
-    if (!$GLOBALS['conf']['comments']['allow']) {
-        return false;
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-
-    @list($source, $key) = explode('.', $id, 2);
-    if (isset($cfgSources[$source]) && $key) {
-        $driver = &Turba_Driver::singleton($source);
-        if (!is_a($driver, 'PEAR_Error')) {
-            $object = $driver->getObject($key);
-            if (!is_a($object, 'PEAR_Error')) {
-                return $object->getValue('name');
-            }
-        }
-    }
-
-    return false;
-}
-
-/**
- * Returns if applications allows comments
- *
- * @return boolean
- */
-function _turba_hasComments()
-{
-    return $GLOBALS['conf']['comments']['allow'];
-}
-
-/**
- * Returns a list of available permissions.
- *
- * @return array  An array describing all available permissions.
- */
-function _turba_perms()
-{
-    static $perms = array();
-    if (!empty($perms)) {
-        return $perms;
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-    require TURBA_BASE . '/config/sources.php';
-
-    $perms['tree']['turba']['sources'] = false;
-    $perms['title']['turba:sources'] = _("Sources");
-
-    // Run through every contact source.
-    foreach ($cfgSources as $source => $curSource) {
-        $perms['tree']['turba']['sources'][$source] = false;
-        $perms['title']['turba:sources:' . $source] = $curSource['title'];
-        $perms['tree']['turba']['sources'][$source]['max_contacts'] = false;
-        $perms['title']['turba:sources:' . $source . ':max_contacts'] = _("Maximum Number of Contacts");
-        $perms['type']['turba:sources:' . $source . ':max_contacts'] = 'int';
-    }
-
-    return $perms;
-}
-
-/**
- * Returns a list of available sources.
- *
- * @param boolean $writeable  Set to true to limit to writeable sources.
- *
- * @return array  An array of the available sources.
- */
-function _turba_sources($writeable = false)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    $addressbooks = Turba::getAddressBooks($writeable ? PERMS_EDIT : PERMS_READ);
-    foreach ($addressbooks as $addressbook => $config) {
-        $addressbooks[$addressbook] = $config['title'];
-    }
-
-    return $addressbooks;
-}
-
-/**
- * Returns a list of fields avaiable in a source.
- *
- * @param string $source  The name of the source
- *
- * @return mixed  An array describing the fields | PEAR_Error
- */
-function _turba_fields($source = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources, $attributes;
-
-    if (empty($source) || !isset($cfgSources[$source])) {
-        return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
-    }
-
-    $fields = array();
-    foreach ($cfgSources[$source]['map'] as $field_name => $null) {
-        if (substr($field_name, 0, 2) != '__') {
-            $fields[$field_name] = array('name' => $field_name,
-                                         'type' => $attributes[$field_name]['type'],
-                                         'label' => $attributes[$field_name]['label'],
-                                         'search' => in_array($field_name, $cfgSources[$source]['search']));
-        }
-    }
-
-    return $fields;
-}
-
-/**
- * Retrieve the UID for the current user's default Turba share.
- *
- */
-function _turba_getDefaultShare()
-{
-    global $prefs;
-
-    // Bring in turba's base and a clean copy of sources.
-    require_once dirname(__FILE__) . '/base.php';
-    require TURBA_BASE . '/config/sources.php';
-
-    if (!empty($_SESSION['turba']['has_share'])) {
-        $shares = Turba::listShares(true);
-        if (is_a($shares, 'PEAR_Error')) {
-            return false;
-        }
-        foreach ($shares as $uid => $share) {
-            $params = @unserialize($share->get('params'));
-            if (empty($params['source'])) {
-                continue;
-            }
-            $driver = &Turba_Driver::factory($params['source'], $cfgSources[$params['source']]);
-            if (is_a($driver, 'PEAR_Error')) {
-                continue;
-            }
-            if ($driver->checkDefaultShare($share, $cfgSources[$params['source']])) {
-                return $uid;
-            }
-        }
-    }
-
-    // Return Turba's default_dir as default
-    return $prefs->getValue('default_dir');
-}
-
-/**
- * Browses through Turba's object tree.
- *
- * @param string $path       The path of the tree to browse.
- * @param array $properties  The item properties to return. Defaults to 'name',
- *                           'icon', and 'browseable'.
- *
- * @return array  Content of the specified path.
- */
-function _turba_browse($path = '', $properties = array())
-{
-    function _modified($uid)
-    {
-        $modified = _turba_getActionTimestamp($uid, 'modify');
-        if (empty($modified)) {
-            $modified = _turba_getActionTimestamp($uid, 'add');
-        }
-        return $modified;
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-    global $registry, $cfgSources;
-
-    // Default properties.
-    if (!$properties) {
-        $properties = array('name', 'icon', 'browseable');
-    }
-
-    // Strip off the application name if present
-    if (substr($path, 0, 5) == 'turba') {
-        $path = substr($path, 5);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    $now = time();
-    $results = array();
-    if (empty($path)) {
-        // We always provide the "global" folder which contains address book
-        // sources that are shared among all users.  Per-user shares are shown
-        // in a folder for each respective user.
-        $results = array();
-        $shares = Turba::listShares();
-        $owners = array('global' => true);
-        foreach ($shares as $share) {
-            $owners[$share->get('owner')] = true;
-        }
-
-        foreach (array_keys($owners) as $owner) {
-            if (in_array('name', $properties)) {
-                $results['turba/' . $owner]['name'] = $owner;
-            }
-            if (in_array('icon', $properties)) {
-                $results['turba/' . $owner]['icon'] = $registry->getImageDir() . '/turba.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results['turba/' . $owner]['browseable'] = true;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results['turba/' . $owner]['contenttype'] = 'httpd/unix-directory';
-            }
-            if (in_array('contentlength', $properties)) {
-                $results['turba/' . $owner]['contentlength'] = 0;
-            }
-            if (in_array('modified', $properties)) {
-                // @TODO: Get a real modification date
-                $results['turba/' . $owner]['modified'] = $now;
-            }
-            if (in_array('created', $properties)) {
-                // @TODO Get a real creation date
-                $results['turba/' . $owner]['created'] = 0;
-            }
-        }
-        return $results;
-    } elseif (count($parts) == 1) {
-        //
-        // We should either have the username that is a valid share owner or
-        // 'global'
-        //
-        if (empty($parts[0])) {
-            // We need either 'global' or a valid username with shares
-            return array();
-        }
-
-        if ($parts[0] == 'global') {
-            // The client is requesting a list of global address books.
-            $addressbooks = Turba::getAddressBooks();
-            foreach ($addressbooks as $addressbook => $info) {
-                if ($info['type'] == 'share') {
-                    // Ignore address book shares in the 'global' folder
-                    unset($addressbooks[$addressbook]);
-                }
-            }
-        } else {
-            // Assume $parts[0] is a valid username and we need to list their
-            // shared addressbooks.
-            if (empty($_SESSION['turba']['has_share'])) {
-                // No backends are configured to provide shares
-                return array();
-            }
-            $addressbooks = $GLOBALS['turba_shares']->listShares($parts[0],
-                                                                 PERMS_READ,
-                                                                 $parts[0]);
-            // The last check returns all addressbooks for the requested user,
-            // but that does not mean the requesting user has access to them.
-            // Filter out those address books for which the requesting user has
-            // no access.
-            $addressbooks = Turba::permissionsFilter($addressbooks);
-        }
-
-        $curpath = 'turba/' . $parts[0] . '/';
-        foreach ($addressbooks as $addressbook => $info) {
-            if (in_array('name', $properties)) {
-                if (is_a($info, 'Horde_Share_Object')) {
-                    $name = $info->get('title');
-                } else {
-                    $name = $info['title'];
-                 }
-                $results[$curpath . $addressbook]['name'] = $name;
-            }
-            if (in_array('icon', $properties)) {
-                $results[$curpath . $addressbook]['icon'] = $registry->getImageDir() . '/turba.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results[$curpath . $addressbook]['browseable'] = true;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results[$curpath . $addressbook]['contenttype'] = 'httpd/unix-directory';
-            }
-            if (in_array('contentlength', $properties)) {
-                $results[$curpath . $addressbook]['contentlength'] = 0;
-            }
-            if (in_array('modified', $properties)) {
-                // @TODO: Get a real modification date
-                $results[$curpath . $addressbook]['modified'] = $now;
-            }
-            if (in_array('created', $properties)) {
-                // @TODO Get a real creation date
-                $results[$curpath . $addressbook]['created'] = 0;
-            }
-        }
-        return $results;
-
-    } elseif (count($parts) == 2) {
-        //
-        // The client is requesting all contacts from a given addressbook
-        //
-        if (empty($parts[0]) || empty($parts[1])) {
-            // $parts[0] must be either 'global' or a valid user with shares
-            // $parts[1] must be an address book ID
-            return array();
-        }
-
-        $addressbooks = Turba::getAddressBooks();
-        if (!isset($addressbooks[$parts[1]])) {
-            // We must have a valid addressbook to continue.
-            return array();
-        }
-
-        // Load the Turba driver.
-        $driver = &Turba_Driver::singleton($parts[1]);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $cfgSources[$parts[1]]);
-        }
-
-        $contacts = $driver->search(array());
-        if (is_a($contacts, 'PEAR_Error')) {
-            return $contacts;
-        }
-
-        $contacts->reset();
-        $curpath = 'turba/' . $parts[0] . '/' . $parts[1] . '/';
-        while ($contact = $contacts->next()) {
-            $key = $curpath . $contact->getValue('__key');
-            if (in_array('name', $properties)) {
-                $results[$key]['name'] = Turba::formatName($contact);
-            }
-            if (in_array('icon', $properties)) {
-                $results[$key]['icon'] = $registry->getImageDir('horde') . '/mime/vcard.png';
-            }
-            if (in_array('browseable', $properties)) {
-                $results[$key]['browseable'] = false;
-            }
-            if (in_array('contenttype', $properties)) {
-                $results[$key]['contenttype'] = 'text/x-vcard';
-            }
-            if (in_array('contentlength', $properties)) {
-                $data = _turba_export($contact->getValue('__uid'), 'text/x-vcard', $contact->getSource());
-                if (is_a($data, 'PEAR_Error')) {
-                    $data = '';
-                }
-                $results[$key]['contentlength'] = strlen($data);
-            }
-            if (in_array('modified', $properties)) {
-                $results[$key]['modified'] = _modified($contact->getValue('__uid'));
-            }
-            if (in_array('created', $properties)) {
-                $results[$key]['created'] = _turba_getActionTimestamp($contact->getValue('__uid'), 'add');
-            }
-        }
-
-        return $results;
-
-    } elseif (count($parts) == 3) {
-        //
-        // The client is requesting an individual contact
-        //
-        $addressbooks = Turba::getAddressBooks();
-        if (!isset($addressbooks[$parts[1]])) {
-            // We must have a valid addressbook to continue.
-            return array();
-        }
-
-        // Load the Turba driver.
-        $driver = &Turba_Driver::singleton($parts[1]);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $cfgSources[$parts[1]]);
-        }
-
-        $contact = &$driver->getObject($parts[2]);
-        if (is_a($contact, 'PEAR_Error')) {
-            return $contact;
-        }
-
-        $result = array('data' => _turba_export($contact->getValue('__uid'), 'text/x-vcard', $contact->getSource()),
-                        'mimetype' => 'text/x-vcard');
-        $modified = _modified($contact->getValue('__uid'));
-        if (!empty($modified)) {
-            $result['mtime'] = $modified;
-        }
-        return $result;
-    } else {
-        return PEAR::raiseError(_("Malformed request."));
-    }
-}
-
-/**
- * Deletes a file from the Turba tree.
- *
- * @param string $path  The path to the file.
- *
- * @return mixed  The event's UID, or a PEAR_Error on failure.
- */
-function _turba_path_delete($path)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $registry, $cfgSources;
-
-    // Strip off the application name if present
-    if (substr($path, 0, 5) == 'turba') {
-        $path = substr($path, 5);
-    }
-    $path = trim($path, '/');
-    $parts = explode('/', $path);
-
-    $now = time();
-    $results = array();
-
-    if (count($parts) < 3) {
-        // Deletes must be on individual contacts
-        return PEAR::raiseError(_("Delete denied."), 403);
-    }
-    if (!array_key_exists($parts[1], Turba::getAddressBooks())) {
-        return PEAR::raiseError("Address book does not exist", 404);
-    }
-
-    // Load the Turba driver.
-    $driver = &Turba_Driver::singleton($parts[1]);
-    if (is_a($driver, 'PEAR_Error')) {
-        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 500, null, null, $cfgSources[$parts]);
-    }
-
-    $ret = $driver->delete($parts[2]);
-    if (is_a($ret, 'PEAR_Error')) {
-        // A deeper error occurred.  Make sure the code is a valid HTTP response
-        $ret->code = 500;
-        return $ret;
-    }
-}
-
-/**
- * Returns an array of UIDs for all contacts that the current user is
- * authorized to see.
- *
- * @param string|array $sources  The name(s) of the source(s) to return
- *                               contacts of. If left empty, the current user's
- *                               sync sources or default source are used.
- *
- * @return array  An array of UIDs for all contacts the user can access.
- */
-function _turba_list($sources = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-
-    global $cfgSources, $prefs;
-
-    /* Get default address book from user preferences. */
-    if (empty($sources)) {
-        $sources = @unserialize($prefs->getValue('sync_books'));
-    } elseif (!is_array($sources)) {
-        $sources = array($sources);
-    }
-    if (empty($sources)) {
-        $sources = array(Turba::getDefaultAddressbook());
-    }
-    if (empty($sources)) {
-        return PEAR::raiseError(_("No address book specified"), 'horde.error');
-    }
-
-    $uids = array();
-    foreach ($sources as $source) {
-        if (empty($source) || !isset($cfgSources[$source])) {
-            return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
-        }
-
-        $storage = &Turba_Driver::singleton($source);
-        if (is_a($storage, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $storage->getMessage()), 'horde.error', null, null, $source);
-        }
-
-        $results = $storage->search(array());
-
-        if (is_a($results, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Error searching the address book: %s"), $results->getMessage()), 'horde.error', null, null, $source);
-        }
-
-        foreach ($results->objects as $o) {
-            $uids[] = $o->getValue('__uid');
-        }
-    }
-
-    return $uids;
-}
-
-/**
- * Returns an array of UIDs for contacts that have had $action happen since
- * $timestamp.
- *
- * @param string  $action        The action to check for - add, modify, or
- *                               delete.
- * @param integer $timestamp     The time to start the search.
- * @param string|array $sources  The source(s) for which to retrieve the
- *                               history.
- *
- * @return array  An array of UIDs matching the action and time criteria.
- */
-function _turba_listBy($action, $timestamp, $sources = null)
-{
-    global $prefs, $cfgSources;
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* Get default address book from user preferences. */
-    if (empty($sources)) {
-        $sources = @unserialize($prefs->getValue('sync_books'));
-    } elseif (!is_array($sources)) {
-        $sources = array($sources);
-    }
-    if (empty($sources)) {
-        $sources = array(Turba::getDefaultAddressbook());
-    }
-    if (empty($sources)) {
-        return PEAR::raiseError(_("No address book specified"), 'horde.error');
-    }
-
-    $uids = array();
-    foreach ($sources as $source) {
-        if (empty($source) || !isset($cfgSources[$source])) {
-            return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
-        }
-
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
-        }
-
-        $history = &Horde_History::singleton();
-        $histories = $history->getByTimestamp('>', $timestamp,
-                                              array(array('op' => '=',
-                                                          'field' => 'action',
-                                                          'value' => $action)),
-                                              'turba:' . $driver->getName());
-        if (is_a($histories, 'PEAR_Error')) {
-            return $histories;
-        }
-
-        // Strip leading turba:addressbook:.
-        $uids = array_merge($uids,
-                            str_replace('turba:' . $driver->getName() . ':',
-                                        '',
-                                        array_keys($histories)));
-    }
-
-    return $uids;
-}
-
-/**
- * Returns the timestamp of an operation for a given uid an action.
- *
- * @param string $uid            The uid to look for.
- * @param string $action         The action to check for - add, modify, or
- *                               delete.
- * @param string|array $sources  The source(s) for which to retrieve the
- *                               history.
- *
- * @return integer  The timestamp for this action.
- */
-function _turba_getActionTimestamp($uid, $action, $sources = null)
-{
-    global $prefs, $cfgSources;
-    require_once dirname(__FILE__) . '/base.php';
-
-    /* Get default address book from user preferences. */
-    if (empty($sources)) {
-        $sources = @unserialize($prefs->getValue('sync_books'));
-    } elseif (!is_array($sources)) {
-        $sources = array($sources);
-    }
-    if (empty($sources)) {
-        $sources = array(Turba::getDefaultAddressbook());
-    }
-    if (empty($sources)) {
-        return PEAR::raiseError(_("No address book specified"), 'horde.error');
-    }
-
-    foreach ($sources as $source) {
-        if (empty($source) || !isset($cfgSources[$source])) {
-            return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
-        }
-
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
-        }
-
-        $history = &Horde_History::singleton();
-        $ts = $history->getActionTimestamp('turba:' . $driver->getName()
-                                           . ':' . $uid,
-                                           $action);
-        if (!empty($ts)) {
-            return $ts;
-        }
-    }
-
-    return 0;
-}
-
-/**
- * Import a contact represented in the specified contentType.
- *
- * @param string $content      The content of the contact.
- * @param string $contentType  What format is the data in? Currently supports
- *                             array, text/directory, text/vcard and
- *                             text/x-vcard.
- * @param string $source       The source into which the contact will be
- *                             imported.
- *
- * @return string  The new UID, or false on failure.
- */
-function _turba_import($content, $contentType = 'array', $import_source = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources, $prefs;
-
-    /* Get default address book from user preferences. */
-    if (empty($import_source)) {
-        $import_source = $prefs->getValue('default_dir');
-        /* On new installations default_dir is not set, use first source
-         * instead. */
-        if (empty($import_source)) {
-            $import_source = key(Turba::getAddressBooks(PERMS_EDIT));
-        }
-    }
-
-    // Check existance of and permissions on the specified source.
-    if (!isset($cfgSources[$import_source])) {
-        return PEAR::raiseError(sprintf(_("Invalid address book: %s"),
-                                        $import_source),
-                                'horde.warning');
-    }
-
-    $driver = &Turba_Driver::singleton($import_source);
-    if (is_a($driver, 'PEAR_Error')) {
-        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $import_source);
-    }
-
-    if (!$driver->hasPermission(PERMS_EDIT)) {
-        return PEAR::raiseError(_("Permission denied"), 'horde.error', null, null, $import_source);
-    }
-
-    /* Create a category manager. */
-    require_once 'Horde/Prefs/CategoryManager.php';
-    $cManager = new Prefs_CategoryManager();
-    $categories = $cManager->get();
-
-    if (!is_a($content, 'Horde_iCalendar_vcard')) {
-        switch ($contentType) {
-        case 'array':
-            break;
-
-        case 'text/x-vcard':
-        case 'text/vcard':
-        case 'text/directory':
-            require_once 'Horde/iCalendar.php';
-            $iCal = new Horde_iCalendar();
-            if (!$iCal->parsevCalendar($content)) {
-                return PEAR::raiseError(_("There was an error importing the iCalendar data."));
-            }
-            switch ($iCal->getComponentCount()) {
-            case 0:
-                return PEAR::raiseError(_("No vCard data was found."));
-
-            case 1:
-                $content = $iCal->getComponent(0);
-                break;
-
-            default:
-                $ids = array();
-                foreach ($iCal->getComponents() as $c) {
-                    if (is_a($c, 'Horde_iCalendar_vcard')) {
-                        $content = $driver->toHash($c);
-                        $result = $driver->search($content);
-                        if (is_a($result, 'PEAR_Error')) {
-                            return $result;
-                        } elseif ($result->count() > 0) {
-                            continue;
-                        }
-                        $result = $driver->add($content);
-                        if (is_a($result, 'PEAR_Error')) {
-                            return $result;
-                        }
-                        if (!empty($content['category']) &&
-                            !in_array($content['category'], $categories)) {
-                            $cManager->add($content['category']);
-                            $categories[] = $content['category'];
-                        }
-                        $ids[] = $result;
-                    }
-                }
-                return $ids;
-            }
-            break;
-
-        default:
-            return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-        }
-    }
-
-    if (is_a($content, 'Horde_iCalendar_vcard')) {
-        $content = $driver->toHash($content);
-    }
-
-    // Check if the entry already exists in the data source:
-    $result = $driver->search($content);
-    if (is_a($result, 'PEAR_Error')) {
-        return $result;
-    } elseif ($result->count() > 0) {
-        $o = $result->objects[0];
-        return PEAR::raiseError(_("Already Exists"), 'horde.message', null, null, $o->getValue('__uid'));
-    }
-
-    $result = $driver->add($content);
-    if (is_a($result, 'PEAR_Error')) {
-        return $result;
-    }
-
-    if (!empty($content['category']) &&
-        !in_array($content['category'], $categories)) {
-        $cManager->add($content['category']);
-    }
-
-    $object = &$driver->getObject($result);
-    return is_a($object, 'PEAR_Error') ? $object : $object->getValue('__uid');
-}
-
-/**
- * Export a contact, identified by UID, in the requested contentType.
- *
- * @param string $uid            Identify the contact to export.
- * @param mixed $contentType     What format should the data be in?
- *                               Either a string with one of:
- *                               - text/directory
- *                               - text/vcard
- *                               - text/x-vcard
- *                               The first two produce a vcard3.0 (rfc2426),
- *                               the second produces a vcard in old 2.1 format
- *                               defined by imc.org
- * @param string|array $sources  The source(s) from which the contact will be
- *                               exported.
- *
- * @return mixed  The requested data | PEAR_Error
- */
-function _turba_export($uid, $contentType, $sources = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources, $prefs;
-
-    /* Get default address book from user preferences. */
-    if (empty($sources)) {
-        $sources = @unserialize($prefs->getValue('sync_books'));
-    } elseif (!is_array($sources)) {
-        $sources = array($sources);
-    }
-    if (empty($sources)) {
-        $sources = array(Turba::getDefaultAddressbook());
-    }
-    if (empty($sources)) {
-        return PEAR::raiseError(_("No address book specified"), 'horde.error');
-    }
-
-    foreach ($sources as $source) {
-        if (empty($source) || !isset($cfgSources[$source])) {
-            return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
-        }
-
-        if (empty($uid)) {
-            return PEAR::raiseError(_("Invalid ID"), 'horde.error', null, null, $source);
-        }
-
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
-        }
-
-        if (!$driver->hasPermission(PERMS_READ)) {
-            continue;
-        }
-
-        $result = $driver->search(array('__uid' => $uid));
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        } elseif ($result->count() == 0) {
-            continue;
-        } elseif ($result->count() > 1) {
-            return PEAR::raiseError("Internal Horde Error: multiple turba objects with same objectId.", 'horde.error', null, null, $source);
-        }
-
-        $version = '3.0';
-        list($contentType,) = explode(';', $contentType);
-        switch ($contentType) {
-        case 'text/x-vcard':
-            $version = '2.1';
-        case 'text/vcard':
-        case 'text/directory':
-            require_once 'Horde/iCalendar.php';
-            $export = '';
-            foreach ($result->objects as $obj) {
-                $vcard = $driver->tovCard($obj, $version);
-                /* vCards are not enclosed in BEGIN:VCALENDAR..END:VCALENDAR.
-                 * Export the individual cards instead. */
-                $export .= $vcard->exportvCalendar();
-            }
-            return $export;
-        }
-
-        return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-    }
-
-    return PEAR::raiseError(_("Object not found"));
-}
-
-/**
- * Exports the user's own contact as a vCard string.
- *
- * @return string  The requested vCard data or PEAR_Error.
- */
-function _turba_ownVCard()
-{
-    $contact = _turba_getOwnContactObject();
-    if (is_a($contact, 'PEAR_Error')) {
-        return $contact;
-    }
-    $driver = &Turba_Driver::singleton($contact['source']);
-    if (is_a($driver, 'PEAR_Error')) {
-        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()));
-    }
-    require_once 'Horde/iCalendar.php';
-    $vcard = $driver->tovCard($contact['contact'], '3.0');
-    $vcard->setAttribute('VERSION', '3.0');
-
-    return $vcard->exportvCalendar();
-}
-
-/**
- * Export the user's own contact as a hash
- *
- * @return Array  The contact hash or PEAR_Error
- */
-function _turba_ownContact()
-{
-    $contact = _turba_getOwnContactObject();
-    if (is_a($contact, 'PEAR_Error')) {
-        return $contact;
-    }
-
-    return $contact['contact']->getAttributes();
-}
-
-/**
- * Helper function to  return the user's own contact object
- *
- * @return Array  A hash containing the Turba_Object representing the user's
- *                own contact and the source that it is from or PEAR_Error.
- */
-function _turba_getOwnContactObject()
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-
-    $own_contact = $GLOBALS['prefs']->getValue('own_contact');
-    if (empty($own_contact)) {
-        return PEAR::raiseError(_("You didn't mark a contact as your own yet."));
-    }
-    @list($source, $id) = explode(';', $own_contact);
-
-    if (!isset($cfgSources[$source])) {
-        return PEAR::raiseError(_("The address book with your own contact doesn't exist anymore."));
-    }
-
-    $driver = &Turba_Driver::singleton($source);
-    if (is_a($driver, 'PEAR_Error')) {
-        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()));
-    }
-
-    if (!$driver->hasPermission(PERMS_READ)) {
-        return PEAR::raiseError(_("You don't have sufficient permissions to read the address book that contains your own contact."));
-    }
-
-    $contact = $driver->getObject($id);
-    if (is_a($contact, 'PEAR_Error')) {
-        return PEAR::raiseError(_("Your own contact cannot be found in the address book."));
-    }
-
-    $return = array('contact' => $contact,
-                    'source'=> $source);
-
-    return $return;
-}
-
-
-/**
- * Deletes a contact identified by UID.
- *
- * @param string|array $uid      Identify the contact to delete, either a
- *                               single UID or an array.
- * @param string|array $sources  The source(s) from which the contact will be
- *                               deleted.
- *
- * @return boolean  Success or failure.
- */
-function _turba_delete($uid, $sources = null)
-{
-    // Handle an array of UIDs for convenience of deleting multiple contacts
-    // at once.
-    if (is_array($uid)) {
-        foreach ($uid as $g) {
-            $result = _turba_delete($uid, $source);
-            if (is_a($result, 'PEAR_Error')) {
-                return $result;
-            }
-        }
-
-        return true;
-    }
-
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources, $prefs;
-
-    /* Get default address book from user preferences. */
-    if (empty($sources)) {
-        $sources = @unserialize($prefs->getValue('sync_books'));
-    } elseif (!is_array($sources)) {
-        $sources = array($sources);
-    }
-    if (empty($sources)) {
-        $sources = array(Turba::getDefaultAddressbook());
-    }
-    if (empty($sources)) {
-        return PEAR::raiseError(_("No address book specified"), 'horde.error');
-    }
-
-    foreach ($sources as $source) {
-        if (empty($source) || !isset($cfgSources[$source])) {
-            return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
-        }
-
-        if (empty($uid)) {
-            return PEAR::raiseError(_("Invalid ID"), 'horde.error', null, null, $source);
-        }
-
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
-        }
-
-        if (!Horde_Auth::isAdmin() && !$driver->hasPermission(PERMS_DELETE)) {
-            continue;
-        }
-
-        // If the objectId isn't in $source in the first place, just return
-        // true. Otherwise, try to delete it and return success or failure.
-        $result = $driver->search(array('__uid' => $uid));
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        } elseif ($result->count() == 0) {
-            continue;
-        } else {
-            $r = $result->objects[0];
-            return $driver->delete($r->getValue('__key'));
-        }
-    }
-
-    return true;
-}
-
-/**
- * Replaces the contact identified by UID with the content represented in the
- * specified contentType.
- *
- * @param string $uid            Idenfity the contact to replace.
- * @param string $content        The content of the contact.
- * @param string $contentType    What format is the data in? Currently supports
- *                               array, text/directory, text/vcard and
- *                               text/x-vcard.
- * @param string|array $sources  The source(s) where the contact will be
- *                               replaced.
- *
- * @return boolean  Success or failure.
- */
-function _turba_replace($uid, $content, $contentType, $sources = null)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources, $prefs;
-
-    /* Get default address book from user preferences. */
-    if (empty($sources)) {
-        $sources = @unserialize($prefs->getValue('sync_books'));
-    } elseif (!is_array($sources)) {
-        $sources = array($sources);
-    }
-    if (empty($sources)) {
-        $sources = array(Turba::getDefaultAddressbook());
-    }
-    if (empty($sources)) {
-        return PEAR::raiseError(_("No address book specified"), 'horde.error');
-    }
-
-    foreach ($sources as $source) {
-        if (empty($source) || !isset($cfgSources[$source])) {
-            return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
-        }
-
-        if (empty($uid)) {
-            return PEAR::raiseError(_("Invalid contact unique ID"), 'horde.error', null, null, $source);
-        }
-
-        // Check permissions.
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
-        }
-        if (!$driver->hasPermission(PERMS_EDIT)) {
-            continue;
-        }
-        $result = $driver->search(array('__uid' => $uid));
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        } elseif (!$result->count()) {
-            continue;
-        } elseif ($result->count() > 1) {
-            return PEAR::raiseError("Multiple contacts found with same unique ID.", 'horde.error', null, null, $source);
-        }
-
-        $object = $result->objects[0];
-
-        switch ($contentType) {
-        case 'array':
-            break;
-
-        case 'text/x-vcard':
-        case 'text/vcard':
-        case 'text/directory':
-            require_once 'Horde/iCalendar.php';
-            $iCal = new Horde_iCalendar();
-            if (!$iCal->parsevCalendar($content)) {
-                return PEAR::raiseError(_("There was an error importing the iCalendar data."));
-            }
-
-            switch ($iCal->getComponentCount()) {
-            case 0:
-                return PEAR::raiseError(_("No vCard data was found."));
-
-            case 1:
-                $content = $iCal->getComponent(0);
-                $content = $driver->toHash($content);
-                break;
-
-            default:
-                return PEAR::raiseError(_("Only one vcard supported."));
-            }
-            break;
-
-        default:
-            return PEAR::raiseError(sprintf(_("Unsupported Content-Type: %s"), $contentType));
-        }
-
-        foreach ($content as $attribute => $value) {
-            if ($attribute != '__key') {
-                $object->setValue($attribute, $value);
-            }
-        }
-
-        return $object->store();
-    }
-
-    return PEAR::raiseError(_("Object not found"));
-}
-
-/**
- * Returns a contact search result.
- *
- * @param array $names          The search filter values
- * @param array $sources        The sources to serach in
- * @param array $fields         The fields to serach on
- * @param boolean $matchBegin   Match word boundaries only
- * @param boolean $forceSource  Whether to use the specified sources, even if
- *                              they have been disabled in the preferences.
- *
- * @return array  Hash containing the search results.
- */
-function _turba_search($names = array(), $sources = array(), $fields = array(),
-                       $matchBegin = false, $forceSource = false)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    require_once 'Horde/Mime/Address.php';
-    global $cfgSources, $attributes, $prefs;
-
-    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
-        return array();
-    }
-
-    if (!is_array($names)) {
-        $names = is_null($names) ? array() : array($names);
-    }
-
-    if (!$forceSource) {
-        // Make sure the selected source is activated in Turba.
-        $addressbooks = array_keys(Turba::getAddressBooks());
-        foreach (array_keys($sources) as $id) {
-            if (!in_array($sources[$id], $addressbooks)) {
-                unset($sources[$id]);
-            }
-        }
-    }
-
-    // ...and ensure the default source is used as a default.
-    if (!count($sources)) {
-        $sources = array(Turba::getDefaultAddressBook());
-    }
-
-    // Read the columns to display from the preferences.
-    $sort_columns = Turba::getColumns();
-
-    $results = array();
-    $seen = array();
-    foreach ($sources as $source) {
-        // Skip invalid sources.
-        if (!isset($cfgSources[$source])) {
-            continue;
-        }
-
-        // Skip sources that aren't browseable if the search is empty.
-        if (empty($cfgSources[$source]['browse']) &&
-            (!count($names) || (count($names) == 1 && empty($names[0])))) {
-            continue;
-        }
-
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
-        }
-
-        // Determine the name of the column to sort by.
-        $columns = isset($sort_columns[$source])
-            ? $sort_columns[$source] : array();
-
-        foreach ($names as $name) {
-            $criteria = array();
-            if (isset($fields[$source])) {
-                foreach ($fields[$source] as $field) {
-                    $criteria[$field] = trim($name);
-                }
-            }
-            if (count($criteria) == 0) {
-                $criteria['name'] = trim($name);
-            }
-
-            $search = $driver->search($criteria, Turba::getPreferredSortOrder(), 'OR', array(), array(), $matchBegin);
-            if (!is_a($search, 'Turba_List')) {
-                continue;
-            }
-
-            while ($ob = $search->next()) {
-                if (!$ob->isGroup()) {
-                    /* Not a group. */
-                    $att = array('__key' => $ob->getValue('__key'));
-                    foreach ($ob->driver->getCriteria() as $info_key => $info_val) {
-                        $att[$info_key] = $ob->getValue($info_key);
-                    }
-                    $email = array();
-                    foreach (array_keys($att) as $key) {
-                        if (!$ob->getValue($key) ||
-                            !isset($attributes[$key]) ||
-                            $attributes[$key]['type'] != 'email') {
-                            continue;
-                        }
-                        $email_val = $ob->getValue($key);
-
-                        // Multiple addresses support
-                        if (isset($attributes[$key]['params'])
-                            && is_array($attributes[$key]['params'])
-                            && !empty($attributes[$key]['params']['allow_multi'])) {
-                            $addrs = Horde_Mime_Address::explode($email_val);
-                        } else {
-                            $addrs = array($email_val);
-                        }
-
-                        foreach ($addrs as $addr) {
-                            $email[] = trim($addr);
-                        }
-                    }
-
-                    if ($ob->hasValue('name') ||
-                        !isset($ob->driver->alternativeName)) {
-                        $display_name = Turba::formatName($ob);
-                    } else {
-                        $display_name = $ob->getValue($ob->driver->alternativeName);
-                    }
-                    if (count($email)) {
-                        for ($i = 0; $i < count($email); $i++) {
-                            $seen_key = trim(Horde_String::lower($display_name)) . '/' . trim(Horde_String::lower($email[$i]));
-                            if (!empty($seen[$seen_key])) {
-                                continue;
-                            }
-                            $seen[$seen_key] = true;
-                            if (!isset($results[$name])) {
-                                $results[$name] = array();
-                            }
-                            $results[$name][] = array_merge($att,
-                                array('id' => $att['__key'],
-                                      'name' => $display_name,
-                                      'email' => $email[$i],
-                                      '__type' => 'Object',
-                                      'source' => $source));
-                        }
-                    } else {
-                        if (!isset($results[$name])) {
-                            $results[$name] = array();
-                        }
-                        $results[$name][] = array_merge($att,
-                            array('id' => $att['__key'],
-                                  'name' => $display_name,
-                                  'email' => null,
-                                  '__type' => 'Object',
-                                  'source' => $source));
-                    }
-                } else {
-                    /* Is a distribution list. */
-                    $listatt = $ob->getAttributes();
-                    $seeninlist = array();
-                    $members = $ob->listMembers();
-                    $listName = $ob->getValue('name');
-                    if (!is_a($members, 'Turba_List')) {
-                        continue;
-                    }
-                    if ($members->count() > 0) {
-                        if (!isset($results[$name])) {
-                            $results[$name] = array();
-                        }
-                        $emails = array();
-                        while ($ob = $members->next()) {
-                            $att = $ob->getAttributes();
-                            foreach (array_keys($att) as $key) {
-                                $value = $ob->getValue($key);
-                                if (empty($value)) {
-                                    continue;
-                                }
-                                if (!is_array($value)) {
-                                    $seen_key = trim(Horde_String::lower($ob->getValue('name')))
-                                        . trim(Horde_String::lower($value));
-                                } else {
-                                    $seen_key = trim(Horde_String::lower($ob->getValue('name')))
-                                        . trim(Horde_String::lower($value['load']['file']));
-                                }
-                                if (isset($attributes[$key]) &&
-                                    $attributes[$key]['type'] == 'email' &&
-                                    empty($seeninlist[$seen_key])) {
-                                    $emails[] = $value;
-                                    $seeninlist[$seen_key] = true;
-                                }
-                            }
-                        }
-                        $results[$name][] = array('name' => $listName,
-                                                  'email' => implode(', ', $emails),
-                                                  'id' => $listatt['__key'],
-                                                  'source' => $source);
-                    }
-                }
-            }
-        }
-    }
-
-    return $results;
-}
-
-/**
- * Retrieves a contact.
- *
- * @param string $source    The source name where the contact is stored
- * @param string $objectId  The unique id of the contact to retrieve
- *
- * @return array  The retrieved contact.
- */
-function _turba_getContact($source = null, $objectId = '')
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-
-    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
-        return array();
-    }
-
-    if (isset($cfgSources[$source])) {
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            return $driver;
-        }
-
-        $object = $driver->getObject($objectId);
-        if (is_a($object, 'PEAR_Error')) {
-            return $object;
-        }
-
-        $attributes = array();
-        foreach ($cfgSources[$source]['map'] as $field => $map) {
-            $attributes[$field] = $object->getValue($field);
-        }
-        return $attributes;
-    }
-
-    return array();
-}
-
-/**
- * Retrieves a set of contacts from a single source.
- *
- * @param string $source    The source name where the contact is stored
- * @param array $objectIds  The unique ids of the contact to retrieve.
- *
- * @return mixed  The retrieved contact | PEAR_Error
- */
-function _turba_getContacts($source = '', $objectIds = array())
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-    $results = array();
-    if (!is_array($objectIds)) {
-        $objectIds = array($objectIds);
-    }
-
-    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
-        return array();
-    }
-
-    if (isset($cfgSources[$source])) {
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            return $driver;
-        }
-
-        $objects = $driver->getObjects($objectIds);
-        if (is_a($objects, 'PEAR_Error')) {
-            return $objects;
-        }
-
-        foreach ($objects as $object) {
-            $attributes = array();
-            foreach ($cfgSources[$source]['map'] as $field => $map) {
-                $attributes[$field] = $object->getValue($field);
-            }
-            $results[] = $attributes;
-        }
-    }
-
-    return $results;
-}
-
-/**
- * Retrieves a list of all possible values of a field in specified source(s).
- *
- * @param string $field   Field name to check
- * @param array $sources  Array containing the sources to look in
- *
- * @return mixed  An array of fields and possible values | PEAR_Error
- */
-function _turba_getAllAttributeValues($field = '', $sources = array())
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-
-    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
-        return array();
-    }
-
-    if (!count($sources)) {
-        $sources = array(Turba::getDefaultAddressBook());
-    }
-
-    $results = array();
-    foreach ($sources as $source) {
-        if (isset($cfgSources[$source])) {
-            $driver = &Turba_Driver::singleton($source);
-            if (is_a($driver, 'PEAR_Error')) {
-                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
-            }
-
-            $res = $driver->search(array());
-            if (!is_a($res, 'Turba_List')) {
-                return PEAR::raiseError(_("Search failed"), 'horde.error', null, null, $source);
-            }
-
-            while ($ob = $res->next()) {
-                if ($ob->hasValue($field)) {
-                    $results[$source . ':' . $ob->getValue('__key')] = array(
-                        'name' => $ob->getValue('name'),
-                        'email' => $ob->getValue('email'),
-                        $field => $ob->getValue($field));
-                }
-            }
-        }
-    }
-
-    return $results;
-}
-
-/**
- * Retrieves a list of available time objects categories
- *
- * @return array  An array of all configured time object categories.
- */
-function _turba_listTimeObjectCategories()
-{
-    include dirname(__FILE__) . '/../config/attributes.php';
-    include dirname(__FILE__) . '/../config/sources.php';
-    $categories = array();
-    foreach ($attributes as $key => $attribute) {
-        if ($attribute['type'] == 'monthdayyear' &&
-            !empty($attribute['time_object_label'])) {
-
-            foreach ($cfgSources as $source) {
-                if (!empty($source['map'][$key])) {
-                    $categories[$key] = $attribute['time_object_label'];
-                    break;
-                }
-            }
-        }
-    }
-
-
-
-    return $categories;
-}
-
-/**
- * Lists birthdays and/or anniversaries as time objects.
- *
- * @param array $time_categories  The time categories (from
- *                                listTimeObjectCategories) to list.
- * @param mixed $start            The start date of the period.
- * @param mixed $end              The end date of the period.
- *
- * @return mixed  An array of timeObject results || PEAR_Error
- */
-function _turba_listTimeObjects($time_categories, $start, $end)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-
-    $start = new Horde_Date($start);
-    $end = new Horde_Date($end);
-
-    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
-        return array();
-    }
-
-    $objects = array();
-    foreach ($cfgSources as $name => $source) {
-        // Check if we even have to load the driver.
-        $check = array();
-        foreach ($time_categories as $category) {
-            if (!empty($source['map'][$category])) {
-                $check[] = $category;
-            }
-        }
-        if (!count($check)) {
-            continue;
-        }
-        $driver = &Turba_Driver::singleton($name);
-        if (is_a($driver, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Connection failed: %s"),
-                                    $driver->getMessage()), 'horde.error',
-                                    null, null, $name);
-        }
-        foreach ($check as $category) {
-            $new_objects = $driver->listTimeObjects($start, $end, $category);
-            if (is_a($new_objects, 'PEAR_Error')) {
-                return $new_objects;
-            }
-            $objects = array_merge($objects, $new_objects);
-        }
-    }
-
-    return $objects;
-}
-
-/**
- * Returns the client source name
- *
- * @return string  The name of the source to use with the clients api.
- */
-function _turba_getClientSource()
-{
-    return !empty($GLOBALS['conf']['client']['addressbook']) ? $GLOBALS['conf']['client']['addressbook'] : false;
-}
-
-/**
- * Returns the availabble client fields
- *
- * @return mixed  An array describing the fields | PEAR_Error
- */
-function _turba_clientFields()
-{
-    return _turba_fields($GLOBALS['conf']['client']['addressbook']);
-}
-
-/**
- * Returns a contact from the client source.
- *
- * @param string $objectId  Client unique ID
- *
- * @return mixed  Array of client data | PEAR_Error
- */
-function _turba_getClient($objectId = '')
-{
-    return _turba_getContact($GLOBALS['conf']['client']['addressbook'],
-                             $objectId);
-}
-
-/**
- * Returns mulitple contacts from the client source
- *
- * @param array $objectIds  client unique ids
- *
- * @return mixed  An array of clients data | PEAR_Error
- */
-function _turba_getClients($objectIds = array())
-{
-    return _turba_getContacts($GLOBALS['conf']['client']['addressbook'],
-                              $objectIds);
-}
-
-/**
- * Adds a client to the client source
- *
- * @param array $attributes  Array containing the client attributes
- */
-function _turba_addClient($attributes = array())
-{
-    return _turba_import($attributes, 'array', _turba_getClientSource());
-}
-
-/**
- * Updates client data
- *
- * @param string $objectId   The unique id of the client
- * @param array $attributes  An array of client attributes
- *
- * @return boolean
- */
-function _turba_updateClient($objectId = '', $attributes = array())
-{
-    return _turba_replace(_turba_getClientSource() . ':' . $objectId,
-                          $attributes, 'array');
-}
-
-/**
- * Deletes a client
- *
- * @param string $objectId  The unique id of the client
- *
- * @return boolean
- */
-function _turba_deleteClient($objectId = '')
-{
-    return _turba_delete(_turba_getClientSource() . ':' . $objectId);
-}
-
-/**
- * Search for clients
- *
- * @param array $names         The search filter values
- * @param array $fields        The fields to serach in
- * @param boolean $matchBegin  Match word boundaries only
- *
- * @return mixed  A hash containing the search results | PEAR_Error
- */
-function _turba_searchClients($names = array(), $fields = array(), $matchBegin = false)
-{
-    return _turba_search(
-        $names,
-        array($GLOBALS['conf']['client']['addressbook']),
-        array($GLOBALS['conf']['client']['addressbook'] => $fields),
-        $matchBegin,
-        true);
-}
-
-/**
- * Sets the value of the specified attribute of a contact
- *
- * @param string $address  Contact email address
- * @param string $name     Contact name
- * @param string $field    Field to update
- * @param string $value    Field value to set
- * @param string $source   Contact source
- *
- * @return mixed  The new __key value on success | PEAR_Error on failure
- */
-function _turba_addField($address = '', $name = '', $field = '', $value = '',
-                         $source = '')
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-
-    if (empty($source) || !isset($cfgSources[$source])) {
-        return PEAR::raiseError(sprintf(_("Invalid address book: %s"), $source), 'horde.error', null, null, $source);
-    }
-
-    if (empty($address)) {
-        return PEAR::raiseError(_("Invalid email"), 'horde.error', null, null, $source);
-    }
-
-    if (empty($name)) {
-        return PEAR::raiseError(_("Invalid name"), 'horde.error', null, null, $source);
-    }
-
-    if (empty($value)) {
-        return PEAR::raiseError(_("Invalid entry"), 'horde.error', null, null, $source);
-    }
-
-    $driver = &Turba_Driver::singleton($source);
-    if (is_a($driver, 'PEAR_Error')) {
-        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
-    }
-
-    if (!$driver->hasPermission(PERMS_EDIT)) {
-        return PEAR::raiseError(_("Permission denied"), 'horde.error', null, null, $source);
-    }
-
-    $res = $driver->search(array('email' => trim($address)), null, 'AND');
-    if (is_a($res, 'PEAR_Error')) {
-        return PEAR::raiseError(sprintf(_("Search failed: %s"), $res->getMessage()), 'horde.message', null, null, $source);
-    }
-
-    if ($res->count() > 1) {
-        $res2 = $driver->search(array('email' => trim($address), 'name' => trim($name)), null, 'AND');
-        if (is_a($res2, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Search failed: %s"), $res2->getMessage()), 'horde.message', null, null, $source);
-        }
-
-        if (!$res2->count()) {
-            return PEAR::raiseError(sprintf(_("Multiple persons with address [%s], but none with name [%s] already exist"), trim($address), trim($name)), 'horde.message', null, null, $source);
-        }
-
-        $res3 = $driver->search(array('email' => $address, 'name' => $name, $field => $value));
-        if (is_a($res3, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Search failed: %s"), $res3->getMessage()), 'horde.message', null, null, $source);
-        }
-
-        if ($res3->count()) {
-            return PEAR::raiseError(sprintf(_("This person already has a %s entry in the address book"), $field), 'horde.message', null, null, $source);
-        }
-
-        $ob = $res2->next();
-        $ob->setValue($field, $value);
-        $ob->store();
-    } elseif ($res->count() == 1) {
-        $res4 = $driver->search(array('email' => $address, $field => $value));
-        if (is_a($res4, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf(_("Search failed: %s"), $res4->getMessage()), 'horde.message', null, null, $source);
-        }
-
-        if ($res4->count()) {
-            return PEAR::raiseError(sprintf(_("This person already has a %s entry in the address book"), $field), 'horde.message', null, null, $source);
-        }
-
-        $ob = $res->next();
-        $ob->setValue($field, $value);
-        $ob->store();
-    } else {
-        return $driver->add(array('email' => $address, 'name' => $name, $field => $value, '__owner' => Horde_Auth::getAuth()));
-    }
-
-    return;
-}
-
-/**
- * Returns a field value
- *
- * @param string $address    Contact email address
- * @param string $field      Field to get
- * @param array $sources     Sources to check
- * @param boolean $strict    Match the email address strictly
- * @param boolean $multiple  Return more than one entry if found and true,
- *                           return an error if this is false.
- *
- * @return mixed  An array of field value(s) | PEAR_Error on failure.
- */
-function _turba_getField($address = '', $field = '', $sources = array(),
-                         $strict = false, $multiple = false)
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-
-    if (empty($address)) {
-        return PEAR::raiseError(_("Invalid email"), 'horde.error');
-    }
-
-    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
-        return array();
-    }
-
-    if (!count($sources)) {
-        $sources = array(Turba::getDefaultAddressbook());
-    }
-
-    $result = array();
-    foreach ($sources as $source) {
-        if (!isset($cfgSources[$source])) {
-            continue;
-        }
-
-        $driver = &Turba_Driver::singleton($source);
-        if (is_a($driver, 'PEAR_Error')) {
-            continue;
-        }
-
-        $list = $driver->search(array('email' => $address), null, 'AND', array(), $strict ? array('email') : array());
-        if (!is_a($list, 'Turba_List')) {
-            continue;
-        }
-
-        while ($ob = $list->next()) {
-            if ($ob->hasValue($field)) {
-                $result[] = $ob->getValue($field);
-            }
-        }
-    }
-
-    if (count($result) > 1) {
-        if ($multiple) {
-            return $result;
-        } else {
-            return PEAR::raiseError(_("More than 1 entry found"), 'horde.warning', null, null, $source);
-        }
-    } elseif (empty($result)) {
-        return PEAR::raiseError(sprintf(_("No %s entry found for %s"), $field, $address), 'horde.warning', null, null, $source);
-    }
-    return reset($result);
-}
-
-/**
- * Deletes a field value
- *
- * @param string $address Contact email address
- * @param string $field   Field to delete value for
- * @param array $sources  Sources to delete value from
- *
- * @return boolean
- */
-function _turba_deleteField($address = '', $field = '', $sources = array())
-{
-    require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
-
-    if (empty($address)) {
-        return PEAR::raiseError(_("Invalid email"), 'horde.error');
-    }
-
-    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
-        return array();
-    }
-
-    if (count($sources) == 0) {
-        $sources = array(Turba::getDefaultAddressbook());
-    }
-
-    $success = false;
-
-    foreach ($sources as $source) {
-        if (isset($cfgSources[$source])) {
-            $driver = &Turba_Driver::singleton($source);
-            if (is_a($driver, 'PEAR_Error')) {
-                continue;
-            }
-            if (!$driver->hasPermission(PERMS_EDIT)) {
-                continue;
-            }
-
-            $res = $driver->search(array('email' => $address));
-            if (is_a($res, 'Turba_List')) {
-                if ($res->count() > 1) {
-                    continue;
-                }
-
-                $ob = $res->next();
-                if (is_object($ob) && $ob->hasValue($field)) {
-                    $ob->setValue($field, '');
-                    $ob->store();
-                    $success = true;
-                }
-            }
-        }
-    }
-
-    if (!$success) {
-        return PEAR::raiseError(sprintf(_("No %s entry found for %s"), $field, $address), 'horde.error');
-    }
-
-    return;
-}
diff --git a/turba/lib/version.php b/turba/lib/version.php
deleted file mode 100644 (file)
index db55362..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php define('TURBA_VERSION', 'H3 (3.0-git)') ?>
index b1c0f48..cd395c4 100644 (file)
@@ -32,8 +32,9 @@ $horde_test = new Horde_Test();
 
 /* Turba version. */
 $module = 'Turba';
-require_once TURBA_BASE . '/lib/version.php';
-$module_version = TURBA_VERSION;
+require_once dirname(__FILE__) . '/lib/Api.php';
+$api = new Turba_Api();
+$module_version = $api->version;
 
 require TEST_TEMPLATES . 'header.inc';
 require TEST_TEMPLATES . 'version.inc';