Break out the various classes from lib/Ansel.php into seperate files.
authorMichael J. Rubinsky <mrubinsk@horde.org>
Wed, 12 Aug 2009 15:30:02 +0000 (11:30 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Wed, 12 Aug 2009 15:30:02 +0000 (11:30 -0400)
ansel/lib/Ansel.php
ansel/lib/Gallery.php [new file with mode: 0644]
ansel/lib/Image.php [new file with mode: 0644]
ansel/lib/Storage.php [new file with mode: 0644]

index c817b6c..751becf 100644 (file)
@@ -19,8 +19,8 @@ require_once 'Horde/Share/sql_hierarchical.php';
  * @author  Michael J. Rubinsky <mrubinsk@horde.org>
  * @package Ansel
  */
-class Ansel {
-
+class Ansel
+{
     /**
      * Build initial Ansel javascript object.
      *
@@ -1064,2944 +1064,3 @@ class Ansel {
     }
 
 }
-
-/**
- * Class to encapsulate a single gallery. Implemented as an extension of
- * the Horde_Share_Object class.
- *
- * @author  Michael J. Rubinsky <mrubinsk@horde.org>
- * @package Ansel
- */
-class Ansel_Gallery extends Horde_Share_Object_sql_hierarchical {
-
-    /**
-     * Cache the Gallery Id - to match the Ansel_Image interface
-     */
-    var $id;
-
-    /**
-     * The gallery mode helper
-     *
-     * @var Ansel_Gallery_Mode object
-     */
-    var $_modeHelper;
-
-    /**
-     *
-     */
-    function __sleep()
-    {
-        $properties = get_object_vars($this);
-        unset($properties['_shareOb']);
-        unset($properties['_modeHelper']);
-        $properties = array_keys($properties);
-        return $properties;
-    }
-
-    function __wakeup()
-    {
-        $this->setShareOb($GLOBALS['ansel_storage']->shares);
-        $mode = $this->get('view_mode');
-        $this->_setModeHelper($mode);
-    }
-
-    /**
-     * The Ansel_Gallery constructor.
-     *
-     * @param string $name  The name of the gallery
-     */
-    function Ansel_Gallery($attributes = array())
-    {
-        /* Existing gallery? */
-        if (!empty($attributes['share_id'])) {
-            $this->id = (int)$attributes['share_id'];
-        }
-
-        /* Pass on up the chain */
-        parent::Horde_Share_Object_sql_hierarchical($attributes);
-        $this->setShareOb($GLOBALS['ansel_storage']->shares);
-        $mode = isset($attributes['attribute_view_mode']) ? $attributes['attribute_view_mode'] : 'Normal';
-        $this->_setModeHelper($mode);
-    }
-
-    /**
-     * Check for special capabilities of this gallery.
-     *
-     */
-    function hasFeature($feature)
-    {
-
-        // First check for purely Ansel_Gallery features
-        // Currently we have none of these.
-
-        // Delegate to the modeHelper
-        return $this->_modeHelper->hasFeature($feature);
-
-    }
-
-    /**
-     * Simple factory to retrieve the proper mode object.
-     *
-     * @param string $type  The mode to use
-     *
-     * @return Ansel_Gallery_Mode object
-     */
-    function _setModeHelper($type = 'Normal')
-    {
-        $type = basename($type);
-        $class = 'Ansel_GalleryMode_' . $type;
-        $this->_modeHelper = new $class($this);
-        $this->_modeHelper->init();
-    }
-
-    /**
-     * Checks if the user can download the full photo
-     *
-     * @return boolean  Whether or not user can download full photos
-     */
-    function canDownload()
-    {
-        if (Horde_Auth::getAuth() == $this->data['share_owner'] || Horde_Auth::isAdmin('ansel:admin')) {
-            return true;
-        }
-
-        switch ($this->data['attribute_download']) {
-        case 'all':
-            return true;
-
-        case 'authenticated':
-            return Horde_Auth::isAuthenticated();
-
-        case 'edit':
-            return $this->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT);
-
-        case 'hook':
-            return Horde::callHook('_ansel_hook_can_download', array($this->id));
-
-        default:
-            return false;
-        }
-    }
-
-    /**
-     * Saves any changes to this object to the backend permanently.
-     *
-     * @return mixed true || PEAR_Error on failure.
-     */
-    function _save()
-    {
-        // Check for invalid characters in the slug.
-        if (!empty($this->data['attribute_slug']) &&
-            preg_match('/[^a-zA-Z0-9_@]/', $this->data['attribute_slug'])) {
-
-            return PEAR::raiseError(
-                sprintf(_("Could not save gallery, the slug, \"%s\", contains invalid characters."),
-                        $this->data['attribute_slug']));
-        }
-
-        // Check for slug uniqueness
-        $slugGalleryId = $GLOBALS['ansel_storage']->slugExists($this->data['attribute_slug']);
-        if ($slugGalleryId > 0 && $slugGalleryId <> $this->id) {
-            return PEAR::raiseError(sprintf(_("Could not save gallery, the slug, \"%s\", already exists."),
-                                            $this->data['attribute_slug']));
-        }
-
-        if ($GLOBALS['conf']['ansel_cache']['usecache']) {
-            $GLOBALS['cache']->expire('Ansel_Gallery' . $this->id);
-        }
-        return parent::_save();
-    }
-
-    /**
-     * Update the gallery image count.
-     *
-     * @param integer $images      Number of images in action
-     * @param boolean $add         Action to take (add or remove)
-     * @param integer $gallery_id  Gallery id to update images for
-     */
-    function _updateImageCount($images, $add = true, $gallery_id = null)
-    {
-        // We do the query directly here to avoid having to instantiate a
-        // gallery object just to increment/decrement one value in the table.
-        $sql = 'UPDATE ' . $this->_shareOb->_table
-            . ' SET attribute_images = attribute_images '
-            . ($add ? ' + ' : ' - ') . $images . ' WHERE share_id = '
-            . ($gallery_id ? $gallery_id : $this->id);
-
-        // Make sure to update the local value as well, so it doesn't get
-        // overwritten by any other updates from ->set() calls.
-        if (is_null($gallery_id) || $gallery_id === $this->id) {
-            if ($add) {
-                $this->data['attribute_images'] += $images;
-            } else {
-                $this->data['attribute_images'] -= $images;
-            }
-        }
-
-        /* Need to expire the cache for the gallery that was changed */
-        if ($GLOBALS['conf']['ansel_cache']['usecache']) {
-            $id = (is_null($gallery_id) ? $this->id : $gallery_id);
-            $GLOBALS['cache']->expire('Ansel_Gallery' . $id);
-        }
-
-        return $this->_shareOb->_write_db->exec($sql);
-
-    }
-
-    /**
-     * Add an image to this gallery.
-     *
-     * @param array $image_data  The image to add. Required keys include
-     *                           'image_caption', and 'data'. Optional keys
-     *                           include 'image_filename' and 'image_type'
-     *
-     * @param boolean $default   Make this image the new default tile image.
-     *
-     * @return integer  The id of the new image.
-     */
-    function addImage($image_data, $default = false)
-    {
-        global $conf;
-
-        /* Normal is the only view mode that can accurately update gallery counts */
-        $vMode = $this->get('view_mode');
-        if ($vMode != 'Normal') {
-            $this->_setModeHelper('Normal');
-        }
-
-        $resetStack = false;
-        if (!isset($image_data['image_filename'])) {
-            $image_data['image_filename'] = 'Untitled';
-        }
-        $image_data['gallery_id'] = $this->id;
-        $image_data['image_sort'] = $this->countImages();
-
-        /* Create the image object */
-        $image = new Ansel_Image($image_data);
-        $result = $image->save();
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        }
-
-        if (empty($image_data['image_id'])) {
-            $this->_updateImageCount(1);
-            if ($this->countImages() < 5) {
-                $resetStack = true;
-            }
-        }
-
-        /* Should this be the default image? */
-        if (!$default && $this->data['attribute_default_type'] == 'auto') {
-            $this->data['attribute_default'] = $image->id;
-            $resetStack = true;
-        } elseif ($default) {
-            $this->data['attribute_default'] = $image->id;
-            $this->data['default_type'] = 'manual';
-        }
-
-        /* Reset the gallery default image stacks if needed. */
-        if ($resetStack) {
-            $this->clearStacks();
-        }
-
-        /* Update the modified flag and save gallery changes */
-        $this->data['attribute_last_modified'] = time();
-
-        /* Save all changes to the gallery */
-        $this->save();
-
-        /* Return to the proper view mode */
-        if ($vMode != 'Normal') {
-            $this->_setModeHelper($vMode);
-        }
-
-        /* Return the ID of the new image. */
-        return $image->id;
-    }
-
-    /**
-     * Clear all of this gallery's default image stacks from the VFS and the
-     * gallery's data store.
-     *
-     */
-    function clearStacks()
-    {
-        $ids = @unserialize($this->data['attribute_default_prettythumb']);
-        if (is_array($ids)) {
-            foreach ($ids as $imageId) {
-                $this->removeImage($imageId, true);
-            }
-        }
-
-        // Using the set function here so we can efficently update the db
-        $this->set('default_prettythumb', '', true);
-    }
-
-    /**
-     * Removes all generated and cached 'prettythumb' thumbnails for this
-     * gallery
-     *
-     */
-    function clearThumbs()
-    {
-        $images = $this->listImages();
-        foreach ($images as $id) {
-            $image = $this->getImage($id);
-            $image->deleteCache('prettythumb');
-        }
-    }
-
-    /**
-     * Removes all generated and cached views for this gallery
-     *
-     */
-    function clearViews()
-    {
-        $images = $this->listImages();
-        foreach ($images as $id) {
-            $image = $this->getImage($id);
-            $image->deleteCache('all');
-        }
-    }
-
-    /**
-     * Move images from this gallery to a new gallery.
-     *
-     * @param array $images          An array of image ids.
-     * @param Ansel_Gallery $gallery The gallery to move the images to.
-     *
-     * @return integer | PEAR_Error The number of images moved, or an error message.
-     */
-    function moveImagesTo($images, $gallery)
-    {
-        return $this->_modeHelper->moveImagesTo($images, $gallery);
-    }
-
-    /**
-     * Copy image and related data to specified gallery.
-     *
-     * @param array $images           An array of image ids.
-     * @param Ansel_Gallery $gallery  The gallery to copy images to.
-     *
-     * @return integer | PEAR_Error The number of images copied or error message
-     */
-    function copyImagesTo($images, $gallery)
-    {
-        if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
-            return PEAR::raiseError(
-                sprintf(_("Access denied copying photos to \"%s\"."),
-                          $gallery->get('name')));
-        }
-
-        $db = $this->_shareOb->_write_db;
-        $imgCnt = 0;
-        foreach ($images as $imageId) {
-            $img = &$this->getImage($imageId);
-            // Note that we don't pass the tags when adding the image..see below
-            $newId = $gallery->addImage(array(
-                               'image_caption' => $img->caption,
-                               'data' => $img->raw(),
-                               'image_filename' => $img->filename,
-                               'image_type' => $img->getType(),
-                               'image_uploaded_date' => $img->uploaded));
-            if (is_a($newId, 'PEAR_Error')) {
-                return $newId;
-            }
-            /* Copy any tags */
-            // Since we know that the tags already exist, no need to
-            // go through Ansel_Tags::writeTags() - this saves us a SELECT query
-            // for each tag - just write the data into the DB ourselves.
-            $tags = $img->getTags();
-            $query = $this->_shareOb->_write_db->prepare('INSERT INTO ansel_images_tags (image_id, tag_id) VALUES(' . $newId . ',?);');
-            if (is_a($query, 'PEAR_Error')) {
-                return $query;
-            }
-            foreach ($tags as $tag_id => $tag_name) {
-                $result = $query->execute($tag_id);
-                if (is_a($result, 'PEAR_Error')) {
-                    return $result;
-                }
-            }
-            $query->free();
-
-            /* exif data */
-            // First check to see if the exif data was present in the raw data.
-            $count = $db->queryOne('SELECT COUNT(image_id) FROM ansel_image_attributes WHERE image_id = ' . (int) $newId . ';');
-            if ($count == 0) {
-                $exif = $db->queryAll('SELECT attr_name, attr_value FROM ansel_image_attributes WHERE image_id = ' . (int) $imageId . ';',null, MDB2_FETCHMODE_ASSOC);
-                if (is_array($exif) && count($exif) > 0) {
-                    $insert = $db->prepare('INSERT INTO ansel_image_attributes (image_id, attr_name, attr_value) VALUES (?, ?, ?)');
-                    if (is_a($insert, 'PEAR_Error')) {
-                        return $insert;
-                    }
-                    foreach ($exif as $attr){
-                        $result = $insert->execute(array($newId, $attr['attr_name'], $attr['attr_value']));
-                        if (is_a($result, 'PEAR_Error')) {
-                            return $result;
-                        }
-                    }
-                    $insert->free();
-                }
-            }
-            ++$imgCnt;
-        }
-
-        return $imgCnt;
-    }
-
-    /**
-     * Set the order of an image in this gallery.
-     *
-     * @param integer $imageId The image to sort.
-     * @param integer $pos     The sort position of the image.
-     */
-    function setImageOrder($imageId, $pos)
-    {
-        return $this->_shareOb->_write_db->exec('UPDATE ansel_images SET image_sort = ' . (int)$pos . ' WHERE image_id = ' . (int)$imageId);
-    }
-
-    /**
-     * Remove the given image from this gallery.
-     *
-     * @param mixed   $image   Image to delete. Can be an Ansel_Image
-     *                         or an image ID.
-     *
-     * @return boolean  True on success, false on failure.
-     */
-    function removeImage($image, $isStack = false)
-    {
-        return $this->_modeHelper->removeImage($image, $isStack);
-    }
-
-    /**
-     * Returns this share's owner's Identity object.
-     *
-     * @return Identity object for the owner of this gallery.
-     */
-    function getOwner()
-    {
-        require_once 'Horde/Identity.php';
-        $identity = Identity::singleton('none', $this->data['share_owner']);
-        return $identity;
-    }
-
-    /**
-     * Output the HTML for this gallery's tile.
-     *
-     * @param Ansel_Gallery $parent  The parent Ansel_Gallery object
-     * @param string $style          A named gallery style to use.
-     * @param boolean $mini          Force the use of a mini thumbnail?
-     * @param array $params          Any additional parameters the Ansel_Tile
-     *                               object may need.
-     */
-    function getTile($parent = null, $style = null, $mini = false,
-                     $params = array())
-    {
-        if (!is_null($parent) && is_null($style)) {
-            $style = $parent->getStyle();
-        } else {
-            $style = Ansel::getStyleDefinition($style);
-        }
-
-        if (!empty($view_url)) {
-            $view_url = str_replace('%g', $this->id, $view_url);
-        }
-
-        return Ansel_Tile_Gallery::getTile($this, $style, $mini, $params);
-    }
-
-    /**
-     * Get the children of this gallery.
-     *
-     * @param integer $perm    The permissions to limit to.
-     * @param integer $from    The child to start at.
-     * @param integer $to      The child to end with.
-     * @param boolean $noauto  Prevent auto
-     *
-     * @return A mixed array of Ansel_Gallery and Ansel_Image objects that are
-     *         children of this gallery.
-     */
-    function getGalleryChildren($perm = PERMS_SHOW, $from = 0, $to = 0, $noauto = true)
-    {
-        return $this->_modeHelper->getGalleryChildren($perm, $from, $to, $noauto);
-    }
-
-
-    /**
-     * Return the count of this gallery's children
-     *
-     * @param integer $perm            The permissions to require.
-     * @param boolean $galleries_only  Only include galleries, no images.
-     *
-     * @return integer The count of this gallery's children.
-     */
-    function countGalleryChildren($perm = PERMS_SHOW, $galleries_only = false, $noauto = true)
-    {
-        return $this->_modeHelper->countGalleryChildren($perm, $galleries_only, $noauto);
-    }
-
-    /**
-     * Lists a slice of the image ids in this gallery.
-     *
-     * @param integer $from  The image to start listing.
-     * @param integer $count The numer of images to list.
-     *
-     * @return mixed  An array of image_ids | PEAR_Error
-     */
-    function listImages($from = 0, $count = 0)
-    {
-        return $this->_modeHelper->listImages($from, $count);
-    }
-
-    /**
-     * Gets a slice of the images in this gallery.
-     *
-     * @param integer $from  The image to start fetching.
-     * @param integer $count The numer of images to return.
-     *
-     * @param mixed An array of Ansel_Image objects | PEAR_Error
-     */
-    function getImages($from = 0, $count = 0)
-    {
-        return $this->_modeHelper->getImages($from, $count);
-    }
-
-    /**
-     * Return the most recently added images in this gallery.
-     *
-     * @param integer $limit  The maximum number of images to return.
-     *
-     * @return mixed  An array of Ansel_Image objects | PEAR_Error
-     */
-    function getRecentImages($limit = 10)
-    {
-        return $GLOBALS['ansel_storage']->getRecentImages(array($this->id),
-                                                          $limit);
-    }
-
-    /**
-     * Returns the image in this gallery corresponding to the given id.
-     *
-     * @param integer $id  The ID of the image to retrieve.
-     *
-     * @return Ansel_Image  The image object corresponding to the given id.
-     */
-    function &getImage($id)
-    {
-        return $GLOBALS['ansel_storage']->getImage($id);
-    }
-
-    /**
-     * Checks if the gallery has any subgallery
-     */
-    function hasSubGalleries()
-    {
-        return $this->_modeHelper->hasSubGalleries();
-    }
-
-    /**
-     * Returns the number of images in this gallery and, optionally, all
-     * sub-galleries.
-     *
-     * @param boolean $subgalleries  Determines whether subgalleries should
-     *                               be counted or not.
-     *
-     * @return integer number of images in this gallery
-     */
-    function countImages($subgalleries = false)
-    {
-        return $this->_modeHelper->countImages($subgalleries);
-    }
-
-    /**
-     * Returns the default image for this gallery.
-     *
-     * @param string $style  Force the use of this style, if it's available
-     *                       otherwise use whatever style is choosen for this
-     *                       gallery. If prettythumbs are not available then
-     *                       we always use ansel_default style.
-     *
-     * @return mixed  The image_id of the default image or false.
-     */
-    function getDefaultImage($style = null)
-    {
-       // Check for explicitly requested style
-        if (!is_null($style)) {
-            $gal_style = Ansel::getStyleDefinition($style);
-        } else {
-            // Use gallery's default.
-            $gal_style = $this->getStyle();
-            if (!isset($GLOBALS['ansel_styles'][$gal_style['name']])) {
-                $gal_style = $GLOBALS['ansel_styles']['ansel_default'];
-            }
-        }
-        Horde::logMessage(sprintf("using gallery style: %s in Ansel::getDefaultImage()", $gal_style['name']), __FILE__, __LINE__, PEAR_LOG_DEBUG);
-        if (!empty($gal_style['default_galleryimage_type']) &&
-            $gal_style['default_galleryimage_type'] != 'plain') {
-
-            $thumbstyle = $gal_style['default_galleryimage_type'];
-            $styleHash = $this->_getViewHash($thumbstyle, $style);
-
-            // First check for the existence of a default image in the style
-            // we are looking for.
-            if (!empty($this->data['attribute_default_prettythumb'])) {
-                $thumbs = @unserialize($this->data['attribute_default_prettythumb']);
-            }
-            if (!isset($thumbs) || !is_array($thumbs)) {
-                $thumbs = array();
-            }
-
-            if (!empty($thumbs[$styleHash])) {
-                return $thumbs[$styleHash];
-            }
-
-            // Don't already have one, must generate it.
-            $params = array('gallery' => $this, 'style' => $gal_style);
-            $iview = Ansel_ImageView::factory(
-                $gal_style['default_galleryimage_type'], $params);
-
-            if (!is_a($iview, 'PEAR_Error')) {
-                $img = $iview->create();
-                if (!is_a($img, 'PEAR_Error')) {
-                     // Note the gallery_id is negative for generated stacks
-                     $iparams = array('image_filename' => $this->get('name'),
-                                      'image_caption' => $this->get('name'),
-                                      'data' => $img->raw(),
-                                      'image_sort' => 0,
-                                      'gallery_id' => -$this->id);
-                     $newImg = new Ansel_Image($iparams);
-                     $newImg->save();
-                     $prettyData = serialize(
-                         array_merge($thumbs,
-                                     array($styleHash => $newImg->id)));
-
-                     $this->set('default_prettythumb', $prettyData, true);
-                     return $newImg->id;
-                } else {
-                    Horde::logMessage($img, __FILE__, __LINE__, PEAR_LOG_ERR);
-                }
-            } else {
-                // Might not support the requested style...try ansel_default
-                // but protect against infinite recursion.
-                Horde::logMessage($iview, __FILE__, __LINE__, PEAR_LOG_DEBUG);
-                if ($style != 'ansel_default') {
-                    return $this->getDefaultImage('ansel_default');
-                }
-                Horde::logMessage($iview, __FILE__, __LINE__, PEAR_LOG_ERR);
-            }
-        } else {
-            // We are just using an image thumbnail for the gallery default.
-            if ($this->countImages()) {
-                if (!empty($this->data['attribute_default']) &&
-                    $this->data['attribute_default'] > 0) {
-
-                    return $this->data['attribute_default'];
-                }
-                $keys = $this->listImages();
-                if (is_a($keys, 'PEAR_Error')) {
-                    return $keys;
-                }
-                $this->data['attribute_default'] = $keys[count($keys) - 1];
-                $this->data['attribute_default_type'] = 'auto';
-                $this->save();
-                return $keys[count($keys) - 1];
-            }
-
-            if ($this->hasSubGalleries()) {
-                // Fall through to a default image of a sub gallery.
-                $galleries = $GLOBALS['ansel_storage']->listGalleries(
-                    PERMS_SHOW, null, $this, false);
-                if ($galleries && !is_a($galleries, 'PEAR_Error')) {
-                    foreach ($galleries as $galleryId => $gallery) {
-                        if ($default_img = $gallery->getDefaultImage($style)) {
-                            return $default_img;
-                        }
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Returns this gallery's tags.
-     */
-    function getTags() {
-        if ($this->hasPermission(Horde_Auth::getAuth(), PERMS_READ)) {
-            return Ansel_Tags::readTags($this->id, 'gallery');
-        } else {
-            return PEAR::raiseError(_("Access denied viewing this gallery."));
-        }
-    }
-
-    /**
-     * Set/replace this gallery's tags.
-     *
-     * @param array $tags  AN array of tag names to associate with this image.
-     */
-    function setTags($tags)
-    {
-        if ($this->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
-            return Ansel_Tags::writeTags($this->id, $tags, 'gallery');
-        } else {
-            return PEAR::raiseError(_("Access denied adding tags to this gallery."));
-        }
-    }
-
-    /**
-     * Return the style definition for this gallery. Returns the first available
-     * style in this order: Explicitly configured style if available, if
-     * configured style is not available, use ansel_default.  If nothing has
-     * been configured, the user's selected default is attempted.
-     *
-     * @return array  The style definition array.
-     */
-    function getStyle()
-    {
-        if (empty($this->data['attribute_style'])) {
-            $style = $GLOBALS['prefs']->getValue('default_gallerystyle');
-        } else {
-            $style = $this->data['attribute_style'];
-        }
-        return Ansel::getStyleDefinition($style);
-
-    }
-
-    /**
-     * Return a hash key for the given view and style.
-     *
-     * @param string $view   The view (thumb, prettythumb etc...)
-     * @param string $style  The named style.
-     *
-     * @return string  A md5 hash suitable for use as a key.
-     */
-    function _getViewHash($view, $style = null)
-    {
-        if (is_null($style)) {
-            $style = $this->getStyle();
-        } else {
-            $style = Ansel::getStyleDefinition($style);
-        }
-        if ($view != 'screen' && $view != 'thumb' && $view != 'mini' &&
-            $view != 'full') {
-
-            $view = md5($style['thumbstyle'] . '.' . $style['background']);
-        }
-        return $view;
-    }
-    /**
-     * Checks to see if a user has a given permission.
-     *
-     * @param string $userid       The userid of the user.
-     * @param integer $permission  A PERMS_* constant to test for.
-     * @param string $creator      The creator of the event.
-     *
-     * @return boolean  Whether or not $userid has $permission.
-     */
-    function hasPermission($userid, $permission, $creator = null)
-    {
-        if ($userid == $this->data['share_owner'] ||
-            Horde_Auth::isAdmin('ansel:admin')) {
-
-            return true;
-        }
-
-
-        return $GLOBALS['perms']->hasPermission($this->getPermission(),
-                                                $userid, $permission, $creator);
-    }
-
-    /**
-     * Check user age limtation
-     *
-     * @return boolean
-     */
-    function isOldEnough()
-    {
-        if ($this->data['share_owner'] == Horde_Auth::getAuth() ||
-            empty($GLOBALS['conf']['ages']['limits']) ||
-            empty($this->data['attribute_age'])) {
-
-            return true;
-        }
-
-        // Do we have the user age already cheked?
-        if (!isset($_SESSION['ansel']['user_age'])) {
-            $_SESSION['ansel']['user_age'] = 0;
-        } elseif ($_SESSION['ansel']['user_age'] >= $this->data['attribute_age']) {
-            return true;
-        }
-
-        // Can we hook user's age?
-        if ($GLOBALS['conf']['ages']['hook'] && Horde_Auth::isAuthenticated()) {
-            $result = Horde::callHook('_ansel_hook_user_age');
-            if (is_int($result)) {
-                $_SESSION['ansel']['user_age'] = $result;
-            }
-        }
-
-        return ($_SESSION['ansel']['user_age'] >= $this->data['attribute_age']);
-    }
-
-    /**
-     * Determine if we need to unlock a password protected gallery
-     *
-     * @return boolean
-     */
-    function hasPasswd()
-    {
-        if (Horde_Auth::getAuth() == $this->get('owner') || Horde_Auth::isAdmin('ansel:admin')) {
-            return false;
-        }
-
-        $passwd = $this->get('passwd');
-        if (empty($passwd) ||
-            (!empty($_SESSION['ansel']['passwd'][$this->id])
-                && $_SESSION['ansel']['passwd'][$this->id] = md5($this->get('passwd')))) {
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * Sets this gallery's parent gallery.
-     *
-     * @TODO: Check how this interacts with date galleries - shouldn't be able
-     *        to remove a subgallery from a date gallery anyway, but just incase
-     * @param mixed $parent    An Ansel_Gallery or a gallery_id.
-     *
-     * @return mixed  Ture || PEAR_Error
-     */
-    function setParent($parent)
-    {
-        /* Make sure we have a gallery object */
-        if (!is_null($parent) && !is_a($parent, 'Ansel_Gallery')) {
-            $parent = $GLOBALS['ansel_storage']->getGallery($parent);
-            if (is_a($parent, 'PEAR_Error')) {
-                return $parent;
-            }
-        }
-
-        /* Check this now since we don't know if we are updating the DB or not */
-        $old = $this->getParent();
-        $reset_has_subgalleries = false;
-        if (!is_null($old)) {
-            $cnt = $old->countGalleryChildren(PERMS_READ, true);
-            if ($cnt == 1) {
-                /* Count is 1, and we are about to delete it */
-                $reset_has_subgalleries = true;
-            }
-        }
-
-        /* Call the parent class method */
-        $result = parent::setParent($parent);
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        }
-
-        /* Tell the parent the good news */
-        if (!is_null($parent) && !$parent->get('has_subgalleries')) {
-            return $parent->set('has_subgalleries', '1', true);
-        }
-        Horde::logMessage('Ansel_Gallery parent successfully set', __FILE__,
-                          __LINE__, PEAR_LOG_DEBUG);
-
-       /* Gallery parent changed, safe to change the parent's attributes */
-       if ($reset_has_subgalleries) {
-           $old->set('has_subgalleries', 0, true);
-       }
-
-        return true;
-    }
-
-    /**
-     * Sets an attribute value in this object.
-     *
-     * @param string $attribute  The attribute to set.
-     * @param mixed $value       The value for $attribute.
-     * @param boolean $update    Commit only this change to storage.
-     *
-     * @return mixed  True if setting the attribute did succeed, a PEAR_Error
-     *                otherwise.
-     */
-    function set($attribute, $value, $update = false)
-    {
-        /* Translate the keys */
-        if ($attribute == 'owner') {
-            $driver_key = 'share_owner';
-        } else {
-            $driver_key = 'attribute_' . $attribute;
-        }
-
-        if ($driver_key == 'attribute_view_mode' &&
-            !empty($this->data[$driver_key]) &&
-            $value != $this->data[$driver_key]) {
-
-            $mode = isset($attributes['attribute_view_mode']) ? $attributes['attribute_view_mode'] : 'Normal';
-            $this->_setModeHelper($mode);
-        }
-
-        $this->data[$driver_key] = $value;
-
-        /* Update the backend, but only this current change */
-        if ($update) {
-            $db = $this->_shareOb->_write_db;
-            // Manually convert the charset since we're not going through save()
-            $data = $this->_shareOb->_toDriverCharset(array($driver_key => $value));
-            $query = $db->prepare('UPDATE ' . $this->_shareOb->_table . ' SET ' . $driver_key . ' = ? WHERE share_id = ?', null, MDB2_PREPARE_MANIP);
-            if ($GLOBALS['conf']['ansel_cache']['usecache']) {
-                $GLOBALS['cache']->expire('Ansel_Gallery' . $this->id);
-            }
-            $result = $query->execute(array($data[$driver_key], $this->id));
-            $query->free();
-
-            return $result;
-        }
-
-        return true;
-    }
-
-    function setDate($date)
-    {
-        $this->_modeHelper->setDate($date);
-    }
-
-    function getDate()
-    {
-        return $this->_modeHelper->getDate();
-    }
-
-    /**
-     * Get an array describing where this gallery is in a breadcrumb trail.
-     *
-     * @return  An array of 'title' and 'navdata' hashes with the [0] element
-     *          being the deepest part.
-     */
-    function getGalleryCrumbData()
-    {
-        return $this->_modeHelper->getGalleryCrumbData();
-    }
-
-}
-
-/**
- * Class to describe a single Ansel image.
- *
- * @author Chuck Hagenbuch <chuck@horde.org>
- * @author Michael J. Rubinsky <mrubinsk@horde.org>
- * @package Ansel
- */
-class Ansel_Image {
-
-    /**
-     * @var integer  The gallery id of this image's parent gallery
-     */
-    var $gallery;
-
-    /**
-     * @var Horde_Image  Horde_Image object for this image.
-     */
-    var $_image;
-
-    var $id = null;
-    var $filename = 'Untitled';
-    var $caption = '';
-    var $type = 'image/jpeg';
-
-    /**
-     * timestamp of uploaded date
-     *
-     * @var integer
-     */
-    var $uploaded;
-
-    var $sort;
-    var $commentCount;
-    var $facesCount;
-    var $lat;
-    var $lng;
-    var $location;
-    var $geotag_timestamp;
-
-    var $_dirty;
-
-
-    /**
-     * Timestamp of original date.
-     *
-     * @var integer
-     */
-    var $originalDate;
-
-    /**
-     * Holds an array of tags for this image
-     * @var array
-     */
-    var $_tags = array();
-
-    var $_loaded = array();
-    var $_data = array();
-
-    /**
-     * Cache the raw EXIF data locally
-     *
-     * @var array
-     */
-    var $_exif = array();
-
-    /**
-     * TODO: refactor Ansel_Image to use a ::get() method like Ansel_Gallery
-     * instead of direct instance variable access and all the nonsense below.
-     *
-     * @param unknown_type $image
-     * @return Ansel_Image
-     */
-    function Ansel_Image($image = array())
-    {
-        if ($image) {
-            $this->filename = $image['image_filename'];
-            $this->caption = $image['image_caption'];
-            $this->sort = $image['image_sort'];
-            $this->gallery = $image['gallery_id'];
-
-            // New image?
-            if (!empty($image['image_id'])) {
-                $this->id = $image['image_id'];
-            }
-
-            if (!empty($image['data'])) {
-                $this->_data['full'] = $image['data'];
-            }
-
-            if (!empty($image['image_uploaded_date'])) {
-                $this->uploaded = $image['image_uploaded_date'];
-            } else {
-                $this->uploaded = time();
-            }
-
-            if (!empty($image['image_type'])) {
-                $this->type = $image['image_type'];
-            }
-
-            if (!empty($image['tags'])) {
-                $this->_tags = $image['tags'];
-            }
-
-            if (!empty($image['image_faces'])) {
-               $this->facesCount = $image['image_faces'];
-            }
-
-            $this->location = !empty($image['image_location']) ? $image['image_location'] : '';
-
-            // The following may have to be rewritten by EXIF.
-            // EXIF requires both an image id and a stream, so we can't
-            // get EXIF data before we save the image to the VFS.
-            if (!empty($image['image_original_date'])) {
-                $this->originalDate = $image['image_original_date'];
-            } else {
-                $this->originalDate = $this->uploaded;
-            }
-            $this->lat = !empty($image['image_latitude']) ? $image['image_latitude'] : '';
-            $this->lng = !empty($image['image_longitude']) ? $image['image_longitude'] : '';
-            $this->geotag_timestamp = !empty($image['image_geotag_date']) ? $image['image_geotag_date'] : '0';
-        }
-
-        $this->_image = Ansel::getImageObject();
-        $this->_image->reset();
-    }
-
-    /**
-     * Return the vfs path for this image.
-     *
-     * @param string $view   The view we want.
-     * @param string $style  A named gallery style.
-     *
-     * @return string  The vfs path for this image.
-     */
-    function getVFSPath($view = 'full', $style = null)
-    {
-        $view = $this->_getViewHash($view, $style);
-        return '.horde/ansel/'
-               . substr(str_pad($this->id, 2, 0, STR_PAD_LEFT), -2)
-               . '/' . $view;
-    }
-
-    /**
-     * Returns the file name of this image as used in the VFS backend.
-     *
-     * @return string  This image's VFS file name.
-     */
-    function getVFSName($view)
-    {
-        $vfsname = $this->id;
-
-        if ($view == 'full' && $this->type) {
-            $type = strpos($this->type, '/') === false ? 'image/' . $this->type : $this->type;
-            if ($ext = Horde_Mime_Magic::mimeToExt($type)) {
-                $vfsname .= '.' . $ext;
-            }
-        } elseif (($GLOBALS['conf']['image']['type'] == 'jpeg') || $view == 'screen') {
-            $vfsname .= '.jpg';
-        } else {
-            $vfsname .= '.png';
-        }
-
-        return $vfsname;
-    }
-
-    /**
-     * Loads the given view into memory.
-     *
-     * @param string $view   Which view to load.
-     * @param string $style  The named gallery style.
-     *
-     * @return mixed  True || PEAR_Error
-     */
-    function load($view = 'full', $style = null)
-    {
-        // If this is a new image that hasn't been saved yet, we will
-        // already have the full data loaded. If we auto-rotate the image
-        // then there is no need to save it just to load it again.
-        if ($view == 'full' && !empty($this->_data['full'])) {
-            $this->_image->loadString('original', $this->_data['full']);
-            $this->_loaded['full'] = true;
-            return true;
-        }
-
-        $viewHash = $this->_getViewHash($view, $style);
-        /* If we've already loaded the data, just return now. */
-        if (!empty($this->_loaded[$viewHash])) {
-            return true;
-        }
-
-        $result = $this->createView($view, $style);
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        }
-
-        /* If createView() had to resize the full image, we've already
-         * loaded the data, so return now. */
-        if (!empty($this->_loaded[$viewHash])) {
-            return;
-        }
-
-        /* We've definitely successfully loaded the image now. */
-        $this->_loaded[$viewHash] = true;
-
-        /* Get the VFS info. */
-        $vfspath = $this->getVFSPath($view, $style);
-        if (is_a($vfspath, 'PEAR_Error')) {
-            return $vfspath;
-        }
-
-        /* Read in the requested view. */
-        $data = $GLOBALS['ansel_vfs']->read($vfspath, $this->getVFSName($view));
-        if (is_a($data, 'PEAR_Error')) {
-            Horde::logMessage($date, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $data;
-        }
-
-        $this->_data[$viewHash] = $data;
-        $this->_image->loadString($vfspath . '/' . $this->id, $data);
-        return true;
-    }
-
-    /**
-     * Check if an image view exists and returns the vfs name complete with
-     * the hash directory name prepended if appropriate.
-     *
-     * @param integer $id    Image id to check
-     * @param string $view   Which view to check for
-     * @param string $style  A named gallery style
-     *
-     * @return mixed  False if image does not exists | string vfs name
-     *
-     * @static
-     */
-    function viewExists($id, $view, $style)
-    {
-        /* We cannot check empty styles since we cannot get the hash */
-        if (empty($style)) {
-            return false;
-        }
-
-        /* Get the VFS path. */
-        $view = Ansel_Gallery::_getViewHash($view, $style);
-
-        /* Can't call the various vfs methods here, since this method needs
-        to be called statically */
-        $vfspath = '.horde/ansel/' . substr(str_pad($id, 2, 0, STR_PAD_LEFT), -2) . '/' . $view;
-
-        /* Get VFS name */
-        $vfsname = $id . '.';
-        if ($GLOBALS['conf']['image']['type'] == 'jpeg' || $view == 'screen') {
-            $vfsname .= 'jpg';
-        } else {
-            $vfsname .= 'png';
-        }
-
-        if ($GLOBALS['ansel_vfs']->exists($vfspath, $vfsname)) {
-            return $view . '/' . $vfsname;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Creates and caches the given view.
-     *
-     * @param string $view  Which view to create.
-     * @param string $style  A named gallery style
-     */
-    function createView($view, $style = null)
-    {
-        // HACK: Need to replace the image object with a JPG typed image if
-        //       we are generating a screen image. Need to do the replacement
-        //       and do it *here* for BC reasons with Horde_Image...and this
-        //       needs to be done FIRST, since the view might already be cached
-        //       in the VFS.
-        if ($view == 'screen' && $GLOBALS['conf']['image']['type'] != 'jpeg') {
-            $this->_image = Ansel::getImageObject(array('type' => 'jpeg'));
-            $this->_image->reset();
-        }
-
-        /* Get the VFS info. */
-        $vfspath = $this->getVFSPath($view, $style);
-        if ($GLOBALS['ansel_vfs']->exists($vfspath, $this->getVFSName($view))) {
-            return true;
-        }
-
-        $data = $GLOBALS['ansel_vfs']->read($this->getVFSPath('full'),
-                                            $this->getVFSName('full'));
-        if (is_a($data, 'PEAR_Error')) {
-            Horde::logMessage($data, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $data;
-        }
-        $this->_image->loadString($this->getVFSPath('full') . '/' . $this->id, $data);
-        $styleDef = Ansel::getStyleDefinition($style);
-        if ($view == 'prettythumb') {
-            $viewType = $styleDef['thumbstyle'];
-        } else {
-            $viewType = $view;
-        }
-        $iview = Ansel_ImageView::factory($viewType, array('image' => $this,
-                                                           'style' => $style));
-
-        if (is_a($iview, 'PEAR_Error')) {
-            // It could be we don't support the requested effect, try
-            // ansel_default before giving up.
-            if ($view == 'prettythumb') {
-                $iview = Ansel_ImageView::factory(
-                    'thumb', array('image' => $this,
-                                   'style' => 'ansel_default'));
-
-                if (is_a($iview, 'PEAR_Error')) {
-                    return $iview;
-                }
-            }
-        }
-
-        $res = $iview->create();
-        if (is_a($res, 'PEAR_Error')) {
-            return $res;
-        }
-
-        $view = $this->_getViewHash($view, $style);
-
-        $this->_data[$view] = $this->_image->raw();
-        $this->_image->loadString($vfspath . '/' . $this->id,
-                                  $this->_data[$view]);
-        $this->_loaded[$view] = true;
-        $GLOBALS['ansel_vfs']->writeData($vfspath, $this->getVFSName($view),
-                                         $this->_data[$view], true);
-
-        // Autowatermark the screen view
-        if ($view == 'screen' &&
-            $GLOBALS['prefs']->getValue('watermark_auto') &&
-            $GLOBALS['prefs']->getValue('watermark_text') != '') {
-
-            $this->watermark('screen');
-            $GLOBALS['ansel_vfs']->writeData($vfspath, $this->getVFSName($view),
-                                             $this->_image->_data);
-        }
-
-        return true;
-    }
-
-    /**
-     * Writes the current data to vfs, used when creating a new image
-     */
-    function _writeData()
-    {
-        $this->_dirty = false;
-        return $GLOBALS['ansel_vfs']->writeData($this->getVFSPath('full'),
-                                                $this->getVFSName('full'),
-                                                $this->_data['full'], true);
-    }
-
-    /**
-     * Change the image data. Deletes old cache and writes the new
-     * data to the VFS. Used when updating an image
-     *
-     * @param string $data  The new data for this image.
-     * @param string $view  If specified, the $data represents only this
-     *                      particular view. Cache will not be deleted.
-     */
-    function updateData($data, $view = 'full')
-    {
-        if (is_a($data, 'PEAR_Error')) {
-            return $data;
-        }
-
-        /* Delete old cached data if we are replacing the full image */
-        if ($view == 'full') {
-            $this->deleteCache();
-        }
-
-        return $GLOBALS['ansel_vfs']->writeData($this->getVFSPath($view),
-                                                $this->getVFSName($view),
-                                                $data, true);
-    }
-
-    /**
-     * Update the geotag data
-     */
-    function geotag($lat, $lng, $location = '')
-    {
-        $this->lat = $lat;
-        $this->lng = $lng;
-        $this->location = $location;
-        $this->geotag_timestamp = time();
-        $this->save();
-    }
-
-    /**
-     * Save basic image details
-     *
-     * @TODO: Move all SQL queries to Ansel_Storage::?
-     */
-    function save()
-    {
-        /* If we have an id, then it's an existing image.*/
-        if ($this->id) {
-            $update = $GLOBALS['ansel_db']->prepare('UPDATE ansel_images SET image_filename = ?, image_type = ?, image_caption = ?, image_sort = ?, image_original_date = ?, image_latitude = ?, image_longitude = ?, image_location = ?, image_geotag_date = ? WHERE image_id = ?');
-            if (is_a($update, 'PEAR_Error')) {
-                Horde::logMessage($update, __FILE__, __LINE__, PEAR_LOG_ERR);
-                return $update;
-            }
-            $result = $update->execute(array(Horde_String::convertCharset($this->filename, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset']),
-                                             $this->type,
-                                             Horde_String::convertCharset($this->caption, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset']),
-                                             $this->sort,
-                                             $this->originalDate,
-                                             $this->lat,
-                                             $this->lng,
-                                             $this->location,
-                                             $this->geotag_timestamp,
-                                             $this->id));
-            if (is_a($result, 'PEAR_Error')) {
-                Horde::logMessage($update, __FILE__, __LINE__, PEAR_LOG_ERR);
-            } else {
-                $update->free();
-            }
-            return $result;
-        }
-
-        /* Saving a new Image */
-        if (!$this->gallery || !strlen($this->filename) || !$this->type) {
-            $error = PEAR::raiseError(_("Incomplete photo"));
-            Horde::logMessage($error, __FILE__, __LINE__, PEAR_LOG_ERR);
-        }
-
-        /* Get the next image_id */
-        $image_id = $GLOBALS['ansel_db']->nextId('ansel_images');
-        if (is_a($image_id, 'PEAR_Error')) {
-            return $image_id;
-        }
-
-        /* Prepare the SQL statement */
-        $insert = $GLOBALS['ansel_db']->prepare('INSERT INTO ansel_images (image_id, gallery_id, image_filename, image_type, image_caption, image_uploaded_date, image_sort, image_original_date, image_latitude, image_longitude, image_location, image_geotag_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
-        if (is_a($insert, 'PEAR_Error')) {
-            Horde::logMessage($insert, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $insert;
-        }
-
-        /* Perform the INSERT */
-        $result = $insert->execute(array($image_id,
-                                         $this->gallery,
-                                         Horde_String::convertCharset($this->filename, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset']),
-                                         $this->type,
-                                         Horde_String::convertCharset($this->caption, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset']),
-                                         $this->uploaded,
-                                         $this->sort,
-                                         $this->originalDate,
-                                         $this->lat,
-                                         $this->lng,
-                                         $this->location,
-                                         (empty($this->lat) ? 0 : $this->uploaded)));
-        $insert->free();
-        if (is_a($result, 'PEAR_Error')) {
-            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $result;
-        }
-
-        /* Keep the image_id */
-        $this->id = $image_id;
-
-        /* The EXIF functions require a stream, so we need to save before we read */
-        $this->_writeData();
-
-        /* Get the EXIF data if we are not a gallery key image. */
-        if ($this->gallery > 0) {
-            $needUpdate = $this->_getEXIF();
-        }
-
-        /* Create tags from exif data if desired */
-        $fields = @unserialize($GLOBALS['prefs']->getValue('exif_tags'));
-        if ($fields) {
-            $this->_exifToTags($fields);
-        }
-
-        /* Save the tags */
-        if (count($this->_tags)) {
-            $result = $this->setTags($this->_tags);
-            if (is_a($result, 'PEAR_Error')) {
-                // Since we got this far, the image has been added, so
-                // just log the tag failure.
-                Horde::logMessage($result, __LINE__, __FILE__, PEAR_LOG_ERR);
-            }
-        }
-
-        /* Save again if EXIF changed any values */
-        if (!empty($needUpdate)) {
-            $this->save();
-        }
-
-        return $this->id;
-    }
-
-   /**
-    * Replace this image's image data.
-    *
-    */
-    function replace($imageData)
-    {
-        /* Reset the data array and remove all cached images */
-        $this->_data = array();
-        $this->reset();
-
-        /* Remove attributes */
-        $result = $GLOBALS['ansel_db']->exec('DELETE FROM ansel_image_attributes WHERE image_id = ' . (int)$this->id);
-        if (is_a($result, 'PEAR_Error')) {
-            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERROR);
-            return $result;
-        }
-        /* Load the new image data */
-        $this->_getEXIF();
-        $this->updateData($imageData);
-
-        return true;
-    }
-
-    /**
-     * Adds specified EXIF fields to this image's tags. Called during image
-     * upload/creation.
-     *
-     * @param array $fields  An array of EXIF fields to import as a tag.
-     *
-     */
-    function _exifToTags($fields = array())
-    {
-        $tags = array();
-        foreach ($fields as $field) {
-            if (!empty($this->_exif[$field])) {
-                if (substr($field, 0, 8) == 'DateTime') {
-                    $d = new Horde_Date(strtotime($this->_exif[$field]));
-                    $tags[] = $d->format("Y-m-d");
-                } else {
-                    $tags[] = $this->_exif[$field];
-                }
-            }
-        }
-
-        $this->_tags = array_merge($this->_tags, $tags);
-    }
-
-    /**
-     * Reads the EXIF data from the image and stores in _exif array() as well
-     * also populates any local properties that come from the EXIF data.
-     *
-     * @return mixed  true if any local properties were modified, false otherwise, PEAR_Error on failure
-     */
-    function _getEXIF()
-    {
-        /* Clear the local copy */
-        $this->_exif = array();
-
-        /* Get the data */
-        $imageFile = $GLOBALS['ansel_vfs']->readFile($this->getVFSPath('full'),
-                                                     $this->getVFSName('full'));
-        if (is_a($imageFile, 'PEAR_Error')) {
-            return $imageFile;
-        }
-        $exif = Horde_Image_Exif::factory($GLOBALS['conf']['exif'], $GLOBALS['conf']['exif']['params']);
-        $exif_fields = $exif->getData($imageFile);
-
-        /* Flag to determine if we need to resave the image data */
-        $needUpdate = false;
-
-        /* Populate any local properties that come from EXIF
-         * Save any geo data to a seperate table as well */
-        if (!empty($exif_fields['GPSLatitude'])) {
-            $this->lat = $exif_fields['GPSLatitude'];
-            $this->lng = $exif_fields['GPSLongitude'];
-            $this->geotag_timestamp = time();
-            $needUpdate = true;
-        }
-
-        if (!empty($exif_fields['DateTimeOriginal'])) {
-            $this->originalDate = $exif_fields['DateTimeOriginal'];
-            $needUpdate = true;
-        }
-
-        /* Attempt to autorotate based on Orientation field */
-        $this->_autoRotate();
-
-        /* Save attributes. */
-        $insert = $GLOBALS['ansel_db']->prepare('INSERT INTO ansel_image_attributes (image_id, attr_name, attr_value) VALUES (?, ?, ?)');
-        foreach ($exif_fields as $name => $value) {
-            $result = $insert->execute(array($this->id, $name, Horde_String::convertCharset($value, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset'])));
-            if (is_a($result, 'PEAR_Error')) {
-                return $result;
-            }
-            /* Cache it locally */
-            $this->_exif[$name] = Horde_Image_Exif::getHumanReadable($name, $value);
-        }
-        $insert->free();
-
-
-        return $needUpdate;
-    }
-
-    /**
-     * Autorotate based on EXIF orientation field. Updates the data in memory
-     * only.
-     *
-     */
-    function _autoRotate()
-    {
-        if (isset($this->_exif['Orientation']) && $this->_exif['Orientation'] != 1) {
-            switch ($this->_exif['Orientation']) {
-            case 2:
-                 $this->mirror();
-                break;
-
-            case 3:
-                $this->rotate('full', 180);
-                break;
-
-            case 4:
-                $this->mirror();
-                $this->rotate('full', 180);
-                break;
-
-            case 5:
-                $this->flip();
-                $this->rotate('full', 90);
-                break;
-
-            case 6:
-                $this->rotate('full', 90);
-                break;
-
-            case 7:
-                $this->mirror();
-                $this->rotate('full', 90);
-                break;
-
-            case 8:
-                $this->rotate('full', 270);
-                break;
-            }
-
-            if ($this->_dirty) {
-                $this->_exif['Orientation'] = 1;
-                $this->data['full'] = $this->raw();
-                $this->_writeData();
-            }
-        }
-    }
-
-    /**
-     * Reset the image, removing all loaded views.
-     */
-    function reset()
-    {
-        $this->_image->reset();
-        $this->_loaded = array();
-    }
-
-    /**
-     * Deletes the specified cache file.
-     *
-     * If none is specified, deletes all of the cache files.
-     *
-     * @param string $view  Which cache file to delete.
-     */
-    function deleteCache($view = 'all')
-    {
-        /* Delete cached screen image. */
-        if ($view == 'all' || $view == 'screen') {
-            $GLOBALS['ansel_vfs']->deleteFile($this->getVFSPath('screen'),
-                                              $this->getVFSName('screen'));
-        }
-
-        /* Delete cached thumbnail. */
-        if ($view == 'all' || $view == 'thumb') {
-            $GLOBALS['ansel_vfs']->deleteFile($this->getVFSPath('thumb'),
-                                              $this->getVFSName('thumb'));
-        }
-
-        /* Delete cached mini image. */
-        if ($view == 'all' || $view == 'mini') {
-            $GLOBALS['ansel_vfs']->deleteFile($this->getVFSPath('mini'),
-                                              $this->getVFSName('mini'));
-        }
-
-        if ($view == 'all' || $view == 'prettythumb') {
-
-            /* No need to try to delete a hash we already removed */
-            $deleted = array();
-
-            /* Need to generate hashes for each possible style */
-            $styles = Horde::loadConfiguration('styles.php', 'styles', 'ansel');
-            foreach ($styles as $style) {
-                $hash =  md5($style['thumbstyle'] . '.' . $style['background']);
-                if (empty($deleted[$hash])) {
-                    $GLOBALS['ansel_vfs']->deleteFile($this->getVFSPath($hash),
-                                                      $this->getVFSName($hash));
-                    $deleted[$hash] = true;
-                }
-            }
-        }
-    }
-
-    /**
-     * Returns the raw data for the given view.
-     *
-     * @param string $view  Which view to return.
-     */
-    function raw($view = 'full')
-    {
-        if ($this->_dirty) {
-          return $this->_image->raw();
-        } else {
-            $this->load($view);
-            return $this->_data[$view];
-        }
-    }
-
-    /**
-     * Sends the correct HTTP headers to the browser to download this image.
-     *
-     * @param string $view  The view to download.
-     */
-    function downloadHeaders($view = 'full')
-    {
-        global $browser, $conf;
-
-        $filename = $this->filename;
-        if ($view != 'full') {
-            if ($ext = Horde_Mime_Magic::mimeToExt('image/' . $conf['image']['type'])) {
-                $filename .= '.' . $ext;
-            }
-        }
-
-        $browser->downloadHeaders($filename);
-    }
-
-    /**
-     * Display the requested view.
-     *
-     * @param string $view   Which view to display.
-     * @param string $style  Force use of this gallery style.
-     */
-    function display($view = 'full', $style = null)
-    {
-        if ($view == 'full' && !$this->_dirty) {
-
-            // Check full photo permissions
-            $gallery = $GLOBALS['ansel_storage']->getGallery($this->gallery);
-            if (is_a($gallery, 'PEAR_Error')) {
-                return $gallery;
-            }
-            if (!$gallery->canDownload()) {
-                return PEAR::RaiseError(sprintf(_("Access denied downloading photos from \"%s\"."), $gallery->get('name')));
-            }
-
-            $data = $GLOBALS['ansel_vfs']->read($this->getVFSPath('full'),
-                                                $this->getVFSName('full'));
-
-            if (is_a($data, 'PEAR_Error')) {
-                return $data;
-            }
-            echo $data;
-            return;
-        }
-
-        if (is_a($result = $this->load($view, $style), 'PEAR_Error')) {
-            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $result;
-        }
-
-        $this->_image->display();
-    }
-
-    /**
-     * Wraps the given view into a file.
-     *
-     * @param string $view  Which view to wrap up.
-     */
-    function toFile($view = 'full')
-    {
-        if (is_a(($result = $this->load($view)), 'PEAR_Error')) {
-            return $result;
-        }
-        return $this->_image->toFile($this->_dirty ? false : $this->_data[$view]);
-    }
-
-    /**
-     * Returns the dimensions of the given view.
-     *
-     * @param string $view  The view (size) to check dimensions for.
-     */
-    function getDimensions($view = 'full')
-    {
-        if (is_a(($result = $this->load($view)), 'PEAR_Error')) {
-            return $result;
-        }
-        return $this->_image->getDimensions();
-    }
-
-    /**
-     * Rotates the image.
-     *
-     * @param string $view The view (size) to work with.
-     * @param integer $angle  What angle to rotate the image by.
-     */
-    function rotate($view = 'full', $angle)
-    {
-        $this->load($view);
-        $this->_dirty = true;
-        $this->_image->rotate($angle);
-    }
-
-    function crop($x1, $y1, $x2, $y2)
-    {
-        $this->_dirty = true;
-        $this->_image->crop($x1, $y1, $x2, $y2);
-    }
-
-    /**
-     * Converts the image to grayscale.
-     *
-     * @param string $view The view (size) to work with.
-     */
-    function grayscale($view = 'full')
-    {
-        $this->load($view);
-        $this->_dirty = true;
-        $this->_image->grayscale();
-    }
-
-    /**
-     * Watermarks the image.
-     *
-     * @param string $view The view (size) to work with.
-     * @param string $watermark  String to use as the watermark.
-     */
-    function watermark($view = 'full', $watermark = null, $halign = null,
-                       $valign = null, $font = null)
-    {
-        if (empty($watermark)) {
-            $watermark = $GLOBALS['prefs']->getValue('watermark_text');
-        }
-
-        if (empty($halign)) {
-            $halign = $GLOBALS['prefs']->getValue('watermark_horizontal');
-        }
-
-        if (empty($valign)) {
-            $valign = $GLOBALS['prefs']->getValue('watermark_vertical');
-        }
-
-        if (empty($font)) {
-            $font = $GLOBALS['prefs']->getValue('watermark_font');
-        }
-
-        if (empty($watermark)) {
-            require_once 'Horde/Identity.php';
-            $identity = Identity::singleton();
-            $name = $identity->getValue('fullname');
-            if (empty($name)) {
-                $name = Horde_Auth::getAuth();
-            }
-            $watermark = sprintf(_("(c) %s %s"), date('Y'), $name);
-        }
-
-        $this->load($view);
-        $this->_dirty = true;
-        $params = array('text' => $watermark,
-                        'halign' => $halign,
-                        'valign' => $valign,
-                        'fontsize' => $font);
-        if (!empty($GLOBALS['conf']['image']['font'])) {
-            $params['font'] = $GLOBALS['conf']['image']['font'];
-        }
-        $this->_image->addEffect('TextWatermark', $params);
-    }
-
-    /**
-     * Flips the image.
-     *
-     * @param string $view The view (size) to work with.
-     */
-    function flip($view = 'full')
-    {
-        $this->load($view);
-        $this->_dirty = true;
-        $this->_image->flip();
-    }
-
-    /**
-     * Mirrors the image.
-     *
-     * @param string $view The view (size) to work with.
-     */
-    function mirror($view = 'full')
-    {
-        $this->load($view);
-        $this->_dirty = true;
-        $this->_image->mirror();
-    }
-
-    /**
-     * Returns this image's tags.
-     *
-     * @return mixed  An array of tags | PEAR_Error
-     * @see Ansel_Tags::readTags()
-     */
-    function getTags()
-    {
-        global $ansel_storage;
-
-        if (count($this->_tags)) {
-            return $this->_tags;
-        }
-        $gallery = $ansel_storage->getGallery($this->gallery);
-        if (is_a($gallery, 'PEAR_Error')) {
-            return $gallery;
-        }
-        if ($gallery->hasPermission(Horde_Auth::getAuth(), PERMS_READ)) {
-            $res = Ansel_Tags::readTags($this->id);
-            if (!is_a($res, 'PEAR_Error')) {
-                $this->_tags = $res;
-                return $this->_tags;
-            } else {
-                return $res;
-            }
-        } else {
-            return PEAR::raiseError(_("Access denied viewing this photo."));
-        }
-    }
-
-    /**
-     * Set/replace this image's tags.
-     *
-     * @param array $tags  An array of tag names to associate with this image.
-     */
-    function setTags($tags)
-    {
-        global $ansel_storage;
-
-        $gallery = $ansel_storage->getGallery(abs($this->gallery));
-        if ($gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
-            // Clear the local cache.
-            $this->_tags = array();
-            return Ansel_Tags::writeTags($this->id, $tags);
-        } else {
-            return PEAR::raiseError(_("Access denied adding tags to this photo."));
-        }
-    }
-
-    /**
-     * Get the Ansel_View_Image_Thumb object
-     *
-     * @param Ansel_Gallery $parent  The parent Ansel_Gallery object.
-     * @param string $style          A named gallery style to use.
-     * @param boolean $mini          Force the use of a mini thumbnail?
-     * @param array $params          Any additional parameters the Ansel_Tile
-     *                               object may need.
-     *
-     */
-    function getTile($parent = null, $style = null, $mini = false,
-                     $params = array())
-    {
-        if (!is_null($parent) && is_null($style)) {
-            $style = $parent->getStyle();
-        } else {
-            $style = Ansel::getStyleDefinition($style);
-        }
-
-        return Ansel_Tile_Image::getTile($this, $style, $mini, $params);
-    }
-
-    /**
-     * Get the image type for the requested view.
-     */
-    function getType($view = 'full')
-    {
-        if ($view == 'full') {
-           return $this->type;
-        } elseif ($view == 'screen') {
-            return 'image/jpg';
-        } else {
-            return 'image/' . $GLOBALS['conf']['image']['type'];
-        }
-    }
-
-    /**
-     * Return a hash key for the given view and style.
-     *
-     * @param string $view   The view (thumb, prettythumb etc...)
-     * @param string $style  The named style.
-     *
-     * @return string  A md5 hash suitable for use as a key.
-     */
-    function _getViewHash($view, $style = null)
-    {
-        global $ansel_storage;
-
-        // These views do not care about style...just return the $view value.
-        if ($view == 'screen' || $view == 'thumb' || $view == 'mini' ||
-            $view == 'full') {
-
-            return $view;
-        }
-        if (is_null($style)) {
-            $gallery = $ansel_storage->getGallery(abs($this->gallery));
-            if (is_a($gallery, 'PEAR_Error')) {
-                return $gallery;
-            }
-            $style = $gallery->getStyle();
-        } else {
-            $style = Ansel::getStyleDefinition($style);
-        }
-
-       $view = md5($style['thumbstyle'] . '.' . $style['background']);
-       return $view;
-    }
-
-    /**
-     * Get the image attributes from the backend.
-     *
-     * @param Ansel_Image $image  The image to retrieve attributes for.
-     *                            attributes for.
-     * @param boolean $format     Format the EXIF data. If false, the raw data
-     *                            is returned.
-     *
-     * @return array  The EXIF data.
-     * @static
-     */
-    function getAttributes($format = false)
-    {
-        $attributes = $GLOBALS['ansel_storage']->getImageAttributes($this->id);
-        $fields = Horde_Image_Exif::getFields();
-        $output = array();
-
-        foreach ($fields as $field => $data) {
-            if (!isset($attributes[$field])) {
-                continue;
-            }
-            $value = Horde_Image_Exif::getHumanReadable($field, Horde_String::convertCharset($attributes[$field], $GLOBALS['conf']['sql']['charset']));
-            if (!$format) {
-                $output[$field] = $value;
-            } else {
-                $description = isset($data['description']) ? $data['description'] : $field;
-                $output[] = '<td><strong>' . $description . '</strong></td><td>' . htmlspecialchars($value, ENT_COMPAT, Horde_Nls::getCharset()) . '</td>';
-            }
-        }
-
-        return $output;
-    }
-
-}
-
-/**
- * Class for interfacing with back end data storage.
- *
- * @author Michael J. Rubinsky <mrubinsk@horde.org>
- *
- * @package Ansel
- */
-class Ansel_Storage {
-
-    var $_scope = 'ansel';
-    var $_db = null;
-    var $galleries = array();
-
-    /**
-     * The Horde_Shares object to use for this scope.
-     *
-     * @var Horde_Share
-     */
-    var $shares = null;
-
-    /* Local cache of retrieved images */
-    var $images = array();
-
-    function Ansel_Storage($scope = null)
-    {
-        /* Check for a scope other than the default Ansel scope.*/
-        if (!is_null($scope)) {
-            $this->_scope = $scope;
-        }
-
-        /* This is the only supported share backend for Ansel */
-        $this->shares = Horde_Share::singleton($this->_scope,
-                                               'sql_hierarchical');
-
-        /* Ansel_Gallery is just a subclass of Horde_Share_Object */
-        $this->shares->_shareObject = 'Ansel_Gallery';
-
-        /* Database handle */
-        $this->_db = $GLOBALS['ansel_db'];
-    }
-
-   /**
-    * Create and initialise a new gallery object.
-    *
-    * @param array $attributes     The gallery attributes
-    * @param object Perms $perm    The permissions for the gallery if the
-    *                              defaults are not desirable.
-    * @param mixed  $parent       The gallery id of the parent (if any)
-    *
-    * @return Ansel_Gallery  A new gallery object or PEAR_Error.
-    */
-    function createGallery($attributes = array(), $perm = null, $parent = null)
-    {
-        /* Required values. */
-        if (empty($attributes['owner'])) {
-            $attributes['owner'] = Horde_Auth::getAuth();
-        }
-        if (empty($attributes['name'])) {
-            $attributes['name'] = _("Unnamed");
-        }
-        if (empty($attributes['desc'])) {
-            $attributes['desc'] = '';
-        }
-
-        /* Default values */
-        $attributes['default_type'] = isset($attributes['default_type']) ? $attributes['default_type'] : 'auto';
-        $attributes['default'] = isset($attributes['default']) ? (int)$attributes['default'] : 0;
-        $attributes['default_prettythumb'] = isset($attributes['default_prettythumb']) ? $attributes['default_prettythumb'] : '';
-        $attributes['style'] = isset($attributes['style']) ? $attributes['style'] : $GLOBALS['prefs']->getValue('default_gallerystyle');
-        $attributes['category'] = isset($attributes['category']) ? $attributes['category'] : $GLOBALS['prefs']->getValue('default_category');
-        $attributes['date_created'] = time();
-        $attributes['last_modified'] = $attributes['date_created'];
-        $attributes['images'] = isset($attributes['images']) ? (int)$attributes['images'] : 0;
-        $attributes['slug'] = isset($attributes['slug']) ? $attributes['slug'] : '';
-        $attributes['age'] = isset($attributes['age']) ? (int)$attributes['age'] : 0;
-        $attributes['download'] = isset($attributes['download']) ? $attributes['download'] : $GLOBALS['prefs']->getValue('default_download');
-        $attributes['view_mode'] = isset($attributes['view_mode']) ? $attributes['view_mode'] : 'Normal';
-        $attributes['passwd'] = isset($attributes['passwd']) ? $attributes['passwd'] : '';
-
-        /* Don't pass tags to the share creation method */
-        if (isset($attributes['tags'])) {
-            $tags = $attributes['tags'];
-            unset($attributes['tags']);
-        } else {
-            $tags = array();
-        }
-
-        /* Check for slug uniqueness */
-        if (!empty($attributes['slug']) &&
-            $this->slugExists($attributes['slug'])) {
-            return PEAR::raiseError(sprintf(_("The slug \"%s\" already exists."),
-                                            $attributes['slug']));
-        }
-
-        /* Create the gallery */
-        $gallery = $this->shares->newShare('');
-        if (is_a($gallery, 'PEAR_Error')) {
-            Horde::logMessage($gallery, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $gallery;
-        }
-        Horde::logMessage('New Ansel_Gallery object instantiated', __FILE__, __LINE__, PEAR_LOG_DEBUG);
-
-        /* Set the gallery's parent if needed */
-        if (!is_null($parent)) {
-            $result = $gallery->setParent($parent);
-
-            /* Clear the parent from the cache */
-            if ($GLOBALS['conf']['ansel_cache']['usecache']) {
-                $GLOBALS['cache']->expire('Ansel_Gallery' . $parent);
-            }
-            if (is_a($result, 'PEAR_Error')) {
-                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-                return $result;
-            }
-        }
-
-        /* Fill up the new gallery */
-        // TODO: New private method to bulk load these (it's done this way
-        // since the data is stored in the Share_Object class keyed by the
-        // DB specific fields and set() translates them.
-        foreach ($attributes as $key => $value) {
-            $gallery->set($key, $value);
-        }
-
-        /* Save it to storage */
-        $result = $this->shares->addShare($gallery);
-        if (is_a($result, 'PEAR_Error')) {
-            $error = sprintf(_("The gallery \"%s\" could not be created: %s"),
-                             $attributes['name'], $result->getMessage());
-            Horde::logMessage($error, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return PEAR::raiseError($error);
-        }
-
-        /* Convenience */
-        $gallery->id = $gallery->getId();
-
-        /* Add default permissions. */
-        if (empty($perm)) {
-            $perm = $gallery->getPermission();
-
-            /* Default permissions for logged in users */
-            switch ($GLOBALS['prefs']->getValue('default_permissions')) {
-            case 'read':
-                $perms = PERMS_SHOW | PERMS_READ;
-                break;
-            case 'edit':
-                $perms = PERMS_SHOW | PERMS_READ | PERMS_EDIT;
-                break;
-            case 'none':
-                $perms = 0;
-                break;
-            }
-            $perm->addDefaultPermission($perms, false);
-
-            /* Default guest permissions */
-            switch ($GLOBALS['prefs']->getValue('guest_permissions')) {
-            case 'read':
-                $perms = PERMS_SHOW | PERMS_READ;
-                break;
-            case 'none':
-            default:
-                $perms = 0;
-                break;
-            }
-            $perm->addGuestPermission($perms, false);
-
-            /* Default user groups permissions */
-            switch ($GLOBALS['prefs']->getValue('group_permissions')) {
-            case 'read':
-                $perms = PERMS_SHOW | PERMS_READ;
-                break;
-            case 'edit':
-                $perms = PERMS_SHOW | PERMS_READ | PERMS_EDIT;
-                break;
-            case 'delete':
-                $perms = PERMS_SHOW | PERMS_READ | PERMS_EDIT | PERMS_DELETE;
-                break;
-            case 'none':
-            default:
-                $perms = 0;
-                break;
-            }
-
-            if ($perms) {
-                $groups = Group::singleton();
-                $group_list = $groups->getGroupMemberships(Horde_Auth::getAuth());
-                if (!is_a($group_list, 'PEAR_Error') && count($group_list)) {
-                    foreach ($group_list as $group_id => $group_name) {
-                        $perm->addGroupPermission($group_id, $perms, false);
-                    }
-                }
-            }
-        }
-        $gallery->setPermission($perm, true);
-
-        /* Initial tags */
-        if (count($tags)) {
-            $gallery->setTags($tags);
-        }
-
-        return $gallery;
-    }
-
-    /**
-     * Check that a slug exists.
-     *
-     * @param string $slug  The slug name
-     *
-     * @return integer  The share_id the slug represents, or 0 if not found.
-     */
-    function slugExists($slug)
-    {
-        // An empty slug should never match.
-        if (!strlen($slug)) {
-            return 0;
-        }
-
-        $stmt = $this->_db->prepare('SELECT share_id FROM '
-            . $this->shares->_table . ' WHERE attribute_slug = ?');
-
-        if (is_a($stmt, 'PEAR_Error')) {
-            Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return 0;
-        }
-
-        $result = $stmt->execute($slug);
-        if (is_a($result, 'PEAR_Error')) {
-            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-        }
-        if (!$result->numRows()) {
-            return 0;
-        }
-
-        $slug = $result->fetchRow();
-
-        $result->free();
-        $stmt->free();
-
-        return $slug[0];
-    }
-
-    /**
-     * Retrieve an Ansel_Gallery given the gallery's slug
-     *
-     * @param string $slug  The gallery slug
-     * @param array $overrides  An array of attributes that should be overridden
-     *                          when the gallery is returned.
-     *
-     * @return mixed  Ansel_Gallery object | PEAR_Error
-     */
-    function &getGalleryBySlug($slug, $overrides = array())
-    {
-        $id = $this->slugExists($slug);
-        if ($id) {
-            return $this->getGallery($id, $overrides);
-        } else {
-            return PEAR::raiseError(sprintf(_("Gallery %s not found."), $slug));
-        }
-     }
-
-    /**
-     * Retrieve an Ansel_Gallery given the share id
-     *
-     * @param integer $gallery_id  The share_id to fetch
-     * @param array $overrides     An array of attributes that should be
-     *                             overridden when the gallery is returned.
-     *
-     * @return mixed  Ansel_Gallery | PEAR_Error
-     */
-    function &getGallery($gallery_id, $overrides = array())
-    {
-        // avoid cache server hits
-        if (isset($this->galleries[$gallery_id]) && !count($overrides)) {
-            return $this->galleries[$gallery_id];
-        }
-
-       if (!count($overrides) && $GLOBALS['conf']['ansel_cache']['usecache'] &&
-           ($gallery = $GLOBALS['cache']->get('Ansel_Gallery' . $gallery_id, $GLOBALS['conf']['cache']['default_lifetime'])) !== false) {
-
-               $this->galleries[$gallery_id] = unserialize($gallery);
-
-               return $this->galleries[$gallery_id];
-       }
-
-       $result = &$this->shares->getShareById($gallery_id);
-       if (is_a($result, 'PEAR_Error')) {
-           return $result;
-       }
-       $this->galleries[$gallery_id] = &$result;
-
-       // Don't cache if we have overridden anything
-       if (!count($overrides)) {
-           if ($GLOBALS['conf']['ansel_cache']['usecache']) {
-               $GLOBALS['cache']->set('Ansel_Gallery' . $gallery_id, serialize($result));
-           }
-       } else {
-           foreach ($overrides as $key => $value) {
-               $this->galleries[$gallery_id]->set($key, $value, false);
-           }
-       }
-        return $this->galleries[$gallery_id];
-    }
-
-    /**
-     * Retrieve an array of Ansel_Gallery objects for the given slugs.
-     *
-     * @param array $slugs  The gallery slugs
-     *
-     * @return mixed  Array of Ansel_Gallery objects | PEAR_Error
-     */
-    function getGalleriesBySlugs($slugs)
-    {
-        $sql = 'SELECT share_id FROM ' . $this->shares->_table
-            . ' WHERE attribute_slug IN (' . str_repeat('?, ', count($slugs) - 1) . '?)';
-
-        $stmt = $this->shares->_db->prepare($sql);
-        if (is_a($stmt, 'PEAR_Error')) {
-            return $stmt;
-        }
-        $result = $stmt->execute($slugs);
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        }
-        $ids = array_values($result->fetchCol());
-        $shares = $this->shares->getShares($ids);
-
-        $stmt->free();
-        $result->free();
-
-        return $shares;
-    }
-
-    /**
-     * Retrieve an array of Ansel_Gallery objects for the requested ids
-     */
-    function getGalleries($ids)
-    {
-        return $this->shares->getShares($ids);
-    }
-
-    /**
-     * Empties a gallery of all images.
-     *
-     * @param Ansel_Gallery $gallery  The ansel gallery to empty.
-     */
-    function emptyGallery($gallery)
-    {
-        $images = $gallery->listImages();
-        foreach ($images as $image) {
-            // Pretend we are a stack so we don't update the images count
-            // for every image deletion, since we know the end result will
-            // be zero.
-            $gallery->removeImage($image, true);
-        }
-        $gallery->set('images', 0, true);
-
-        // Clear the OtherGalleries widget cache
-        if ($GLOBALS['conf']['ansel_cache']['usecache']) {
-            $GLOBALS['cache']->expire('Ansel_OtherGalleries' . $gallery->get('owner'));
-        }
-
-    }
-
-    /**
-     * Removes an Ansel_Gallery.
-     *
-     * @param Ansel_Gallery $gallery  The gallery to delete
-     *
-     * @return mixed  True || PEAR_Error
-     */
-    function removeGallery($gallery)
-    {
-        /* Get any children and empty them */
-        $children = $gallery->getChildren(null, true);
-        if (is_a($children, 'PEAR_Error')) {
-            return $children;
-        }
-        foreach ($children as $child) {
-            $this->emptyGallery($child);
-            $child->setTags(array());
-        }
-
-        /* Now empty the selected gallery of images */
-        $this->emptyGallery($gallery);
-
-        /* Clear all the tags. */
-        $gallery->setTags(array());
-
-        /* Get the parent, if it exists, before we delete the gallery. */
-        $parent = $gallery->getParent();
-        $id = $gallery->id;
-
-        /* Delete the gallery from storage */
-        $result = $this->shares->removeShare($gallery);
-        if (is_a($result, 'PEAR_Error')) {
-            return $result;
-        }
-
-        /* Expire the cache */
-        if ($GLOBALS['conf']['ansel_cache']['usecache']) {
-            $GLOBALS['cache']->expire('Ansel_Gallery' . $id);
-        }
-        unset($this->galleries[$id]);
-
-        /* See if we need to clear the has_subgalleries field */
-        if (is_a($parent, 'Ansel_Gallery')) {
-            if (!$parent->countChildren(PERMS_SHOW, false)) {
-                $parent->set('has_subgalleries', 0, true);
-
-                if ($GLOBALS['conf']['ansel_cache']['usecache']) {
-                    $GLOBALS['cache']->expire('Ansel_Gallery' . $parent->id);
-                }
-                unset($this->galleries[$id]);
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns the image corresponding to the given id.
-     *
-     * @param integer $id  The ID of the image to retrieve.
-     *
-     * @return Ansel_Image  The image object corresponding to the given name.
-     */
-    function &getImage($id)
-    {
-        if (isset($this->images[$id])) {
-            return $this->images[$id];
-        }
-
-        $q = $this->_db->prepare('SELECT ' . $this->_getImageFields() . ' FROM ansel_images WHERE image_id = ?');
-        if (is_a($q, 'PEAR_Error')) {
-            Horde::logMessage($q, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $q;
-        }
-        $result = $q->execute((int)$id);
-        if (is_a($result, 'PEAR_Error')) {
-            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $result;
-        }
-        $image = $result->fetchRow(MDB2_FETCHMODE_ASSOC);
-        $q->free();
-        $result->free();
-        if (is_null($image)) {
-            return PEAR::raiseError(_("Photo not found"));
-        } elseif (is_a($image, 'PEAR_Error')) {
-            Horde::logMessage($image, __FILE__, __LINE__, PEAR_LOG_ERR);
-            return $image;
-        } else {
-            $image['image_filename'] = Horde_String::convertCharset($image['image_filename'], $GLOBALS['conf']['sql']['charset']);
-            $image['image_caption'] = Horde_String::convertCharset($image['image_caption'], $GLOBALS['conf']['sql']['charset']);
-            $this->images[$id] = new Ansel_Image($image);
-
-            return $this->images[$id];
-        }
-    }
-
-    /**
-     * Returns the images corresponding to the given ids.
-     *
-     * @param array $ids  An array of image ids.
-     *
-     * @return array of Ansel_Image objects.
-     */
-    function getImages($ids, $preserve_order = false)
-    {
-        if (is_array($ids) && count($ids) > 0) {
-            $sql = 'SELECT ' . $this->_getImageFields() . ' FROM ansel_images WHERE image_id IN (';
-            $i = 1;
-            $cnt = count($ids);
-            foreach ($ids as $id) {
-                $sql .= (int)$id . (($i++ < $cnt) ? ',' : ');');
-            }
-
-            $images = $this->_db->query($sql);
-            if (is_a($images, 'PEAR_Error')) {
-                return $images;
-            } elseif ($images->numRows() == 0) {
-                $images->free();
-                return PEAR::raiseError(_("Photos not found"));
-            }
-
-            $return = array();
-            while ($image = $images->fetchRow(MDB2_FETCHMODE_ASSOC)) {
-                $image['image_filename'] = Horde_String::convertCharset($image['image_filename'], $GLOBALS['conf']['sql']['charset']);
-                $image['image_caption'] = Horde_String::convertCharset($image['image_caption'], $GLOBALS['conf']['sql']['charset']);
-                $return[$image['image_id']] = new Ansel_Image($image);
-                $this->images[(int)$image['image_id']] = &$return[$image['image_id']];
-            }
-            $images->free();
-
-            /* Need to get comment counts if comments are enabled */
-            $ccounts = $this->_getImageCommentCounts(array_keys($return));
-            if (!is_a($ccounts, 'PEAR_Error') && count($ccounts)) {
-                foreach ($return as $key => $image) {
-                    $return[$key]->commentCount = (!empty($ccounts[$key]) ? $ccounts[$key] : 0);
-                }
-            }
-
-            /* Preserve the order the ids were passed in) */
-            if ($preserve_order) {
-                foreach ($ids as $id) {
-                    $ordered[$id] = $return[$id];
-                }
-                return $ordered;
-            }
-            return $return;
-        } else {
-            return array();
-        }
-    }
-
-    function _getImageCommentCounts($ids)
-    {
-        global $conf, $registry;
-
-        /* Need to get comment counts if comments are enabled */
-        if (($conf['comments']['allow'] == 'all' || ($conf['comments']['allow'] == 'authenticated' && Horde_Auth::getAuth())) &&
-            $registry->hasMethod('forums/numMessagesBatch')) {
-
-            return $registry->call('forums/numMessagesBatch',
-                                   array($ids, 'ansel'));
-        }
-
-        return array();
-    }
-
-    /**
-     * Return a list of image ids of the most recently added images.
-     *
-     * @param array $galleries  An array of gallery ids to search in. If
-     *                          left empty, will search all galleries
-     *                          with PERMS_SHOW.
-     * @param integer $limit    The maximum number of images to return
-     * @param string $slugs     An array of gallery slugs.
-     * @param string $where     Additional where clause
-     *
-     * @return array An array of Ansel_Image objects
-     */
-    function getRecentImages($galleries = array(), $limit = 10, $slugs = array())
-    {
-        $results = array();
-
-        if (!count($galleries) && !count($slugs)) {
-            $sql = 'SELECT DISTINCT ' . $this->_getImageFields('i') . ' FROM ansel_images i, '
-            . str_replace('WHERE' , ' WHERE i.gallery_id = s.share_id AND (', substr($this->shares->_getShareCriteria(Horde_Auth::getAuth()), 5)) . ')';
-        } elseif (!count($slugs) && count($galleries)) {
-            // Searching by gallery_id
-            $sql = 'SELECT ' . $this->_getImageFields() . ' FROM ansel_images '
-                   . 'WHERE gallery_id IN ('
-                   . str_repeat('?, ', count($galleries) - 1) . '?) ';
-        } elseif (count($slugs)) {
-            // Searching by gallery_slug so we need to join the share table
-            $sql = 'SELECT ' . $this->_getImageFields() . ' FROM ansel_images LEFT JOIN '
-                . $this->shares->_table . ' ON ansel_images.gallery_id = '
-                . $this->shares->_table . '.share_id ' . 'WHERE attribute_slug IN ('
-                . str_repeat('?, ', count($slugs) - 1) . '?) ';
-        } else {
-            return array();
-        }
-
-        $sql .= ' ORDER BY image_uploaded_date DESC LIMIT ' . (int)$limit;
-        $query = $this->_db->prepare($sql);
-        if (is_a($query, 'PEAR_Error')) {
-            return $query;
-        }
-
-        if (count($slugs)) {
-            $images = $query->execute($slugs);
-        } else {
-            $images = $query->execute($galleries);
-        }
-        $query->free();
-        if (is_a($images, 'PEAR_Error')) {
-            return $images;
-        } elseif ($images->numRows() == 0) {
-            return array();
-        }
-
-        while ($image = $images->fetchRow(MDB2_FETCHMODE_ASSOC)) {
-            $image['image_filename'] = Horde_String::convertCharset($image['image_filename'], $GLOBALS['conf']['sql']['charset']);
-            $image['image_caption'] = Horde_String::convertCharset($image['image_caption'], $GLOBALS['conf']['sql']['charset']);
-            $results[] = new Ansel_Image($image);
-        }
-
-        $images->free();
-        return $results;
-    }
-
-    /**
-     * Check if a gallery exists. Need to do this here instead of Horde_Share
-     * since Horde_Share::exists() takes a share_name, not a share_id plus we
-     * might also be checking by gallery_slug and this is more efficient than
-     * a listShares() call for one gallery.
-     *
-     * @param integer $gallery_id  The gallery id
-     * @param string  $slug        The gallery slug
-     *
-     * @return mixed  true | false | PEAR_Error
-     */
-    function galleryExists($gallery_id, $slug = null)
-    {
-        if (empty($slug)) {
-            return (bool)$this->_db->queryOne(
-                'SELECT COUNT(share_id) FROM ' . $this->shares->_table
-                . ' WHERE share_id = ' . (int)$gallery_id);
-        } else {
-            return (bool)$this->slugExists($slug);
-        }
-    }
-
-   /**
-    * Return a list of categories containing galleries with the given
-    * permissions for the current user.
-    *
-    * @param integer $perm   The level of permissions required.
-    * @param integer $from   The gallery to start listing at.
-    * @param integer $count  The number of galleries to return.
-    *
-    * @return mixed  List of categories | PEAR_Error
-    */
-    function listCategories($perm = PERMS_SHOW, $from = 0, $count = 0)
-    {
-        $sql = 'SELECT DISTINCT attribute_category FROM '
-               . $this->shares->_table;
-        $results = $this->shares->_db->query($sql);
-        if (is_a($results, 'PEAR_Error')) {
-            return $results;
-        }
-        $all_categories = $results->fetchCol('attribute_category');
-        $results->free();
-        if (count($all_categories) < $from) {
-            return array();
-        } else {
-            $categories = array();
-            foreach ($all_categories as $category) {
-                $categories[] = Horde_String::convertCharset(
-                    $category, $GLOBALS['conf']['sql']['charset']);
-            }
-            if ($count > 0) {
-                return array_slice($categories, $from, $count);
-            } else {
-                return array_slice($categories, $from);
-            }
-        }
-    }
-
-    function countCategories($perms = PERMS_SHOW)
-    {
-        return count($this->listCategories($perms));
-    }
-
-   /**
-    * Return the count of galleries that the user has specified permissions to
-    * and that match any of the requested attributes.
-    *
-    * @param string  $userid       The user to check access for.
-    * @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/values pairs or a gallery owner
-    *                              username.
-    * @param string  $parent       The parent share to start counting at.
-    * @param boolean $allLevels    Return all levels, or just the direct
-    *                              children of $parent? Defaults to all levels.
-    */
-    function countGalleries($userid, $perm = PERMS_SHOW, $attributes = null,
-                            $parent = null, $allLevels = true)
-    {
-        static $counts;
-
-        if (is_a($parent, 'Ansel_Gallery')) {
-            $parent_id = $parent->getId();
-        } else {
-            $parent_id = $parent;
-        }
-
-        $key = "$userid,$perm,$parent_id,$allLevels"
-               . serialize($attributes);
-        if (isset($counts[$key])) {
-            return $counts[$key];
-        }
-
-        $count = $this->shares->countShares($userid, $perm, $attributes,
-                                            $parent, $allLevels);
-
-        $counts[$key] = $count;
-
-        return $count;
-    }
-
-   /**
-    * Retrieves the current user's gallery list from storage.
-    *
-    * @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/values pairs or a gallery owner
-    *                              username.
-    * @param mixed   $parent       The parent gallery to start listing at.
-    *                              (Ansel_Gallery, 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 string  $sort_by      The field to order the results by.
-    * @param integer $direction    Sort direction:
-    *                               0 - ascending
-    *                               1 - descending
-    *
-    * @return mixed An array of Ansel_Gallery objects | PEAR_Error
-    */
-    function listGalleries($perm = PERMS_SHOW,
-                           $attributes = null,
-                           $parent = null,
-                           $allLevels = true,
-                           $from = 0,
-                           $count = 0,
-                           $sort_by = null,
-                           $direction = 0)
-    {
-        return $this->shares->listShares(Horde_Auth::getAuth(), $perm, $attributes,
-                                         $from, $count, $sort_by, $direction,
-                                         $parent, $allLevels);
-    }
-
-    /**
-     * Retrieve json data for an arbitrary list of image ids, not necessarily
-     * from the same gallery.
-     *
-     * @param array $images        An array of image ids
-     * @param string $style        A named gallery style to force if requesting
-     *                             pretty thumbs.
-     * @param boolean $full        Generate full urls
-     * @param string $image_view   Which image view to use? screen, thumb etc..
-     * @param boolean $view_links  Include links to the image view
-     *
-     * @return string  The json data || PEAR_Error
-     */
-    function getImageJson($images, $style = null, $full = false,
-                          $image_view = 'mini', $view_links = false)
-    {
-        $galleries = array();
-        if (is_null($style)) {
-            $style = 'ansel_default';
-        }
-
-        $json = array();
-
-        foreach ($images as $id) {
-            $image = $this->getImage($id);
-            if (!is_a($image, 'PEAR_Error')) {
-                $gallery_id = abs($image->gallery);
-
-                if (empty($galleries[$gallery_id])) {
-                    $galleries[$gallery_id]['gallery'] = $GLOBALS['ansel_storage']->getGallery($gallery_id);
-                    if (is_a($galleries[$gallery_id]['gallery'], 'PEAR_Error')) {
-                        return $galleries[$gallery_id];
-                    }
-                }
-
-                // Any authentication that needs to take place for any of the
-                // images included here MUST have already taken place or the
-                // image will not be incldued in the output.
-                if (!isset($galleries[$gallery_id]['perm'])) {
-                    $galleries[$gallery_id]['perm'] =
-                        ($galleries[$gallery_id]['gallery']->hasPermission(Horde_Auth::getAuth(), PERMS_READ) &&
-                         $galleries[$gallery_id]['gallery']->isOldEnough() &&
-                         !$galleries[$gallery_id]['gallery']->hasPasswd());
-                }
-
-                if ($galleries[$gallery_id]['perm']) {
-                    $data = array(Ansel::getImageUrl($image->id, $image_view, $full, $style),
-                        htmlspecialchars($image->filename, ENT_COMPAT, Horde_Nls::getCharset()),
-                        Horde_Text_Filter::filter($image->caption, 'text2html', array('parselevel' => Horde_Text_Filter_Text2html::MICRO_LINKURL)),
-                        $image->id,
-                        0);
-
-                    if ($view_links) {
-                        $data[] = Ansel::getUrlFor('view',
-                            array('gallery' => $image->gallery,
-                                  'image' => $image->id,
-                                  'view' => 'Image',
-                                  'slug' => $galleries[$gallery_id]['gallery']->get('slug')),
-                            $full);
-
-                        $data[] = Ansel::getUrlFor('view',
-                            array('gallery' => $image->gallery,
-                                  'slug' => $galleries[$gallery_id]['gallery']->get('slug'),
-                                  'view' => 'Gallery'),
-                            $full);
-                    }
-
-                    $json[] = $data;
-                }
-            }
-        }
-
-        if (count($json)) {
-            return Horde_Serialize::serialize($json, Horde_Serialize::JSON, Horde_Nls::getCharset());
-        } else {
-            return '';
-        }
-    }
-
-    /**
-     * Returns a random Ansel_Gallery from a list fitting the search criteria.
-     *
-     * @see Ansel_Storage::listGalleries()
-     */
-    function getRandomGallery($perm = PERMS_SHOW, $attributes = null,
-                              $parent = null, $allLevels = true)
-    {
-        $num_galleries = $this->countGalleries(Horde_Auth::getAuth(), $perm,
-                                               $attributes, $parent,
-                                               $allLevels);
-        if (!$num_galleries) {
-            return $num_galleries;
-        }
-
-        $galleries = $this->listGalleries($perm, $attributes, $parent,
-                                          $allLevels,
-                                          rand(0, $num_galleries - 1),
-                                          1);
-        $gallery = array_pop($galleries);
-        return $gallery;
-    }
-
-    /**
-     * Lists a slice of the image ids in the given gallery.
-     *
-     * @param integer $gallery_id  The gallery to list from.
-     * @param integer $from        The image to start listing.
-     * @param integer $count       The numer of images to list.
-     * @param mixed $fields        The fields to return (either an array of
-     *                             fileds or a single string).
-     * @param string $where        A SQL where clause ($gallery_id will be
-     *                             ignored if this is non-empty).
-     * @param mixed $sort          The field(s) to sort by.
-     *
-     * @return mixed  An array of image_ids | PEAR_Error
-     */
-    function listImages($gallery_id, $from = 0, $count = 0,
-                        $fields = 'image_id', $where = '', $sort = 'image_sort')
-    {
-        if (is_array($fields)) {
-            $field_count = count($fields);
-            $fields = implode(', ', $fields);
-        } elseif ($fields == '*') {
-            // The count is not important, as long as it's > 1
-            $field_count = 2;
-        } else {
-            $field_count = substr_count($fields, ',') + 1;
-        }
-
-        if (is_array($sort)) {
-            $sort = implode(', ', $sort);
-        }
-
-        if (!empty($where)) {
-            $query_where = 'WHERE ' . $where;
-        } else {
-            $query_where = 'WHERE gallery_id = ' . $gallery_id;
-        }
-        $this->_db->setLimit($count, $from);
-        $sql = 'SELECT ' . $fields . ' FROM ansel_images ' . $query_where . ' ORDER BY ' . $sort;
-        Horde::logMessage('Query by Ansel_Storage::listImages: ' . $sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
-        $results = $this->_db->query('SELECT ' . $fields . ' FROM ansel_images '
-            . $query_where . ' ORDER BY ' . $sort);
-        if (is_a($results, 'PEAR_Error')) {
-            return $results;
-        }
-        if ($field_count > 1) {
-            return $results->fetchAll(MDB2_FETCHMODE_ASSOC, true, true, false);
-        } else {
-            return $results->fetchCol();
-        }
-    }
-
-    /**
-     * Return images' geolocation data.
-     *
-     * @param array $image_ids  An array of image_ids to look up.
-     * @param integer $gallery  A gallery id. If this is provided, will return
-     *                          all images in the gallery that have geolocation
-     *                          data ($image_ids would be ignored).
-     *
-     * @return mixed An array of geodata || PEAR_Error
-     */
-    function getImagesGeodata($image_ids = array(), $gallery = null)
-    {
-        if ((!is_array($image_ids) || count($image_ids) == 0) && empty($gallery)) {
-            return array();
-        }
-
-        if (!empty($gallery)) {
-            $where = 'gallery_id = ' . (int)$gallery . ' AND LENGTH(image_latitude) > 0';
-        } elseif (count($image_ids) > 0) {
-            $where = 'image_id IN(' . implode(',', $image_ids) . ') AND LENGTH(image_latitude) > 0';
-        } else {
-            return array();
-        }
-
-        return $this->listImages(0, 0, 0, array('image_id as id', 'image_id', 'image_latitude', 'image_longitude', 'image_location'), $where);
-    }
-
-    /**
-     * Get image attribtues from ansel_image_attributes table
-     *
-     * @param $image_id
-     * @return unknown_type
-     */
-    function getImageAttributes($image_id)
-    {
-        return $GLOBALS['ansel_db']->queryAll('SELECT attr_name, attr_value FROM ansel_image_attributes WHERE image_id = ' . (int)$image_id, null, MDB2_FETCHMODE_ASSOC, true);
-    }
-
-    /**
-     * Like getRecentImages, but returns geotag data for the most recently added
-     * images from the current user. Useful for providing images to help locate
-     * images at the same place.
-     */
-    function getRecentImagesGeodata($user = null, $start = 0, $count = 8)
-    {
-        $galleries = $this->listGalleries('PERMS_EDIT', $user);
-        $where = 'gallery_id IN(' . implode(',', array_keys($galleries)) . ') AND LENGTH(image_latitude) > 0 GROUP BY image_latitude, image_longitude';
-        return $this->listImages(0, $start, $count, array('image_id as id', 'image_id', 'gallery_id', 'image_latitude', 'image_longitude', 'image_location'), $where, 'image_geotag_date DESC');
-    }
-
-    function searchLocations($search = '')
-    {
-        $sql = 'SELECT DISTINCT image_location, image_latitude, image_longitude'
-            . ' FROM ansel_images WHERE image_location LIKE "' . $search . '%"';
-        $results = $this->_db->query($sql);
-        if (is_a($results, 'PEAR_Error')) {
-            return $results;
-        }
-
-        return $results->fetchAll(MDB2_FETCHMODE_ASSOC, true, true, false);
-    }
-
-    /**
-     * Helper function to get a string of field names
-     *
-     * @return string
-     */
-    function _getImageFields($alias = '')
-    {
-        $fields = array('image_id', 'gallery_id', 'image_filename', 'image_type',
-                        'image_caption', 'image_uploaded_date', 'image_sort',
-                        'image_faces', 'image_original_date', 'image_latitude',
-                        'image_longitude', 'image_location', 'image_geotag_date');
-        if (!empty($alias)) {
-            foreach ($fields as $field) {
-                $new[] = $alias . '.' . $field;
-            }
-            return implode(', ', $new);
-        }
-
-        return implode(', ', $fields);
-    }
-
-}
diff --git a/ansel/lib/Gallery.php b/ansel/lib/Gallery.php
new file mode 100644 (file)
index 0000000..481eda5
--- /dev/null
@@ -0,0 +1,925 @@
+<?php
+/**
+ * Class to encapsulate a single gallery. Implemented as an extension of
+ * the Horde_Share_Object class.
+ *
+ * Copyright 2001-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  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Ansel
+ */
+class Ansel_Gallery extends Horde_Share_Object_sql_hierarchical
+{
+    /**
+     * Cache the Gallery Id - to match the Ansel_Image interface
+     */
+    var $id;
+
+    /**
+     * The gallery mode helper
+     *
+     * @var Ansel_Gallery_Mode object
+     */
+    var $_modeHelper;
+
+    /**
+     *
+     */
+    function __sleep()
+    {
+        $properties = get_object_vars($this);
+        unset($properties['_shareOb']);
+        unset($properties['_modeHelper']);
+        $properties = array_keys($properties);
+        return $properties;
+    }
+
+    function __wakeup()
+    {
+        $this->setShareOb($GLOBALS['ansel_storage']->shares);
+        $mode = $this->get('view_mode');
+        $this->_setModeHelper($mode);
+    }
+
+    /**
+     * The Ansel_Gallery constructor.
+     *
+     * @param string $name  The name of the gallery
+     */
+    function Ansel_Gallery($attributes = array())
+    {
+        /* Existing gallery? */
+        if (!empty($attributes['share_id'])) {
+            $this->id = (int)$attributes['share_id'];
+        }
+
+        /* Pass on up the chain */
+        parent::Horde_Share_Object_sql_hierarchical($attributes);
+        $this->setShareOb($GLOBALS['ansel_storage']->shares);
+        $mode = isset($attributes['attribute_view_mode']) ? $attributes['attribute_view_mode'] : 'Normal';
+        $this->_setModeHelper($mode);
+    }
+
+    /**
+     * Check for special capabilities of this gallery.
+     *
+     */
+    function hasFeature($feature)
+    {
+
+        // First check for purely Ansel_Gallery features
+        // Currently we have none of these.
+
+        // Delegate to the modeHelper
+        return $this->_modeHelper->hasFeature($feature);
+
+    }
+
+    /**
+     * Simple factory to retrieve the proper mode object.
+     *
+     * @param string $type  The mode to use
+     *
+     * @return Ansel_Gallery_Mode object
+     */
+    function _setModeHelper($type = 'Normal')
+    {
+        $type = basename($type);
+        $class = 'Ansel_GalleryMode_' . $type;
+        $this->_modeHelper = new $class($this);
+        $this->_modeHelper->init();
+    }
+
+    /**
+     * Checks if the user can download the full photo
+     *
+     * @return boolean  Whether or not user can download full photos
+     */
+    function canDownload()
+    {
+        if (Horde_Auth::getAuth() == $this->data['share_owner'] || Horde_Auth::isAdmin('ansel:admin')) {
+            return true;
+        }
+
+        switch ($this->data['attribute_download']) {
+        case 'all':
+            return true;
+
+        case 'authenticated':
+            return Horde_Auth::isAuthenticated();
+
+        case 'edit':
+            return $this->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT);
+
+        case 'hook':
+            return Horde::callHook('_ansel_hook_can_download', array($this->id));
+
+        default:
+            return false;
+        }
+    }
+
+    /**
+     * Saves any changes to this object to the backend permanently.
+     *
+     * @return mixed true || PEAR_Error on failure.
+     */
+    function _save()
+    {
+        // Check for invalid characters in the slug.
+        if (!empty($this->data['attribute_slug']) &&
+            preg_match('/[^a-zA-Z0-9_@]/', $this->data['attribute_slug'])) {
+
+            return PEAR::raiseError(
+                sprintf(_("Could not save gallery, the slug, \"%s\", contains invalid characters."),
+                        $this->data['attribute_slug']));
+        }
+
+        // Check for slug uniqueness
+        $slugGalleryId = $GLOBALS['ansel_storage']->slugExists($this->data['attribute_slug']);
+        if ($slugGalleryId > 0 && $slugGalleryId <> $this->id) {
+            return PEAR::raiseError(sprintf(_("Could not save gallery, the slug, \"%s\", already exists."),
+                                            $this->data['attribute_slug']));
+        }
+
+        if ($GLOBALS['conf']['ansel_cache']['usecache']) {
+            $GLOBALS['cache']->expire('Ansel_Gallery' . $this->id);
+        }
+        return parent::_save();
+    }
+
+    /**
+     * Update the gallery image count.
+     *
+     * @param integer $images      Number of images in action
+     * @param boolean $add         Action to take (add or remove)
+     * @param integer $gallery_id  Gallery id to update images for
+     */
+    function _updateImageCount($images, $add = true, $gallery_id = null)
+    {
+        // We do the query directly here to avoid having to instantiate a
+        // gallery object just to increment/decrement one value in the table.
+        $sql = 'UPDATE ' . $this->_shareOb->_table
+            . ' SET attribute_images = attribute_images '
+            . ($add ? ' + ' : ' - ') . $images . ' WHERE share_id = '
+            . ($gallery_id ? $gallery_id : $this->id);
+
+        // Make sure to update the local value as well, so it doesn't get
+        // overwritten by any other updates from ->set() calls.
+        if (is_null($gallery_id) || $gallery_id === $this->id) {
+            if ($add) {
+                $this->data['attribute_images'] += $images;
+            } else {
+                $this->data['attribute_images'] -= $images;
+            }
+        }
+
+        /* Need to expire the cache for the gallery that was changed */
+        if ($GLOBALS['conf']['ansel_cache']['usecache']) {
+            $id = (is_null($gallery_id) ? $this->id : $gallery_id);
+            $GLOBALS['cache']->expire('Ansel_Gallery' . $id);
+        }
+
+        return $this->_shareOb->_write_db->exec($sql);
+
+    }
+
+    /**
+     * Add an image to this gallery.
+     *
+     * @param array $image_data  The image to add. Required keys include
+     *                           'image_caption', and 'data'. Optional keys
+     *                           include 'image_filename' and 'image_type'
+     *
+     * @param boolean $default   Make this image the new default tile image.
+     *
+     * @return integer  The id of the new image.
+     */
+    function addImage($image_data, $default = false)
+    {
+        global $conf;
+
+        /* Normal is the only view mode that can accurately update gallery counts */
+        $vMode = $this->get('view_mode');
+        if ($vMode != 'Normal') {
+            $this->_setModeHelper('Normal');
+        }
+
+        $resetStack = false;
+        if (!isset($image_data['image_filename'])) {
+            $image_data['image_filename'] = 'Untitled';
+        }
+        $image_data['gallery_id'] = $this->id;
+        $image_data['image_sort'] = $this->countImages();
+
+        /* Create the image object */
+        $image = new Ansel_Image($image_data);
+        $result = $image->save();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (empty($image_data['image_id'])) {
+            $this->_updateImageCount(1);
+            if ($this->countImages() < 5) {
+                $resetStack = true;
+            }
+        }
+
+        /* Should this be the default image? */
+        if (!$default && $this->data['attribute_default_type'] == 'auto') {
+            $this->data['attribute_default'] = $image->id;
+            $resetStack = true;
+        } elseif ($default) {
+            $this->data['attribute_default'] = $image->id;
+            $this->data['default_type'] = 'manual';
+        }
+
+        /* Reset the gallery default image stacks if needed. */
+        if ($resetStack) {
+            $this->clearStacks();
+        }
+
+        /* Update the modified flag and save gallery changes */
+        $this->data['attribute_last_modified'] = time();
+
+        /* Save all changes to the gallery */
+        $this->save();
+
+        /* Return to the proper view mode */
+        if ($vMode != 'Normal') {
+            $this->_setModeHelper($vMode);
+        }
+
+        /* Return the ID of the new image. */
+        return $image->id;
+    }
+
+    /**
+     * Clear all of this gallery's default image stacks from the VFS and the
+     * gallery's data store.
+     *
+     */
+    function clearStacks()
+    {
+        $ids = @unserialize($this->data['attribute_default_prettythumb']);
+        if (is_array($ids)) {
+            foreach ($ids as $imageId) {
+                $this->removeImage($imageId, true);
+            }
+        }
+
+        // Using the set function here so we can efficently update the db
+        $this->set('default_prettythumb', '', true);
+    }
+
+    /**
+     * Removes all generated and cached 'prettythumb' thumbnails for this
+     * gallery
+     *
+     */
+    function clearThumbs()
+    {
+        $images = $this->listImages();
+        foreach ($images as $id) {
+            $image = $this->getImage($id);
+            $image->deleteCache('prettythumb');
+        }
+    }
+
+    /**
+     * Removes all generated and cached views for this gallery
+     *
+     */
+    function clearViews()
+    {
+        $images = $this->listImages();
+        foreach ($images as $id) {
+            $image = $this->getImage($id);
+            $image->deleteCache('all');
+        }
+    }
+
+    /**
+     * Move images from this gallery to a new gallery.
+     *
+     * @param array $images          An array of image ids.
+     * @param Ansel_Gallery $gallery The gallery to move the images to.
+     *
+     * @return integer | PEAR_Error The number of images moved, or an error message.
+     */
+    function moveImagesTo($images, $gallery)
+    {
+        return $this->_modeHelper->moveImagesTo($images, $gallery);
+    }
+
+    /**
+     * Copy image and related data to specified gallery.
+     *
+     * @param array $images           An array of image ids.
+     * @param Ansel_Gallery $gallery  The gallery to copy images to.
+     *
+     * @return integer | PEAR_Error The number of images copied or error message
+     */
+    function copyImagesTo($images, $gallery)
+    {
+        if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
+            return PEAR::raiseError(
+                sprintf(_("Access denied copying photos to \"%s\"."),
+                          $gallery->get('name')));
+        }
+
+        $db = $this->_shareOb->_write_db;
+        $imgCnt = 0;
+        foreach ($images as $imageId) {
+            $img = &$this->getImage($imageId);
+            // Note that we don't pass the tags when adding the image..see below
+            $newId = $gallery->addImage(array(
+                               'image_caption' => $img->caption,
+                               'data' => $img->raw(),
+                               'image_filename' => $img->filename,
+                               'image_type' => $img->getType(),
+                               'image_uploaded_date' => $img->uploaded));
+            if (is_a($newId, 'PEAR_Error')) {
+                return $newId;
+            }
+            /* Copy any tags */
+            // Since we know that the tags already exist, no need to
+            // go through Ansel_Tags::writeTags() - this saves us a SELECT query
+            // for each tag - just write the data into the DB ourselves.
+            $tags = $img->getTags();
+            $query = $this->_shareOb->_write_db->prepare('INSERT INTO ansel_images_tags (image_id, tag_id) VALUES(' . $newId . ',?);');
+            if (is_a($query, 'PEAR_Error')) {
+                return $query;
+            }
+            foreach ($tags as $tag_id => $tag_name) {
+                $result = $query->execute($tag_id);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+            $query->free();
+
+            /* exif data */
+            // First check to see if the exif data was present in the raw data.
+            $count = $db->queryOne('SELECT COUNT(image_id) FROM ansel_image_attributes WHERE image_id = ' . (int) $newId . ';');
+            if ($count == 0) {
+                $exif = $db->queryAll('SELECT attr_name, attr_value FROM ansel_image_attributes WHERE image_id = ' . (int) $imageId . ';',null, MDB2_FETCHMODE_ASSOC);
+                if (is_array($exif) && count($exif) > 0) {
+                    $insert = $db->prepare('INSERT INTO ansel_image_attributes (image_id, attr_name, attr_value) VALUES (?, ?, ?)');
+                    if (is_a($insert, 'PEAR_Error')) {
+                        return $insert;
+                    }
+                    foreach ($exif as $attr){
+                        $result = $insert->execute(array($newId, $attr['attr_name'], $attr['attr_value']));
+                        if (is_a($result, 'PEAR_Error')) {
+                            return $result;
+                        }
+                    }
+                    $insert->free();
+                }
+            }
+            ++$imgCnt;
+        }
+
+        return $imgCnt;
+    }
+
+    /**
+     * Set the order of an image in this gallery.
+     *
+     * @param integer $imageId The image to sort.
+     * @param integer $pos     The sort position of the image.
+     */
+    function setImageOrder($imageId, $pos)
+    {
+        return $this->_shareOb->_write_db->exec('UPDATE ansel_images SET image_sort = ' . (int)$pos . ' WHERE image_id = ' . (int)$imageId);
+    }
+
+    /**
+     * Remove the given image from this gallery.
+     *
+     * @param mixed   $image   Image to delete. Can be an Ansel_Image
+     *                         or an image ID.
+     *
+     * @return boolean  True on success, false on failure.
+     */
+    function removeImage($image, $isStack = false)
+    {
+        return $this->_modeHelper->removeImage($image, $isStack);
+    }
+
+    /**
+     * Returns this share's owner's Identity object.
+     *
+     * @return Identity object for the owner of this gallery.
+     */
+    function getOwner()
+    {
+        require_once 'Horde/Identity.php';
+        $identity = Identity::singleton('none', $this->data['share_owner']);
+        return $identity;
+    }
+
+    /**
+     * Output the HTML for this gallery's tile.
+     *
+     * @param Ansel_Gallery $parent  The parent Ansel_Gallery object
+     * @param string $style          A named gallery style to use.
+     * @param boolean $mini          Force the use of a mini thumbnail?
+     * @param array $params          Any additional parameters the Ansel_Tile
+     *                               object may need.
+     */
+    function getTile($parent = null, $style = null, $mini = false,
+                     $params = array())
+    {
+        if (!is_null($parent) && is_null($style)) {
+            $style = $parent->getStyle();
+        } else {
+            $style = Ansel::getStyleDefinition($style);
+        }
+
+        if (!empty($view_url)) {
+            $view_url = str_replace('%g', $this->id, $view_url);
+        }
+
+        return Ansel_Tile_Gallery::getTile($this, $style, $mini, $params);
+    }
+
+    /**
+     * Get the children of this gallery.
+     *
+     * @param integer $perm    The permissions to limit to.
+     * @param integer $from    The child to start at.
+     * @param integer $to      The child to end with.
+     * @param boolean $noauto  Prevent auto
+     *
+     * @return A mixed array of Ansel_Gallery and Ansel_Image objects that are
+     *         children of this gallery.
+     */
+    function getGalleryChildren($perm = PERMS_SHOW, $from = 0, $to = 0, $noauto = true)
+    {
+        return $this->_modeHelper->getGalleryChildren($perm, $from, $to, $noauto);
+    }
+
+
+    /**
+     * Return the count of this gallery's children
+     *
+     * @param integer $perm            The permissions to require.
+     * @param boolean $galleries_only  Only include galleries, no images.
+     *
+     * @return integer The count of this gallery's children.
+     */
+    function countGalleryChildren($perm = PERMS_SHOW, $galleries_only = false, $noauto = true)
+    {
+        return $this->_modeHelper->countGalleryChildren($perm, $galleries_only, $noauto);
+    }
+
+    /**
+     * Lists a slice of the image ids in this gallery.
+     *
+     * @param integer $from  The image to start listing.
+     * @param integer $count The numer of images to list.
+     *
+     * @return mixed  An array of image_ids | PEAR_Error
+     */
+    function listImages($from = 0, $count = 0)
+    {
+        return $this->_modeHelper->listImages($from, $count);
+    }
+
+    /**
+     * Gets a slice of the images in this gallery.
+     *
+     * @param integer $from  The image to start fetching.
+     * @param integer $count The numer of images to return.
+     *
+     * @param mixed An array of Ansel_Image objects | PEAR_Error
+     */
+    function getImages($from = 0, $count = 0)
+    {
+        return $this->_modeHelper->getImages($from, $count);
+    }
+
+    /**
+     * Return the most recently added images in this gallery.
+     *
+     * @param integer $limit  The maximum number of images to return.
+     *
+     * @return mixed  An array of Ansel_Image objects | PEAR_Error
+     */
+    function getRecentImages($limit = 10)
+    {
+        return $GLOBALS['ansel_storage']->getRecentImages(array($this->id),
+                                                          $limit);
+    }
+
+    /**
+     * Returns the image in this gallery corresponding to the given id.
+     *
+     * @param integer $id  The ID of the image to retrieve.
+     *
+     * @return Ansel_Image  The image object corresponding to the given id.
+     */
+    function &getImage($id)
+    {
+        return $GLOBALS['ansel_storage']->getImage($id);
+    }
+
+    /**
+     * Checks if the gallery has any subgallery
+     */
+    function hasSubGalleries()
+    {
+        return $this->_modeHelper->hasSubGalleries();
+    }
+
+    /**
+     * Returns the number of images in this gallery and, optionally, all
+     * sub-galleries.
+     *
+     * @param boolean $subgalleries  Determines whether subgalleries should
+     *                               be counted or not.
+     *
+     * @return integer number of images in this gallery
+     */
+    function countImages($subgalleries = false)
+    {
+        return $this->_modeHelper->countImages($subgalleries);
+    }
+
+    /**
+     * Returns the default image for this gallery.
+     *
+     * @param string $style  Force the use of this style, if it's available
+     *                       otherwise use whatever style is choosen for this
+     *                       gallery. If prettythumbs are not available then
+     *                       we always use ansel_default style.
+     *
+     * @return mixed  The image_id of the default image or false.
+     */
+    function getDefaultImage($style = null)
+    {
+       // Check for explicitly requested style
+        if (!is_null($style)) {
+            $gal_style = Ansel::getStyleDefinition($style);
+        } else {
+            // Use gallery's default.
+            $gal_style = $this->getStyle();
+            if (!isset($GLOBALS['ansel_styles'][$gal_style['name']])) {
+                $gal_style = $GLOBALS['ansel_styles']['ansel_default'];
+            }
+        }
+        Horde::logMessage(sprintf("using gallery style: %s in Ansel::getDefaultImage()", $gal_style['name']), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        if (!empty($gal_style['default_galleryimage_type']) &&
+            $gal_style['default_galleryimage_type'] != 'plain') {
+
+            $thumbstyle = $gal_style['default_galleryimage_type'];
+            $styleHash = $this->_getViewHash($thumbstyle, $style);
+
+            // First check for the existence of a default image in the style
+            // we are looking for.
+            if (!empty($this->data['attribute_default_prettythumb'])) {
+                $thumbs = @unserialize($this->data['attribute_default_prettythumb']);
+            }
+            if (!isset($thumbs) || !is_array($thumbs)) {
+                $thumbs = array();
+            }
+
+            if (!empty($thumbs[$styleHash])) {
+                return $thumbs[$styleHash];
+            }
+
+            // Don't already have one, must generate it.
+            $params = array('gallery' => $this, 'style' => $gal_style);
+            $iview = Ansel_ImageView::factory(
+                $gal_style['default_galleryimage_type'], $params);
+
+            if (!is_a($iview, 'PEAR_Error')) {
+                $img = $iview->create();
+                if (!is_a($img, 'PEAR_Error')) {
+                     // Note the gallery_id is negative for generated stacks
+                     $iparams = array('image_filename' => $this->get('name'),
+                                      'image_caption' => $this->get('name'),
+                                      'data' => $img->raw(),
+                                      'image_sort' => 0,
+                                      'gallery_id' => -$this->id);
+                     $newImg = new Ansel_Image($iparams);
+                     $newImg->save();
+                     $prettyData = serialize(
+                         array_merge($thumbs,
+                                     array($styleHash => $newImg->id)));
+
+                     $this->set('default_prettythumb', $prettyData, true);
+                     return $newImg->id;
+                } else {
+                    Horde::logMessage($img, __FILE__, __LINE__, PEAR_LOG_ERR);
+                }
+            } else {
+                // Might not support the requested style...try ansel_default
+                // but protect against infinite recursion.
+                Horde::logMessage($iview, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                if ($style != 'ansel_default') {
+                    return $this->getDefaultImage('ansel_default');
+                }
+                Horde::logMessage($iview, __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        } else {
+            // We are just using an image thumbnail for the gallery default.
+            if ($this->countImages()) {
+                if (!empty($this->data['attribute_default']) &&
+                    $this->data['attribute_default'] > 0) {
+
+                    return $this->data['attribute_default'];
+                }
+                $keys = $this->listImages();
+                if (is_a($keys, 'PEAR_Error')) {
+                    return $keys;
+                }
+                $this->data['attribute_default'] = $keys[count($keys) - 1];
+                $this->data['attribute_default_type'] = 'auto';
+                $this->save();
+                return $keys[count($keys) - 1];
+            }
+
+            if ($this->hasSubGalleries()) {
+                // Fall through to a default image of a sub gallery.
+                $galleries = $GLOBALS['ansel_storage']->listGalleries(
+                    PERMS_SHOW, null, $this, false);
+                if ($galleries && !is_a($galleries, 'PEAR_Error')) {
+                    foreach ($galleries as $galleryId => $gallery) {
+                        if ($default_img = $gallery->getDefaultImage($style)) {
+                            return $default_img;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns this gallery's tags.
+     */
+    function getTags() {
+        if ($this->hasPermission(Horde_Auth::getAuth(), PERMS_READ)) {
+            return Ansel_Tags::readTags($this->id, 'gallery');
+        } else {
+            return PEAR::raiseError(_("Access denied viewing this gallery."));
+        }
+    }
+
+    /**
+     * Set/replace this gallery's tags.
+     *
+     * @param array $tags  AN array of tag names to associate with this image.
+     */
+    function setTags($tags)
+    {
+        if ($this->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
+            return Ansel_Tags::writeTags($this->id, $tags, 'gallery');
+        } else {
+            return PEAR::raiseError(_("Access denied adding tags to this gallery."));
+        }
+    }
+
+    /**
+     * Return the style definition for this gallery. Returns the first available
+     * style in this order: Explicitly configured style if available, if
+     * configured style is not available, use ansel_default.  If nothing has
+     * been configured, the user's selected default is attempted.
+     *
+     * @return array  The style definition array.
+     */
+    function getStyle()
+    {
+        if (empty($this->data['attribute_style'])) {
+            $style = $GLOBALS['prefs']->getValue('default_gallerystyle');
+        } else {
+            $style = $this->data['attribute_style'];
+        }
+        return Ansel::getStyleDefinition($style);
+
+    }
+
+    /**
+     * Return a hash key for the given view and style.
+     *
+     * @param string $view   The view (thumb, prettythumb etc...)
+     * @param string $style  The named style.
+     *
+     * @return string  A md5 hash suitable for use as a key.
+     */
+    function _getViewHash($view, $style = null)
+    {
+        if (is_null($style)) {
+            $style = $this->getStyle();
+        } else {
+            $style = Ansel::getStyleDefinition($style);
+        }
+        if ($view != 'screen' && $view != 'thumb' && $view != 'mini' &&
+            $view != 'full') {
+
+            $view = md5($style['thumbstyle'] . '.' . $style['background']);
+        }
+        return $view;
+    }
+    /**
+     * Checks to see if a user has a given permission.
+     *
+     * @param string $userid       The userid of the user.
+     * @param integer $permission  A PERMS_* constant to test for.
+     * @param string $creator      The creator of the event.
+     *
+     * @return boolean  Whether or not $userid has $permission.
+     */
+    function hasPermission($userid, $permission, $creator = null)
+    {
+        if ($userid == $this->data['share_owner'] ||
+            Horde_Auth::isAdmin('ansel:admin')) {
+
+            return true;
+        }
+
+
+        return $GLOBALS['perms']->hasPermission($this->getPermission(),
+                                                $userid, $permission, $creator);
+    }
+
+    /**
+     * Check user age limtation
+     *
+     * @return boolean
+     */
+    function isOldEnough()
+    {
+        if ($this->data['share_owner'] == Horde_Auth::getAuth() ||
+            empty($GLOBALS['conf']['ages']['limits']) ||
+            empty($this->data['attribute_age'])) {
+
+            return true;
+        }
+
+        // Do we have the user age already cheked?
+        if (!isset($_SESSION['ansel']['user_age'])) {
+            $_SESSION['ansel']['user_age'] = 0;
+        } elseif ($_SESSION['ansel']['user_age'] >= $this->data['attribute_age']) {
+            return true;
+        }
+
+        // Can we hook user's age?
+        if ($GLOBALS['conf']['ages']['hook'] && Horde_Auth::isAuthenticated()) {
+            $result = Horde::callHook('_ansel_hook_user_age');
+            if (is_int($result)) {
+                $_SESSION['ansel']['user_age'] = $result;
+            }
+        }
+
+        return ($_SESSION['ansel']['user_age'] >= $this->data['attribute_age']);
+    }
+
+    /**
+     * Determine if we need to unlock a password protected gallery
+     *
+     * @return boolean
+     */
+    function hasPasswd()
+    {
+        if (Horde_Auth::getAuth() == $this->get('owner') || Horde_Auth::isAdmin('ansel:admin')) {
+            return false;
+        }
+
+        $passwd = $this->get('passwd');
+        if (empty($passwd) ||
+            (!empty($_SESSION['ansel']['passwd'][$this->id])
+                && $_SESSION['ansel']['passwd'][$this->id] = md5($this->get('passwd')))) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Sets this gallery's parent gallery.
+     *
+     * @TODO: Check how this interacts with date galleries - shouldn't be able
+     *        to remove a subgallery from a date gallery anyway, but just incase
+     * @param mixed $parent    An Ansel_Gallery or a gallery_id.
+     *
+     * @return mixed  Ture || PEAR_Error
+     */
+    function setParent($parent)
+    {
+        /* Make sure we have a gallery object */
+        if (!is_null($parent) && !is_a($parent, 'Ansel_Gallery')) {
+            $parent = $GLOBALS['ansel_storage']->getGallery($parent);
+            if (is_a($parent, 'PEAR_Error')) {
+                return $parent;
+            }
+        }
+
+        /* Check this now since we don't know if we are updating the DB or not */
+        $old = $this->getParent();
+        $reset_has_subgalleries = false;
+        if (!is_null($old)) {
+            $cnt = $old->countGalleryChildren(PERMS_READ, true);
+            if ($cnt == 1) {
+                /* Count is 1, and we are about to delete it */
+                $reset_has_subgalleries = true;
+            }
+        }
+
+        /* Call the parent class method */
+        $result = parent::setParent($parent);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Tell the parent the good news */
+        if (!is_null($parent) && !$parent->get('has_subgalleries')) {
+            return $parent->set('has_subgalleries', '1', true);
+        }
+        Horde::logMessage('Ansel_Gallery parent successfully set', __FILE__,
+                          __LINE__, PEAR_LOG_DEBUG);
+
+       /* Gallery parent changed, safe to change the parent's attributes */
+       if ($reset_has_subgalleries) {
+           $old->set('has_subgalleries', 0, true);
+       }
+
+        return true;
+    }
+
+    /**
+     * Sets an attribute value in this object.
+     *
+     * @param string $attribute  The attribute to set.
+     * @param mixed $value       The value for $attribute.
+     * @param boolean $update    Commit only this change to storage.
+     *
+     * @return mixed  True if setting the attribute did succeed, a PEAR_Error
+     *                otherwise.
+     */
+    function set($attribute, $value, $update = false)
+    {
+        /* Translate the keys */
+        if ($attribute == 'owner') {
+            $driver_key = 'share_owner';
+        } else {
+            $driver_key = 'attribute_' . $attribute;
+        }
+
+        if ($driver_key == 'attribute_view_mode' &&
+            !empty($this->data[$driver_key]) &&
+            $value != $this->data[$driver_key]) {
+
+            $mode = isset($attributes['attribute_view_mode']) ? $attributes['attribute_view_mode'] : 'Normal';
+            $this->_setModeHelper($mode);
+        }
+
+        $this->data[$driver_key] = $value;
+
+        /* Update the backend, but only this current change */
+        if ($update) {
+            $db = $this->_shareOb->_write_db;
+            // Manually convert the charset since we're not going through save()
+            $data = $this->_shareOb->_toDriverCharset(array($driver_key => $value));
+            $query = $db->prepare('UPDATE ' . $this->_shareOb->_table . ' SET ' . $driver_key . ' = ? WHERE share_id = ?', null, MDB2_PREPARE_MANIP);
+            if ($GLOBALS['conf']['ansel_cache']['usecache']) {
+                $GLOBALS['cache']->expire('Ansel_Gallery' . $this->id);
+            }
+            $result = $query->execute(array($data[$driver_key], $this->id));
+            $query->free();
+
+            return $result;
+        }
+
+        return true;
+    }
+
+    function setDate($date)
+    {
+        $this->_modeHelper->setDate($date);
+    }
+
+    function getDate()
+    {
+        return $this->_modeHelper->getDate();
+    }
+
+    /**
+     * Get an array describing where this gallery is in a breadcrumb trail.
+     *
+     * @return  An array of 'title' and 'navdata' hashes with the [0] element
+     *          being the deepest part.
+     */
+    function getGalleryCrumbData()
+    {
+        return $this->_modeHelper->getGalleryCrumbData();
+    }
+
+}
diff --git a/ansel/lib/Image.php b/ansel/lib/Image.php
new file mode 100644 (file)
index 0000000..42c6b0a
--- /dev/null
@@ -0,0 +1,1058 @@
+<?php
+/**
+ * Class to describe a single Ansel image.
+ *
+ * Copyright 2001-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 Chuck Hagenbuch <chuck@horde.org>
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Ansel
+ */
+class Ansel_Image
+{
+    /**
+     * @var integer  The gallery id of this image's parent gallery
+     */
+    var $gallery;
+
+    /**
+     * @var Horde_Image  Horde_Image object for this image.
+     */
+    var $_image;
+
+    var $id = null;
+    var $filename = 'Untitled';
+    var $caption = '';
+    var $type = 'image/jpeg';
+
+    /**
+     * timestamp of uploaded date
+     *
+     * @var integer
+     */
+    var $uploaded;
+
+    var $sort;
+    var $commentCount;
+    var $facesCount;
+    var $lat;
+    var $lng;
+    var $location;
+    var $geotag_timestamp;
+
+    var $_dirty;
+
+
+    /**
+     * Timestamp of original date.
+     *
+     * @var integer
+     */
+    var $originalDate;
+
+    /**
+     * Holds an array of tags for this image
+     * @var array
+     */
+    var $_tags = array();
+
+    var $_loaded = array();
+    var $_data = array();
+
+    /**
+     * Cache the raw EXIF data locally
+     *
+     * @var array
+     */
+    var $_exif = array();
+
+    /**
+     * TODO: refactor Ansel_Image to use a ::get() method like Ansel_Gallery
+     * instead of direct instance variable access and all the nonsense below.
+     *
+     * @param unknown_type $image
+     * @return Ansel_Image
+     */
+    function Ansel_Image($image = array())
+    {
+        if ($image) {
+            $this->filename = $image['image_filename'];
+            $this->caption = $image['image_caption'];
+            $this->sort = $image['image_sort'];
+            $this->gallery = $image['gallery_id'];
+
+            // New image?
+            if (!empty($image['image_id'])) {
+                $this->id = $image['image_id'];
+            }
+
+            if (!empty($image['data'])) {
+                $this->_data['full'] = $image['data'];
+            }
+
+            if (!empty($image['image_uploaded_date'])) {
+                $this->uploaded = $image['image_uploaded_date'];
+            } else {
+                $this->uploaded = time();
+            }
+
+            if (!empty($image['image_type'])) {
+                $this->type = $image['image_type'];
+            }
+
+            if (!empty($image['tags'])) {
+                $this->_tags = $image['tags'];
+            }
+
+            if (!empty($image['image_faces'])) {
+               $this->facesCount = $image['image_faces'];
+            }
+
+            $this->location = !empty($image['image_location']) ? $image['image_location'] : '';
+
+            // The following may have to be rewritten by EXIF.
+            // EXIF requires both an image id and a stream, so we can't
+            // get EXIF data before we save the image to the VFS.
+            if (!empty($image['image_original_date'])) {
+                $this->originalDate = $image['image_original_date'];
+            } else {
+                $this->originalDate = $this->uploaded;
+            }
+            $this->lat = !empty($image['image_latitude']) ? $image['image_latitude'] : '';
+            $this->lng = !empty($image['image_longitude']) ? $image['image_longitude'] : '';
+            $this->geotag_timestamp = !empty($image['image_geotag_date']) ? $image['image_geotag_date'] : '0';
+        }
+
+        $this->_image = Ansel::getImageObject();
+        $this->_image->reset();
+    }
+
+    /**
+     * Return the vfs path for this image.
+     *
+     * @param string $view   The view we want.
+     * @param string $style  A named gallery style.
+     *
+     * @return string  The vfs path for this image.
+     */
+    function getVFSPath($view = 'full', $style = null)
+    {
+        $view = $this->_getViewHash($view, $style);
+        return '.horde/ansel/'
+               . substr(str_pad($this->id, 2, 0, STR_PAD_LEFT), -2)
+               . '/' . $view;
+    }
+
+    /**
+     * Returns the file name of this image as used in the VFS backend.
+     *
+     * @return string  This image's VFS file name.
+     */
+    function getVFSName($view)
+    {
+        $vfsname = $this->id;
+
+        if ($view == 'full' && $this->type) {
+            $type = strpos($this->type, '/') === false ? 'image/' . $this->type : $this->type;
+            if ($ext = Horde_Mime_Magic::mimeToExt($type)) {
+                $vfsname .= '.' . $ext;
+            }
+        } elseif (($GLOBALS['conf']['image']['type'] == 'jpeg') || $view == 'screen') {
+            $vfsname .= '.jpg';
+        } else {
+            $vfsname .= '.png';
+        }
+
+        return $vfsname;
+    }
+
+    /**
+     * Loads the given view into memory.
+     *
+     * @param string $view   Which view to load.
+     * @param string $style  The named gallery style.
+     *
+     * @return mixed  True || PEAR_Error
+     */
+    function load($view = 'full', $style = null)
+    {
+        // If this is a new image that hasn't been saved yet, we will
+        // already have the full data loaded. If we auto-rotate the image
+        // then there is no need to save it just to load it again.
+        if ($view == 'full' && !empty($this->_data['full'])) {
+            $this->_image->loadString('original', $this->_data['full']);
+            $this->_loaded['full'] = true;
+            return true;
+        }
+
+        $viewHash = $this->_getViewHash($view, $style);
+        /* If we've already loaded the data, just return now. */
+        if (!empty($this->_loaded[$viewHash])) {
+            return true;
+        }
+
+        $result = $this->createView($view, $style);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* If createView() had to resize the full image, we've already
+         * loaded the data, so return now. */
+        if (!empty($this->_loaded[$viewHash])) {
+            return;
+        }
+
+        /* We've definitely successfully loaded the image now. */
+        $this->_loaded[$viewHash] = true;
+
+        /* Get the VFS info. */
+        $vfspath = $this->getVFSPath($view, $style);
+        if (is_a($vfspath, 'PEAR_Error')) {
+            return $vfspath;
+        }
+
+        /* Read in the requested view. */
+        $data = $GLOBALS['ansel_vfs']->read($vfspath, $this->getVFSName($view));
+        if (is_a($data, 'PEAR_Error')) {
+            Horde::logMessage($date, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $data;
+        }
+
+        $this->_data[$viewHash] = $data;
+        $this->_image->loadString($vfspath . '/' . $this->id, $data);
+        return true;
+    }
+
+    /**
+     * Check if an image view exists and returns the vfs name complete with
+     * the hash directory name prepended if appropriate.
+     *
+     * @param integer $id    Image id to check
+     * @param string $view   Which view to check for
+     * @param string $style  A named gallery style
+     *
+     * @return mixed  False if image does not exists | string vfs name
+     *
+     * @static
+     */
+    function viewExists($id, $view, $style)
+    {
+        /* We cannot check empty styles since we cannot get the hash */
+        if (empty($style)) {
+            return false;
+        }
+
+        /* Get the VFS path. */
+        $view = Ansel_Gallery::_getViewHash($view, $style);
+
+        /* Can't call the various vfs methods here, since this method needs
+        to be called statically */
+        $vfspath = '.horde/ansel/' . substr(str_pad($id, 2, 0, STR_PAD_LEFT), -2) . '/' . $view;
+
+        /* Get VFS name */
+        $vfsname = $id . '.';
+        if ($GLOBALS['conf']['image']['type'] == 'jpeg' || $view == 'screen') {
+            $vfsname .= 'jpg';
+        } else {
+            $vfsname .= 'png';
+        }
+
+        if ($GLOBALS['ansel_vfs']->exists($vfspath, $vfsname)) {
+            return $view . '/' . $vfsname;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Creates and caches the given view.
+     *
+     * @param string $view  Which view to create.
+     * @param string $style  A named gallery style
+     */
+    function createView($view, $style = null)
+    {
+        // HACK: Need to replace the image object with a JPG typed image if
+        //       we are generating a screen image. Need to do the replacement
+        //       and do it *here* for BC reasons with Horde_Image...and this
+        //       needs to be done FIRST, since the view might already be cached
+        //       in the VFS.
+        if ($view == 'screen' && $GLOBALS['conf']['image']['type'] != 'jpeg') {
+            $this->_image = Ansel::getImageObject(array('type' => 'jpeg'));
+            $this->_image->reset();
+        }
+
+        /* Get the VFS info. */
+        $vfspath = $this->getVFSPath($view, $style);
+        if ($GLOBALS['ansel_vfs']->exists($vfspath, $this->getVFSName($view))) {
+            return true;
+        }
+
+        $data = $GLOBALS['ansel_vfs']->read($this->getVFSPath('full'),
+                                            $this->getVFSName('full'));
+        if (is_a($data, 'PEAR_Error')) {
+            Horde::logMessage($data, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $data;
+        }
+        $this->_image->loadString($this->getVFSPath('full') . '/' . $this->id, $data);
+        $styleDef = Ansel::getStyleDefinition($style);
+        if ($view == 'prettythumb') {
+            $viewType = $styleDef['thumbstyle'];
+        } else {
+            $viewType = $view;
+        }
+        $iview = Ansel_ImageView::factory($viewType, array('image' => $this,
+                                                           'style' => $style));
+
+        if (is_a($iview, 'PEAR_Error')) {
+            // It could be we don't support the requested effect, try
+            // ansel_default before giving up.
+            if ($view == 'prettythumb') {
+                $iview = Ansel_ImageView::factory(
+                    'thumb', array('image' => $this,
+                                   'style' => 'ansel_default'));
+
+                if (is_a($iview, 'PEAR_Error')) {
+                    return $iview;
+                }
+            }
+        }
+
+        $res = $iview->create();
+        if (is_a($res, 'PEAR_Error')) {
+            return $res;
+        }
+
+        $view = $this->_getViewHash($view, $style);
+
+        $this->_data[$view] = $this->_image->raw();
+        $this->_image->loadString($vfspath . '/' . $this->id,
+                                  $this->_data[$view]);
+        $this->_loaded[$view] = true;
+        $GLOBALS['ansel_vfs']->writeData($vfspath, $this->getVFSName($view),
+                                         $this->_data[$view], true);
+
+        // Autowatermark the screen view
+        if ($view == 'screen' &&
+            $GLOBALS['prefs']->getValue('watermark_auto') &&
+            $GLOBALS['prefs']->getValue('watermark_text') != '') {
+
+            $this->watermark('screen');
+            $GLOBALS['ansel_vfs']->writeData($vfspath, $this->getVFSName($view),
+                                             $this->_image->_data);
+        }
+
+        return true;
+    }
+
+    /**
+     * Writes the current data to vfs, used when creating a new image
+     */
+    function _writeData()
+    {
+        $this->_dirty = false;
+        return $GLOBALS['ansel_vfs']->writeData($this->getVFSPath('full'),
+                                                $this->getVFSName('full'),
+                                                $this->_data['full'], true);
+    }
+
+    /**
+     * Change the image data. Deletes old cache and writes the new
+     * data to the VFS. Used when updating an image
+     *
+     * @param string $data  The new data for this image.
+     * @param string $view  If specified, the $data represents only this
+     *                      particular view. Cache will not be deleted.
+     */
+    function updateData($data, $view = 'full')
+    {
+        if (is_a($data, 'PEAR_Error')) {
+            return $data;
+        }
+
+        /* Delete old cached data if we are replacing the full image */
+        if ($view == 'full') {
+            $this->deleteCache();
+        }
+
+        return $GLOBALS['ansel_vfs']->writeData($this->getVFSPath($view),
+                                                $this->getVFSName($view),
+                                                $data, true);
+    }
+
+    /**
+     * Update the geotag data
+     */
+    function geotag($lat, $lng, $location = '')
+    {
+        $this->lat = $lat;
+        $this->lng = $lng;
+        $this->location = $location;
+        $this->geotag_timestamp = time();
+        $this->save();
+    }
+
+    /**
+     * Save basic image details
+     *
+     * @TODO: Move all SQL queries to Ansel_Storage::?
+     */
+    function save()
+    {
+        /* If we have an id, then it's an existing image.*/
+        if ($this->id) {
+            $update = $GLOBALS['ansel_db']->prepare('UPDATE ansel_images SET image_filename = ?, image_type = ?, image_caption = ?, image_sort = ?, image_original_date = ?, image_latitude = ?, image_longitude = ?, image_location = ?, image_geotag_date = ? WHERE image_id = ?');
+            if (is_a($update, 'PEAR_Error')) {
+                Horde::logMessage($update, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $update;
+            }
+            $result = $update->execute(array(Horde_String::convertCharset($this->filename, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset']),
+                                             $this->type,
+                                             Horde_String::convertCharset($this->caption, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset']),
+                                             $this->sort,
+                                             $this->originalDate,
+                                             $this->lat,
+                                             $this->lng,
+                                             $this->location,
+                                             $this->geotag_timestamp,
+                                             $this->id));
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($update, __FILE__, __LINE__, PEAR_LOG_ERR);
+            } else {
+                $update->free();
+            }
+            return $result;
+        }
+
+        /* Saving a new Image */
+        if (!$this->gallery || !strlen($this->filename) || !$this->type) {
+            $error = PEAR::raiseError(_("Incomplete photo"));
+            Horde::logMessage($error, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        /* Get the next image_id */
+        $image_id = $GLOBALS['ansel_db']->nextId('ansel_images');
+        if (is_a($image_id, 'PEAR_Error')) {
+            return $image_id;
+        }
+
+        /* Prepare the SQL statement */
+        $insert = $GLOBALS['ansel_db']->prepare('INSERT INTO ansel_images (image_id, gallery_id, image_filename, image_type, image_caption, image_uploaded_date, image_sort, image_original_date, image_latitude, image_longitude, image_location, image_geotag_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
+        if (is_a($insert, 'PEAR_Error')) {
+            Horde::logMessage($insert, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $insert;
+        }
+
+        /* Perform the INSERT */
+        $result = $insert->execute(array($image_id,
+                                         $this->gallery,
+                                         Horde_String::convertCharset($this->filename, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset']),
+                                         $this->type,
+                                         Horde_String::convertCharset($this->caption, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset']),
+                                         $this->uploaded,
+                                         $this->sort,
+                                         $this->originalDate,
+                                         $this->lat,
+                                         $this->lng,
+                                         $this->location,
+                                         (empty($this->lat) ? 0 : $this->uploaded)));
+        $insert->free();
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+
+        /* Keep the image_id */
+        $this->id = $image_id;
+
+        /* The EXIF functions require a stream, so we need to save before we read */
+        $this->_writeData();
+
+        /* Get the EXIF data if we are not a gallery key image. */
+        if ($this->gallery > 0) {
+            $needUpdate = $this->_getEXIF();
+        }
+
+        /* Create tags from exif data if desired */
+        $fields = @unserialize($GLOBALS['prefs']->getValue('exif_tags'));
+        if ($fields) {
+            $this->_exifToTags($fields);
+        }
+
+        /* Save the tags */
+        if (count($this->_tags)) {
+            $result = $this->setTags($this->_tags);
+            if (is_a($result, 'PEAR_Error')) {
+                // Since we got this far, the image has been added, so
+                // just log the tag failure.
+                Horde::logMessage($result, __LINE__, __FILE__, PEAR_LOG_ERR);
+            }
+        }
+
+        /* Save again if EXIF changed any values */
+        if (!empty($needUpdate)) {
+            $this->save();
+        }
+
+        return $this->id;
+    }
+
+   /**
+    * Replace this image's image data.
+    *
+    */
+    function replace($imageData)
+    {
+        /* Reset the data array and remove all cached images */
+        $this->_data = array();
+        $this->reset();
+
+        /* Remove attributes */
+        $result = $GLOBALS['ansel_db']->exec('DELETE FROM ansel_image_attributes WHERE image_id = ' . (int)$this->id);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERROR);
+            return $result;
+        }
+        /* Load the new image data */
+        $this->_getEXIF();
+        $this->updateData($imageData);
+
+        return true;
+    }
+
+    /**
+     * Adds specified EXIF fields to this image's tags. Called during image
+     * upload/creation.
+     *
+     * @param array $fields  An array of EXIF fields to import as a tag.
+     *
+     */
+    function _exifToTags($fields = array())
+    {
+        $tags = array();
+        foreach ($fields as $field) {
+            if (!empty($this->_exif[$field])) {
+                if (substr($field, 0, 8) == 'DateTime') {
+                    $d = new Horde_Date(strtotime($this->_exif[$field]));
+                    $tags[] = $d->format("Y-m-d");
+                } else {
+                    $tags[] = $this->_exif[$field];
+                }
+            }
+        }
+
+        $this->_tags = array_merge($this->_tags, $tags);
+    }
+
+    /**
+     * Reads the EXIF data from the image and stores in _exif array() as well
+     * also populates any local properties that come from the EXIF data.
+     *
+     * @return mixed  true if any local properties were modified, false otherwise, PEAR_Error on failure
+     */
+    function _getEXIF()
+    {
+        /* Clear the local copy */
+        $this->_exif = array();
+
+        /* Get the data */
+        $imageFile = $GLOBALS['ansel_vfs']->readFile($this->getVFSPath('full'),
+                                                     $this->getVFSName('full'));
+        if (is_a($imageFile, 'PEAR_Error')) {
+            return $imageFile;
+        }
+        $exif = Horde_Image_Exif::factory($GLOBALS['conf']['exif'], $GLOBALS['conf']['exif']['params']);
+        $exif_fields = $exif->getData($imageFile);
+
+        /* Flag to determine if we need to resave the image data */
+        $needUpdate = false;
+
+        /* Populate any local properties that come from EXIF
+         * Save any geo data to a seperate table as well */
+        if (!empty($exif_fields['GPSLatitude'])) {
+            $this->lat = $exif_fields['GPSLatitude'];
+            $this->lng = $exif_fields['GPSLongitude'];
+            $this->geotag_timestamp = time();
+            $needUpdate = true;
+        }
+
+        if (!empty($exif_fields['DateTimeOriginal'])) {
+            $this->originalDate = $exif_fields['DateTimeOriginal'];
+            $needUpdate = true;
+        }
+
+        /* Attempt to autorotate based on Orientation field */
+        $this->_autoRotate();
+
+        /* Save attributes. */
+        $insert = $GLOBALS['ansel_db']->prepare('INSERT INTO ansel_image_attributes (image_id, attr_name, attr_value) VALUES (?, ?, ?)');
+        foreach ($exif_fields as $name => $value) {
+            $result = $insert->execute(array($this->id, $name, Horde_String::convertCharset($value, Horde_Nls::getCharset(), $GLOBALS['conf']['sql']['charset'])));
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+            /* Cache it locally */
+            $this->_exif[$name] = Horde_Image_Exif::getHumanReadable($name, $value);
+        }
+        $insert->free();
+
+
+        return $needUpdate;
+    }
+
+    /**
+     * Autorotate based on EXIF orientation field. Updates the data in memory
+     * only.
+     *
+     */
+    function _autoRotate()
+    {
+        if (isset($this->_exif['Orientation']) && $this->_exif['Orientation'] != 1) {
+            switch ($this->_exif['Orientation']) {
+            case 2:
+                 $this->mirror();
+                break;
+
+            case 3:
+                $this->rotate('full', 180);
+                break;
+
+            case 4:
+                $this->mirror();
+                $this->rotate('full', 180);
+                break;
+
+            case 5:
+                $this->flip();
+                $this->rotate('full', 90);
+                break;
+
+            case 6:
+                $this->rotate('full', 90);
+                break;
+
+            case 7:
+                $this->mirror();
+                $this->rotate('full', 90);
+                break;
+
+            case 8:
+                $this->rotate('full', 270);
+                break;
+            }
+
+            if ($this->_dirty) {
+                $this->_exif['Orientation'] = 1;
+                $this->data['full'] = $this->raw();
+                $this->_writeData();
+            }
+        }
+    }
+
+    /**
+     * Reset the image, removing all loaded views.
+     */
+    function reset()
+    {
+        $this->_image->reset();
+        $this->_loaded = array();
+    }
+
+    /**
+     * Deletes the specified cache file.
+     *
+     * If none is specified, deletes all of the cache files.
+     *
+     * @param string $view  Which cache file to delete.
+     */
+    function deleteCache($view = 'all')
+    {
+        /* Delete cached screen image. */
+        if ($view == 'all' || $view == 'screen') {
+            $GLOBALS['ansel_vfs']->deleteFile($this->getVFSPath('screen'),
+                                              $this->getVFSName('screen'));
+        }
+
+        /* Delete cached thumbnail. */
+        if ($view == 'all' || $view == 'thumb') {
+            $GLOBALS['ansel_vfs']->deleteFile($this->getVFSPath('thumb'),
+                                              $this->getVFSName('thumb'));
+        }
+
+        /* Delete cached mini image. */
+        if ($view == 'all' || $view == 'mini') {
+            $GLOBALS['ansel_vfs']->deleteFile($this->getVFSPath('mini'),
+                                              $this->getVFSName('mini'));
+        }
+
+        if ($view == 'all' || $view == 'prettythumb') {
+
+            /* No need to try to delete a hash we already removed */
+            $deleted = array();
+
+            /* Need to generate hashes for each possible style */
+            $styles = Horde::loadConfiguration('styles.php', 'styles', 'ansel');
+            foreach ($styles as $style) {
+                $hash =  md5($style['thumbstyle'] . '.' . $style['background']);
+                if (empty($deleted[$hash])) {
+                    $GLOBALS['ansel_vfs']->deleteFile($this->getVFSPath($hash),
+                                                      $this->getVFSName($hash));
+                    $deleted[$hash] = true;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the raw data for the given view.
+     *
+     * @param string $view  Which view to return.
+     */
+    function raw($view = 'full')
+    {
+        if ($this->_dirty) {
+          return $this->_image->raw();
+        } else {
+            $this->load($view);
+            return $this->_data[$view];
+        }
+    }
+
+    /**
+     * Sends the correct HTTP headers to the browser to download this image.
+     *
+     * @param string $view  The view to download.
+     */
+    function downloadHeaders($view = 'full')
+    {
+        global $browser, $conf;
+
+        $filename = $this->filename;
+        if ($view != 'full') {
+            if ($ext = Horde_Mime_Magic::mimeToExt('image/' . $conf['image']['type'])) {
+                $filename .= '.' . $ext;
+            }
+        }
+
+        $browser->downloadHeaders($filename);
+    }
+
+    /**
+     * Display the requested view.
+     *
+     * @param string $view   Which view to display.
+     * @param string $style  Force use of this gallery style.
+     */
+    function display($view = 'full', $style = null)
+    {
+        if ($view == 'full' && !$this->_dirty) {
+
+            // Check full photo permissions
+            $gallery = $GLOBALS['ansel_storage']->getGallery($this->gallery);
+            if (is_a($gallery, 'PEAR_Error')) {
+                return $gallery;
+            }
+            if (!$gallery->canDownload()) {
+                return PEAR::RaiseError(sprintf(_("Access denied downloading photos from \"%s\"."), $gallery->get('name')));
+            }
+
+            $data = $GLOBALS['ansel_vfs']->read($this->getVFSPath('full'),
+                                                $this->getVFSName('full'));
+
+            if (is_a($data, 'PEAR_Error')) {
+                return $data;
+            }
+            echo $data;
+            return;
+        }
+
+        if (is_a($result = $this->load($view, $style), 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+
+        $this->_image->display();
+    }
+
+    /**
+     * Wraps the given view into a file.
+     *
+     * @param string $view  Which view to wrap up.
+     */
+    function toFile($view = 'full')
+    {
+        if (is_a(($result = $this->load($view)), 'PEAR_Error')) {
+            return $result;
+        }
+        return $this->_image->toFile($this->_dirty ? false : $this->_data[$view]);
+    }
+
+    /**
+     * Returns the dimensions of the given view.
+     *
+     * @param string $view  The view (size) to check dimensions for.
+     */
+    function getDimensions($view = 'full')
+    {
+        if (is_a(($result = $this->load($view)), 'PEAR_Error')) {
+            return $result;
+        }
+        return $this->_image->getDimensions();
+    }
+
+    /**
+     * Rotates the image.
+     *
+     * @param string $view The view (size) to work with.
+     * @param integer $angle  What angle to rotate the image by.
+     */
+    function rotate($view = 'full', $angle)
+    {
+        $this->load($view);
+        $this->_dirty = true;
+        $this->_image->rotate($angle);
+    }
+
+    function crop($x1, $y1, $x2, $y2)
+    {
+        $this->_dirty = true;
+        $this->_image->crop($x1, $y1, $x2, $y2);
+    }
+
+    /**
+     * Converts the image to grayscale.
+     *
+     * @param string $view The view (size) to work with.
+     */
+    function grayscale($view = 'full')
+    {
+        $this->load($view);
+        $this->_dirty = true;
+        $this->_image->grayscale();
+    }
+
+    /**
+     * Watermarks the image.
+     *
+     * @param string $view The view (size) to work with.
+     * @param string $watermark  String to use as the watermark.
+     */
+    function watermark($view = 'full', $watermark = null, $halign = null,
+                       $valign = null, $font = null)
+    {
+        if (empty($watermark)) {
+            $watermark = $GLOBALS['prefs']->getValue('watermark_text');
+        }
+
+        if (empty($halign)) {
+            $halign = $GLOBALS['prefs']->getValue('watermark_horizontal');
+        }
+
+        if (empty($valign)) {
+            $valign = $GLOBALS['prefs']->getValue('watermark_vertical');
+        }
+
+        if (empty($font)) {
+            $font = $GLOBALS['prefs']->getValue('watermark_font');
+        }
+
+        if (empty($watermark)) {
+            require_once 'Horde/Identity.php';
+            $identity = Identity::singleton();
+            $name = $identity->getValue('fullname');
+            if (empty($name)) {
+                $name = Horde_Auth::getAuth();
+            }
+            $watermark = sprintf(_("(c) %s %s"), date('Y'), $name);
+        }
+
+        $this->load($view);
+        $this->_dirty = true;
+        $params = array('text' => $watermark,
+                        'halign' => $halign,
+                        'valign' => $valign,
+                        'fontsize' => $font);
+        if (!empty($GLOBALS['conf']['image']['font'])) {
+            $params['font'] = $GLOBALS['conf']['image']['font'];
+        }
+        $this->_image->addEffect('TextWatermark', $params);
+    }
+
+    /**
+     * Flips the image.
+     *
+     * @param string $view The view (size) to work with.
+     */
+    function flip($view = 'full')
+    {
+        $this->load($view);
+        $this->_dirty = true;
+        $this->_image->flip();
+    }
+
+    /**
+     * Mirrors the image.
+     *
+     * @param string $view The view (size) to work with.
+     */
+    function mirror($view = 'full')
+    {
+        $this->load($view);
+        $this->_dirty = true;
+        $this->_image->mirror();
+    }
+
+    /**
+     * Returns this image's tags.
+     *
+     * @return mixed  An array of tags | PEAR_Error
+     * @see Ansel_Tags::readTags()
+     */
+    function getTags()
+    {
+        global $ansel_storage;
+
+        if (count($this->_tags)) {
+            return $this->_tags;
+        }
+        $gallery = $ansel_storage->getGallery($this->gallery);
+        if (is_a($gallery, 'PEAR_Error')) {
+            return $gallery;
+        }
+        if ($gallery->hasPermission(Horde_Auth::getAuth(), PERMS_READ)) {
+            $res = Ansel_Tags::readTags($this->id);
+            if (!is_a($res, 'PEAR_Error')) {
+                $this->_tags = $res;
+                return $this->_tags;
+            } else {
+                return $res;
+            }
+        } else {
+            return PEAR::raiseError(_("Access denied viewing this photo."));
+        }
+    }
+
+    /**
+     * Set/replace this image's tags.
+     *
+     * @param array $tags  An array of tag names to associate with this image.
+     */
+    function setTags($tags)
+    {
+        global $ansel_storage;
+
+        $gallery = $ansel_storage->getGallery(abs($this->gallery));
+        if ($gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) {
+            // Clear the local cache.
+            $this->_tags = array();
+            return Ansel_Tags::writeTags($this->id, $tags);
+        } else {
+            return PEAR::raiseError(_("Access denied adding tags to this photo."));
+        }
+    }
+
+    /**
+     * Get the Ansel_View_Image_Thumb object
+     *
+     * @param Ansel_Gallery $parent  The parent Ansel_Gallery object.
+     * @param string $style          A named gallery style to use.
+     * @param boolean $mini          Force the use of a mini thumbnail?
+     * @param array $params          Any additional parameters the Ansel_Tile
+     *                               object may need.
+     *
+     */
+    function getTile($parent = null, $style = null, $mini = false,
+                     $params = array())
+    {
+        if (!is_null($parent) && is_null($style)) {
+            $style = $parent->getStyle();
+        } else {
+            $style = Ansel::getStyleDefinition($style);
+        }
+
+        return Ansel_Tile_Image::getTile($this, $style, $mini, $params);
+    }
+
+    /**
+     * Get the image type for the requested view.
+     */
+    function getType($view = 'full')
+    {
+        if ($view == 'full') {
+           return $this->type;
+        } elseif ($view == 'screen') {
+            return 'image/jpg';
+        } else {
+            return 'image/' . $GLOBALS['conf']['image']['type'];
+        }
+    }
+
+    /**
+     * Return a hash key for the given view and style.
+     *
+     * @param string $view   The view (thumb, prettythumb etc...)
+     * @param string $style  The named style.
+     *
+     * @return string  A md5 hash suitable for use as a key.
+     */
+    function _getViewHash($view, $style = null)
+    {
+        global $ansel_storage;
+
+        // These views do not care about style...just return the $view value.
+        if ($view == 'screen' || $view == 'thumb' || $view == 'mini' ||
+            $view == 'full') {
+
+            return $view;
+        }
+        if (is_null($style)) {
+            $gallery = $ansel_storage->getGallery(abs($this->gallery));
+            if (is_a($gallery, 'PEAR_Error')) {
+                return $gallery;
+            }
+            $style = $gallery->getStyle();
+        } else {
+            $style = Ansel::getStyleDefinition($style);
+        }
+
+       $view = md5($style['thumbstyle'] . '.' . $style['background']);
+       return $view;
+    }
+
+    /**
+     * Get the image attributes from the backend.
+     *
+     * @param Ansel_Image $image  The image to retrieve attributes for.
+     *                            attributes for.
+     * @param boolean $format     Format the EXIF data. If false, the raw data
+     *                            is returned.
+     *
+     * @return array  The EXIF data.
+     * @static
+     */
+    function getAttributes($format = false)
+    {
+        $attributes = $GLOBALS['ansel_storage']->getImageAttributes($this->id);
+        $fields = Horde_Image_Exif::getFields();
+        $output = array();
+
+        foreach ($fields as $field => $data) {
+            if (!isset($attributes[$field])) {
+                continue;
+            }
+            $value = Horde_Image_Exif::getHumanReadable($field, Horde_String::convertCharset($attributes[$field], $GLOBALS['conf']['sql']['charset']));
+            if (!$format) {
+                $output[$field] = $value;
+            } else {
+                $description = isset($data['description']) ? $data['description'] : $field;
+                $output[] = '<td><strong>' . $description . '</strong></td><td>' . htmlspecialchars($value, ENT_COMPAT, Horde_Nls::getCharset()) . '</td>';
+            }
+        }
+
+        return $output;
+    }
+
+}
diff --git a/ansel/lib/Storage.php b/ansel/lib/Storage.php
new file mode 100644 (file)
index 0000000..b97aa3b
--- /dev/null
@@ -0,0 +1,972 @@
+<?php
+/**
+ * Class for interfacing with back end data storage.
+ *
+ * Copyright 2001-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  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Ansel
+ */
+class Ansel_Storage
+{
+    var $_scope = 'ansel';
+    var $_db = null;
+    var $galleries = array();
+
+    /**
+     * The Horde_Shares object to use for this scope.
+     *
+     * @var Horde_Share
+     */
+    var $shares = null;
+
+    /* Local cache of retrieved images */
+    var $images = array();
+
+    function Ansel_Storage($scope = null)
+    {
+        /* Check for a scope other than the default Ansel scope.*/
+        if (!is_null($scope)) {
+            $this->_scope = $scope;
+        }
+
+        /* This is the only supported share backend for Ansel */
+        $this->shares = Horde_Share::singleton($this->_scope,
+                                               'sql_hierarchical');
+
+        /* Ansel_Gallery is just a subclass of Horde_Share_Object */
+        $this->shares->_shareObject = 'Ansel_Gallery';
+
+        /* Database handle */
+        $this->_db = $GLOBALS['ansel_db'];
+    }
+
+   /**
+    * Create and initialise a new gallery object.
+    *
+    * @param array $attributes     The gallery attributes
+    * @param object Perms $perm    The permissions for the gallery if the
+    *                              defaults are not desirable.
+    * @param mixed  $parent       The gallery id of the parent (if any)
+    *
+    * @return Ansel_Gallery  A new gallery object or PEAR_Error.
+    */
+    function createGallery($attributes = array(), $perm = null, $parent = null)
+    {
+        /* Required values. */
+        if (empty($attributes['owner'])) {
+            $attributes['owner'] = Horde_Auth::getAuth();
+        }
+        if (empty($attributes['name'])) {
+            $attributes['name'] = _("Unnamed");
+        }
+        if (empty($attributes['desc'])) {
+            $attributes['desc'] = '';
+        }
+
+        /* Default values */
+        $attributes['default_type'] = isset($attributes['default_type']) ? $attributes['default_type'] : 'auto';
+        $attributes['default'] = isset($attributes['default']) ? (int)$attributes['default'] : 0;
+        $attributes['default_prettythumb'] = isset($attributes['default_prettythumb']) ? $attributes['default_prettythumb'] : '';
+        $attributes['style'] = isset($attributes['style']) ? $attributes['style'] : $GLOBALS['prefs']->getValue('default_gallerystyle');
+        $attributes['category'] = isset($attributes['category']) ? $attributes['category'] : $GLOBALS['prefs']->getValue('default_category');
+        $attributes['date_created'] = time();
+        $attributes['last_modified'] = $attributes['date_created'];
+        $attributes['images'] = isset($attributes['images']) ? (int)$attributes['images'] : 0;
+        $attributes['slug'] = isset($attributes['slug']) ? $attributes['slug'] : '';
+        $attributes['age'] = isset($attributes['age']) ? (int)$attributes['age'] : 0;
+        $attributes['download'] = isset($attributes['download']) ? $attributes['download'] : $GLOBALS['prefs']->getValue('default_download');
+        $attributes['view_mode'] = isset($attributes['view_mode']) ? $attributes['view_mode'] : 'Normal';
+        $attributes['passwd'] = isset($attributes['passwd']) ? $attributes['passwd'] : '';
+
+        /* Don't pass tags to the share creation method */
+        if (isset($attributes['tags'])) {
+            $tags = $attributes['tags'];
+            unset($attributes['tags']);
+        } else {
+            $tags = array();
+        }
+
+        /* Check for slug uniqueness */
+        if (!empty($attributes['slug']) &&
+            $this->slugExists($attributes['slug'])) {
+            return PEAR::raiseError(sprintf(_("The slug \"%s\" already exists."),
+                                            $attributes['slug']));
+        }
+
+        /* Create the gallery */
+        $gallery = $this->shares->newShare('');
+        if (is_a($gallery, 'PEAR_Error')) {
+            Horde::logMessage($gallery, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $gallery;
+        }
+        Horde::logMessage('New Ansel_Gallery object instantiated', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Set the gallery's parent if needed */
+        if (!is_null($parent)) {
+            $result = $gallery->setParent($parent);
+
+            /* Clear the parent from the cache */
+            if ($GLOBALS['conf']['ansel_cache']['usecache']) {
+                $GLOBALS['cache']->expire('Ansel_Gallery' . $parent);
+            }
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $result;
+            }
+        }
+
+        /* Fill up the new gallery */
+        // TODO: New private method to bulk load these (it's done this way
+        // since the data is stored in the Share_Object class keyed by the
+        // DB specific fields and set() translates them.
+        foreach ($attributes as $key => $value) {
+            $gallery->set($key, $value);
+        }
+
+        /* Save it to storage */
+        $result = $this->shares->addShare($gallery);
+        if (is_a($result, 'PEAR_Error')) {
+            $error = sprintf(_("The gallery \"%s\" could not be created: %s"),
+                             $attributes['name'], $result->getMessage());
+            Horde::logMessage($error, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return PEAR::raiseError($error);
+        }
+
+        /* Convenience */
+        $gallery->id = $gallery->getId();
+
+        /* Add default permissions. */
+        if (empty($perm)) {
+            $perm = $gallery->getPermission();
+
+            /* Default permissions for logged in users */
+            switch ($GLOBALS['prefs']->getValue('default_permissions')) {
+            case 'read':
+                $perms = PERMS_SHOW | PERMS_READ;
+                break;
+            case 'edit':
+                $perms = PERMS_SHOW | PERMS_READ | PERMS_EDIT;
+                break;
+            case 'none':
+                $perms = 0;
+                break;
+            }
+            $perm->addDefaultPermission($perms, false);
+
+            /* Default guest permissions */
+            switch ($GLOBALS['prefs']->getValue('guest_permissions')) {
+            case 'read':
+                $perms = PERMS_SHOW | PERMS_READ;
+                break;
+            case 'none':
+            default:
+                $perms = 0;
+                break;
+            }
+            $perm->addGuestPermission($perms, false);
+
+            /* Default user groups permissions */
+            switch ($GLOBALS['prefs']->getValue('group_permissions')) {
+            case 'read':
+                $perms = PERMS_SHOW | PERMS_READ;
+                break;
+            case 'edit':
+                $perms = PERMS_SHOW | PERMS_READ | PERMS_EDIT;
+                break;
+            case 'delete':
+                $perms = PERMS_SHOW | PERMS_READ | PERMS_EDIT | PERMS_DELETE;
+                break;
+            case 'none':
+            default:
+                $perms = 0;
+                break;
+            }
+
+            if ($perms) {
+                $groups = Group::singleton();
+                $group_list = $groups->getGroupMemberships(Horde_Auth::getAuth());
+                if (!is_a($group_list, 'PEAR_Error') && count($group_list)) {
+                    foreach ($group_list as $group_id => $group_name) {
+                        $perm->addGroupPermission($group_id, $perms, false);
+                    }
+                }
+            }
+        }
+        $gallery->setPermission($perm, true);
+
+        /* Initial tags */
+        if (count($tags)) {
+            $gallery->setTags($tags);
+        }
+
+        return $gallery;
+    }
+
+    /**
+     * Check that a slug exists.
+     *
+     * @param string $slug  The slug name
+     *
+     * @return integer  The share_id the slug represents, or 0 if not found.
+     */
+    function slugExists($slug)
+    {
+        // An empty slug should never match.
+        if (!strlen($slug)) {
+            return 0;
+        }
+
+        $stmt = $this->_db->prepare('SELECT share_id FROM '
+            . $this->shares->_table . ' WHERE attribute_slug = ?');
+
+        if (is_a($stmt, 'PEAR_Error')) {
+            Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return 0;
+        }
+
+        $result = $stmt->execute($slug);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+        if (!$result->numRows()) {
+            return 0;
+        }
+
+        $slug = $result->fetchRow();
+
+        $result->free();
+        $stmt->free();
+
+        return $slug[0];
+    }
+
+    /**
+     * Retrieve an Ansel_Gallery given the gallery's slug
+     *
+     * @param string $slug  The gallery slug
+     * @param array $overrides  An array of attributes that should be overridden
+     *                          when the gallery is returned.
+     *
+     * @return mixed  Ansel_Gallery object | PEAR_Error
+     */
+    function &getGalleryBySlug($slug, $overrides = array())
+    {
+        $id = $this->slugExists($slug);
+        if ($id) {
+            return $this->getGallery($id, $overrides);
+        } else {
+            return PEAR::raiseError(sprintf(_("Gallery %s not found."), $slug));
+        }
+     }
+
+    /**
+     * Retrieve an Ansel_Gallery given the share id
+     *
+     * @param integer $gallery_id  The share_id to fetch
+     * @param array $overrides     An array of attributes that should be
+     *                             overridden when the gallery is returned.
+     *
+     * @return mixed  Ansel_Gallery | PEAR_Error
+     */
+    function &getGallery($gallery_id, $overrides = array())
+    {
+        // avoid cache server hits
+        if (isset($this->galleries[$gallery_id]) && !count($overrides)) {
+            return $this->galleries[$gallery_id];
+        }
+
+       if (!count($overrides) && $GLOBALS['conf']['ansel_cache']['usecache'] &&
+           ($gallery = $GLOBALS['cache']->get('Ansel_Gallery' . $gallery_id, $GLOBALS['conf']['cache']['default_lifetime'])) !== false) {
+
+               $this->galleries[$gallery_id] = unserialize($gallery);
+
+               return $this->galleries[$gallery_id];
+       }
+
+       $result = &$this->shares->getShareById($gallery_id);
+       if (is_a($result, 'PEAR_Error')) {
+           return $result;
+       }
+       $this->galleries[$gallery_id] = &$result;
+
+       // Don't cache if we have overridden anything
+       if (!count($overrides)) {
+           if ($GLOBALS['conf']['ansel_cache']['usecache']) {
+               $GLOBALS['cache']->set('Ansel_Gallery' . $gallery_id, serialize($result));
+           }
+       } else {
+           foreach ($overrides as $key => $value) {
+               $this->galleries[$gallery_id]->set($key, $value, false);
+           }
+       }
+        return $this->galleries[$gallery_id];
+    }
+
+    /**
+     * Retrieve an array of Ansel_Gallery objects for the given slugs.
+     *
+     * @param array $slugs  The gallery slugs
+     *
+     * @return mixed  Array of Ansel_Gallery objects | PEAR_Error
+     */
+    function getGalleriesBySlugs($slugs)
+    {
+        $sql = 'SELECT share_id FROM ' . $this->shares->_table
+            . ' WHERE attribute_slug IN (' . str_repeat('?, ', count($slugs) - 1) . '?)';
+
+        $stmt = $this->shares->_db->prepare($sql);
+        if (is_a($stmt, 'PEAR_Error')) {
+            return $stmt;
+        }
+        $result = $stmt->execute($slugs);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $ids = array_values($result->fetchCol());
+        $shares = $this->shares->getShares($ids);
+
+        $stmt->free();
+        $result->free();
+
+        return $shares;
+    }
+
+    /**
+     * Retrieve an array of Ansel_Gallery objects for the requested ids
+     */
+    function getGalleries($ids)
+    {
+        return $this->shares->getShares($ids);
+    }
+
+    /**
+     * Empties a gallery of all images.
+     *
+     * @param Ansel_Gallery $gallery  The ansel gallery to empty.
+     */
+    function emptyGallery($gallery)
+    {
+        $images = $gallery->listImages();
+        foreach ($images as $image) {
+            // Pretend we are a stack so we don't update the images count
+            // for every image deletion, since we know the end result will
+            // be zero.
+            $gallery->removeImage($image, true);
+        }
+        $gallery->set('images', 0, true);
+
+        // Clear the OtherGalleries widget cache
+        if ($GLOBALS['conf']['ansel_cache']['usecache']) {
+            $GLOBALS['cache']->expire('Ansel_OtherGalleries' . $gallery->get('owner'));
+        }
+
+    }
+
+    /**
+     * Removes an Ansel_Gallery.
+     *
+     * @param Ansel_Gallery $gallery  The gallery to delete
+     *
+     * @return mixed  True || PEAR_Error
+     */
+    function removeGallery($gallery)
+    {
+        /* Get any children and empty them */
+        $children = $gallery->getChildren(null, true);
+        if (is_a($children, 'PEAR_Error')) {
+            return $children;
+        }
+        foreach ($children as $child) {
+            $this->emptyGallery($child);
+            $child->setTags(array());
+        }
+
+        /* Now empty the selected gallery of images */
+        $this->emptyGallery($gallery);
+
+        /* Clear all the tags. */
+        $gallery->setTags(array());
+
+        /* Get the parent, if it exists, before we delete the gallery. */
+        $parent = $gallery->getParent();
+        $id = $gallery->id;
+
+        /* Delete the gallery from storage */
+        $result = $this->shares->removeShare($gallery);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Expire the cache */
+        if ($GLOBALS['conf']['ansel_cache']['usecache']) {
+            $GLOBALS['cache']->expire('Ansel_Gallery' . $id);
+        }
+        unset($this->galleries[$id]);
+
+        /* See if we need to clear the has_subgalleries field */
+        if (is_a($parent, 'Ansel_Gallery')) {
+            if (!$parent->countChildren(PERMS_SHOW, false)) {
+                $parent->set('has_subgalleries', 0, true);
+
+                if ($GLOBALS['conf']['ansel_cache']['usecache']) {
+                    $GLOBALS['cache']->expire('Ansel_Gallery' . $parent->id);
+                }
+                unset($this->galleries[$id]);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the image corresponding to the given id.
+     *
+     * @param integer $id  The ID of the image to retrieve.
+     *
+     * @return Ansel_Image  The image object corresponding to the given name.
+     */
+    function &getImage($id)
+    {
+        if (isset($this->images[$id])) {
+            return $this->images[$id];
+        }
+
+        $q = $this->_db->prepare('SELECT ' . $this->_getImageFields() . ' FROM ansel_images WHERE image_id = ?');
+        if (is_a($q, 'PEAR_Error')) {
+            Horde::logMessage($q, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $q;
+        }
+        $result = $q->execute((int)$id);
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $result;
+        }
+        $image = $result->fetchRow(MDB2_FETCHMODE_ASSOC);
+        $q->free();
+        $result->free();
+        if (is_null($image)) {
+            return PEAR::raiseError(_("Photo not found"));
+        } elseif (is_a($image, 'PEAR_Error')) {
+            Horde::logMessage($image, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $image;
+        } else {
+            $image['image_filename'] = Horde_String::convertCharset($image['image_filename'], $GLOBALS['conf']['sql']['charset']);
+            $image['image_caption'] = Horde_String::convertCharset($image['image_caption'], $GLOBALS['conf']['sql']['charset']);
+            $this->images[$id] = new Ansel_Image($image);
+
+            return $this->images[$id];
+        }
+    }
+
+    /**
+     * Returns the images corresponding to the given ids.
+     *
+     * @param array $ids  An array of image ids.
+     *
+     * @return array of Ansel_Image objects.
+     */
+    function getImages($ids, $preserve_order = false)
+    {
+        if (is_array($ids) && count($ids) > 0) {
+            $sql = 'SELECT ' . $this->_getImageFields() . ' FROM ansel_images WHERE image_id IN (';
+            $i = 1;
+            $cnt = count($ids);
+            foreach ($ids as $id) {
+                $sql .= (int)$id . (($i++ < $cnt) ? ',' : ');');
+            }
+
+            $images = $this->_db->query($sql);
+            if (is_a($images, 'PEAR_Error')) {
+                return $images;
+            } elseif ($images->numRows() == 0) {
+                $images->free();
+                return PEAR::raiseError(_("Photos not found"));
+            }
+
+            $return = array();
+            while ($image = $images->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+                $image['image_filename'] = Horde_String::convertCharset($image['image_filename'], $GLOBALS['conf']['sql']['charset']);
+                $image['image_caption'] = Horde_String::convertCharset($image['image_caption'], $GLOBALS['conf']['sql']['charset']);
+                $return[$image['image_id']] = new Ansel_Image($image);
+                $this->images[(int)$image['image_id']] = &$return[$image['image_id']];
+            }
+            $images->free();
+
+            /* Need to get comment counts if comments are enabled */
+            $ccounts = $this->_getImageCommentCounts(array_keys($return));
+            if (!is_a($ccounts, 'PEAR_Error') && count($ccounts)) {
+                foreach ($return as $key => $image) {
+                    $return[$key]->commentCount = (!empty($ccounts[$key]) ? $ccounts[$key] : 0);
+                }
+            }
+
+            /* Preserve the order the ids were passed in) */
+            if ($preserve_order) {
+                foreach ($ids as $id) {
+                    $ordered[$id] = $return[$id];
+                }
+                return $ordered;
+            }
+            return $return;
+        } else {
+            return array();
+        }
+    }
+
+    function _getImageCommentCounts($ids)
+    {
+        global $conf, $registry;
+
+        /* Need to get comment counts if comments are enabled */
+        if (($conf['comments']['allow'] == 'all' || ($conf['comments']['allow'] == 'authenticated' && Horde_Auth::getAuth())) &&
+            $registry->hasMethod('forums/numMessagesBatch')) {
+
+            return $registry->call('forums/numMessagesBatch',
+                                   array($ids, 'ansel'));
+        }
+
+        return array();
+    }
+
+    /**
+     * Return a list of image ids of the most recently added images.
+     *
+     * @param array $galleries  An array of gallery ids to search in. If
+     *                          left empty, will search all galleries
+     *                          with PERMS_SHOW.
+     * @param integer $limit    The maximum number of images to return
+     * @param string $slugs     An array of gallery slugs.
+     * @param string $where     Additional where clause
+     *
+     * @return array An array of Ansel_Image objects
+     */
+    function getRecentImages($galleries = array(), $limit = 10, $slugs = array())
+    {
+        $results = array();
+
+        if (!count($galleries) && !count($slugs)) {
+            $sql = 'SELECT DISTINCT ' . $this->_getImageFields('i') . ' FROM ansel_images i, '
+            . str_replace('WHERE' , ' WHERE i.gallery_id = s.share_id AND (', substr($this->shares->_getShareCriteria(Horde_Auth::getAuth()), 5)) . ')';
+        } elseif (!count($slugs) && count($galleries)) {
+            // Searching by gallery_id
+            $sql = 'SELECT ' . $this->_getImageFields() . ' FROM ansel_images '
+                   . 'WHERE gallery_id IN ('
+                   . str_repeat('?, ', count($galleries) - 1) . '?) ';
+        } elseif (count($slugs)) {
+            // Searching by gallery_slug so we need to join the share table
+            $sql = 'SELECT ' . $this->_getImageFields() . ' FROM ansel_images LEFT JOIN '
+                . $this->shares->_table . ' ON ansel_images.gallery_id = '
+                . $this->shares->_table . '.share_id ' . 'WHERE attribute_slug IN ('
+                . str_repeat('?, ', count($slugs) - 1) . '?) ';
+        } else {
+            return array();
+        }
+
+        $sql .= ' ORDER BY image_uploaded_date DESC LIMIT ' . (int)$limit;
+        $query = $this->_db->prepare($sql);
+        if (is_a($query, 'PEAR_Error')) {
+            return $query;
+        }
+
+        if (count($slugs)) {
+            $images = $query->execute($slugs);
+        } else {
+            $images = $query->execute($galleries);
+        }
+        $query->free();
+        if (is_a($images, 'PEAR_Error')) {
+            return $images;
+        } elseif ($images->numRows() == 0) {
+            return array();
+        }
+
+        while ($image = $images->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+            $image['image_filename'] = Horde_String::convertCharset($image['image_filename'], $GLOBALS['conf']['sql']['charset']);
+            $image['image_caption'] = Horde_String::convertCharset($image['image_caption'], $GLOBALS['conf']['sql']['charset']);
+            $results[] = new Ansel_Image($image);
+        }
+
+        $images->free();
+        return $results;
+    }
+
+    /**
+     * Check if a gallery exists. Need to do this here instead of Horde_Share
+     * since Horde_Share::exists() takes a share_name, not a share_id plus we
+     * might also be checking by gallery_slug and this is more efficient than
+     * a listShares() call for one gallery.
+     *
+     * @param integer $gallery_id  The gallery id
+     * @param string  $slug        The gallery slug
+     *
+     * @return mixed  true | false | PEAR_Error
+     */
+    function galleryExists($gallery_id, $slug = null)
+    {
+        if (empty($slug)) {
+            return (bool)$this->_db->queryOne(
+                'SELECT COUNT(share_id) FROM ' . $this->shares->_table
+                . ' WHERE share_id = ' . (int)$gallery_id);
+        } else {
+            return (bool)$this->slugExists($slug);
+        }
+    }
+
+   /**
+    * Return a list of categories containing galleries with the given
+    * permissions for the current user.
+    *
+    * @param integer $perm   The level of permissions required.
+    * @param integer $from   The gallery to start listing at.
+    * @param integer $count  The number of galleries to return.
+    *
+    * @return mixed  List of categories | PEAR_Error
+    */
+    function listCategories($perm = PERMS_SHOW, $from = 0, $count = 0)
+    {
+        $sql = 'SELECT DISTINCT attribute_category FROM '
+               . $this->shares->_table;
+        $results = $this->shares->_db->query($sql);
+        if (is_a($results, 'PEAR_Error')) {
+            return $results;
+        }
+        $all_categories = $results->fetchCol('attribute_category');
+        $results->free();
+        if (count($all_categories) < $from) {
+            return array();
+        } else {
+            $categories = array();
+            foreach ($all_categories as $category) {
+                $categories[] = Horde_String::convertCharset(
+                    $category, $GLOBALS['conf']['sql']['charset']);
+            }
+            if ($count > 0) {
+                return array_slice($categories, $from, $count);
+            } else {
+                return array_slice($categories, $from);
+            }
+        }
+    }
+
+    function countCategories($perms = PERMS_SHOW)
+    {
+        return count($this->listCategories($perms));
+    }
+
+   /**
+    * Return the count of galleries that the user has specified permissions to
+    * and that match any of the requested attributes.
+    *
+    * @param string  $userid       The user to check access for.
+    * @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/values pairs or a gallery owner
+    *                              username.
+    * @param string  $parent       The parent share to start counting at.
+    * @param boolean $allLevels    Return all levels, or just the direct
+    *                              children of $parent? Defaults to all levels.
+    */
+    function countGalleries($userid, $perm = PERMS_SHOW, $attributes = null,
+                            $parent = null, $allLevels = true)
+    {
+        static $counts;
+
+        if (is_a($parent, 'Ansel_Gallery')) {
+            $parent_id = $parent->getId();
+        } else {
+            $parent_id = $parent;
+        }
+
+        $key = "$userid,$perm,$parent_id,$allLevels"
+               . serialize($attributes);
+        if (isset($counts[$key])) {
+            return $counts[$key];
+        }
+
+        $count = $this->shares->countShares($userid, $perm, $attributes,
+                                            $parent, $allLevels);
+
+        $counts[$key] = $count;
+
+        return $count;
+    }
+
+   /**
+    * Retrieves the current user's gallery list from storage.
+    *
+    * @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/values pairs or a gallery owner
+    *                              username.
+    * @param mixed   $parent       The parent gallery to start listing at.
+    *                              (Ansel_Gallery, 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 string  $sort_by      The field to order the results by.
+    * @param integer $direction    Sort direction:
+    *                               0 - ascending
+    *                               1 - descending
+    *
+    * @return mixed An array of Ansel_Gallery objects | PEAR_Error
+    */
+    function listGalleries($perm = PERMS_SHOW,
+                           $attributes = null,
+                           $parent = null,
+                           $allLevels = true,
+                           $from = 0,
+                           $count = 0,
+                           $sort_by = null,
+                           $direction = 0)
+    {
+        return $this->shares->listShares(Horde_Auth::getAuth(), $perm, $attributes,
+                                         $from, $count, $sort_by, $direction,
+                                         $parent, $allLevels);
+    }
+
+    /**
+     * Retrieve json data for an arbitrary list of image ids, not necessarily
+     * from the same gallery.
+     *
+     * @param array $images        An array of image ids
+     * @param string $style        A named gallery style to force if requesting
+     *                             pretty thumbs.
+     * @param boolean $full        Generate full urls
+     * @param string $image_view   Which image view to use? screen, thumb etc..
+     * @param boolean $view_links  Include links to the image view
+     *
+     * @return string  The json data || PEAR_Error
+     */
+    function getImageJson($images, $style = null, $full = false,
+                          $image_view = 'mini', $view_links = false)
+    {
+        $galleries = array();
+        if (is_null($style)) {
+            $style = 'ansel_default';
+        }
+
+        $json = array();
+
+        foreach ($images as $id) {
+            $image = $this->getImage($id);
+            if (!is_a($image, 'PEAR_Error')) {
+                $gallery_id = abs($image->gallery);
+
+                if (empty($galleries[$gallery_id])) {
+                    $galleries[$gallery_id]['gallery'] = $GLOBALS['ansel_storage']->getGallery($gallery_id);
+                    if (is_a($galleries[$gallery_id]['gallery'], 'PEAR_Error')) {
+                        return $galleries[$gallery_id];
+                    }
+                }
+
+                // Any authentication that needs to take place for any of the
+                // images included here MUST have already taken place or the
+                // image will not be incldued in the output.
+                if (!isset($galleries[$gallery_id]['perm'])) {
+                    $galleries[$gallery_id]['perm'] =
+                        ($galleries[$gallery_id]['gallery']->hasPermission(Horde_Auth::getAuth(), PERMS_READ) &&
+                         $galleries[$gallery_id]['gallery']->isOldEnough() &&
+                         !$galleries[$gallery_id]['gallery']->hasPasswd());
+                }
+
+                if ($galleries[$gallery_id]['perm']) {
+                    $data = array(Ansel::getImageUrl($image->id, $image_view, $full, $style),
+                        htmlspecialchars($image->filename, ENT_COMPAT, Horde_Nls::getCharset()),
+                        Horde_Text_Filter::filter($image->caption, 'text2html', array('parselevel' => Horde_Text_Filter_Text2html::MICRO_LINKURL)),
+                        $image->id,
+                        0);
+
+                    if ($view_links) {
+                        $data[] = Ansel::getUrlFor('view',
+                            array('gallery' => $image->gallery,
+                                  'image' => $image->id,
+                                  'view' => 'Image',
+                                  'slug' => $galleries[$gallery_id]['gallery']->get('slug')),
+                            $full);
+
+                        $data[] = Ansel::getUrlFor('view',
+                            array('gallery' => $image->gallery,
+                                  'slug' => $galleries[$gallery_id]['gallery']->get('slug'),
+                                  'view' => 'Gallery'),
+                            $full);
+                    }
+
+                    $json[] = $data;
+                }
+            }
+        }
+
+        if (count($json)) {
+            return Horde_Serialize::serialize($json, Horde_Serialize::JSON, Horde_Nls::getCharset());
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Returns a random Ansel_Gallery from a list fitting the search criteria.
+     *
+     * @see Ansel_Storage::listGalleries()
+     */
+    function getRandomGallery($perm = PERMS_SHOW, $attributes = null,
+                              $parent = null, $allLevels = true)
+    {
+        $num_galleries = $this->countGalleries(Horde_Auth::getAuth(), $perm,
+                                               $attributes, $parent,
+                                               $allLevels);
+        if (!$num_galleries) {
+            return $num_galleries;
+        }
+
+        $galleries = $this->listGalleries($perm, $attributes, $parent,
+                                          $allLevels,
+                                          rand(0, $num_galleries - 1),
+                                          1);
+        $gallery = array_pop($galleries);
+        return $gallery;
+    }
+
+    /**
+     * Lists a slice of the image ids in the given gallery.
+     *
+     * @param integer $gallery_id  The gallery to list from.
+     * @param integer $from        The image to start listing.
+     * @param integer $count       The numer of images to list.
+     * @param mixed $fields        The fields to return (either an array of
+     *                             fileds or a single string).
+     * @param string $where        A SQL where clause ($gallery_id will be
+     *                             ignored if this is non-empty).
+     * @param mixed $sort          The field(s) to sort by.
+     *
+     * @return mixed  An array of image_ids | PEAR_Error
+     */
+    function listImages($gallery_id, $from = 0, $count = 0,
+                        $fields = 'image_id', $where = '', $sort = 'image_sort')
+    {
+        if (is_array($fields)) {
+            $field_count = count($fields);
+            $fields = implode(', ', $fields);
+        } elseif ($fields == '*') {
+            // The count is not important, as long as it's > 1
+            $field_count = 2;
+        } else {
+            $field_count = substr_count($fields, ',') + 1;
+        }
+
+        if (is_array($sort)) {
+            $sort = implode(', ', $sort);
+        }
+
+        if (!empty($where)) {
+            $query_where = 'WHERE ' . $where;
+        } else {
+            $query_where = 'WHERE gallery_id = ' . $gallery_id;
+        }
+        $this->_db->setLimit($count, $from);
+        $sql = 'SELECT ' . $fields . ' FROM ansel_images ' . $query_where . ' ORDER BY ' . $sort;
+        Horde::logMessage('Query by Ansel_Storage::listImages: ' . $sql, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        $results = $this->_db->query('SELECT ' . $fields . ' FROM ansel_images '
+            . $query_where . ' ORDER BY ' . $sort);
+        if (is_a($results, 'PEAR_Error')) {
+            return $results;
+        }
+        if ($field_count > 1) {
+            return $results->fetchAll(MDB2_FETCHMODE_ASSOC, true, true, false);
+        } else {
+            return $results->fetchCol();
+        }
+    }
+
+    /**
+     * Return images' geolocation data.
+     *
+     * @param array $image_ids  An array of image_ids to look up.
+     * @param integer $gallery  A gallery id. If this is provided, will return
+     *                          all images in the gallery that have geolocation
+     *                          data ($image_ids would be ignored).
+     *
+     * @return mixed An array of geodata || PEAR_Error
+     */
+    function getImagesGeodata($image_ids = array(), $gallery = null)
+    {
+        if ((!is_array($image_ids) || count($image_ids) == 0) && empty($gallery)) {
+            return array();
+        }
+
+        if (!empty($gallery)) {
+            $where = 'gallery_id = ' . (int)$gallery . ' AND LENGTH(image_latitude) > 0';
+        } elseif (count($image_ids) > 0) {
+            $where = 'image_id IN(' . implode(',', $image_ids) . ') AND LENGTH(image_latitude) > 0';
+        } else {
+            return array();
+        }
+
+        return $this->listImages(0, 0, 0, array('image_id as id', 'image_id', 'image_latitude', 'image_longitude', 'image_location'), $where);
+    }
+
+    /**
+     * Get image attribtues from ansel_image_attributes table
+     *
+     * @param $image_id
+     * @return unknown_type
+     */
+    function getImageAttributes($image_id)
+    {
+        return $GLOBALS['ansel_db']->queryAll('SELECT attr_name, attr_value FROM ansel_image_attributes WHERE image_id = ' . (int)$image_id, null, MDB2_FETCHMODE_ASSOC, true);
+    }
+
+    /**
+     * Like getRecentImages, but returns geotag data for the most recently added
+     * images from the current user. Useful for providing images to help locate
+     * images at the same place.
+     */
+    function getRecentImagesGeodata($user = null, $start = 0, $count = 8)
+    {
+        $galleries = $this->listGalleries('PERMS_EDIT', $user);
+        $where = 'gallery_id IN(' . implode(',', array_keys($galleries)) . ') AND LENGTH(image_latitude) > 0 GROUP BY image_latitude, image_longitude';
+        return $this->listImages(0, $start, $count, array('image_id as id', 'image_id', 'gallery_id', 'image_latitude', 'image_longitude', 'image_location'), $where, 'image_geotag_date DESC');
+    }
+
+    function searchLocations($search = '')
+    {
+        $sql = 'SELECT DISTINCT image_location, image_latitude, image_longitude'
+            . ' FROM ansel_images WHERE image_location LIKE "' . $search . '%"';
+        $results = $this->_db->query($sql);
+        if (is_a($results, 'PEAR_Error')) {
+            return $results;
+        }
+
+        return $results->fetchAll(MDB2_FETCHMODE_ASSOC, true, true, false);
+    }
+
+    /**
+     * Helper function to get a string of field names
+     *
+     * @return string
+     */
+    function _getImageFields($alias = '')
+    {
+        $fields = array('image_id', 'gallery_id', 'image_filename', 'image_type',
+                        'image_caption', 'image_uploaded_date', 'image_sort',
+                        'image_faces', 'image_original_date', 'image_latitude',
+                        'image_longitude', 'image_location', 'image_geotag_date');
+        if (!empty($alias)) {
+            foreach ($fields as $field) {
+                $new[] = $alias . '.' . $field;
+            }
+            return implode(', ', $new);
+        }
+
+        return implode(', ', $fields);
+    }
+
+}