bookmark_borderDAVx5 CalDAV may break with Unicode symbols in Horde/Kronolith syncs.

If you get user complaints about broken CalDAV syncs with Horde, there’s many places to look at. In one particular instance, an event was created from travelling app Transportr into the stock android Calendar app. Through the DAVx5 sync app, the user wanted to push these events to Horde’s SabreDAV interface – and from there, also sync it to his desktop email solution, Mozilla Thunderbird.

5 programa efectivo de entrenamiento de culturismo – programa de entrenamiento de culturismo de 5 días estore de culturismo halotestin szkani hombre sin mangas con capucha chaleco de fitness culturismo largueros entrenamiento camisetas sin mangas.

However, his sync application told him about an error. The server administrator saw a 500 status code in the server log. - user [24/Jan/2021:18:48:26 +0000] "PUT /horde/rpc/calendars/user/calendar~KL14jYhCMpbet4ecYzAg1bn/2bace303-f0d8-4df6-9652-baa5fb9e86c4.ics HTTP/1.1" 500 892 "-" "DAVx5/3.3.8-ose (2021/01/13; dav4jvm; okhttp/4.9.0) Android/11

The root cause was actually not in the software code but in the MariaDB database configuration. The calendar entry from Transportr included some Unicode icon characters like a fast train and some arrows. These characters are part of the standard unicode encoding, utf-8.

Now you might wonder: New installations of mysql and MariaDB default to a character set they call utf-8 since 2010 or so. This shouldn’t be an issue. However, what they call utf-8 is not what you would expect.

Some years ago, TV sets which did not support the full HD resolution were marketed as “HD ready”. In some sense, the default character set is “unicode ready” at best. The default data type saves on disk space by encoding a subset of utf-8 into up to three bytes. While this supports most natural language characters, it is only a fraction of what Unicode can offer. Database manufacturers are well aware that this is not something you should run nowadays that unicode icons like the hamburger are all over the place is user generated content. The mysql manual even says:

“Please use utf8mb4 instead. Although utf8 is currently an alias for utf8mb3, at some point utf8 is expected to become a reference to utf8mb4. To avoid ambiguity about the meaning of utf8, consider specifying utf8mb4 explicitly for character set references instead of utf8.”

Now that’s what I did. First I changed horde’s database encoding to utf8mb4 in conf.php:

$conf['sql']['protocol'] = 'tcp';
$conf['sql']['charset'] = 'utf8mb4';

Then I changed the mysql server’s and client’s default charset:

cat /etc/mysql/my.cnf
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-character-set = utf8mb4
default-character-set = utf8mb4
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

I have obviously stripped a lot of file content not relevant to our story. After reloading the database, all new connections should use the real utf8 encoding and new tables should be created with the new standard. But what about existing content? We need to convert all tables and all their text-like columns, varchars, mediumtexts etc.

First, it’s backup time – better safe than sorry.

Then, let’s find all tables in our db server and feed them conversion commands.

mysql --database=horde -B -N -e "SHOW TABLES" | awk '{print "ALTER TABLE", $1, "CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"}' | mysql --database=horde

Do this in a downtime or ensure HA some way or other. It will take some time. Some sources suggest it might be sensible to also rebuild the tables with the optimize command. I am not very convinced, but it won’t harm.

mysqlcheck --auto-repair --optimize --user="hidden" -p --databases mysql horde

After this, repeat the sync test. It should work this time.

bookmark_borderHorde Development Review: October 2020

