A Look Back, A Look Ahead
Back in March I wrote about what I planned to focus on once the Horde 4 release process was complete. Well, here we are 9 months later and my personal roadmap has gotten a bit clouded in my head. So I thought with this being the end of the year, it is a good time to take stock and organize what I plan to focus on in the months ahead.
It's always nice to look back and see how well one stuck to plan, and to also take a minute to enjoy one's accomplishments. While there are still things left outstanding on that list, given how little time I have had lately to contribute, I am happy with what I managed to complete.
The work on Ansel that was necessary for a Horde 4 release, including a complete rewrite of the geotagging support, was completed. This has lead to lots of improvements in Horde's mapping library. I also pushed out an alpha release of iPhoto and Aperture export plugins. These can be downloaded from Ansel's download page. It felt really good to get those things finally off my todo list and out the door. There are lots of enhancement requests
The Hermes Ajax interface, while not feature complete, is functional for day to day time entry. The main missing piece is the search/report functionality, and I hope to complete that in the next few months. Unfortunately, the mobile interface for Hermes has not yet been started. I'll hopefully get to it one of these days, but there seems to always be something else that has more importance for me. It likely won't be finished by the time Hermes for Horde 4 is released.
We had our annual Hackathon this past November in Boston, and LOTS of great work was done by all of our team memebers. Personally - in addition to eating Lucky Burgers and attempting to juggle - I mostly focused on completing the new Service_Weather library, adding basic tag navigation in Trean now that Chuck has migrated Trean away from shares to tags, and a buch of other small bug fixes. It was wonderful to see everybody in person again, a great time was had by all! I'm already itching for our next get together.
The ActiveSync library has also received a considerable amount of work in the last few months; Support for additional devices, improved recurrence series/exception support along with revamped timezone support to name a few. Going forward, I'm looking at implementing the minimum amount of email support that would be required to properly support meeting invitation requests and responses on the device. Once that is figured out, we'll see how much more work it would be for full email support (well, "full" support for what EAS 2.5 allows anyway). There is also talk about implementing a more recent version of the EAS protocol - at least 12.1 - which would give us the ability to not only sync more efficiently, but to sync with Apple's iCal application as well. Stay tuned!
Kronolith has a number of missing features that haven't been implemented/ported from the traditional view yet. Some years ago, I added support for resource management. At the time, the AJAX view was not released, nor was it even fully functional, so the resource features were only added to the traditional view. These need to be ported to the dynamic view, along with better support for recurrence series editing.
All in all, another busy, but fun, year of Horde development is ahead!
Service_Weather for Developers Part 1
As promised in my last post, here is a basic run down on using Horde_Service_Weather in your own projects.
First, make sure you have the package installed. As of this writing, the latest available packaged release is 1.0.0RC2:
If you have not yet installed any Horde 4 packages, you will need to setup PEAR for Horde 4 (this is not meant to be a HOWTO on installing Horde. See the Horde install docs for more information).
// If you have not yet discovered Horde's PEAR channel: pear channel-discover pear.horde.org // Install the package: pear install horde/Horde_Service_Weather
Next, we need to decide on the actual weather data provider. I recommend using Wunderground, as it is, by far, the most complete of the available choices. It requires registration for at least a free developer's account. Once you have your API key, you can create the weather object:
// Parameters for all driver types
// Note that below we use the global $injector to get the HttpClient and Cache instances.
// If not using the $injector, substitute your own instances in place of the $injector call.
$params = array(
'http_client' => $injector->createInstance('Horde_Core_Factory_HttpClient')->create(),
'cache' => $injector->getInstance('Horde_Cache'),
'cache_lifetime' => 3600
);
$params['apikey'] = 'yourAPIKey';
$weather = new Horde_Service_Weather_WeatherUnderground($params);
Of course, if you choose to use, e.g., Google instead of Wunderground, just create the appropriate object:
// Google returns already localized strings, // just pass it your language code. $params['language'] = 'en'; $weather = new Horde_Service_Weather_Google($params);
Now we have our weather object, connected to the desired backend data provider. Let's fetch some weather information:
// Set the desired units
// Defaults to Horde_Service_Weather::UNITS_STANDARD
$weather->units = Horde_Service_Weather::UNITS_METRIC;
// Get current conditions.
// The location identifier can take a wide range of formats.
$conditions = $weather->getCurrentConditions('boston,ma');
// Unit labels
$units = $weather->getUnits();
// Basic condition description:
// e.g., "Sunny" or "Partly Cloudy" etc.
echo $conditions->condition;
// Current temp
echo $conditions->temp . $units['temp'];
Of course, lots of other properties are available. Check the documentation for details. Now, let's get a forecast:
// Get a 5 day forecast.
$forecast = $weather->getForecast('boston,ma', Horde_Service_Weather::FORECAST_5DAY);
// Each forecast result contains a collection of "Period" objects:
foreach ($forecast as $period) {
echo 'Date: ' . (string)$period->date; // Horde_Date object
echo 'Hi: ' $period->high . $units['temp'];
// Display other properties etc...
}
// If you want just a specific period:
$periodOne = $forecast->getForecastDay(0)
;
// Total snow accumulation for the day:
echo $periodOne->snow_total . $units['snow'];
Again, check the documentation for details on available properties.
In the next installment, we'll look at validating locations, searching locations and using a location autocompleter.
Have fun!
Service_Weather
With the recent discontinuation of The Weather Channel's public API access, Horde was left without a data feed for weather information other than the aviation style METAR/TAF reports. Weather information has historically been used in two places in Horde; The WeatherDotCom portal block, and in the Timeobjects module, where we export the weather information to other applications - like Kronolith, Horde's calendaring application.
After an audit of available (and free) weather services, I settled on the following three services as suitable as alternatives to TWC's dead data feed.
Weather Underground: Of the three providers we decided to support, this one provides the most detailed data. You must sign up for an account for your Horde install. There is a free "developer" account option, though it does have relatively low usage limits which may be a problem if you have a large user base. We of course cache every request to help with getting the most out of those limits. They also offer very reasonable paid options as well.
World Weather Online: Another free service that provides a fair amount of data, though it's not as detailed as Weather Underground. Free account required, with higher limits than Weather Underground.
Google: Google does not provide an official weather API, but they do have an API interface that is used internally for Google's weather portal block. The data provided is not very detailed, but if you are looking for a provider that does not require any registration, this might be a solution for you. No registration, no known limits, though this is unofficial, so keep that in mind.
It's worth noting that the biggest thing missing, even from Weather Underground's feed is the day/night forecast style. They provide an hourly forecast, but no simple day/night forecast. The non-hourly forecast data is provided as a single set of conditions for the entire day. Another fairly well known provider AccuWeather, appears to provide this (and fairly detailed data as well), but sadly, they have informed me that they no longer provide free data feeds - even for FOSS projects. Also, before anyone asks, yes I did look at Yahoo's weather feed. This is an RSS feed, which in and of itself is not a problem, but they only provided very basic data, for only a day or two in the future...not enough for our needs.
The end result of all this is the new Horde_Service_Weather library, a new Weather block, and support for the new weather drivers in the Timeobjects application for exporting the weather to applications like Kronolith. As a side effect of all this, the weather support in Horde has, IMO, been greatly improved. The weather portal block code received a much needed overhaul including the ability to dynamically change the location being displayed directly from the portal screen, along with autocompletion of the locations.
At the time of this writing, Service_Weather is in Beta, and available via Horde's PEAR server. The new weather block is included in the most recent Horde release, and the latest Timeobjects release contains support for the new code as well.
For developers interested in learning how to use Service_Weather in their own applications - look forward to a blog entry in the near future detailing the usage.
Building a custom Horde_Block
Anyone who has used Horde at all should know what a Horde_Block is. These are the individual bits of content that are displayed on the "portal" page in Horde. Things like the Mail Summary, Calendar Summary etc...![]()
If you have a custom application, or even just need some standalone content presented as a Horde_Block, it's fairly easy to put one together. For this example, let's assume that you have some custom content you want to display as a block, but that it's not tied to any Horde application (similar to the WeatherDotCom block). First order of business is to create a new php file for your block. The easiest way to do that is to copy the Example.php file from skeleton/lib/Block and edit it appropriately. The content of that file when you are done should be similar to:
<?php
/**
* @package Horde
*/
class Horde_Block_Foo extends Horde_Core_Block
{
/**
*/
public function __construct($app, $params = array())
{
parent::__construct($app, $params);
$this->_name = _("Foo Summary");
}
/**
*/
protected function _params()
{
return array(
'color' => array(
'type' => 'text',
'name' => _("Color"),
'default' => '#ff0000'
)
);
}
/**
*/
protected function _title()
{
return _("Some special Foo content");
}
/**
*/
protected function _content()
{
$html = '<table width="100" height="100" bgcolor="%s">';
$html .= '<tr><td> </td></tr>';
$html .= '</table>';
return sprintf($html, $this->_params['color']);
}
}
Note the name of the Class is Horde_Block_Foo this file should be saved as horde/lib/Block/Foo.php. If the block were to be called "Bar" instead, the class name would be Horde_Block_Bar and would have been saved as horde/lib/Block/Bar.php - you get the idea.
The main method you are interested in is the _content() method. This is where the block content is generated. The HTML for the block should be built as a string and returned from this method. If you want to be able to configure anything about the block, you should add to the _params() method. In this example, a text value named "color", with a default value or "#ff0000' is available. As shown in the example, to obtain the value of a setting, you use $this->_params['setting_name']. There are other types of values available as well. For example, if you wanted to provide a select list of choices instead you could do something like:
array(
'units' => array(
'type' => 'enum',
'name' => _("Units"),
'default' => 'standard',
'values' => array(
'standard' => _("Standard"),
'metric' => _("Metric")
)
),
)
This provides a select list named "Units" and allows either "Standard" or "Metric" as choices.
After adding this file and adding the content you want it to show, it will be available as a block to add to your users' portals the next time they login.
Initial support for ActiveSync added to git master
Work on ActiveSync support for Horde has reached a milestone of sorts. The initial codebase has been merged into the master branch of our Git repository.
The work is not yet production-ready, but has shown to be fairly usable on the devices I am able to test with. There are some basic instructions and other information available on the ActiveSync wiki page.
If you feel adventurous, please feel free to try it out - just please make sure to back up all your data first! If you are able to test on a device not already listed in the wiki, please drop us a note on the dev@lists.horde.org mailing list so we know how things went.
Ansel, Kronolith, and more...
Wow, it's been since June 10th, almost 4 months since my last entry. Time flies...especially when you are busy. In the interest of keeping people informed, here are some of the new things I've been working on with regards to The Horde Project, with an indication as to what version of Horde the work applies to:
Horde_Service_Twitter (H4 Only)
Since stating to use twitter, I figured it would be helpful to have my Twitter timeline appear in Horde, since that's what is usually loaded in my browser. Following my typical NIH rule when it comes to Horde, the result is the new Horde_Service_Twitter library and the twitter_timeline block for Horde's portal. Horde_Service_Twitter supports authentication to Twitter via both the standard http authentication method as well as via OAuth. The latter making use of the Horde_Oauth library. The portal block allows you to publish a new tweet, shows the most recent tweets by the people you are following and allows you to reply to a displayed tweet.
The addition of Horde_Service_Twitter, along with Horde_Service_Facebook, adds some exciting possibilities for integration points with other Horde applications. Horde already has some address book and calendar integration with Facebook, but other possibilities include things like automatically posting a notification to Twitter or Facebook when a set of new images are uploaded to Ansel, or maybe when a new blog post is published with Jonah.
Ansel (H3 and H4)
Ansel has gotten a fair amount of work recently and is ready for a 1.1 release. The most obvious change is full support of geo-tagging features. Ansel has always been able to read,and display an image's meta data...but up until now you couldn't do much with any of the location data. Now, Ansel will recognize GPS coordinates in the meta data and display small thumbnails of those images in an embedded Google Map. There are various locations throughout Ansel where you can view these maps. You can also add location data to images that do not contain it as well as edit any existing location data. Full support for reverse geocoding means that you can (re)tag an image by either typing a textual name for the location (such as Philadelphia, PA) or by typing in actual GPS lat/lng coordinates. Of course, you can also (re)tag an image simply by browsing the Google Map and clicking where you want the image to be located.
Ansel's bleeding edge code has officially moved out of Horde's CVS repository and into the git repository, horde-hatchery. A fair amount of refactoring and internal improvements have already been done in getting Ansel and Horde_Image ready for Horde 4. Among these changes is better support for image meta data, with a new driver based on exif tool. This allows recognition of not only EXIF tags, but also IPTC and XMP data as well.
iPhoto/Aperture Export Plug-Ins (H3 and H4)
Related to the Ansel application, are new export plug-ins for both of Apple's image management applications, iPhoto and Aperture. Currently available via Horde's horde-hatchery git repository, these plug-ins allow you to upload your images directly to an Ansel server from within iPhoto or Aperture. All meta data is retained when uploaded, including keywords that added using Aperture or iPhoto. You are able to create new galleries from the plug-in's interface, browse thumbnails of existing Ansel galleries (to see what images you have previously uploaded), and choose if the images should be resized (and to what size) before uploading. Both plug-ins support configuring multiple Ansel servers if you happen to have access to different installations.
Even though these live in horde-hatchery, they will work with both Ansel 1.x as well as the bleeding edge Ansel code that lives in the hatchery. The iPhoto exporter supports iPhoto '08 and later, and the Aperture exporter is written for Aperture 2.1 or later. Both require OS X 10.5 or later. They should run on either PPC or Intel hardware, but have only been tested on Intel. Currently they are available only as source (which can easily be compiled using XCode) but a development build should be available shortly.
Kronolith (H4 only)
I've been tasked with adding support for resource scheduling to Kronolith, and the work is mostly complete. Resources may be invited to events by the event organizer using the existing attendees interface. Resources can be set up to automatically determine if they are available, and respond to the request automatically. There is also support for resource groups. Resource Groups are just a grouping of resources that are similar. When a group is invited to a meeting, the first available resource from that group will accept the invitation. For example, you have 10 projectors available and it doesn't really matter which projector is used for a meeting. Instead of going through all the projectors to see which one is available, you can just invite the projector group to the event. The first projector that is available during the meeting time will accept the invitation.
Weather forecasts in Kronolith
During some recent quality-time I spent with my schedule (read: "trying to figure out how to add more time to a day"), I had an a-ha moment. Why am I switching back and forth between weather data and my calendar to see the weather for a day of interest in my calendar? Why can't it just display in the calendar? We already have some code in Horde that interfaces with the weather.com API (thanks to PEAR's Services_Weather package), so why not provide the weather data via the listTimeObjects API so Kronolith can pick it up?
The first step along this path was to create a new "mini" application - or an application that does nothing other than expose data via the listTimeObjects API. This resulted in the lightweight TimeObjects application that now lives in the horde-hatchery git repository. This does put another level of abstraction between the weather data and Kronolith, but what I didn't want to do was start a trend of having to write a new Kronolith Event driver for any new type of time data that might be desired. With TimeObjects, now all that is needed is to drop a new driver into TimeObjects' lib/Driver/ directory and it will be picked up and exposed via the API.
With the addition of the new TimeObjects application, Kronolith can now display the forecast data for up to the next 5 days directly in the calendar view. The high/low temperature along with the general conditions for that day are displayed, with a tooltip popup showing more detail. Currently, for this to work, you will need a contact in Turba that is marked as your own and containing enough of your location information to satisfy weather.com's service. Horde will also need to be configured with the weather.com api keys...just like the weather.com block requires. A future addition will be to allow choosing (multiple?) locations via a Google map in Horde's preferences.
Also included in TimeObjects is a driver for exposing Facebook Events via the listTimeObjects API.
Since the listTimeObjects API really isn't documented anywhere other than our source code, a little introduction may be in order. If you are not interested in Horde internals, you can skip to the end.
The API allows any Horde application to expose it's data as events to be displayed in Kronolith. For example, via this API, Turba can provide the data needed to display contact birthdays and anniversaries in Kronolith. Nag can display task due dates etc... For this to work, an application needs to expose two methods via it's own API: listTimeObjectCategories(), which returns the categories of time objects available (birthday, anniversary etc..) and listTimeObjects() which returns the actual data. The data returned includes information such as the start and end times, a title, a description, icon, and link. For more information I will direct you to the phpdoc at http://dev.horde.org.
As always, a warning that the TimeObjects code is Horde 4 only, and as such is not considered stable.
New Horde Shops Open
There are now two new Horde merchandise shops open. These Spreadshirt shops are in addition to the existing CafePress shop we have.
The shop at
Your Facebook Stream in Horde
Keeping up with Facebook's Open Stream API, Horde just got a new Horde Block, the Facebook Stream Block. With this block you can view your stream feed (filtered by any of the same filters available on your Facebook Home page), add a "like" directly from the block, update your Facebook status,
and see how many new notifications you have. This block will replace the previous Facebook Summary block that I wrote about previously.
Integrating Horde with Facebook
Like most of the stuff I work on, this one started out as a personal "wouldn't it be cool". I wanted to add a block on my portal that would allow me to quickly update my Facebook status, while seeing the most recent status updates of my friends without having to actually login to Facebook. I started by looking at the official Facebook PHP Client library, but wasn't happy with it's code structure, and the quality of the code wasn't really up to my standards, so I decided to completely rewrite it. The result is Horde_Service_Facebook and is available only on the Horde 4 development line, from the horde-hatchery git repository.
In addition to the new library, I've also started to add some integration points in Horde for users to interact with Facebook. The first such integration point is the Facebook block - which allows the user set their status, and see the most recent status updates.
Next up was an attempt to scratch an itch that another Horde developer had. He wanted to fill in missing email addresses in his Turba address book from his friends in Facebook. Unfortunately, I quickly found out that Facebook does not allow applications to obtain a user's email address. Well, actually, it will give you a proxied email address, but only if the user whose email you want has added your application and given it permission to get the proxied address. Not helpful at all for our purposes. I continued on with the Turba integration anyway, and the result is a Facebook driver for Turba that does provide at least some useful information, such as the friend's birthday - which can then be displayed in the user's Kronolith calendar.
The most recent integration point was made easier to implement thanks to the just released Open Streams API. Ansel now has initial support for automatically posting to the user's Facebook stream after images are uploaded to any of the user's public galleries. A small thumbnail of up to 5 images (linked to the Ansel view for that image) will be shown on the stream, along with the name of the gallery. The new Streams.publish method removes the requirement to create templates (and thus have to manage them) like we would have to do with the older PublishUserAction method. Eventually, there will probably be user prefs in Ansel to allow/disallow such automatic posting either per gallery or per upload.
Setting up the integration is not too difficult, and involves creating a new Web Application on Facebook. You will need to provide Facebook with what they call the Canvas Callback URL. This should be set to the URL of the horde/services/facebook.php page on your Horde server. This is the page that Facebook will redirect and post values back to during each user's initial application authentication. Next, you will need to set the API Key and Application Secret in Horde's configuration. This is done on Horde's API Keys configuration tab.
That's it. Now each user can visit his or her Facebook Integration Options section of their user preferences and authorize the application as well as some of the permissions it will require.
Keep in mind that this is still very much bleeding edge development code, and as such may still contain bugs and/or missing features. Comments and feedback are always welcome...
When two itches collide
Recently, I've been working on a Horde library to interface with the Vimeo video service. It only works against Vimeo's Simple API for now, as that provided the data that I needed for the original 'itch' that I was scratching. The itch was to include video content on a personal website, displaying thumbnails for the available videos and having it fit with the existing look of that site. In writing some test/example code for it, it dawned on me that this might be a good fit for something else I've been meaning to test - testing to see that adding a completely new gallery style for Ansel would work as expected.
Ansel supports different gallery styles - you can have your photo galleries look like Polaroid images, view them in a Lightbox, etc.. My eventual goal with that feature is to not only allow the existing styles to be tweaked, but to make it "easy" to add a custom style that might require it's own custom php code. Creating a flickr-like style, for example, would require not only a different layout, but would almost certainly require some custom javascript and the php code to generate it.
So, back to the Vimeo code - the site that I'm displaying videos on also has photo galleries that are rendered via Ansel's api. I thought it would be an interesting exercise to see if I could extend Ansel easily to render the Vimeo videos as a new gallery style. In this case "easy" means getting it to work by only adding new gallery views and templates - without touching any existing Ansel code. This would allow me to take advantage of Ansel's existing infrastructure to render the videos - with the same single line of code I'm using to render the photo galleries.
Turns out, it wasn't too difficult at all. I got the basic code to do it worked out in a few hours - it's still rough, but the basic premise is there and actually works! In fact, I think that it's a great example of how Ansel can be extended with gallery styles. So, with that in mind, let's walk through the process of what it took. Note that it's beyond the scope of this post to explain the inner workings of Ansel. Obviously some knowledge of Ansel's internals would be required to do something like this. I have provided a link to download the files that are required to do this for those that may be interested in tinkering.
First, a quick introduction to the Horde_Service_Vimeo class. It's still very new, and I would hesitate to even call it alpha code, so there are no guarantees that the interface won't change going forward. As it stands now, this is a simple example of how you would go about getting a list of videos for a particular user:
$params = array('http_client' => new Horde_Http_Client());
$v = Horde_Service_Vimeo::factory('Simple', $params);
$videos = $v->user('userid')->clips()->run();
That's it. The results are returned, by default, as a serialized PHP array so you would call unserialize() on the results to actually get at the data.
$videos = unserialize($v->user('userid')->clips()->run();
To get the necessary HTML to embed the actual video player on your site, you would use the following:
$results = $v->getEmbedJSON(array('url' => $thumb['url']));
This returns a JSON encoded object that can either be output directly on your page's javascript or unserialized in PHP and used there. There are also other options you can pass to that method in the parameter array to control things like the embedded player's size and what text is or is not displayed over the still image when the video is not playing. There is a whole wealth of information available, and I point you to Vimeo's API documentation to see exactly what is returned and what the structure of the data is.
Moving on to Ansel now - each gallery style is implemented by both an Ansel_View_GalleryRenderer_* object in ansel/lib/Views/ and a template file for the style in ansel/templates/view/. For this example, I'm going to create a new Ansel_View_GalleryRenderer_GalleryVimeo class and a corresponding template file named galleryvimeo.inc. I used the GalleryLightbox renderer as a starting point, copied those files, ripped out the code regarding all the gallery actions since things like delete and move don't make sense for this type of view. I also decided, for simplicity, to use an overlay to play the videos instead of creating another view. I therefore included the javascript necessary to use the RedBox overlay.
The first thing I needed to figure out was how to populate the class' private members with the video thumbnails. Luckily, the parent class Ansel_View_GalleryRenderer uses a single method, fetchChildren(), to fetch the items that are to be displayed in this gallery. This method can be overridden in our class so we can download the data from Vimeo instead of from our internal storage. You can see the code used to do this in the example file, though it's not all that different from the example listed above.
$thumbs = unserialize(
$this->_vimeo->user($vimeo_id)->clips()->run());
foreach ($thumbs as $thumb) {
$this->_json[$thumb['clip_id']] =
$this->_vimeo->getEmbedJSON(
array('url' => $thumb['url'],
'byline' => 'false',
'portrait' => 'false'));
}
In the sample file, I'm hard-coding my Vimeo user id. If this were to mature into an actual feature in Ansel, I would use a new user preference for storing this value. The other thing to notice is that the Simple API doesn't support any type of paging. You get all the videos that match your request - up to whatever maximum value Vimeo itself imposes, so we have to emulate paging in our overridden method. The code to do that is simple, using the per-page value, the current page number and then getting an appropriate slice of the video array we retrieved from Vimeo.
// Total number of thumbnails in the gallery
$this->numTiles = count($thumbs);
// The last one to display on this page
$this->pageend = min(
$this->numTiles,
$this->pagestart + $this->perpage - 1);
// The actual data for what is to be displayed on this page
$this->children = array_slice($thumbs,
$this->pagestart - 1,
$this->perpage, true);
The other issue was how to actually display the image tiles. Ansel uses a separate class to represent image and gallery tiles, retrieving them via method calls on the Gallery and Image objects. For this, I just implemented another method in the GalleryRenderer, getTile() and use it in the template to generate the tile. All the information needed is present in the array returned from Vimeo, so it's extremely simple to build the tile.
Yes, there are still some things that don't quite work right - for instance, Ansel will happily allow you to upload images to this gallery - but will never show them, or let you delete them. Image counts for this gallery will still always show as zero when browsing your galleries since there are no images for this gallery in the database. Some of these issues could be fairly easily addressed either in the new renderer class, or more appropriately, by adding a new type of GalleryMode class - where all the image count calculations are normally done.
While not a traditional Ansel gallery, I feel this demonstrates what is possible with Ansel's gallery style architecture. I'm actually currently using this on my family website to host videos of my daughter. Future plans for Ansel do include native support for videos, but I think this is a great alternative in the mean time - and off loads some bandwidth to Vimeo to boot. Who knows, maybe some type of remote gallery style will eventually make it's way into the official code base...
You can find the code for this at: http://theupstairsroom.com/downloads/vimeogallery.tgz
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.
Embedding Ansel galleries with javascript
<p>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:</p><code><script type="text/javascript" src="path/to/ansel/xrequest.php.php?
requestType=Embed/gallery_id={gallery id}/container=anseldiv1">
</script>
<div id="anseldiv1"></div></code>
<script src="http://portal.theupstairsroom.com/horde/ansel/xrequest.php?requestType=Embed/gallery_id=3318/container=ansel1" type="text/javascript"></script><div id="ansel1"></div>
<p>There are also a number of options available for passing to the widget. For example, you can add a <em>start</em> and <em>count</em> 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 <em>thumbsize</em> parameter to <em>thumb </em>or<em> prettythumb</em>.</p>
<script type="text/javascript" src="http://portal.theupstairsroom.com/horde/ansel/xrequest.php?requestType=Embed/gallery_id=13/container=anseldiv2/thumbsize=prettythumb/start=14/count=12/perpage=3"></script><div id="anseldiv2"></div><style type="text/css"> #anseldiv2 .anselGalleryWidget img {border:none;}</style>
<p>For an example of what this would look like on a Blogger site, take a look at my <a href="http://mrubinsk.blogspot.com/" target="_blank">Blogger sandbox</a>. </p>
<p>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. </p>
<p>Stay tuned!<br /></p>
Diving into Horde_Routes
<p><a target="_blank" href="http://dev.horde.org/routes">Horde_Routes</a> is a new Horde library that is derived from the <a target="_blank" href="http://routes.groovie.org/">Python Routes</a> project. I've been meaning to give it a look for some time now, and a recent rewrite / cleanup of an Ansel powered <a title="My father-in-law's artwork" target="_blank" href="http://theabramsgallery.com">gallery site</a> gave me the perfect opportunity to dive in.</p>
<p>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. </p>
<p>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:</p>
<p> <code>
/ - The home, or default route <br /> /galleries - The top level, paged gallery list.<br />/x - A gallery view where x represents the gallery id.<br />/x/y - An image view where y represents the image id.
</code> </p>
<p>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:</p>
<p> <code>
/content/about
</code> </p>
<p>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 <em>dispatcher.php</em> 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 <em>lighttpd</em> for my sites, and, as I found out, this has a particular 'gotcha' when dealing with a Routes enabled site. </p>
<p>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:</p>
<p> <code>
"^(.*)$" => "/dispatcher.php?url=$1"
</code> </p>
<p>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 <em>displatcher.php</em> 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, <em>/themes/default.css</em> will fail. The same for images, javascript files etc... To overcome this in lighttpd, you need to add a rewrite rule such as:</p>
<p><code> </code></p>
<p>"^/(css|files|img|js)/.*$" => "$0"</p>
<p>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:</p>
<p><code> $HTTP["host"] =~ "^(www.)?theabramsgallery\.com$" {
url.rewrite-once += (
"^/?$" => "/dispatcher.php?url=/",
"^/(css|files|img|js)/.*$" => "$0",
"^(.*)$" => "/dispatcher.php?url=$1")
}
</code> </p>
<p>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:<br /></p>
<p><code> <span style="color: #007700;">* </span><span style="color: #0000bb;">Set up the Routes </span><span style="color: #007700;">*/
</span><span style="color: #0000bb;">$m </span><span style="color: #007700;">= new </span><span style="color: #0000bb;">Horde_Routes_Mapper</span><span style="color: #007700;">();
</span><span style="color: #ff8000;">/* 'Home' route */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-></span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'home'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">''</span><span style="color: #007700;">, array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'index'</span><span style="color: #007700;">));
</span><span style="color: #ff8000;">/* General content Pages */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-></span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'content'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'/content/:content'</span><span style="color: #007700;">,
array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'content'</span><span style="color: #007700;">,
</span><span style="color: #dd0000;">'action' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'view'</span><span style="color: #007700;">));
</span><span style="color: #ff8000;">/* Gallery List */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-></span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'list'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'galleries'</span><span style="color: #007700;">, array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'galleries'</span><span style="color: #007700;">,
</span><span style="color: #dd0000;">'action' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'index'</span><span style="color: #007700;">));
</span><span style="color: #ff8000;">/* Gallery View */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-></span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'gallery'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'/:id'</span><span style="color: #007700;">, array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'galleries'</span><span style="color: #007700;">,
</span><span style="color: #dd0000;">'action' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'view'</span><span style="color: #007700;">));
</span><span style="color: #ff8000;">/* Image View */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-></span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'image'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'/:id/:image'</span><span style="color: #007700;">, array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'images'</span><span style="color: #007700;">,
</span><span style="color: #dd0000;">'action' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'view'</span><span style="color: #007700;">));
</span><span style="color: #ff8000;">/* Advertise our controllers */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-></span><span style="color: #0000bb;">createRegs</span><span style="color: #007700;">(array(</span><span style="color: #dd0000;">'index'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'galleries'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'images'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'content'</span><span style="color: #007700;">));</span></code></p>
<p>The first line creates a new instance of the Mapper object. With it, we 'connect' new mappings with the <em>connect()</em> method. Each <em>connect() </em>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 <em>Route Path</em> and can be composed of both <em>static </em>and <em>dynamic</em> parts. Static parts of the path are not preceded by a ':' , dynamic parts are. For example, the <em>list</em> route contains only a static path - <em>galleries. </em>This means that only the URL /galleries will match this route. The <em>gallery</em> 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 <em>galleries</em> controller for both the <em>list</em> and the <em>gallery</em> routes.<br /></p>
<p> </p>
<p>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 <em>dispatcher.php</em>. In that file is:</p>
<p><code> <span style="color: #007700;">require_once </span><span style="color: #0000bb;">dirname</span><span style="color: #007700;">(</span><span style="color: #0000bb;">__FILE__</span><span style="color: #007700;">) . </span><span style="color: #dd0000;">'/lib/base.php'</span><span style="color: #007700;">;
</span><span style="color: #ff8000;">/* Grab, and hopefully match, the URL */
</span><span style="color: #0000bb;">$url </span><span style="color: #007700;">= </span><span style="color: #0000bb;">Util</span><span style="color: #007700;">::</span><span style="color: #0000bb;">getFormData</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'url'</span><span style="color: #007700;">);
</span><span style="color: #ff8000;">/* Get rid of any query args */
</span><span style="color: #007700;">if ((</span><span style="color: #0000bb;">$pos </span><span style="color: #007700;">= </span><span style="color: #0000bb;">strpos</span><span style="color: #007700;">(</span><span style="color: #0000bb;">$url</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'?'</span><span style="color: #007700;">)) !== </span><span style="color: #0000bb;">false</span><span style="color: #007700;">) {
list(</span><span style="color: #0000bb;">$url</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$query</span><span style="color: #007700;">) = </span><span style="color: #0000bb;">explode</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'?'</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$url</span><span style="color: #007700;">, </span><span style="color: #0000bb;">2</span><span style="color: #007700;">);
</span><span style="color: #0000bb;">parse_str</span><span style="color: #007700;">(</span><span style="color: #0000bb;">$query</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$args</span><span style="color: #007700;">);
} else {
</span><span style="color: #0000bb;">$args </span><span style="color: #007700;">= array();
}
</span><span style="color: #0000bb;">$match </span><span style="color: #007700;">= </span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-></span><span style="color: #0000bb;">match</span><span style="color: #007700;">(</span><span style="color: #0000bb;">$url</span><span style="color: #007700;">);</span> <span style="color: #007700;">.</span> <span style="color: #007700;">. // Do stuff</span> <span style="color: #007700;">.</span> <span style="color: #007700;"></span><span style="color: #007700;"> </span><span style="color: #ff8000;">/* Hand off to the proper controller */
</span><span style="color: #0000bb;">$action </span><span style="color: #007700;">= </span><span style="color: #0000bb;">$match</span><span style="color: #007700;">[</span><span style="color: #dd0000;">'action'</span><span style="color: #007700;">];
include </span><span style="color: #0000bb;">dirname</span><span style="color: #007700;">(</span><span style="color: #0000bb;">__FILE__</span><span style="color: #007700;">) . </span><span style="color: #dd0000;">'/' </span><span style="color: #007700;">. </span><span style="color: #0000bb;">$match</span><span style="color: #007700;">[</span><span style="color: #dd0000;">'controller'</span><span style="color: #007700;">] . </span><span style="color: #dd0000;">'.php'</span><span style="color: #007700;">;</span> </code></p>
<p>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 <em>match()</em> method of our <em>Mapper</em> 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.</p>
<p>The only thing left really, for a basic Routes driven site is generating the URLs for the site. That's done with the <em>Horde_Routes_Utils#urlFor </em>method like so:</p>
<p><code> <span style="color: #0000bb;">$url = $m</span><span style="color: #007700;">-></span><span style="color: #0000bb;">utils</span><span style="color: #007700;">-></span><span style="color: #0000bb;">urlFor</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'image'</span><span style="color: #007700;">, array(</span><span style="color: #007700;"> </span><span style="color: #dd0000;">'id' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'5'</span><span style="color: #007700;">,
</span><span style="color: #dd0000;">'image' </span><span style="color: #007700;">=> </span><span style="color: #dd0000;">'10'</span><span style="color: #007700;">));</span> </code></p>
<p> 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 <em>dynamic</em> parts of the route path you defined with the <em>connect()</em> method.</p>
<p>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 <a href="http://dev.horde.org/routes">http://dev.horde.org/routes</a> to learn more!</p>
<p>Many thanks to Chuck who helped me sort out some things while working with Routes.<br /></p>
Using the Horde API to Power External Sites or Applications - Part 3
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:
Using the Horde API to Power External Sites or Applications - Part 2
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.
Using the Horde API to Power External Sites or Applications - Part 1
Every Horde application contains a powerful external API that is used by other Horde applications to communicate with each other. For example, the address book features within IMP are implemented by communicating with Turba via Turba's API. Fortunately, this same API can be used by the web developer for communicating with Horde from other websites or applications - even if they reside on different servers.
In this part of the series, we will examine the basics of communicating with Horde's API through the Horde Registry object. In the next part we'll look at getting the same information from a Horde install that is running on a different server - through the RPC interface.
First off, let's talk about the Registry. As the Horde Wiki says, "The Registry is the glue that holds all the applications together." All API methods are called through the Registry object. Even if you are interacting with Horde through it's RPC server, ultimitely, the API is being called through Registry::call(). To put it simply, when using the Registry to use another application's functionality, the Registry is responsible for knowing what application provides the functionality and where to find the code that implements it. It also ensures that the code runs in the correct application scope or context. See the wiki page for more information on the Registry.
The API itself is defined in each applications' lib/api.php file. There you will find that application's interface to the external world. For example, let's start with something simple. Horde's base API has a method for retrieving the list of currently installed and registred Horde applications, called horde/listApps. Probably not very useful for an external website, but it's a good, simple place to start. The code required to to call this method - when your Horde install is on the same server as your website/application would look like this:
<?php
// Define the path to Horde
define('HORDE_BASE', '/path/to/horde');
// Load the Horde Framework Core
require_once HORDE_BASE . '/lib/core.php';
// Create a registry object
$registry = &Registry::singleton();
// Make the call
$apps = $registry->call('horde/listApps');
?>
This will return the list of applications that are available and registred with Horde. Note that this does not take any kind of authentication into account yet, so the list will only include applications that have SHOW permissions to the world.
Next, let's try an API call that requires a user to be authenticated. Turba has an API method that returns the list of address books a user has access to: contacts/sources. You'll notice that the method names follow a simple pattern. They are in the form of apiName/apiMethod. For example, with Turba, all api methods start with contacts. For the Horde base api, as you saw above, they start with horde. Kronolith, the calendaring application, would start with calendar. Simple, huh? Now, the code:
<?php
// This is the same as above
define('HORDE_BASE', '/path/to/horde');
require_once HORDE_BASE . '/lib/core.php';
// Tell Horde we will deal with Authentication
define('AUTH_HANDLER', true);
// Get a registry object
$registry = &Registry::singleton();
// ...and authenticate
$auth = &Auth::singleton($conf['auth']['driver']);
$auth->authenticate('username',
array('password' => '***'));
// Make the call
$apps = $registry->call('contacts/sources');
This returns an array of sources the user has access to, indexed by source key. You could then take the source key, and use it in another API method such as contacts/search and use it to search that address book. To do that we need to pass parameters to the API method. This next example demonstrates how to do just that. The contacts/search method takes 4 parameters, but we are only going to worry about the first three for this example:
<?php
define('HORDE_BASE', '/horde');
require_once HORDE_BASE . '/lib/core.php';
// Tell Horde we will deal with Authentication
define('AUTH_HANDLER', true);
$registry = &Registry::singleton();
// But now we have to authenticate
$auth = &Auth::singleton($conf['auth']['driver']);
$auth->authenticate('myname',
array('password' => '***'));
// Make the call
$sources = $registry->call('contacts/sources');
// Note the second parameter to the call()
// function. It's an array containing all the
// parameters the api call requires. In this case
// we are passing three parameters:
// An array of search terms
// An array of sources to search
// An array of the fields to search
$results = $registry->call('contacts/search',
array(array('michael'),
array_keys($sources),
array('name')));
You now have a basic understanding of interacting with Horde's API. Next time, we'll look at getting the same information from Horde that we did in this article, but we will do it using Horde's RPC interface, so we can communicate with a Horde install located on a seperate machine, across the network. We'll also start to look at Horde_Blocks and how they provide a quick, easy way to get blocks of Horde content onto your own site.
Update on Ansel development
For those that don't know, Ansel is the Horde Project's image management application, it provides user galleries and some image editing capabilities. I've been using Ansel for quite some time now to host images for friends and family. After coming across an article on Mikko Koppanen's blog, regarding image manipulation with ImageMagick, I decided it was time to give Ansel a facelift.
There have been quite a number of recent improvements and changes to Ansel's code. The most obvious (and coolest, IMO) is the addition of
![]() |
| Polaroid style thumbnails |
Like I said, image manipulation is actually done in the Horde_Image library, so going forward, any types of effects, thumbnails etc... that Horde_Image can generate will be available for possible use in Ansel. I'm planning an
![]() |
| Rounded gallery thumbnail |
Other recent additions to Ansel include the addition of photo tagging, various types of RSS feeds and some useful widgets that will eventually be configured as part of the the gallery styles. Another nice addition builds on a new Horde level configuration option to choose to have Horde applications build "pretty" URLs where they are supported. So galleries can be reached with URLs like: path/to/ansel/user/mike, /path/to/ansel/gallery/gallery_name or even path/to/ansel/user/mike/rss for a feed of all of user mike's recent images. For an idea of some of the other things that are on the way or in the works, you can check out the Ansel page on the Horde website.
Behind the scenes, so to speak, there have also been many performance related enhacements to Ansel contributed by duck, a frequent and very prolific contributor to many of Horde's applications.
The final change I'd like to mention involves using Ansel from other web sites or applications. Like all other Horde applications, Ansel provides an external api that developers can use to interact with it. You can use the api to get or store content for other, non-Horde applications. There is even the ability to render a complete gallery on your own site with a single api call. Another article, in the works, will demonstrate different ways to access Horde content for use in your own webpages. In fact, this website, as well as my family site at rubinskyfamily.com is completely powered by Horde via various api calls.
Ansel gets RSS support
Ansel, which is the Horde Project's photo application now has support for RSS feeds.
Ansel feed seen on My Yahoo page
There are a number of 'photo streams' available via RSS. You can subscribe to streams from a particular user, a specific gallery, a stream for all publicly available images, or even a stream for all images tagged with a specific tag. Also planned are streams for particular categories. To subscribe to a feed, use a URL such as the following in your feed reader:
| Entire Ansel site |
http://path/to/horde/ansel/rss.php |
| Specific user |
http://path/to/horde/ansel/rss.php?stream_type=user&id=username |
| Specific gallery |
http://path/to/horde/ansel/rss.php?stream_type=gallery&id=gallery_id |
| Specific Tag |
http://path/to/horde/ansel/rss.php?stream_type=tag&id=tagexample |
| If you would prefer a RSS 0.91 feed (without media extensions) add type=rss to the parameters. | |
Ansel is not yet released in a stable version, but is quickly nearing a 1.0-alpha release. It is currently available via CVS snapshots or directly from the CVS tree.
Some new work on Ansel
This website is powered by the Horde Application Framework as well as some of the project's applications, such as Ansel for the images, Agora for comments and Jonah for the news content. I've had to add a number of features to these applications in order to do what I wanted to do with this site. Most of the work was done on the apis of the applications so I can get the data I need out of them for display on this site.
Currently, I've been busy adding some new features to the Ansel application itself.
Chuck, from the Horde Project, had recently rewritten the slide show code and combined the slide show and image views. This fixed a few bugs and simplified the code. I helped by finishing up the implementation by making sure that the links for the image properties and actions were updated with each new image that was displayed. The comments were a different story, as they needed to be fetched from Agora's api with each image change. This was the first bit of 'AJAX' type functionality that I have attempted within Horde, and it appears to have worked out well.
Some new capabilities have also been added that allow retrieving recently added as well as most commented images. The latter change prompted some improvements to the Agora api to allow retrieving information on multiple forums with a single api call. These new features are demonstrated with the two new Horde_Blocks that are availble for Ansel. Hovering over the image titles in these blocks, will also show a thumbnail preview of each image in a small pop-up element.
Finally, I'm currently working on adding a "tag" feature to a number of the Horde applications. Since I've been working with Ansel most recently, I started with that. It's mostly finished now, and is a slightly different way of tagging than most other image applications offer. It's been implemented as a way to truly browse your images. It offers a 'tag hierarchy'. The best way of describing it is as if each tag is a folder that is dynamically built with each further selection. You choose your first tag, "Birthday" and see 100 images tagged with it. Then, you see a list of 'related tags' that will further refine your search...you select "Mike" and see the 25 images out of the previous 100 that are tagged with BOTH "Birthday" and "Mike". You can navigate down as far as the number of tags will let you go. You can also remove any single tag from your "tag trail" at any time and modify your criteria.
Next up, will probably be adding similar tagging features to the Jonah application..then maybe Trean, the Bookmark manager.�



