#!/usr/bin/env php
<?php
/**
* This script allows for adding images to an Ansel install using an RPC
* interface. This script requires Horde's Cli, Rpc, and Argv libraries.
* You will need to make sure that those libraries reside somewhere in
* PHP's include path.
*
* See the enclosed file COPYING for license information (GPL). If you
* did not receive this file, see http://www.horde.org/licenses/gpl.
*
* @author Michael J. Rubinsky <mrubinsk@horde.org>
*/
if (file_exists(__DIR__ . '/../../ansel/lib/Application.php')) {
    $baseDir = __DIR__ . '/../';
} else {
    require_once 'PEAR/Config.php';
    $baseDir = PEAR_Config::singleton()
        ->get('horde_dir', null, 'pear.horde.org') . '/ansel/';
}
require_once $baseDir . 'lib/Application.php';
Horde_Registry::appInit('ansel', array('cli' => true));

$parser = new Horde_Argv_Parser(
    array(
        'usage' => "%prog [--options] [command]\n
Available commands are:\n
(Note that some command may require sudo access.)\n
  import: Recursively import images from folder specified with -d
  list:   List all galleries or (if used with -g) all images in specified gallery
  create: Create a new gallery. Optionally used with -n to set name and --description
          to set description
  add:    Add a new image specified with the -f option to a gallery specified with
          either -g or -s
  reset [thumbs|stacks]:  Reset all generated images, optionally restricting to
                          either thumbnails or gallery stacks.
  exif:   Extract exif data for all images in the specified gallery. This will
          replace any existing exif data for all images the requested gallery.",
        'optionList' => array(
            new Horde_Argv_Option(
                '-u',
                '--username',
                array(
                    'help' => 'Horde username'
                )
            ),
            new Horde_Argv_Option(
                '-p',
                '--password',
                array(
                    'help' => 'Horde password'
                )
            ),
            new Horde_Argv_Option(
                '-g',
                '--gallery',
                array(
                    'help' => 'The gallery id'
                )
            ),
            new Horde_Argv_Option(
                '-n',
                '--name',
                array(
                    'help' => 'Name to use for creating new gallery'
                )
            ),
            new Horde_Argv_Option(
                '',
                '--description',
                array(
                    'help' => 'Description to use when creating new gallery.'
                )
            ),
            new Horde_Argv_Option(
                '-s',
                '--slug',
                array(
                    'help' => 'The gallery slug'
                )
            ),
            new Horde_Argv_Option(
                '-k',
                '--keep',
                array(
                    'default' => false,
                    'action' => 'store_true',
                    'help' => 'Do not delete empty galleries on remote server after export is complete.'
                )
            ),
            new Horde_Argv_Option(
                '-z',
                '--gzip',
                array(
                    'default' => false,
                    'action' => 'store_true',
                    'help' => 'Use gzip compression'
                )
            ),
            new Horde_Argv_Option(
                '-l',
                '--lzf',
                array(
                    'default' => false,
                    'action' => 'store_true',
                    'help' => 'Use lzf compression'
                )
            ),
            new Horde_Argv_Option(
                '-r',
                '--remote',
                array(
                    'help' => 'Connect to remote Ansel server. This should be the path to rpc.php'
                )
            ),
            new Horde_Argv_Option(
                '-d',
                '--directory',
                array(
                    'help' => 'Recursively import any images in this local directory.'
                )
            ),
            new Horde_Argv_Option(
                '-f',
                '--file',
                array(
                    'help' => 'Add this single image file to the gallery specified with either -g or -s'
                )
            ),
        )
    )
);

/* Show help and exit if no arguments were set. */
list($opts, $args) = $parser->parseArgs();
if (empty($opts['username']) && empty($opts['password'])) {
    $parser->parserError('Missing Horde credentials.');
}

// Determine if we are local or remote.
if (empty($opts['remote'])) {
    $runner = new Ansel_Cli_Helper_Local(
        $cli,
        array(
            'username' => $opts['username'],
            'password' => $opts['password']
        ));
} else {
    $runner = new Ansel_Cli_Helper_Remote(
        $cli,
        array(
            'remote' => $opts['remote'],
            'compression' => !empty($opts['gzip']) ? 'gzip' : (!empty($opts['lzf']) ? 'lzf' : false),
            'auth' => array(
                'request.username' => $opts['username'],
                'request.password' => $opts['password'])));
}

