More Horde Fun

After a very productive Horde Board meeting last week (see Chuck's article) and the decision to really focus on getting out the next 3.3 series releases, I thought it a good time to summarize some recent work.

My primary focus lately has been on Ansel, the soon-to-be-released photo management application.  I have been busy adding some new features and cleaning up the code preparing it for release.

The latest new feature is the addition of what I call Gallery View Modes.  There are currently two modes, "Normal" mode - which is how Ansel has been displaying galleries - and the new addition, Date Mode. Date Mode lets you set a gallery to be browseable by date.  So, instead of a single gallery containing those hundred plus photos you took on your week-long vacation, you can use Date Mode and your images will automatically be sorted by date within that gallery.  Some screenshots (click for a larger image):

The dates come from either the EXIF data embedded in the image or, if that is not present, from the date the image was uploaded to Ansel. While not implemented yet, these date values will be editable in the final Ansel release.

Gallery Modes can be switched at any time.  If a gallery contains any sub-galleries in Normal Mode, they will be flattened into the parent gallery when viewing by Date. This is non-destructive so you can switch back and forth without worrying about losing your gallery structure.

A new command line script was also recently added, remote_import.php. This script allows you to upload entire local directories of images to a remote Ansel server.  You can have it create a new gallery based on the name of the folder or specify an existing gallery on the command line....and for those of you that are Macintosh users, there is a simple Applescript wrapper included that allows you to just drag and drop a folder onto the Applescript application to upload the entire folder to your Ansel server.  Plans for an iPhoto plugin are also in the works, but no promises on a time line :)

Some other additions include the ability to automatically add an EXIF field to an image's tags when uploading it,  and something I've written about before, the ability to embed images/galleries in external websites such as a personal blog. In fact, the screenshots shown here are embedded from my Ansel server - and they demonstrate another new feature, the ability to view a larger size image in a "lightbox" while remaining on the external web site.  This is really useful if you want to link to larger images from small thumbnails, but don't want your users to leave your blog page.

I've also added similar functionality to Kronolith as part of the sponsored AJAX calendar project.  It's now possible to embed views of your calendar on external websites.  The available views, for the most part, are the same views you can display on Horde's portal page. 

For more examples of these embeddable widgets, visit my blogspot sandbox at http://mrubinsk.blogspot.com.

Ansel now has support for displaying a gallery as a "widget" within things like blog posts or portals.  All that is needed is the ability to include javascript in your editor.  For example, the following images were embedded in this post with code like the following included directly in the post:

<script type="text/javascript" src="path/to/ansel/xrequest.php.php?
requestType=Embed/gallery_id={gallery id}/container=anseldiv1"></script>
<div id="anseldiv1"></div>

There are also a number of options available for passing to the widget. For example, you can add a start and count parameter to determine how many images to include, and which one to start counting at.  You can also select to use the mini thumbnails (the default), the larger thumbnails or even the 'pretty' thumbnails that Ansel can display by setting the thumbsize parameter to thumb or prettythumb.

For an example of what this would look like on a Blogger site, take a look at my Blogger sandbox

Like all new features, there is still some work to be done, and a number of different 'views' will be available such as a small slideshow and an image carousel.

Stay tuned!

Horde_Routes is a new Horde library that is derived from the Python Routes project.  I've been meaning to give it a look for some time now, and a recent rewrite / cleanup of an Ansel powered gallery site gave me the perfect opportunity to dive in.

In previous articles, I've outlined the basics of using Ansel to power an external gallery site.  In this article, we'll look at using Horde_Routes to map 'pretty' URLs to the PHP code.

The site is simple.  It is basically nothing more than a thin wrapper around some of Ansel's views, with an 'About Us' and 'Home' page thrown in for good measure.  I decided to implement the URLs like so:

/           - The home, or default route /galleries - The top level, paged gallery list. /x          - A gallery view where x represents the gallery id. /x/y        - An image view where y represents the image id.

In all cases, paging is done with a 'page' URL parameter tacked on.  For purely static pages, such as the About Us page, I have a path such as:

/content/about

With the paths hashed out, it's time to look at the code. The first thing you need to do to enable Routes is to set up a rewrite rule on your webserver to pass all requests for your site to your controller script.  On my site, I decided to name my controller script dispatcher.php since that pretty accurately represents it's responsibilities.  How to go about setting up the rewrite rules will differ depending on your web server.  I use lighttpd for my sites, and, as I found out, this has a particular 'gotcha' when dealing with a Routes enabled site.