October was a very busy month in Horde development, even though a lot of things happened under the hood and cannot be accessed right now.

  • A decision was made that Horde RPC will default to json-rpc and deprecate xmlrpc in upcoming releases, maybe already dropping xmlrpc in Horde 6. PHP8 will remove xmlrpc from the core distribution and move it into PECL, giving it much less of an installation base.
  • B1 Systems GmbH released a typescript client for the Horde Ajax Framework
  • Horde Injector: I continued my work from September. The Maintaina version of Injector now supports PSR-4, PSR-11, PSR-12 specifications. Backward compatibility is good. The unit tests have been converted to namespaced versions – see
  • Horde Components: The Components tool has been converted for PSR-4 autoloading in a composer setup. Also, some minor improvements on the composer file generator have been added
  • I worked on upgrading Diana Hille’s DNS library for the horde framework, giving it the same PSR-4 makeover, converted it from wrapping AWS cli to wrapping AWS Route53 SDK. I think we can opensource the component soon.
  • I created a little testbed application for the improved controller/routes handling in recent horde/core. horde/frogsie is the first Horde application stub to feature a RESTish interface. Frogsie writes iptables rules to implement a TCP/UDP Front Proxy in plain linux.
    It doesn’t sport web UI or CLI though as it’s both a case study and a stop-gap utility. However, it’s a good example to learn from.
  • Horde Installer Plugin v1.1.0 supports both composer 1.x and composer 2.0 – This is mostly based on work by Kieran Brahney of SupportPal. I also began working on converting horde-deployment to remove the last bits of PEAR and make it compatible with composer 2.0. It’s not released yet.
  • I am looking at the old vilma application at the moment. This will most likely not materialize before november. Vilma is a simple mailbox manager along the lines of postfixadmin. My primary interest is using it together with hordectl for a turnkey default setup. Vilma will get the PSR-4 treatment and likely I will want to change some of the architecture. Focus is on a minimum viable product that nicely integrates with my groupware bundle and fulfills very limited needs. I appreciate the work of Christian Bolz on PostfixAdmin and I do not intend to build a full scale replacement.

What’s next?

  • More library conversions to PSR-4, PHP7 features.
  • Most likely I will upgrade unit tests to a more recent PHPunit
  • Release Cleanup / Upstreaming efforts for existing non-released code.
  • Better theme support for horde-installer-plugin

Results first, more talk later.

bookmark_borderHeads Up: Cannot login as horde admin anymore?

Please be careful: Horde has introduced a new default setting $conf[‘auth’][‘lowercase’] which acts like the auth hook used to do.

This might lead to issues when your default admin account is named “Administrator”.

It is actually a very useful setting. It ensures that any prefs and other profiles also work with case-insensitive backends regardless of how you type the user name.

How to fix?

a) Lowercase all admin names in your $conf[‘auth’][‘admin’] settings in conf.php

b) disable $conf[‘auth’][‘lowercase’] by setting it to false value.

bookmark_borderPEAR down – Taking Horde to Composer

Since Horde 4, the Horde ecosystem heavily relied on the PEAR infrastructure. Sadly, this infrastructure is in bad health. It’s time to add alternatives.

Everybody has noticed the recent PEAR break-in.

A security breach has been found on the webserver, with a tainted go-pear.phar discovered. The PEAR website itself has been disabled until a known clean site can be rebuilt. A more detailed announcement will be on the PEAR Blog once it’s back online. If you have downloaded this go-pear.phar in the past six months, you should get a new copy of the same release version from GitHub (pear/pearweb_phars) and compare file hashes. If different, you may have the infected file.

While I am writing these lines, is down. Retrieval links for individual pear packages are down. Installation of pear packages is still possible from private mirrors or linux software distribution packages (openSUSE, Debian, Ubuntu). Separate pear servers like are not directly affected. However, a lot of pear software relies on one or many libraries from – it’s a tough situation. A lot of software projects have moved on to composer, an alternative solution to dependency distribution. However, some composer projects have dependency on PEAR channels.

I am currently submitting some changes to Horde upstream to make Horde libs (both released and from git) more usable from composer projects.
Short-term goal is making use of some highlight libraries easier in other contexts. For example, Horde_ActiveSync and Horde_Mail, Horde_Smtp, Horde_Imap_Client are really shiny. I use Horde_Date so much I even introduced it in some non-horde software – even though most functionality is also somewhere in php native classes.

The ultimate goal however is to enable horde groupware installations out of composer. This requires more work to be done. There are several issues.

  • The db migration tool checks for some pear path settings during runtime Most likely there are other code paths which need to be addressed.
  • Horde Libraries should not be web readable but horde apps should be in a web accessible structure. Traditionally, they are installed below the base application (“horde dir”) but they can also be installed to separate dirs.
  • Some libraries like Horde_Core contain files like javascript packages which need to be moved or linked to a location inside another package. Traditionally, this is handled either by the “git-tools” tool linking the code directory to a separate web directory or by pear placing various parts of the package to different root paths. Composer doesn’t have that out of the box.