// What are we here for?
switch ($args[0]) {
case 'import':
    if (!empty($opts['directory'])) {
        // Import directory of images
        $runner->processDirectory($opts['directory'], null, $opt['gallery'], $opts['slug']);
    } else {
        $parser->parserError('Missing directory parameter');
    }
    break;
case 'exif':
    if (!empty($opts['gallery'])) {
        $runner->exif($opts['gallery']);
    } else {
        $parser->parserError('Missing gallery parameter');
    }
    break;
case 'list':
    $runner->listObjects($opts['gallery'], $opts['slug']);
    break;
case 'create':
    $runner->create(
        empty($opts['name']) ? 'Untitled' : $opts['name'],
        empty($opts['description']) ? '' : $opts['description'],
        empty($opts['gallery']) ? null : $opts['gallery'],
        empty($opts['slug']) ? null : $opts['slug']);
    break;
case 'add':
    $runner->add(
        empty($opts['gallery']) ? null : $opts['gallery'],
        empty($opts['slug']) ? null : $opts['slug'],
        $opts['file'],
        empty($opts['description']) ? null : $opts['description']);
    break;
case 'reset':
    $runner->reset(empty($args[1]) ? null : $args[1]);
    break;
}
exit(0);

/**
 * Abstract class for communication with Ansel via the CLI
 *
 * @author Michael J. Rubinsky <mrubinsk@horde.org>
 * @package Ansel
 */
abstract class Ansel_Cli_Helper_Base
{
    protected $_params = array();
    protected $_cli;
    protected $_compress = 'none';

    public function __construct(Horde_Cli $cli, array $params = array())
    {
        $this->_cli = $cli;
        $this->_params = array();
    }

    /**
     * Check for, and remove any empty galleries that may have been created
     * during  import.
     *
     * @param Ansel_Gallery $gallery  An ansel gallery object to check.
     *
     * @throws Exception
     */
    public function emptyGalleryCheck(Ansel_Gallery $gallery)
    {
        if ($gallery->hasSubGalleries()) {
            $children = $GLOBALS['injector']
                ->getInstance('Ansel_Storage')
                ->listGalleries(array('parent' => $gallery->id));
            foreach ($children as $child) {
                // First check all children to see if they are empty...
                $this->emptyGalleryCheck($child);
                if (!$child->countImages() && !$child->hasSubGalleries()) {
                    try {
                        $GLOBALS['injector']
                            ->getInstance('Ansel_Storage')
                            ->removeGallery($child);
                    } catch (Ansel_Exception $e) {
                        throw new Exception($e->getMessage());
                    }
                    $GLOBALS['cli']->message(
                        sprintf(
                            _("Deleted empty gallery, \"%s\""), $child->get('name')),
                        'cli.success');
                }

                // Refresh the gallery values since we mucked around a bit with it
                $gallery = $GLOBALS['injector']
                    ->getInstance('Ansel_Storage')
                    ->getGallery($gallery->id);

                // Now that any empty children are removed, see if we are empty
                if (!$gallery->countImages() && !$gallery->hasSubGalleries()) {
                    try {
                        $GLOBALS['injector']
                            ->getInstance('Ansel_Storage')
                            ->removeGallery($gallery);
                    } catch (Ansel_Exception $e) {
                        throw new Exception($e->getMessage());
                    }
                    $GLOBALS['cli']->message(
                        sprintf(_("Deleting empty gallery, \"%s\""), $gallery->get('name')),
                        'cli.success');
                }
            }
        }
    }