Apache has a switch that allows it to ignore any rewrite rules when the requested file already exists.  This makes dealing with things like stylesheets, images and script files easy.  With lighttpd, it's not so easy. Consider the following rewrite rule:

"^(.*)$" => "/dispatcher.php?url=$1"

This basically takes all requests for your site (I'm assuming the Routes site is at the root of your site) and forwards it to displatcher.php and tacks on the requested path as a URL parameter.  See the problem?  Lighttpd does not ignore rewrite rules for existing files, so a request for a stylesheet, /themes/default.css will fail. The same for images, javascript files etc...  To overcome this in lighttpd, you need to add a rewrite rule such as:

"^/(css|files|img|js)/.*$" => "$0"

Which, as you might guess, basically causes lighttpd to not rewrite the URLs that match the pattern given.  With that in mind, and a rewrite rule to make sure that the default route of '/' is properly dealt with, my rewrite rules for this site look like this:

 $HTTP["host"] =~ "^(www.)?theabramsgallery\.com$" {
           url.rewrite-once += (
           "^/?$" => "/dispatcher.php?url=/",
           "^/(css|files|img|js)/.*$" => "$0",
           "^(.*)$" => "/dispatcher.php?url=$1")
}

The next step is to set up Routes and tell it about our desired mappings.  This should be done in either some sort of config file, or a base include file for your site.  First the code, then the explanation:

* Set up the Routes */
$m = new Horde_Routes_Mapper();

/* 'Home' route */
$m->connect('home', '', array('controller' => 'index'));

/* General content Pages */
$m->connect('content', '/content/:content',
            array('controller' => 'content',
                  'action' => 'view'));

/* Gallery List */
$m->connect('list', 'galleries', array('controller' => 'galleries',
                                       'action' => 'index'));
/* Gallery View */
$m->connect('gallery', '/:id', array('controller' => 'galleries',
                                     'action' => 'view'));

/* Image View */
$m->connect('image', '/:id/:image', array('controller' => 'images',
                                          'action' => 'view'));

/* Advertise our controllers */
$m->createRegs(array('index', 'galleries', 'images', 'content'));

The first line creates a new instance of the Mapper object.  With it, we 'connect' new mappings with the connect() method.  Each connect() call as called above, takes 3 arguments (it can actually take a variable number of arguments - see the documentation for details).  The first is the name of the route. It is not used at all when mapping a URL to an action, but it makes it easier when generating a URL within your site (see below).  The second argument is the Route Path and can be composed of both static and dynamic parts.  Static parts of the path are not preceded by a ':' , dynamic parts are. For example, the list route contains only a static path - galleries. This means that only the URL /galleries will match this route. The gallery route contains only a dynamic part, /:id.  So a URL such as /10 will match this route.  The third parameter is what actually determines what controller will be responsible for this action.  As you can see, it does not have to mirror the paths...for example, you can see that I use the galleries controller for both the list and the gallery routes.

OK. So, now we know what controllers are responsible for what routes. Great. Now what?  Well, now it's time to write the code that will handle the requests and pass off to the correct controller.  For this, as stated above, I used a file named dispatcher.php.  In that file is:

require_once dirname(__FILE__) . '/lib/base.php';

/* Grab, and hopefully match, the URL */
$url = Util::getFormData('url');

/* Get rid of any query args */
if (($pos = strpos($url, '?')) !== false) {
    list($url, $query) = explode('?', $url, 2);
    parse_str($query, $args);
} else {
    $args = array();
}
$match = $m->match($url);

.
. // Do stuff
.

/* Hand off to the proper controller */
$action = $match['action'];
include dirname(__FILE__) . '/' . $match['controller'] . '.php';

In the first section of the code, we get the requested path from the query parameter.  We then have to strip off any query parameters that were passed in with the path. Routes will only match URLs with no query arguments. Then, we call the match() method of our Mapper object and are passed back an array representing the matched route.  This is a fairly simple site, I use a separate PHP file for each controller. I've omitted code from my dispatcher that doesn't relate to Routes, mainly I also set up a Horde_View object that I use in all my controllers to handle the displaying of the view template.

The only thing left really, for a basic Routes driven site is generating the URLs for the site. That's done with the Horde_Routes_Utils#urlFor method like so:

$url = $m->utils->urlFor('image', array(
                    'id' => '5',
                    'image' => '10'));

 This line would generate a URL for an image view like /5/10 where 5 is the gallery id and 10 is the image id. In the above code, you see that the array keys match the dynamic parts of the route path you defined with the connect() method.

I plan on refactoring all the websites under my control to use Horde_Routes, and I'd encourage you to take a look at the documentation at http://dev.horde.org/routes  to learn more!

Many thanks to Chuck who helped me sort out some things while working with Routes.

In the last two installments, we looked at the basics required to interact with a Horde server and obtain content for display on external, or "non-Horde" websites.  In this article, we'll take it a step further and give a concrete example of using Horde content to power a website - we are going to use Ansel, the Horde Project's photo management application, to power a personal, or family website.

Ansel is a powerful photo management application that provides many features.  Even so, sometimes you just want to have a dedicated website to showcase your images...or maybe you want to integrate a gallery onto an existing website. Both are very easy using Ansel's api.

For this example, let's assume we are trying to integrate a family photo album into an existing family website.  To do this, we are going to add a 'Gallery' section to the site, and for simplicity, we are going to use a "Lightbox" style gallery, so that when you click on an image thumbnail to view it, an overlay appears displaying the image on the same page. Gallery styles are a key part of Ansel, and allow you to change the look and feel of the Gallery View. You can learn more about styles and how to hack your own by looking at the styles.php file in the config/ directory.

So, let's get started.  Let's assume that you have a number of galleries in Ansel and you only want to show a certain sub-set of those galleries on the new site. For example, let's say that you want all the galleries that have a category of "Family" to appear on this site. (It's also possible to do this with just a list of gallery ids you want included).