Horde already has been generating composer manifest files for quite a while. Unfortunately, they were thin wrappers around the existing pear channel. The original generator even took all package information from the pear manifest file (package.xml) and converted it. Which means, it relied on a working pear installation. I wrote an alternative implementation which directly converts from .horde.yml to composer.json – Calling the packages by their composer-native names. As horde packages have not been released on packagist yet, the composer manifest also includes repository links to the relevant git repository. This should later be disabled for releases and only turned on in master/head scenarios. Releases should be pulled from packagist authority, which is much faster and less reliant on existing repository layouts.

To address the open points, composer needs to be amended. I currently generate the manifests using package types “horde-library” and “horde-application” – I also added a package type “horde-theme” for which no precedent exists yet. Composer doesn’t understand these types unless one adds an installer plugin Once completed and accepted, this should be upstreamed into composer/installers. The plugin currently handles installing apps to appropriate places rather than /vendor/ – however, I think we should avoid having a super-special case “horde-base” and default to installing apps directly below the project dir. Horde base should also live on the same hierarchy. This needs some additional tools and autoconfiguration to make it convenient. Still much way to go.

That said, I don’t think pear support should be dropped anytime soon. It’s the most sensible way for distribution packaging php software. As long as we can bear the cost involved in keeping it up, we should try.

bookmark_borderCurrent (10/2018) Tumbleweed on Raspberry Pi 1


I just had a little struggle getting the current tumbleweed to run on the original Raspberry Pi (first generation, though the revision with larger RAM).

Just in case this helps anybody: I did not have any luck with a fresh openSUSE Tumbleweed image of one of the current arm6 builds. Don’t know why.

Here’s what I did:
– Download a pretty old known-good OpenSUSE 13.1 built by Bernhard Wiedemann

Unzip, dump it to SD Card

xz -d raspberrypi-opensuse-latest.img.xz
dd if=raspberrypi-opensuse-latest.img of=/dev/mmcblk0 bs=8M

Boot up, change to text console (CTRL + ALT + F2)
Log In (root/linux)

Change Password (passwd)

nano /etc/zypp/repos.d/oss131.repo
Change baseurl line to
Save and get out (CTRL+X, Y)

#Resize partition and FS as this build won’t do:
# Adjust to more if your card is larger or to less if you need a more advanced partitioning scheme

parted resize 3 16G

resize2fs /dev/mmcblk0p3

zypper ref

zypper up gzip rpm

zypper dup –download in-advance

#(super conservative, get all needed packages first) – This is going to take quite a while

reboot, power cycle

Note that you will end up with a system booting into X11 login. You should probably change the default systemd target and maybe also get rid of some software. And you really don’t want a server with ssh password “linux”, so better don’t skip changing the PW


bookmark_borderhorde trustr – A new horde CA app step by step

Trustr is my current project to create a simple certificate management app.
I decided that it is just about the right scope to demonstrate a few things about application development in Horde 5.

I have not made any research if the name is already occupied by some other software. Should any problems arise, please contact me and we will find a good solution. I just wanted to start without losing much time on unrelated issues.

My goals as of now:

– Keep everything neat, testable, fairly decoupled and reusable. The core logic should be exportable to a separate library without much change. There won’t be any class of static shortcut methods pulling stuff out of nowhere. Config and registry are only accessed at select points, never in the deeper layers.
– Provide a CLI using Horde_Cli and Horde_Cli_Application (modeled after the backup tool in horde base git)
– Store to relational database using Horde_Db and Horde_Rdo for abstraction
– Use php openssl extension for certificate actions, but design with future options in mind
– Rely on magic openssl defaults as little as possible
– Use conf.xml / conf.php for any global defaults
– Show how to use the inter-app API (reusable for xml-rpc and json-rpc)
– Showcase an approach to REST danabol ds in Horde (experimental)

The app is intended as a resource provider. The UI is NOT a top priority. However, I am currently toying around with a Flux-like design in some unrelated larger project and I may or may not try some ideas later on.

Initial Steps: Creating the working environment