    /**
     * Read an image from the filesystem.
     *
     * @param string $file  The filename of the image.
     *
     * @return array  The image data of the file as an array
     * @throws Exception
     */
    protected function _getImageFromFile($file)
    {
        if (!file_exists($file)) {
            throw new Exception(sprintf(_("The file \"%s\" doesn't exist."), $file));
        }

        // Get the mime type of the file (and make sure it's an image).
        $mime_type = Horde_Mime_Magic::analyzeFile(
            $file,
            isset($conf['mime']['magic_db']) ? $conf['mime']['magic_db'] : null);

        if (strpos($mime_type, 'image') === false) {
            throw new Exception(sprintf(_("Can't get unknown file type \"%s\"."), $file));
        }

        if ($this->_compress == 'gzip' && Horde_Util::loadExtension('zlib')) {
            $data = gzcompress(file_get_contents($file));
        } elseif ($this->_compress == 'gzip') {
            $this->_cli->fatal(_("Could not load the gzip extension"));
        } elseif ($this->_compress == 'lzf' && Horde_Util::loadExtension('lzf')) {
            $data = lzf_compress(file_get_contents($file));
        } elseif ($this->_compress == 'lzf') {
            $this->_cli->fatal(_("Could not load the lzf extension"));
        } else {
            $data = file_get_contents($file);
        }

        return array(
            'filename' => basename($file),
            'description' => '',
            'type' => $mime_type,
            'data' => bin2hex($data));
    }

    abstract public function processDirectory($dir, $parent = null, $gallery_id = null, $slug = null);
    abstract public function listObjects($gallery = null, $slug = null);
    abstract public function create($name, $desc, $parent_id = null);
    abstract public function reset($type = null);
    abstract public function add($gallery, $slug, $file, $caption);
    abstract public function exif($gallery);

}

/**
 * Class for communication with a remote ansel install via the CLI.
 *
 * @author Michael J. Rubinsky <mrubinsk@horde.org>
 * @package Ansel
 */
class Ansel_Cli_Helper_Remote extends Ansel_Cli_Helper_Base
{
    public function __construct($cli, $params = array())
    {
        $this->_auth = $params['auth'];
        $this->_remote = $params['remote'];
        $this->_compression = $params['compression'];
        unset ($params['auth'], $params['remote'], $params['compression']);
        parent::__construct($cli, $params);
    }