First things first, let's define some configuration stuff. (These should probably be in some sort of conf.php file and included on each of your "gallery" pages).

/ These define the root of the site $base_url = 'http://example.com'; $fs_base = '/srv/www/example.com'; // The path to the Horde server. $horde = 'http://another.example.com/horde';// Let's assume we want all the galleries in the // "Family" category $filter = 'Family'; // ...but only those owned by this user. $owner = 'myusername'; // The named Ansel style to use. $gallery_style = 'ansel_lightbox_simple';

Now, before we do anything useful, we will need a Registry instance:

define('HORDE_BASE', '/horde');
require_once HORDE_BASE . '/lib/core.php';
$registry = &Registry::singleton();

Now for the fun:

$content = $registry->call(
    'images/renderView',
    array(array('owner' => $owner,
                'category' => $filter,
                'style' => $gallery_style,
                'gallery_view_url' => $base_url . '/gallery.php?gallery=%g'),
    null,
    'List'));
  

Some explanation: This calls Ansel's images/renderView api method. This method takes 3 arguments. The first is an array of parameters that get passed to the Ansel_View object that will be doing the rendering, the second is the application scope (we are using the default scope - if you don't understand this, it's not important to the task at hand), and the third is the general type of view we want to render (currently supported are Gallery, Image and List).

The various view parameters that a view takes can be browsed by viewing the developer documentation for each view, but a quick explanation for the parameters we are using for the List view are as follows:

  • owner - We are limiting to galleries owned by this username.
  • category - Only galleries that have this category are returned.
  • style - Force the use of this gallery style.
  • gallery_view_url - This is perhaps the most important one, as this sets the url that the gallery thumbnail will point to.  You set this to the page on your site that will render a single gallery - %g is replaced by the choosen gallery's id.

So, what we have now, in $content is the HTML needed to render a List of galleries, that will correctly point to a page on your own website to view an individual gallery. Now, let's look at what it takes to actually render that gallery - in gallery.php (the target page we set above):

/* Grab the form info */
require_once $fs_base . '/lib/Utils.php';
$gallery_id = Util::getFormData('gallery', 0);

$content = $registry->call(
    'images/renderView',
    array(array('gallery_id' => $gallery_id,
                'gallery_view_url' => $base_url . '/photos/gallery.php?gallery=%g',
                'style' => $gallery_style,
                'hide_comments' => true,
                'page' => Util::getFormData('page', 0)),
          null,
          'Gallery'));