I set up a new horde development container using the horde tumbleweed image from Open Build Service and a docker compose file from my colleague Florian Frank. Please mind both are WIP and improve-as-needed projects.

git clone
cd hordeOnTumbelweed
docker-compose -f docker-compose.yml up

This yields a running horde instance on localhost and a database container.
I needed to perform a little manual setup in the web admin ui to get the DB to run and create all default horde schemas.

Next I entered the developer container with a shell
docker exec -it hordeOnTumbelWeed_php_1 bash

There are other ways to work with a container but that’s what I did.


Creating a  skeleton app

The container comes with a fairly complete horde git checkout in /srv/git/horde and a clone of the horde git tools in /srv/git/git-tools

A new skeleton app can be created using

horde-git-tools dev new --author "Ralf Lang <>" --app-name trustr

The new app needs to be linked to the web directory using

horde-git-tools dev install

Also, a registry entry needs to be created by putting a little file into /srv/git/horde/base/config/registry.d

cat trustr-registry.d.php

// Copy this example snipped to horde/registry.d
$this->applications['trustr'] = array(
'name' => _('Certificates'),
'provides' => array('certificates')


This makes the new app show up in the admin menu. To actually use it and make it appear in topbar, you also need to go to /admin/config and create the config file for this app. Even though the settings don’t actually mean anything by now, the file must be present.

I hope to follow up soon with articles on the architecture and sub systems of the little app.

bookmark_borderHorde_Rdo Many to Many relations and Horde DB Migrator

Many to Many relations btween to object types or table rows are usually saved to a database using a third table.

For example, if every server can have multiple services and each service can run on multiple computers, we need a third table to store the relations:

server table:
server_id | server_name
        1 |
        2 |
service table:
service_id | service_name
         1 | tomcat
         2 | dovecot
relation table:
service_id | server_id
         1 | 1
         2 | 2
         2 | 1

Horde’s ORM Layer Horde_Rdo supports creating, checking and changing such relations but it’s not very prominently documented.

Let’s look at an example.

First, we need to create the database schema. Note that the relations table has no autoincrement key, only the two columns used for lookup

/usr/share/php5/PEAR/www/horde/hvview/migration # cat 1_hvview_base_tables.php
* Create Hvview base tables.
* Copyright 2015-2015 B1 Systems GmbH (
* See the enclosed file COPYING for license information (GPL). If you
* did not receive this file, see
* @author Ralf Lang
* @package Hvview
class HvviewBaseTables extends Horde_Db_Migration_Base
* Upgrade
public function up()

$t = $this->createTable('hvview_technical_landscapes', array('autoincrementKey' => 'landscape_id'));
$t->column('landscape_name', 'string', array('limit' => 255, 'null' => false));
$t->column('period_id', 'integer', array('limit' => 11, 'null' => false));

$t = $this->createTable('hvview_resource_pools', array('autoincrementKey' => 'resource_pool_id'));
$t->column('pool_name', 'string', array('limit' => 255, 'null' => false));
$t->column('landscape_id', 'integer', array('limit' => 11, 'null' => false));
$t->column('period_id', 'integer', array('limit' => 11, 'null' => false));

$t = $this->createTable('hvview_hardware_pools', array('autoincrementKey' => 'hardware_pool_id'));
$t->column('pool_name', 'string', array('limit' => 255, 'null' => false));
$t->column('landscape_id', 'integer', array('limit' => 11, 'null' => false)); /* possibly redundant, but may speed up things */
$t->column('period_id', 'integer', array('limit' => 11, 'null' => false));

/*Relations table*/
$t = $this->createTable('hvview_rp_hwps', array('autoincrementKey' => false));
$t->column('resource_pool_id', 'integer', array('limit' => 11, 'null' => false));
$t->column('hardware_pool_id', 'integer', array('limit' => 11, 'null' => false));

$t = $this->createTable('hvview_periods', array('autoincrementKey' => 'period_id'));
$t->column('period_ts', 'integer', array('limit' => 11, 'null' => false));

/* We collapse hypervisor and blade server objects into one for now - let`s see if this scales well */
$t = $this->createTable('hvview_servers', array('autoincrementKey' => 'server_id'));
$t->column('period_id', 'integer', array('limit' => 11, 'null' => false));
$t->column('hardware_pool_id', 'integer', array('limit' => 11, 'null' => false));
$t->column('hostname', 'string', array('limit' => 100, 'null' => false));
$t->column('state', 'string', array('limit' => 20, 'null' => true));
$t->column('os_release', 'string', array('limit' => 20, 'null' => true));
$t->column('comment', 'string', array('limit' => 255, 'null' => true));
$t->column('hv_free_vcpu', 'integer', array('limit' => 11, 'null' => true));
$t->column('hv_free_memory', 'integer', array('limit' => 11, 'null' => true));
$t->column('hv_free_disk', 'integer', array('limit' => 11, 'null' => true));
$t->column('hv_total_vcpu', 'integer', array('limit' => 11, 'null' => true));
$t->column('hv_total_memory', 'integer', array('limit' => 11, 'null' => true));
$t->column('hv_excluded', 'integer', array('limit' => 1, 'null' => true));
$t->column('hv_vm_count', 'integer', array('limit' => 3, 'null' => true));

// Indices not before we have an idea which of them we need most
// $this->addIndex('hvview_items', array('item_owner'));


* Downgrade
public function down()

The relations are defined in a Horde_Rdo_Mapper class which also knows how to spawn objects from the rows.

The Objects

/usr/share/php5/PEAR/www/horde/hvview/lib/Entity # cat ResourcePool.php 

class Hvview_Entity_ResourcePool extends Horde_Rdo_Base {

/usr/share/php5/PEAR/www/horde/hvview/lib/Entity # cat HardwarePool.php 

The Mappers:

/usr/share/php5/PEAR/www/horde/hvview/lib/Entity # cat ResourcePoolMapper.php 
class Hvview_Entity_ResourcePoolMapper extends Horde_Rdo_Mapper
     * Inflector doesn't support Horde-style tables yet
    protected $_classname = 'Hvview_Entity_ResourcePool';
    protected $_table = 'hvview_resource_pools';
    protected $_lazyRelationships = array(
             'hwps' => array('type' => Horde_Rdo::MANY_TO_MANY,
                          'through' => 'hvview_rp_hwps',
                          'mapper' => 'Hvview_Entity_HardwarePoolMapper')


/usr/share/php5/PEAR/www/horde/hvview/lib/Entity # cat HardwarePoolMapper.php 
class Hvview_Entity_HardwarePoolMapper extends Horde_Rdo_Mapper
     * Inflector doesn't support Horde-style tables yet
    protected $_classname = 'Hvview_Entity_HardwarePool';
    protected $_table = 'hvview_hardware_pools';
    protected $_lazyRelationships = array(
             'rps' => array('type' => Horde_Rdo::MANY_TO_MANY,
                          'through' => 'hvview_rp_hwps',
                          'mapper' => 'Hvview_Entity_ResourcePoolMapper')


The relation is defined in both direction and only loaded on-demand ("lazy") as opposed to upfront when the item is created from the database rows.
Now let's fetch two items and link them:

You can do this through the mapper or through one of the two partners

Adding a relation to an object using the object

// $rm is a ResourcePoolMapper instance
// $hm is a HardwarePoolMapper instance
$rp = $rm->findOne(); // In reality, you would not pick a random item but add some criteria
$hwp = $hm->findOne();
$rp->addRelation('hwps', $hwp);

Adding a relation to an object using the mapper

// $rm is a ResourcePoolMapper instance
// $hm is a HardwarePoolMapper instance
$rp = $rm->findOne(); // In reality, you would not pick a random item but add some criteria
$hwp = $hm->findOne();
$rm->addRelation('hwps', $rp, $hwp);

bookmark_borderSara Golemon (Facebook) announces PHP Language Specification for OSCON 2014

For more than 10 years, PHP core developers repeatedly raised the topic of providing a formal language specification for PHP. Now a team of facebook employees has written such a specification. The spec document is currently only available as a preview chapter a preview chapter . PHP veteran Sara Golemon announced on the “PHP internals” list that the full document will be ready for O’Reilly’s OSCON 2014. Sara Golemon published the standard book on “Extending and Embedding PHP” some years ago and now works for Facebook’s own PHP implementation HHVM. The PHP spec defines PHP version 5.6 in about 200 pages and contains all the odd and obscure quirks of the language core. Facebook’s own HHVM aims to be as close to the spec as possible. Currently, PHP developers discuss how amending the spec can become a mandatory part of the language development process. Though some are sceptic that all developers will embrace the change in the process, everybody on the list was happy to have the new document.

Software Architect Stas Malyshev:

Thank you Sara and Facebook team for doing something we’ve been talking
about for more than a decade and before that nobody actually attempting
to do. I think it is a great development and I hope to see the first
version soon.

bookmark_borderSara Golemon (Facebook) kündigt PHP Language Specification auf OSCON 2014 an

Seit über 10 Jahren bringen immer wieder einige der PHP-Sprachentwickler den Plan an, eine formale Spezifikation für den Sprachkern bereitzustellen. Ein Team bei Facebook hat das nun getan. Die Spezifikation, die bisher nur als Vorschau vorliegt, wurde von Sara Golemon auf der Entwickler-Liste angekündigt und soll auf der OSCON 2014 vorgestellt werden. Sara Golemon veröffentlichte schon vor einigen Jahren ein Standardwerk über die Entwicklung von PHP-Erweiterungsmodulen und arbeitet mittlerweile an Facebooks eigener PHP-Version HHVM.

Das rund 200 Seiten starke Dokument orientiert sich an der PHP-Version 5.6 und enthält auch obskure Verhaltensweisen des PHP-Sprachkerns in seltenen Randfällen. Die Facebook-eigene PHP-Version HHVM soll sich möglichst eng an diese Vorgaben halten.

Die PHP-Community berät derzeit, wie sie die Fortschreibung der Spezifikation in den Entwicklungsprozess einbinden kann. Die Ankündigung wurde mit viel Begeisterung aufgenommen.

Software-Architekt Stas Malyshev:

Thank you Sara and Facebook team for doing something we’ve been talking
about for more than a decade and before that nobody actually attempting
to do. I think it is a great development and I hope to see the first
version soon.

bookmark_borderPerl: Semantic Version Sorting via callback puts betas before releases (empty string after text)

Semantic Versioning

Semantic program versions are a great help in administration life: When done right, they help you identify if only bugs have been resolved (2.11.z) or features added (2.y.0) or the program has undergone big changes with chances that an upgrade needs a lot of admin intervention (x.0.0). For developers and early testers, additional suffixes identify alpha (early testing), beta (testing) and Release Candidate (pre-release polishing) versions which are not intended for production use.

Como el kratom puede ser útil para el entrenamiento y el culturismo – the frisky fildena guantes de gimnasia para levantamiento de pesas beace con palma de cuero antideslizante para entrenamiento, ejercicio, entrenamiento, fitness y culturismo para hombres y mujeres

The Problem

Sorting program versions is not exactly trivial. You cannot sort them as strings, otherwise you would put 2.2.2 after 2.12.1. You cannot sort them as numbers either, because they contain multiple dots and possibly alpha characters.


a) Fixed-Length Score string

A traditional method expands the version string to a fixed-format score integer or string which can be safely compared and sorted by basic sorting algorithms. This is based on the assumption that there are not more than a fixed snovitra 20mg number of values per field, i.e. not more than 99 patch versions before the next minor, not more than 99 minor versions before the next major. This is safe for most mainstream projects but corner cases may exist. It also doesn’t play well with development releases.

This solution would first split the version string into its components, then merge them to a string and then run an alphabetic sort on it:

my @versions;
foreach my $version_string (@version_strings) {
  my ($major, $minor, $patch) = split /\./, $version_string;
  push(@versions, sprintf(%02d%02d%02d, $major, $minor, $patch));
my @sorted_versions = sort {$a cmp $b} @versions;

This is a simplified example. You need to get back to the original string format, either by a function for reverse conversion or by storing both together in an array of hashref. I also left out handling for alpha, beta, RC versions.

b) Using a sort callback

The sort() routine allows you to specify a callback which decides for any two values $a and $b if they are equal or which is greater. This works by any criteria you can imagine and is also usable for objects and hashes.

This is what we do: We split the version string into a hashref of its components (and a key for the original format, for convenience) and then we define a callback.

sub release_to_version_hash {
  my $release = shift;
  my ($package, $major, $minor, $patch, $dev) = $release =~ /\/(\w+)-(\d+)\.(\d+)\.(\d+)(\w*)/;
  return { 
    major => $major,
    minor => $minor,
    patch => $patch,
    dev => $dev,
    pkg => $package,
    url => $release,
    string => sprintf("%d.%d.%d%s", $major, $minor, $patch, $dev)

We use a regular expression to retrieve the version fields.
Note: This code is taken from an open source project of mine.
The release strings have this format:
If your source string is just a version, your regular expression would look like this:


Now let’s get down to business. The actual sorting routine is not very exciting. It gets an Array Reference of Hash References, the hashes being in the format produced above. It returns an array reference (note the [] brackets). The sort routine gets the list (ref) of releases, dereferenced to an array and invokes the compare_versions routine many times, comparing any two values $a and $b inside the releases list which is the “bigger” one.

sub sort_releases {
  my $releases = shift;
  return [sort compare_versions @$releases];

Now let’s look at that callback.

## A compare function callback for version hashes, suitable for sort
## returns -1, 0 or 1
sub compare_versions {
    $a->{major} <=> $b->{major} or
    $a->{minor} <=> $b->{minor} or
    $a->{patch} <=> $b->{patch} or
    ## handle development releases
    ( !$a->{dev} && $b->{dev} ? 1 : 0 ) or
    ( !$b->{dev} && $a->{dev} ? -1 : 0 ) or
    lc($a->{dev}) cmp lc($b->{dev})

Now what happens here? This routine returns -1, 0 or 1 depending on which versions is “bigger”. In practice, you should never get 0 because you should never have two identic version strings. After the major versions are compared as a number via the spaceship operator (<=>), perl evaluates the result.
If version $a (major) is smaller (-1) or bigger (1) than version $b (major), then this evaluates to a true (non-zero) value. This fulfills the “or” condition, so perl immediately stops evaluating and returns the value. If the major versions are equal, the first expression evaluates to false (0) and the next part of the comparison is evaluated. This is sufficient for sorting major, minor, patch version in that order.

If we also want to handle development releases, we also need the last three lines.
The last line sorts development releases alphabetically, sorting all alphas of the same major.minor.patch before all betas. Normally, perl would sort all empty strings before all non-empty strings, which is bad. It would sort a production version 2.0.0 before the alpha releases.

To fix this, we use the upper two lines:
If a string is empty, it evaluates to false.
So if the first string is false (empty, production) and
the second string is true (any characters, development), the first version is the preferable version. In this case the expression evaluates to 1 and returns. Otherwise it evaluates to false (0) and the next or clause is evaluated. This one checks for the opposite case where version b is the better version and returns -1. If both versions contain a dev string, we skip to alphabetic comparison.

This results in a list where the best version is the last hash entry. Getting the best version is as easy as

$version = pop @versions;
## or
$version = $versions[-1];

Bonus: A versatile filter for all sorts of tasks

This filter is best applied before sorting but it also works afterwards. It can filter out all development releases or return only releases within a specific major version etc:

sub filter_releases {
  my $releases = shift;
  my $filter = shift || 
    { stable => 1, 
      rc => 0, 
      beta => 0, 
      alpha => 0, 
      major => 0, 
      minor => 0, 
      patch => 0, 
      pkg => '' };
  my @legit;
  foreach my $pkg (@$releases) {
    ## filters
    next if ($filter->{pkg} && $pkg->{pkg} ne $filter->{pkg});
    next if ($pkg->{dev} =~ /RC/ && $filter->{'rc'} == 0);
    next if ($pkg->{dev} =~ /alpha/ && $filter->{'rc'} == 0);
    next if ($pkg->{dev} =~ /beta/ && $filter->{'rc'} == 0);
    next if ($filter->{major} && $filter->{major} != $pkg->{major});
    next if ($filter->{minor} && $filter->{minor} != $pkg->{minor});
    next if ($filter->{patch} && $filter->{minor} != $pkg->{patch});
    push @legit, $pkg;
  return \@legit;