    /**
     * Read all images from a directory into the currently selected gallery.
     *
     * @param string $dir          The directory to create a gallery for and import.
     * @param integer $parent      Parent gallery id to attach the new gallery to.
     * @param integer $gallery_id  Start at this gallery_id.
     * @param string  $slug        Same as $gallery_id, except use this slug
     *
     * @return integer The gallery_id of the newly created gallery
     * @throws Exception
     */
    public function processDirectory($dir, $parent = null, $gallery_id = null, $slug = null)
    {
        $dir = Horde_Util::realPath($dir);
        if (!is_dir($dir)) {
            $this->_cli->fatal(sprintf(_("\"%s\" is not a directory."), $dir));
        }

        $rpc_params = $this->_auth;

        // Set language header
        $language = isset($GLOBALS['language']) ?
             $GLOBALS['language'] :
             (isset($_SERVER['LANG']) ?
                 $_SERVER['LANG'] : '');
        if (!empty($language)) {
            $rpc_params['request.headers'] = array('Accept-Language' => $language);
        }

        // Don't timeout.
        $rpc_params['request.timeout'] = 0;

        // Create the http client
        $http = $GLOBALS['injector']
            ->getInstance('Horde_Core_Factory_HttpClient')
            ->create($rpc_params);

        // Create a gallery or use an existing one?
        if (!empty($gallery_id) || !empty($slug)) {
            $method = 'images.getGalleries';
            $params = array(
                is_null($gallery_id) ? null : array($gallery_id),
                null,
                is_null($slug) ? null : array($slug),
            );
            $result = Horde_Rpc::request(
                'jsonrpc',
                $this->_remote,
                $method,
                $http,
                $params);
            $result = $result->result;
            if (empty($result)) {
                $this->_cli->fatal(
                    sprintf(_("Gallery %s not found."), (empty($slug) ? $gallery_id : $slug)));
            }

            // Should have only one here, but jsonrpc returns an object, not array
            foreach ($result as $gallery_info) {
               $name = $gallery_info->attribute_name;
               $gallery_id = $gallery_info->share_id;
            }
            if (empty($name)) {
                $this->_cli->fatal(sprintf(_("Gallery %s not found."), (empty($slug) ? $gallery_id : $slug)));
            }
        } else {
            $name = basename($dir);
            $this->_cli->message(sprintf(_("Creating gallery: \"%s\""), $name), 'cli.message');
            $method = 'images.createGallery';
            $params = array(array('name' => $name), array('parent' => $parent));
            $result = Horde_Rpc::request('jsonrpc', $this->_remote, $method, $http, $params);
            $gallery_id = $result->result;
            $this->_cli->message(sprintf(_("The gallery \"%s\" was created successfully."), $name), 'cli.success');
        }

        // Get the files and directories
        $files = array();
        $directories = array();
        $h = opendir($dir);
        while (false !== ($entry = readdir($h))) {
            if ($entry == '.' ||
                $entry == '..' ||
                $entry == 'Thumbs.db' ||
                $entry == '.DS_Store' ||
                $entry == '.localized' ||
                $entry == '__MACOSX' ||
                strpos($entry, '.') === 1) {
                continue;
            }

            if (is_dir($dir . '/' . $entry)) {
                $directories[] = $entry;
            } else {
                $files[] = $entry;
            }
        }
        closedir($h);
        if (!empty($this->_params['order'])) {
            usort($files, $this->_params['order'].'Cmp');
            usort($directories, $this->_params['order'].'Cmp');
        }

        if ($files) {
            chdir($dir);
            $added_images = array();
            foreach ($files as $file) {
                try {
                    $image = $this->_getImageFromFile($dir . '/' . $file);
                } catch (Exception $e) {
                    // Not an image file.
                    continue;
                }
                $this->_cli->message(
                    sprintf(_("Storing photo \"%s\"..."), $file),
                    'cli.message');
                $method = 'images.saveImage';
                $params = array(
                    $gallery_id,
                    $image,
                    array(
                        'encoding' => 'binhex',
                        'compression' => $this->_compression));
                try {
                    $result = Horde_Rpc::request(
                        'jsonrpc',
                        $this->_remote,
                        $method,
                        $http,
                        $params);
                } catch (Horde_Rpc_Exception $e) {
                    $this->_cli->fatal(
                        sprintf(
                            _("There was an unspecified error. The server returned: %s"),
                            $e->getMessage()));
                }

                $image_id = $result->result;
                $added_images[] = $file;
            }

            $this->_cli->message(
                sprintf(
                    ngettext("Successfully added %d photo (%s) to gallery \"%s\" from \"%s\".", "Successfully added %d photos (%s) to gallery \"%s\" from \"%s\".", count($added_images)),
                    count($added_images),
                    join(', ', $added_images),
                    $name,
                    $dir),
                'cli.success');
        }

        if ($directories) {
            $this->_cli->message(_("Adding subdirectories:"), 'cli.message');
            foreach ($directories as $directory) {
               $this->processDirectory($dir . '/' . $directory, $gallery_id);
            }
        }

        return $gallery_id;
    }

    public function listObjects($gallery_id = null, $slug = null)
    {

    }

    /**
     * Create a new gallery.
     *
     * @param string $name  The new gallery name.
     * @param string $desc  The gallery's description.
     *
     * @throws Exception
     */
    public function create($name, $desc, $parent_id = null)
    {
        $this->_cli->fatal('reset command unavailable for remote servers at this time.');
    }

    /**
     *
     */
    public function add($gallery, $slug, $file, $caption)
    {
        $this->_cli->fatal('add command unavailable for remote servers at this time.');
    }

    /**
     * Reset images
     *
     * @param string $type  Restrict to this type
     */
    public function reset($type = null)
    {
        $this->_cli->fatal('reset command unavailable for remote servers at this time.');
    }

    public function exif($gallery)
    {
        throw new Ansel_Exception('Not supported for remote galleries');
    }

}

/**
 * Class for communicating with a local Ansel install
 *
 * @package Ansel
 */