Again, we are calling the images/renderView api method. This time we are requesting a Gallery view to be rendered.  The view parameters in the first argument are similar to the first time we called this method - the new parameters are:

  • gallery_id - Yes, this is the gallery id we want to view.
  • page - The pager on the gallery view adds a 'page' url parameter to indicate the current gallery page requested.  The Gallery View needs the current page to be passed to it if it's not the first page.
  • hide_comments - allows the hiding of the comment counts for each image (if comments are enabled in Ansel). Setting this to false or omitting it will cause the number of comments to show in each image "tile". If you do show the image comments, the text is linked to a Image View that displays the image along with the comments.  By default, this links to the Image View in Ansel, but can be overridden with the 'image_view_url' parameter. This works similar to the 'gallery_view_url' - %g and %i are replaced by the gallery id and image id accordingly.

We now have a very basic way to render a complete Ansel gallery on an external website using just a handful of api calls.  This article demonstrates the basic idea, but obviously leaves out a bit of eye candy.

Resources:

Some sites that use Ansel via the api as described here:

In part one of this series, we looked at getting some simple information out of Horde using the api. In this installment, we'll look at getting the same information, but this time we will be using Horde's RPC server so we can get the information from a remote Horde server.

For these examples, we'll be making use of another one of Horde's libraries, Horde_RPC. This library encapsulates all the nastiness and complexities of dealing with remote server communications.  To use it, you must have the Horde_RPC package installed on your local system. (This package is available from CVS, or from the upcoming Horde 3.2 release).  Although any RPC client library would do.

The first example in the first article was to retrieve a list of applications that are installed and registered with Horde.  Following an example given in the Horde_RPC package, the first example would look like this:

// Load the RPC library
require_once 'Horde/RPC.php';

// XML-RPC endpoint
// This is the URL to your remote Horde server's RPC interface
$rpc_endpoint = 'http://example.com/horde/rpc.php';

// XML-RPC method to call
$rpc_method = 'horde.listApps';

// Process the request
$result = Horde_RPC::request(
    'jsonrpc',
    $rpc_endpoint,
    $rpc_method);

// Dump the results
var_dump($result);

Pretty simple, and not all that different than using the api directly.  The second example in the previous article demonstrated calling contacts/sources to get a list of available address books. The main difference between the first and second examples is the need to pass authentication parameters to Horde.  This, too, is simple:

require_once 'Horde/RPC.php';
$rpc_endpoint = 'http://example.com/horde/rpc.php';

// Specify the method to call
$rpc_method = 'contacts.sources';

// Username and password get set here
$rpc_options = array(
    'user' => 'myusername',
    'pass' => '****',
);

// Process the request, sending user/pass in the 'options' parameter.
$result = Horde_RPC::request(
    'jsonrpc',
    $rpc_endpoint,
    $rpc_method,
    array(),
    $rpc_options);

// Dump the results
var_dump($result);

It's worth noting that when using the jsonrpc server, the results are returned as a stdClass object, not as an array, and as you can see in the next example, you can iterate over the results if needed.

Finally, the last example shows how to pass parameters to the api methods using RPC. Just like in the last article, we get a list of address books that are available, and then search those address books for a certain user.

<?php
require_once 'Horde/RPC.php';
$rpc_endpoint = 'http://example.com/horde/rpc.php';
$rpc_method = 'contacts.sources';
$rpc_options = array(
    'user' => 'myusername',
    'pass' => '****',
);

// Process the first request
$results = Horde_RPC::request(
    'jsonrpc',
    $rpc_endpoint,
    $rpc_method,
    array(),
    $rpc_options);
$results = $results->result;

// jsonrpc returns data as a stdClass, so iterate over the results
// to get the source keys
foreach ($results as $key => $name) {
    $sources[] = $key;
}
$rpc_method = 'contacts.search';

// These are the parameters to the serach method
$rpc_parameters = array(array('michael'),
                        $sources,
                        array('name'));

$results = Horde_RPC::request(
    'jsonrpc',
    $rpc_endpoint,
    $rpc_method,
    $rpc_parameters,
    $rpc_options);

// Dump the results
var_dump($results->result);

You now have the tools and knowledge to retrieve any information that Horde exposes through it's api from both local and remote Horde servers.  The next installment will focus more deeply on various methods of building an 'external' website powered by Horde content.