class Ansel_Cli_Helper_Local extends Ansel_Cli_Helper_Base
{
    public function __construct(Horde_Cli $cli, array $params = array())
    {
        parent::__construct($cli, $params);
        $auth = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create();
        if (!$auth->authenticate($params['username'], array('password' => $params['password']))) {
            $this->_cli->fatal(_("Username or password is incorrect."));
        } else {
            $this->_cli->message(sprintf(_("Logged in successfully as \"%s\"."), $params['username']), 'cli.success');
        }
    }

    /**
     * Read all images from a directory into the currently selected
     * gallery.
     *
     * @param string $dir  The directory to create a gallery for and import.
     * @param integer $parent  Parent gallery id to attach the new gallery to.
     *
     * @return mixed  The gallery_id of the newly created gallery || PEAR_Error
     */
    public function processDirectory($dir, $parent = null, $gallery_id = null, $slug = null)
    {
        $dir = Horde_Util::realPath($dir);
        if (!is_dir($dir)) {
            $this->_cli->fatal(sprintf(_("\"%s\" is not a directory."), $dir));
        }

        // Create a gallery or use an existing one?
        if (!$gallery = $this->_getGallery($gallery_id, $slug)) {
            $name = basename($dir);
            $this->_cli->message(sprintf(_("Creating gallery: \"%s\""), $name), 'cli.message');
            $gallery = $GLOBALS['injector']
                ->getInstance('Ansel_Storage')
                ->createGallery(
                    array('name' => $name),
                    null,
                    $parent);
            $this->_cli->message(
                sprintf(_("The gallery \"%s\" was created successfully."), $name),
                'cli.success');
        }

        // Read all the files into an array.
        $files = array();
        $directories = array();
        $h = opendir($dir);
        while (false !== ($entry = readdir($h))) {
            if ($entry == '.' ||
                $entry == '..' ||
                $entry == 'Thumbs.db' ||
                $entry == '.DS_Store' ||
                $entry == '.localized' ||
                $entry == '__MACOSX' ||
                strpos($entry, '.') === 1) {
                continue;
            }
            if (is_dir($dir . '/' . $entry)) {
                $directories[] = $dir . '/' . $entry;
            } else {
                $files[] = $dir . '/' . $entry;
            }
        }
        closedir($h);

        if (!empty($this->_params['order'])) {
            usort($files, $this->_params['order'].'Cmp');
            usort($directories, $this->_params['order'].'Cmp');
        }

        if ($files) {
            // Process each file and upload to the gallery.
            $added_images = array();
            foreach ($files as $file) {
                $image = Ansel::getImageFromFile($file);
                $this->_cli->message(sprintf(_("Storing photo \"%s\"..."), $file), 'cli.message');
                $image_id = $gallery->addImage($image);
                $added_images[] = $file;
            }

            $this->_cli->message(
                sprintf(
                    ngettext("Successfully added %d photo (%s) to gallery \"%s\" from \"%s\".", "Successfully added %d photos (%s) to gallery \"%s\" from \"%s\".", count($added_images)),
                    count($added_images),
                    join(', ', $added_images),
                    $gallery->get('name'),
                    $dir),
                'cli.success');
        }

        if ($directories) {
            $this->_cli->message(_("Adding subdirectories:"), 'cli.message');
            foreach ($directories as $directory) {
                $this->processDirectory($directory, $gallery->id);
            }
        }

        return $gallery->id;
    }

    public function listObjects($gallery = null, $slug = null)
    {
        if ($gallery = $this->_getGallery($gallery, $slug)) {
            $images = $gallery->listImages();
            $this->_cli->message(
                sprintf(_("Listing photos in %s"), $gallery->get('name')), 'cli.success');
            foreach ($images as $id) {
                try {
                    $image = $GLOBALS['injector']->getInstance('Ansel_Storage')->getImage($id);
                    $this->_cli->writeln(str_pad($image->filename, 30) . $image->getVFSPath() . '/' . $id);
                } catch (Exception $e) {
                    $this->_cli->message($e->getMessage(), 'cli.error');
                }
            }
        } else {
            $galleries = $GLOBALS['injector']
                ->getInstance('Ansel_Storage')
                ->listGalleries();
            $this->_cli->message(_("Listing Gallery/Name"), 'cli.success');
            $this->_cli->writeln();
            foreach ($galleries as $gallery) {
                $name = $gallery->get('name');
                $id = $gallery->id;
                $msg = "$id/$name";
                $this->_cli->writeln($msg);
            }
        }
    }

    /**
     * Create a new gallery.
     *
     * @param string $name  The new gallery name.
     * @param string $desc  The gallery's description.
     *
     * @throws Exception
     */
    public function create($name, $desc, $parent_id = null)
    {
        $attributes = array(
            'name' => $name,
            'desc' => $desc,
            'owner' => $GLOBALS['registry']->getAuth());
        try {
            $gallery = $GLOBALS['injector']
                ->getInstance('Ansel_Storage')
                ->createGallery($attributes, null, $parent_id);
            $msg = sprintf(
                _("The gallery \"%s\" was created successfully."),
                $name);
            $this->_cli->message($msg, 'cli.success');
        } catch (Ansel_Exception $e) {
            $galleryId = null;
            $error = sprintf(
                _("The gallery \"%s\" couldn't be created: %s"),
                $gallery_name,
                $e->getMessage());
            Horde::logMessage($error, 'ERR');
            $this->_cli->message($error, 'cli.error');
        }
    }

    /**
     *
     */
    public function add($gallery, $slug, $file, $caption)
    {
        try {
            $image = Ansel::getImageFromFile($file, array('caption' => $caption));
            $this->_cli->message(sprintf('Storing photo \"%s\" ...', $file), 'cli.message');
            $g = $this->_getGallery($gallery, $slug);
            $image_id = $g->addImage($image);
            $this->_cli->message(sprintf('Successfully added photo \"%s\" to gallery \"%s\".'), $file, $gallery);
        } catch (Ansel_Exception $e) {
            $this->_cli->message($e->getMessage(), 'cli.error');
        }
    }

    /**
     * Reset images
     *
     * @param string $type  Restrict to this type
     */
    public function reset($type = null)
    {
        if ($type && $type != 'thumbs' && $type != 'stacks') {
            $this->_cli->message('Invalid image type. Must be either \"stacks\" or \"thumbs\"', 'cli.error');
            return;
        }

        if (!$GLOBALS['registry']->isAdmin()) {
            $this->_cli->message('Requires Admin access');
            return;
        }
        $this->_cli->message('Resetting thumbnails: ', 'cli.info');
        try {
            $galleries = $GLOBALS['injector']
                ->getInstance('Ansel_Storage')
                ->listAllGalleries();
        } catch (Ansel_Exception $e) {
            $this->_cli->fatal($e->getMessage());
        }
        foreach ($galleries as $gallery) {
            try {
                if (empty($resetType) || $resetType == 'stacks') {
                    $gallery->clearStacks();
                    $this->_cli->message(sprintf('Successfully reset stack cache for gallery: %d', $gallery->id), 'cli.success');
                }
                if (empty($resetType) || $resetType == 'thumbs') {
                    $gallery->clearViews();
                    $this->_cli->message(sprintf('Successfully reset image cache for gallery: %d', $gallery->id), 'cli.success');
                }
            } catch (Ansel_Exception $e) {
                $this->_cli->message($e->getMessage(), 'cli.error');
            }
        }

        $this->_cli->message(_("Done."), 'cli.success');
    }

    public function exif($gallery)
    {
        $gallery = $GLOBALS['injector']
            ->getInstance('Ansel_Storage')
            ->getGallery($gallery);

        foreach ($gallery->getImages() as $image) {
            $this->_cli->message(sprintf(_("Extracting EXIF for image %d"), $image->id));
            $image->getEXIF(true);
        }

        $this->_cli->message(_("Done."), 'cli.success');
    }

    /**
     *
     */
    protected function _getGallery($gallery_id = null, $slug = null)
    {
        $gallery = false;
        // Create a gallery or use an existing one?
        if (!empty($gallery_id)) {
            $gallery = $GLOBALS['injector']
                ->getInstance('Ansel_Storage')
                ->getGallery($gallery_id);
        } elseif (!empty($slug)) {
            $gallery = $GLOBALS['injector']
                ->getInstance('Ansel_Storage')
                ->getGalleryBySlug($slug);
        }

        return $gallery;
    }

}