Wednesday, May 13, 2020

Mobile skin and plugin for Roundcube webmail

The "Melanie2" mobile skin and plugin seems to make Roundcube work on phones. And it is easier to install than it would seem when only briefly looking at the github instructions.

These are the commands I needed on a Debian 10 ("Buster") machine where roundcube 1.3.10 was installed. (the base install was done from the Debian repositories: apt install roundcube roundcube-sqlite3. Instead of sqlite3, it is also possible to use PostgreSQL or MySQL with the roundcube-pgsql or roundcube-mysql packages).

For the mobile stuff:

Create a directory to store the extra stuff and make upgrades easier:

mkdir -p /opt/roundcube-stuff
cd /opt/roundcube-stuff/

Get the files from Github:

git clone https://github.com/messagerie-melanie2/roundcube_skin_melanie2_larry_mobile
git clone https://github.com/messagerie-melanie2/roundcube_jquery_mobile
git clone https://github.com/messagerie-melanie2/roundcube_mobile

Instead of renaming and copying the directories, create symlinks and copy them to roundcube's skins and plugins folders:

ln -si $(pwd)/roundcube_skin_melanie2_larry_mobile/ melanie2_larry_mobile
ln -si $(pwd)/roundcube_jquery_mobile/              jquery_mobile
ln -si $(pwd)/roundcube_mobile                      mobile

cp -vd /opt/roundcube-stuff/melanie2_larry_mobile   /var/lib/roundcube/skins/
cp -vd /opt/roundcube-stuff/jquery_mobile           /var/lib/roundcube/plugins/
cp -vd /opt/roundcube-stuff/mobile                  /var/lib/roundcube/plugins/

Finally, add 'mobile' to the $config['plugins'] array in /etc/roundcube/config.inc.php. If doing it by hand is too much work, copy/pasting this should work:

echo 'array_push( $config["plugins"], "mobile" );' | tee -a /etc/roundcube/config.inc.php
#or:
## echo '$config["plugins"][] = "mobile";' | tee -a /etc/roundcube/config.inc.php

Labels: , , , , ,

Tuesday, January 02, 2018

Cartes d'identité et passeports suisses

Une rumeur circule parmi les enfants des écoles en Suisse. Elle dit que le chiffre à la fin du numéro de la carte d'identité indique le nombre de sosies. Et celà serait utile pour les caméras de surveillance et leurs logiciels de reconnaissance faciale.

C'est évidemment absurde, mais le phénomène est tout de même intéressant d'un point de vue sociologique et politique, puisqu'il reflète une perception plutôt inquiétante de nos sociétés par les enfants. Après tout, les logiciels de reconnaissance et les caméras de surveillance sont bien réels...

Cependant, la sociologie et la psychologie enfantine étant des domaines bien trop complexes pour moi, j'ai juste voulu savoir ce qu'étaient réellement ces chiffres. Sûrement des chiffres de contrôle, qui apparaissent à la fin de tous les codes qui doivent pouvoir être lus par des machines, comme le nos des comptes bancaires, des cartes de crédit, etc.

La signification des chiffres est vaguement expliquée sur le site de la Confédération pour les passeports, mais pas pour les cartes d'identité. Quand à l'agorithme utilisé pour le calcul du chiffre de contrôle, il n'est mentionné nulle part. De plus, sur l'exemple qui illustre les chiffres pour le passeport, le chiffre de contrôle est FAUX!

L'exemple indique "9" en bas à droite au lieu de "6"!

Avec une telle avarice d'explications de la part des autorités, il n'est pas étonnant de voir surgir des rumeurs bizarres.

Heureusement, pour la carte d'identité, il y a une page en allemand de Wikipedia qui explique le tout, y compris l'algorithme utilisé pour les chiffres de contrôle.

Ainsi, après mon exploration ancienne du calcul "modulo 10" pour certains chiffres de contrôle de banques et autres, j'ai pu m'amuser à faire un petit script qui donne le nombre de sosies les chiffres de contrôle pour les cartes d'identité et les passeports suisses.
L'algorithme de base en Perl est dans cette fonction "cksum":

sub cksum {
 my $num = shift;
 $num = uc( $num );       # convert tu uppercase
 $num =~ s/[^A-Z0-9<]//g; # and remove spaces etc.

 my @digits = split //, $num;
 my @multipliers = (7,3,1);
 my $cksum = 0;

 for (my $i=0; $i < @digits; $i++) {
  my $n = $digits[$i];

  $n = 0 if ($n eq "<");

  if ($n =~ /[A-Z]/) {  # A=>10, B=>11, ..., Z=>35
   $n = ord( $n ) - 55;
  }

  $cksum += $n * $multipliers[ $i % 3 ];
 }
 return $cksum % 10; # keep only last digit
}

Et pour les geeks, le script complet est ici.

Labels: , ,

Saturday, May 11, 2013

Mediawiki with Postgres on Debian

A short guide to install Mediawiki on Debian with PostgreSQL 9.1.With a fix for this error:

"Attempting to connect to database "postgres" as superuser "postgres"... error: No database connection"

Installing packages

The server is still using Debian Squeeze, but I expect it would quite the same for the new Debian Wheezy. Here I used squeeze-backports.

 Add the backports repository if needed:

echo "deb http://backports.debian.org/debian-backports squeeze-backports main contrib non-free" >> /etc/apt/sources.list

Install everything:

apt-get update
apt-get -t squeeze-backports install apache2 postgresql-9.1 postgresql-contrib php5-pgsql
apt-get -t squeeze-backports install imagemagick libdbd-pg-perl
apt-get -t squeeze-backports install mediawiki

I use a separate IP for the wiki, so need to add it to the interface:

mcedit /etc/network/interfaces
# wiki on it's own IP
auto eth0:3
iface eth0:3 inet static
    address 192.168.10.4
    netmask 255.255.255.0

/etc/init.d/networking restart

Apache configuration

# I use the mod_rewrite module in Apache
a2enmod rewrite

# I prefer the config file in sites-enabled
# (but it's really just a symlink to /etc/mediawiki/apache.conf):
mv /etc/apache2/conf.d/mediawiki.conf /etc/apache2/sites-enabled

My virtual host config:

<VirtualHost *:80>
    ServerName wiki.example.lan
    ServerAlias wiki.example.lan
    ServerAdmin webmaster@example.com
    DocumentRoot /docs/www-wiki

    ErrorLog /var/log/apache2/wiki-error.log
    CustomLog /var/log/apache2/wiki-access.log combined

    ServerSignature On

    Alias /icons/ "/usr/share/apache2/icons/"

    RewriteEngine On
    RewriteRule ^/w(iki)?/(.*)$  http://%{HTTP_HOST}/index.php/$2 [L,NC]

    <Directory /docs/www-wiki/>
        Options +FollowSymLinks
        AllowOverride All
        # Default is Deny. Exceptions listed below with "Allow ...":
        Order Deny,Allow
        Deny from All
        Satisfy any
        # LAN
        Allow from 192.168.10.0/24
        # VPN
        Allow from 10.0.10.0/24

# If using LDAP
#        AuthType Basic
#        AuthName "Example Wiki. Requires user name and password"
#        AuthBasicProvider ldap
#        AuthzLDAPAuthoritative on
#        AuthLDAPURL ldap://localhost:389/ou=People,dc=example,dc=lan?uid
#        AuthLDAPGroupAttribute memberUid
#        AuthLDAPGroupAttributeIsDN off
#        Require ldap-group cn=users,ou=Groups,dc=example,dc=lan
    </Directory>

    # some directories must be protected
    <Directory /docs/www-wiki/config>
        Options -FollowSymLinks
        AllowOverride None
    </Directory>

    <Directory /docs/www-wiki/upload>
        Options -FollowSymLinks
        AllowOverride None
    </Directory>

    <Directory "/usr/share/apache2/icons">
        Options Indexes MultiViews
        AllowOverride None
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

Moving files

I used a directory other than the default /var/lib/mediawiki. So I had to move things over:

cp -rp /var/lib/mediawiki /docs/www-wiki

The only tricky part, with the fix:

Before starting the web configurator in http://wiki.example.lan/config/ you need to define a password for the "postgres" database user. Mediawiki will start the psql client as the www-data system user, but with the -U argument to set the user to "postgres". Even if you defined a password for the system user "postgres", this is not the password of the database user "postgres".

So you need to start psql as the postgres system user, which you can do as root using sudo -c, and then set the password inside the psql client:

sudo -u postgres psql
psql (9.1.9)
Type "help" for help.

postgres=# \password
Enter new password:
Enter it again:
postgres=# \q

If you don't do this, the Mediawiki config will end with this error:

Attempting to connect to database "postgres" as superuser "postgres"... error: No database connection

And a big pink and unhelpful error box below.

The Postgresql log (tail /var/log/postgresql/postgresql-9.1-main.log) will show:

FATAL:  password authentication failed for user "postgres"

Finally

Now you just have to move LocalSettings.php to /etc/mediawiki/.

And if you used a different install root, you have to edit it to change the MW_INSTALL_PATH:

define('MW_INSTALL_PATH','/docs/www-wiki');



Labels: , , , , , , , ,

Sunday, January 06, 2013

scripting disk partitionning in Linux - take 2

It is possible to use parted to script/automate disk partitioning in Linux, as described in "Command-line partitioning and formatting".

Another way is to use sgdisk from the GPT fdisk programs.

In Debian and derivatives, it can be installed with sudo apt-get install gdisk.

The current version 0.8.1 from the Ubuntu 12.04 repository would partition only the first 2TB of a 4 TB. disk. So you may need to get a more recent version from the downloads page. I got version 0.8.5 for x64, and that worked very well.

The following will create and format a single NTFS partition on an entire drive:

disk=/dev/sdb            # Make sure you got this right !!
label="My_Disk_Name"
echo "disk $disk will be completely erased."

sudo sgdisk -Z $disk
sudo sgdisk --new=0:0:-8M -t 1:0700 $disk
sudo sgdisk -p $disk
sudo mkntfs --verbose --fast --label "$label" --no-indexing --with-uuid ${disk}1

-Z removes any left-over partitions

--new=0:0:-8M creates a single partition from the start of the disk to 8MB before the end (just in case it's useful to not end on the very last sector)

-t 1:0700 sets the first partition we just created to type "Microsoft Basic Partition", which is the type we want for a simple NTFS partition. Linux would be -t 1:8300. Use sgdisk -L to get a list of partition types.

Note that for comfortable (and safer) manual partitioning, there is also cgdisk. It is like the old cfdisk, but works with new disks over 2TB.

Labels: , , , , , , , ,

Wednesday, January 02, 2013

Set up your own Dynamic DNS

The problem with external dynamic DNS services like dyndns.org, no-ip.com, etc. is that you constantly have to look after them. Either they are free, but they expire after 1 month and you have to go to their web site to re-activate your account. Or you pay for them, but then you need to take care of the payments, update the credit card info, etc. This is all much too cumbersome for something that should be entirely automated.

If you manage your own DNS anyway, it may be simpler in the long run to set-up your own dynamic DNS system.

Bind has everything needed. There is a lot of info on the Internet on how to do it, but what I found tended to be more complicated than becessary or insecure or both. So here is how I did it on a Debian 6 ("squeeze") server.

The steps described below are:

Initialize variables

To make it easier to copy/paste commands, we initialize a few variables

binddir="/var/cache/bind"
etcdir="/etc/bind"

(In Debian, you can use grep directory /etc/bind/named.conf.options to find the correct binddir value)

For dynamic hosts, we will use a subdomain of our main zone: .dyn.example.com.

host=myhost; zone=dyn.example.com

Create key

Most example use the dnssec-keygen command. That would create 2 files (with ugly names): one .private and one .key (public) file. This is useless since the secret key is the same in both files, and the nsupdate method doesn't use a public/private key mechanism anyway.

There is a less-known and more appropriate command in recent distributions : ddns-confgen. By default, it will just print sample entries with instructions to STDOUT. You can try it out with:

ddns-confgen -r /dev/urandom -s $host.$zone.

The options we use here are to use an "hmac-md5" algorithm instead of the default "hmac-sha256". It simplifies things with nsupdate later. And we also specify the key name to be the same as the host's name. That way, we can use a wildcard in the "update-policy" in named.conf.local and don't need to update it every time we add a host.

ddns-confgen -r /dev/urandom -q -a hmac-md5 -k $host.$zone -s $host.$zone. | tee -a $etcdir/$zone.keys

chown root:bind   $etcdir/$zone.keys
chmod u=rw,g=r,o= $etcdir/$zone.keys

Depending on how you intend to use nsupdate, you may want to also have a separate key file for every host key. nsupdate cannot use the $zone.keys file if it contains multiple keys. So you might prefer to directly create these individual keyfiles by adding something like > $etcdir/key.$host.$zone :

ddns-confgen -r /dev/urandom -q -a hmac-md5 -k $host.$zone -s $host.$zone. | tee -a $etcdir/$zone.keys > $etcdir/key.$host.$zone

chown root:bind   $etcdir/$zone.keys $etcdir/key.*
chmod u=rw,g=r,o= $etcdir/$zone.keys $etcdir/key.*

Configure bind

Create zone file

Edit $binddir/$zone :

$ORIGIN .
$TTL  3600 ; 1 hour

dyn.example.com IN SOA  dns-server.example.com. hostmaster.example.com. (
         1 ; serial (start at 1 for a dynamic zone instead of the usual date-based serial)
      3600 ; refresh by secondaries (but they get NOTIFY-ed anyway)
       600 ; retry (every 10 minutes if refresh fails)
    604800 ; expire (slaves remove the record after 1 week if they could not refresh it)
       300 ; minimum ttl for negative answers (5 minutes)
)

$ORIGIN dyn.example.com.
NS      dns-server.example.com.

Edit /etc/bind/named.conf.local

Edit /etc/bind/named.conf.local to add :

// DDNS keys
include "/etc/bind/dyn.example.com.keys";

// Dynamic zone
zone "dyn.example.com" {
    type master;
    file "/var/cache/bind/dyn.example.com";
    update-policy {
        // allow host to update themselves with a key having their own name
        grant *.dyn.example.com self dyn.example.com.;
    };
};

Reload server config

rndc reload && sleep 3 && grep named /var/log/daemon.log | tail -20

(adjust the sleep and tail values depending on the number of zones your DNS server handles, so that it has time to report any problems)

Test

If you created individual key files, or your $zone.keys file contains only a single key, you can test like this:

host=myhost; ip=10.11.12.13; zone=dyn.example.com; server=dns-server.example.com; keyfile=$etcdir/key.$host.$zone
echo -e "server $server\n zone $zone.\n update delete $host.$zone.\n update add $host.$zone. 600 A $ip\n send" | nsupdate -k "$keyfile"

Or, more readable and with an extra TXT record:

cat <<EOF | nsupdate -k $keyfile
server $server
zone $zone.
update delete $host.$zone.
update add $host.$zone. 600 A $ip
update add $host.$zone. 600 TXT "Updated on $(date)"
send
EOF

(If you get a could not read key from $keyfile: file not found error, and the file actually exists and is owned by the bind process user, you may be using an older version of nsupdate (like the version in Debian Etch). In that case, replace nsupdate -k $keyfile with nsupdate -y "$key_name:$secret" using the key name and secret found in your key file.)

Check the result:

host -t ANY $host.$zone

It should output something like

myhost.dyn.example.com descriptive text "Update on Tue Jan  1 17:16:03 CET 2013"
myhost.dyn.example.com has address 10.11.12.13
If you try to use a file with multiple keys in the -k option to nsupdate, you will get an error like this:

... 'key' redefined near 'key'
could not read key from FILENAME.keys.{private,key}: already exists

Usage

In a /etc/network/if-up.d/ddnsupdate script.

If you have setup an update CGI page on your server, you could use something like this, letting the web server use the IP address it received anyway with your request.

#!/bin/sh

server=dns-server.example.com
host=myhost
secret="xBa2pz6ZCGQJ5obmvmp26w==" # copy the right key from $etcdir/$zone.keys

wget -O /dev/null --no-check-certificate "https://$server/ddns/update.cgi?host=$host;secret=$secret"
Otherwise, you can use nsupdate, but you need to determine your external IP first :

#!/bin/sh

server=dns-server.example.com
zone=dyn.example.com
host=myhost
secret="xBa2pz6ZCGQJ5obmvmp26w==" # copy the right key from $etcdir/$zone.keys

ip=$(wget -q -O - http://example.com/myip.cgi)

cat <<EOF | nsupdate
server $server
zone $zone.
key $host.$zone $secret
update delete $host.$zone.
update add $host.$zone. 600 A $ip
update add $host.$zone. 600 TXT "Updated on $(date)"
send
EOF

I used a very simple myip.cgi script on the web server, to avoid having to parse the output of the various existing services which show your IP in the browser:

#!/bin/sh
echo "Content-type: text/plain"
echo ""
echo $REMOTE_ADDR

This alternative script example uses SNMP to get the WAN IP from the cable router. It only does the update if the address has changed, and logs to syslog.

#!/bin/sh

zone=dyn.example.com
host=myname
secret="nBlw18hxipEyMEVUmwluQx=="
router=192.168.81.3

server=$(dig +short -t SOA $zone | awk '{print $1}')

ip=$( snmpwalk -v1 -m RFC1213-MIB -c public $router ipAdEntAddr | awk '!'"/$router/ {print \$4}" )

if [ -z "$ip" ]; then
 echo "Error getting wan ip from $router" 1>&2
 exit 1
fi

oldip=$(dig +short $host.$zone)

if [ "$ip" == "$oldip" ]; then
 logger -t `basename $0` "No IP change for $host.$zone ($ip)"
 exit
fi

cat <<EOF | nsupdate
server $server
zone $zone.
key $host.$zone $secret
update delete $host.$zone.
update add $host.$zone. 600 A $ip
update add $host.$zone. 600 TXT "Updated on $(date)"
send
EOF

logger -t `basename $0` "IP for $host.$zone changed from $oldip to $ip"

Web server update.cgi

An example update.cgi :

#!/usr/bin/perl

## Use nsupdate to update a DDNS zone.

## (This could be done with the Net::DNS module. It
##  would be more portable (Windows, etc.), but also
##  more complicated. So I chose the nsupdate utility
##  that comes with Bind instead.)

# "mi\x40alma.ch", 2013

use strict;

my $VERSION = 0.2;
my $debug = 1;

my $title = "DDNS update";

my $zone     = "dyn.example.com";
my $server   = "localhost";
my $nsupdate = "/usr/bin/nsupdate";


use CGI qw(:standard);

my $q = new CGI;

my $CR = "\r\n";

print $q->header(),
      $q->start_html(-title => $title),
      $q->h1($title);


if (param("debug")) {
    $debug = 1;
};

my $host   = param("host");
my $secret = param("secret");
my $ip     = param("ip") || $ENV{"REMOTE_ADDR"};
my $time   = localtime(time);

foreach ($host, $secret, $ip) {
    s/[^A-Za-z0-9\.\/\+=]//g; # sanitize, just in case...
    unless (length($_)) {
        die "Missing or bad parameters. host='$host', secret='$secret', ip='$ip'\n";
    }
}

my $commands = qq{
server $server
zone $zone.
key $host.$zone $secret
update delete $host.$zone.
update add $host.$zone. 600 A $ip
update add $host.$zone. 600 TXT "Updated by $0 v. $VERSION, $time"
send
};

print $q->p("sending update commands to $nsupdate:"), $CR,
      $q->pre($commands), $CR;

open( NSUPDATE, "| $nsupdate" ) or die "Cannot open pipe to $nsupdate : $!\n";
print NSUPDATE $commands        or die "Error writing to $nsupdate : $!\n";
close NSUPDATE                  or die "Error closing $nsupdate : $!\n";

print $q->p("Done:"), $CR;

my @result = `host -t ANY $host.$zone`;

foreach (@result) {
    print $q->pre($_), $CR;
}


if ($debug) {
# also log received parameters
    my @lines;
    for my $key (param) {
        my @values = param($key);
        push @lines, "$key=" . join(", ", @values);
    }
    warn join("; ", @lines), "\n";
}

print $q->end_html, $CR;

__END__

Labels: , , , , , , , ,

Wednesday, February 15, 2012

WPKG client in Windows 7

Wpkg is a fantastic tool to manage software installs on groups of Windows machines without a Windows server with Active Directory. However, I had a few problems with it in Windows 7. These were solved by replacing the Wpkg Client with Wpkg-GP.

By default, the Wpkg service runs at startup and does it's installs in the background. But very often, it failed for some reason to get a connection to the network share at the right time when the service was starting, and aborted. The log showed

WNetAddConnection2-> The network location can not be reached.

I tried to add dependencies to the service, but didn't really find a reliable solution.

So in services.msc, I changed the service startup to "Automatic (delayed)". That solved the connection problem, but brought another. If I want to upgrade Thunderbird for example, the installer has a taskkill command to close Thunderbird before upgrading it. But with a delayed start, the user probably has already started Thunderbird, and it seems quite inappropriate to just kill it while it may actually be in use.

In Windows XP, it was possible to delay the login window, so that wpkg could have done it's thing before the user logged in, but for some reason, this doesn't work in Windows 7 anymore.

So the next step was to change the configuration in settings.xml to have wpkg run at shutdown instead. This also failed because, as far as I understand, Windows Vista/7 don't allow a process to prevent shutdown for more than 5 seconds.

Finally, the solution was to remove the standard Wpkg Client, and replace it with Wpkg-GP. That seems to work. I changed the wpkg configuration back to running at startup, and added a wpkg-gp package which also takes care of uninstalling the original wpkg client:

<package id="wpkg-gp" name="Wpkg-GP" revision="%version%">

    <variable name="version" value="0.15" />

    <check type="uninstall" condition="versiongreaterorequal" path="Wpkg-GP %version% .*" value="%version%"/>

    <install cmd="%SOFTWARE%\wpkg-gp\Wpkg-GP-0.15_x64.exe /S /INI %SOFTWARE%\wpkg-gp\Wpkg-GP.ini">
        <exit code="3010" reboot="delayed" />
    </install>
    <install cmd='msiexec /x "%SOFTWARE%\wpkg\WPKG Client 1.3.14-x64.msi" /qn /norestart' />

    <upgrade cmd="%SOFTWARE%\wpkg-gp\Wpkg-GP-0.15_x64.exe /S /INI %SOFTWARE%\wpkg-gp\Wpkg-GP.ini">
        <exit code="3010" reboot="delayed" />
    </upgrade>
</package>
 

Labels: , , , , ,

Sunday, February 12, 2012

NAT over OpenVPN tunnel

Quick NAT to use an existing VPN tunnel in Linux for an additional machine (Windows XP) on your LAN.

My Ubuntu notebook uses OpenVPN to access some other networks. It is also a host to various virtual machines. I wanted a Windows XP virtual machine to access resources on the remote network through my VPN tunnel.

The virtual machine uses "bridged" networking, so it has a separate IP on the LAN. So I guess the following would also work on a physically separate machine.

On the Linux VPN tunnel host:

  • Declare variables for the network interfaces. $lan is your normal network adapter, $wan is the VPN tunnel virtual adapter. 
  • Reset iptables
  • Enable forwarding
  • Configure iptables to provide NAT masquerading
lan=wlan5; wan=tun0
iptables --flush
iptables --table nat --flush
##not needed?:# iptables --delete-chain
##not needed?:# iptables --table nat --delete-chain
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o $wan -j MASQUERADE
iptables -A FORWARD -i $lan -j ACCEPT

(This is a minimal setup, without any security! Don't use this on a host visible to the Internet!)

On the Windows XP machine:

  • Declare IP of your Linux VPN host, and name of your interface (can be seen with the ipconfig command)
  • Set the gateway and DNS to the Linux host
SET HOST=192.168.1.44
SET IFNAME=Local Area Connection 2
route change 0.0.0.0 mask 0.0.0.0 %HOST%
netsh interface ip set dns name="%IFNAME%" static %HOST%

 

Labels: , , , , ,

Tuesday, July 26, 2011

Importing root certificates into Firefox and Thunderbird

Update Feb. 2012: see at the end for an alternative for new profiles.

This is ridiculously complicated and makes me wonder whether I should just drop Firefox in Windows and go back to IE.

The problem:

How to automatically pre-import your self-signed certification authority into all user profiles for Firefox and Thunderbird.

The solution:

You need the Mozilla certutil utility (not the Microsoft certutil.exe).

In Windows, you would need to compile nss tools or use some ancient hard to find Windows binary to get it. But all my user profiles are on a Samba server, so it was much easier to do it on the server, with the added benefit of having Bash and not needing to struggle with the horrible cmd.exe.

First install the tools. In Debian, it would be:

apt-get install libnss3-tools

Then adapt this long command to your paths:

find /path/to/users-profiles -name cert8.db -printf "%h\n" | \
while read dir; do \
  certutil -A -n "My Own CA" -t "C,C,C" -d "$dir" -i "/path/to/my_own_cacert.cer"; \
done

(-printf "%h\n" prints just the directory, without the file name, one per line. That is fed to the $dir variable needed in the certutil command. The -n option is a required nickname for the certificate. -t "C,C,C" is what will make you accept any certificate signed by this CA you are importing).

See also: the certutil documentation, and a better explanation of the trust arguments (-t option).

Alternative:

The above solution works to add a certifcate to an existing profile's cert8.db. To have newly created profiles include the certificate, you need to put a good cert8.db file into the Program's directory.

  1. Either import your certificate(s) manually into an existing profile, or use the steps above to add the certificate(s) to a cert8.db file.
  2. Copy the new cert8.db to the Firefox (or Thunderbird) program directory, into a "/defaults/profile" subdirectory. (ie. "C:\Program Files (x86)\Mozilla Firefox\defaults\profile\").

This way, newly created profiles will copy this cert8.db file instead of creating a new one from scratch.

Labels: , , , , , , , , , , , ,

Tuesday, June 21, 2011

Command-line partitioning and formatting

Automatic non-interactive formatting in Linux is possible with parted.
The following creates 1 single ext3 partition on an entire disk. Of course, if you assign the wrong disk to the $disk variable, it will be a bad day...
# Select the disk device and chose a label for the partition
# ptype=msdos for disks up to 2 TB. ptype=gpt for disks over 2 TB
disk=/dev/sdx; label=my_part_label; ptype=msdos
I have added sleep commands, so I can just copy/paste the whole thing and still have a chance to Ctrl-C if I change my mind at the last second.

# print the current partition(s) state
parted $disk print ; sleep 10

# create a gpt ot msdos partition table (depending on $ptype variable defined above)
parted -a optimal $disk mklabel $ptype ; sleep 5

# create the partition, starting at 1MB which may be better
# with newer disks
parted -a optimal -- $disk unit compact mkpart primary ext3 "1" "-1" ; sleep 5

# format it
mke2fs -j -v -L "$label" ${disk}1 && echo "OK. That's it"

Update: see also scripting disk partitionning in Linux - take 2 for another way, using sgdisk instead of parted.

Labels: , , , , , , ,

Tuesday, June 07, 2011

Windows installers options for silent installs

Different installers use different command-line options for silent or unattended installs. Since I had started these notes, I have found a good overview on unattended.sourceforge.net.

Inno Setup

can be identified with the "Inno Setup" string appearing in various places in the installer's .exe. The options are described here. The most useful ones are:
  • /SAVEINF="filename"
    Save installation settings to the specified file.
  • /LOADINF="filename"
    Load the settings from the specified file after having checked the command line.
  • /SP-
    Disables the This will install... Do you wish to continue? prompt at the beginning of Setup.
  • /SILENT, /VERYSILENT
    When Setup is silent the wizard and the background window are not displayed but the installation progress window is. When a setup is very silent this installation progress window is not displayed. Everything else is normal so for example error messages during installation are displayed.
  • /DIR="x:\dirname"
  • /LANG=language
    Specifies the language to use. language specifies the internal name of the language as specified in a [Languages] section entry.

Nullsoft's NSIS

can be identified with the "NSIS" string appearing in various places in the installer's .exe. The options are described here, but there seem to be only 2 useful ones:
  • /S
    Silent installation
  • /D=C:\Bla
    Set output folder

Labels: , , , , , ,

Saturday, May 28, 2011

Kill the Final Cut registration screen

I came across this nicely detailed post explaining how to get rid of the forced registration screen of Final Cut Pro/Studio, which always pops up when you really don't want to be bothered with this idiocy.

But I felt the solution was worse than the problem. It involved far too much clicking around for my taste. And you need the Property List Editor. You only have that once you have installed over 1 GB (!!) of developer tools. If you can remember where you put your OS X disk, that is.

Surely, there must be a better way to do it, by just copying a command from some web page and pasting it into Terminal?

It turned out to be 3 commands. And getting them right was much worse than the solution I din't like. You need your machine ID, which is in an XML file that defaults read doesn't want to read. And in that file it is encoded in Base64. You need to put this ID into a property list file as data. That can be done with defaults write, but the data needs to be in hex. I should just have registered, I guess...

Anyway, the detailed explanations are in the link of the first sentence, and the 3 ridiculous commands to paste into Terminal are here:

id=$(perl -MMIME::Base64 -ne '/^\s+(\S{64})\s*$/ && print unpack("H*",decode_base64($1));' "/Library/Application Support/ProApps/Final Cut Studio System ID"|tail -1)
sudo defaults write /Library/Preferences/com.apple.RegFinalCutStudio "{ AECoreTechRegister=1; AECoreTechRegSent=1; }"
sudo defaults write /Library/Preferences/com.apple.RegFinalCutStudio AECoreTechRegInfo -data "$id"

Labels: , , , , ,

Saturday, November 20, 2010

Moving IMAP Maildir to another user

A little recipe to move a user's IMAP mails to another user. (Tested on the Courier IMAP server on Debian).

Useful in situations like John leaving the company and Bob needing to have access to John's old emails.

olduser=john; newuser=bob
maildirmake -f $olduser /home/$newuser/Maildir/
cd /home/$olduser/Maildir/
for d in * ; do \
    cp -pr "$d" "/home/$newuser/Maildir/.$olduser/"; \
done
echo "INBOX.$olduser"  >>/home/$newuser/Maildir/courierimapsubscribed
for d in .??*; do \
    cp -pr "$d" "/home/$newuser/Maildir/.$olduser$d"; \
    echo "INBOX.$olduser$d" >>/home/$newuser/Maildir/courierimapsubscribed; \
done
chown -R $newuser /home/$newuser/Maildir

(Beware that if John had a folder with a one-letter name, that one will not be copied. It's because "for d in .*" would do a mess trying to copy "." and "..". So line 6 uses "for d in .??*" instead.)

Labels: , , , , , , ,

Friday, January 01, 2010

Open mbox file in Thunderbird

Unfortunately, there seems to be no straightforward way to ask Thunderbird to open or import an Mbox mail file directly.

Say you have an mbox file, and would like to view it in Thunderbird. For this example, we will view the file in a "temp-mbox" folder under Thunderbird's "Local Folders". The convoluted way which seems to work goes like this:

  • In Thunderbird, under Local Folders, create the new "temp-mbox" folder.
  • Exit Thunderbird.
  • Find your "Local Folders" directory in your profile. It may be something like "~/.thunderbird/random-string.default/Mail/Local Folders/". In there, you will find a temp-mbox and a temp-mbox.msf file.
  • Overwrite temp-mbox with your mbox file,
  • and delete the temp-mbox.msf index file.
  • Re-open Thunderbird
I needed to do this, because of another limitation of Thunderbird: it's poor search capabilities. Since the mails I wanted to group are on my own IMAP server, I did the search there, and put all the mails into a single file. What I wanted is all the last year's emails received from or sent to somedomain. The following got me a suitable mbox file:
mbox=somedomain-2009.mbox; search=@somedomain; \
find ~/Maildir/cur ~/Maildir/.Sent/cur -mtime -365 | \
while read f ; do \
if egrep "^(From|To|Cc):.*$search" "$f"; then \
  echo "From - " >>$mbox; \
  cat "$f" >>$mbox; \
fi; \
done
To achieve this using the TB search, I would have needed to:
  • Search Inbox without subfolders for "From contains @somedomain" or "To contains @somedomain" or "Cc contains @somedomain". This also searches previous years, and takes quite a while on my IMAP folder.
  • Save the search
  • Search Sent for "To contains @somedomain" or "Cc contains @somedomain".
  • Save the search
  • Create a folder for results
  • Open the first saved search folders, sort by date, and copy the 2009 mails to the new results folder
  • Repeat with the second saved search.

Labels: , , , , ,

Friday, January 23, 2009

Encoding video sizes

Video compression usually works on square blocks of pixels. These can have sizes of 8x8 or 16x16 or other powers of 2. H264 (AVC) for example, uses macroblocks of 16x16.

So when compressing video, it helps if the frame size is such that both width and height are evenly divisible by 16 or at least by 8. This is why videos encoded for the web or for portable video players are often not exactly in a 16/9 aspect ratio. The PSP's screen, for example, is 480 x 272 even though true 16/9 would require 480 x 270. But 270 is not divisible by 16 whereas 272 is. Youtube uses 640x360, which is true 16/9 and divisible by 8. If you use other sizes, FFmpeg will print a message like

width or height not divisible by 16 (480x270), compression will suffer.

So what are the sizes which are both the right aspect ratio and nicely divisible by 16 or by 8? This little Perl script will let us know:

#!/usr/bin/perl

my $aspect_width  = 16;
my $aspect_height = 9;
my $max_height = 1200;
my @dividers = (16, 8);

for my $divider (@dividers) {

print "$aspect_width/$aspect_height with both ",
     "width and height divisible by $divider :\n\n";

# try sizes up to Full HD
for my $i (1..$max_height/$aspect_height) {
   my $h = $aspect_height * $i;
   unless ($h % $divider) {
       my $w = $aspect_width * $i;
       printf "$aspect_width/$aspect_height divisible by %2d : %4d x %4d\n",
              $divider, $w, $h;
   }
}

print "\n";
}

For 16/9, this gives, among others, numbers like

16/9 divisible by 16 :  256 x  144
16/9 divisible by 16 :  512 x  288
16/9 divisible by 16 :  768 x  432
16/9 divisible by 16 : 1024 x  576
16/9 divisible by 16 : 1280 x  720

For sizes divisible by 8, you obviously have all of the above, plus (among others):

16/9 divisible by  8 :  384 x  216
16/9 divisible by  8 :  640 x  360
16/9 divisible by  8 :  896 x  504
16/9 divisible by  8 : 1920 x 1080
16/9 divisible by  8 : 2048 x 1152

Labels: , , , , ,

Tuesday, May 06, 2008

CSS and javascript progress meter

For a little video project, my daughter wanted a computer screen displaying a fake progress bar. I haven't played with GUI programming languages for ages (the last time must have been with Delphi Pascal some 15 years ago). So I thought this should certainly be possible to do in a web browser with some CSS and javascript, and would also be an opportunity to learn some javascript and DOM interaction basics.

I first found a few examples on the web, but they all used animated GIFs. I didn't want to bother with creating animated gifs, and besides, I wanted to be able to style the progress window using only CSS, so that sizes and colors could be changed quickly enough for my impatient daughter.

So this is a solution using only CSS and javascript, and no image at all. Have a look at the source code of this CSS and javascript progress bar demo.

Both the CSS and the javascript are embedded in the HTML file.

(Tested in Firefox 2.0.0.14, Opera 9.01, Safari 3.1.1, MSIE 5.01 and 6.0. All in WinXP SP2).

Labels: , , , , ,

Wednesday, January 16, 2008

Simple password management

To easily manage all your passwords, you don't need any freeware/shareware/crapware/malware/whateverware. If you are running Windows, all you need is 2 batch files, each containing a single line.

As a bonus, you can get some very simple security-through-obscurity by using a little known feature of the NTFS file system called "Alternate Data Streams". The security is not great, but the obscurity feels like a cool hack. And it's still better than having passwords.txt on your desktop, or Post-its on your monitor. (Of course, you can also skip the coolness and combine these handy batch files with the excellent TrueCrypt for really strong encryption at the expense of a minimum of additional hassle).

  1. Create a file containing anything (or nothing). Let's call it x, and put it in our profile folder (C:\Documents adn Settings\username\)
  2. Create a batch file (let's call it password-add.bat) with one line:
    @ECHO %* >> "%USERPROFILE%\x:passwords"
  3. Create a second batch file (for example password.bat) also with one line :
    @FIND /I "%1" < "%USERPROFILE%\x:passwords"
  4. Copy these two files to some directory in your path (like C:\Windows or C:\Windows\System32)
To add your new Google user name and password, open a Command Prompt window, and type:

password-add "Google: mystupidname@gmail.com pass: ul7ra-secr37"

To retrieve that password once you have forgotten it, type anything like

password Google
or
password stupid
or
password @gmail.com
etc.

To add some obscurity, call the batch files something else (and shorter so you don't have to type so much): like newp.bat and p.bat.

To add even more obscurity, copy some small .dll file in c:\Windows\System32 to a new name like msp32.dll, and in the batch files replace "%USERPROFILE%\x:passwords" with "c:\Windows\System32\msp32.dll".

To add real security, get TrueCrypt, and put the file on a TrueCrypt volume. (Don't forget to correct the 2 batch files).

Important: This only works on NTFS partitions. If you move your file to a FAT32 partition or send it by email or FTP, all your passwords are lost forever. If your backups are done to an external FAT32 disk, you won't have a backup either. You can move the file around as much as want, providing that it always stays on NTFS partitions. If you copy over a network, the server also needs to be Windows (not Samba).

Labels: , , , , , , ,

Tuesday, July 04, 2006

Liens email dans les pages web

J'ai eu un peu de mal à retrouver ce lien, alors je suppose que ça vaut la peine de l'inclure dans un petit article de blog. (Finalement, mon attention a été détournée par de nombreux à-côtés; les liens pertinents sont dans les tout derniers paragraphes).

Bien qu'on puisse considérer que la lutte anti-spam s'apparente à un ré-arrangement des transats sur le pont du Titanic, il faut quand même encore mettre des liens email dans des pages web. Là, bien sûr, ils sont immédiatement collectés pour servir à nous inonder de "pourriels", comme disent les français.

Comment avoir des liens "mailto:" cliquables, sans que tous les robots de spam en profitent? Les trucs simples comme "user [at] example [dot] com", ou les plus compréhensibles mais très peu pratiques images montrant le texte de l'adresse ne sont pas cliquables. N'essayez même pas les vieux trucs avec les entités HTML (&amp;#64; pour "@" etc.); ça fait longtemps que ça ne marche plus. Il n'existe plus guère de robot qui s'y prenne les pieds.

Ce qui marche bien est le javascript, que les robots collecteurs d'adresse ne décodent pas; du moins pas encore, à ma connaissance. Mais la plupart de ces astuces javascript qu'on trouve sur le web utilisent un chiffrage antique (littéralement), et même pas sa version ROT13, mais un décalage de 1. Etant donné qu'une simple expression Perl comme s/(.)/chr(ord($1)-1)/eg; permet de le décoder, je doute que ça résiste longtemps. Tiens pendant que j'y suis, voilà un script Perl complet qui suffit pour extraire d'une page web les emails cryptés de cette façon (quel que soit le décalage utilisé):
use LWP::Simple;
my $url = shift or die "Usage: $0 URL\n";
my $html = get($url);

while ( $html =~ /href=['"]javascript:.*?\(['"](.*?)['"]\)/g ) {
my $crypted = $1;
foreach my $i (-25..25) {
my $try = $crypted;
$try =~ s/(.)/chr(ord($1)+$i)/eg;
if ($try =~ /@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}/) {
print "$try (i=$i)\n";
}
}
}

Bref, je cherchais un peu mieux que ça.

Un certain Jim à qui on essayait de vendre un de ces scripts minables a poussé la solution javascript usuelle (autre exemple ici) à un niveau de cryptage un peu plus actuel. Avec Email Protector, il fournit une page où on peut crypter son adresse email. Il n'y a plus qu'à copier le code fourni aux bons endroits, après avoir séparé du code le lien mailto: lui-même et les fonctions javascript aux noms "obfusqués" et pittoresques. Certains trouveront cette solution exagérée, mais justement, je trouve aussi un certain charme là où d'autres ne verront que du "over-engineered".

Update: Finalement, j'utilise ça régulièrement, et mes suis même adapté le cryptage des adresses en un petit script Perl (disponible ici en .pl et en .bat), pour pouvoir l'utiliser directement à la ligne de commande et obtenir la partie à coller dans le HTML:

#mailcrypt 11 31 user@example.com
<!-- crypted email link -->
<a href="javascript:var N=341,D=43;bid('127 300 95 104 4 95 263 202 318 7 147 95 271 99 133 318',N,D)"
title="Click to send email"
class="mail"<Email</a>

Au préalable, il faut évidemment ajouter
<script type="text/javascript" src="/js/emailProtector.js"></script> dans la page, et sauver l'excellent script de Jim Tucek.

Labels: , , , , , ,

Monday, April 03, 2006

The Access ODBC PostgreSQL boolean mess

I like using PostgreSQL with an MS Access frontend for various little database applications, but always had trouble with boolean fields. Finally, after all sorts of tests, I think I nailed down what is needed to make it work, and avoid all these errors like "operator does not exist: boolean = integer", "Data Type mismatch in criteria expression", "invalid input syntax for type boolean: "-" (#7)", and another one which I can't remember. What we want is simple:
  • Booleans should be usable in Access queries with simple statements like SELECT x FROM y WHERE z; (and no silly stuff like where z=true, z='t', z=-1 or the insane where cbool(z)=true)
  • Booleans should appear as check boxes in Access forms and tables
I still don't quite understand why this is not hanlded transparently by the ODBC driver, but anyway here is what seems to be needed with PostgreSQL 7.4:
  • In PostgreSQL, make sure your boolean fields have a default value. Access cannot handle NULLs in booleans.
  • Configure the ODBC driver for "Bools as Char": no, "True is -1": yes.
  • Create these functions and operators in your database and/or in template1 for new databases:
    CREATE FUNCTION plpgsql_call_handler() RETURNS language_handler
    AS '$libdir/plpgsql', 'plpgsql_call_handler'
    LANGUAGE c;
    
    CREATE TRUSTED PROCEDURAL LANGUAGE plpgsql HANDLER plpgsql_call_handler;
    
    CREATE FUNCTION inttobool(integer, boolean) RETURNS boolean
    AS '
    begin
      if $1=0 and not $2 then
      return true;
      elsif $1<>0 and $2 then
      return true;
      else
              return false;
      end if;
    end;
    '
    LANGUAGE plpgsql;
    
    CREATE FUNCTION inttobool(boolean, integer) RETURNS boolean
    AS '
    begin
      return inttobool($2, $1);
    end;
    '
    LANGUAGE plpgsql;
    
    CREATE FUNCTION notinttobool(boolean, integer) RETURNS boolean
    AS '
    begin
      return not inttobool($2,$1);
    end;
    '
    LANGUAGE plpgsql;
    
    CREATE FUNCTION notinttobool(integer, boolean) RETURNS boolean
    AS '
    begin
    return not inttobool($1,$2);
    end;
    '
    LANGUAGE plpgsql;
    
    CREATE OPERATOR = (
    PROCEDURE = inttobool,
    LEFTARG = boolean,
    RIGHTARG = integer,
    COMMUTATOR = =,
    NEGATOR = <>
    );
    
    CREATE OPERATOR <> (
    PROCEDURE = notinttobool,
    LEFTARG = integer,
    RIGHTARG = boolean,
    COMMUTATOR = <>,
    NEGATOR = =
    );
    
    CREATE OPERATOR = (
    PROCEDURE = inttobool,
    LEFTARG = integer,
    RIGHTARG = boolean,
    COMMUTATOR = =,
    NEGATOR = <>
    );
    
    CREATE OPERATOR <> (
    PROCEDURE = notinttobool,
    LEFTARG = boolean,
    RIGHTARG = integer,
    COMMUTATOR = <>,
    NEGATOR = =
    );
    After linking the tables in Access, if you want check boxes in table view, you need to go into table design mode. It will show an error because it's a linked table; just ignore it. Select your boolean field(s) and set their Lookup -> Display Control to Check Box.
Update: See also this page.

Labels: , , , , ,

The Access ODBC PostgreSQL boolean mess

I like using PostgreSQL with an MS Access frontend for various little database applications, but always had trouble with boolean fields. Finally, after all sorts of tests, I think I nailed down what is needed to make it work, and avoid all these errors like "operator does not exist: boolean = integer", "Data Type mismatch in criteria expression", "invalid input syntax for type boolean: "-" (#7)", and another one which I can't remember. What we want is simple:
  • Booleans should be usable in Access queries with simple statements like SELECT x FROM y WHERE z; (and no silly stuff like where z=true, z='t', z=-1 or the insane where cbool(z)=true)
  • Booleans should appear as check boxes in Access forms and tables
I still don't quite understand why this is not hanlded transparently by the ODBC driver, but anyway here is what seems to be needed with PostgreSQL 7.4:
  • In PostgreSQL, make sure your boolean fields have a default value. Access cannot handle NULLs in booleans.
  • Configure the ODBC driver for "Bools as Char": no, "True is -1": yes.
  • Create these functions and operators in your database and/or in template1 for new databases:
    CREATE FUNCTION plpgsql_call_handler() RETURNS language_handler
    AS '$libdir/plpgsql', 'plpgsql_call_handler'
    LANGUAGE c;
    
    CREATE TRUSTED PROCEDURAL LANGUAGE plpgsql HANDLER plpgsql_call_handler;
    
    CREATE FUNCTION inttobool(integer, boolean) RETURNS boolean
    AS '
    begin
      if $1=0 and not $2 then
      return true;
      elsif $1<>0 and $2 then
      return true;
      else
              return false;
      end if;
    end;
    '
    LANGUAGE plpgsql;
    
    CREATE FUNCTION inttobool(boolean, integer) RETURNS boolean
    AS '
    begin
      return inttobool($2, $1);
    end;
    '
    LANGUAGE plpgsql;
    
    CREATE FUNCTION notinttobool(boolean, integer) RETURNS boolean
    AS '
    begin
      return not inttobool($2,$1);
    end;
    '
    LANGUAGE plpgsql;
    
    CREATE FUNCTION notinttobool(integer, boolean) RETURNS boolean
    AS '
    begin
    return not inttobool($1,$2);
    end;
    '
    LANGUAGE plpgsql;
    
    CREATE OPERATOR = (
    PROCEDURE = inttobool,
    LEFTARG = boolean,
    RIGHTARG = integer,
    COMMUTATOR = =,
    NEGATOR = <>
    );
    
    CREATE OPERATOR <> (
    PROCEDURE = notinttobool,
    LEFTARG = integer,
    RIGHTARG = boolean,
    COMMUTATOR = <>,
    NEGATOR = =
    );
    
    CREATE OPERATOR = (
    PROCEDURE = inttobool,
    LEFTARG = integer,
    RIGHTARG = boolean,
    COMMUTATOR = =,
    NEGATOR = <>
    );
    
    CREATE OPERATOR <> (
    PROCEDURE = notinttobool,
    LEFTARG = boolean,
    RIGHTARG = integer,
    COMMUTATOR = <>,
    NEGATOR = =
    );
    After linking the tables in Access, if you want check boxes in table view, you need to go into table design mode. It will show an error because it's a linked table; just ignore it. Select your boolean field(s) and set their Lookup -> Display Control to Check Box.
Update: See also this page.

Labels: , , , , ,

Thursday, March 30, 2006

Simple multi-language web site

There are many ways to direct users of a multi lingual web site to the pages
in their language. But the quick search I made didn't return solutions which
I liked, so I did my own. I'm sure the same method has been used many times,
but it's sometimes faster to re-invent a little wheel than to go through zillions
of Google search results.

I had to work on a small site with about 20 pages, in which every language
is in it's own subdirectory (www.example.com/fr/, www.example.com/en/, etc.)
and the file names remain the same.

This simple structure made it pretty easy, and the solution makes it trivial
to add another language.

(For a lot of background about languages and a different solution involving the standard "content language negotiation", see Dan's Web Tips: Languages).

The way I choose works like this:

  • One line in every page calls a PHP script to display the language menu.
  • A cookie holds the visitor's preferred language.
  • Javascript updates the cookie when the visitor switches languages.
  • The main index page redirects visitors to the correct language, using:
    1. either the cookie set in a previous visit,
    2. or the browsers preferred language,
    3. or a pre-configured default language.
  • Finally, a few CSS rules take care of the menu's look

The redirector script

I used a little Perl script on www.example.com/index.cgi. It first looks for
a cookie with the user's preferred language. If there is no cookie (usually
because this is a first-time visit), it uses the HTTP_ACCEPT_LANGUAGE
header sent by the browser to find the best guess. If that doesn't work either,
it uses a default language. Finally, it redirects the user to the selected language,
while also setting the cookie for the next visit.

I probably could have done the index page in PHP, but I didn't know PHP at
all when I started so it was easier for me in Perl. (If someone feels like sharing
a PHP translation, you are welcome to post it here as a comment).

#!/usr/bin/perl

# Redirect to correct language version of the site.
# Use cookie if present. Otherwise, parse the HTTP_ACCEPT_LANGUAGE header

# Author: Milivoj Ivkovic "mi\x40alma.ch"

use CGI qw(:cgi);
my $VERSION = 0.3;

my %available_languages = (
fr => 'fr/',
de => 'de/',
en => 'en/',
default => 'fr/',
);
my $wanted_language = 'default';

my $q = new CGI;

# try cookie
my $cookie_lang = $q->cookie("lang");
if ( exists $available_languages{$cookie_lang} ) {
$wanted_language = $cookie_lang;
}
else { # try HTTP_ACCEPT_LANGUAGE
# Get languages from HTTP_ACCEPT_LANGUAGE, (ignore local variants like en_US, en_GB, etc.).
# Don't bother removing duplicates
my @langs = map { substr($_, 0, 2) } split(/,/, $ENV{HTTP_ACCEPT_LANGUAGE});
foreach my $l (@langs) {
if (exists $available_languages{$l}) {
$wanted_language = $l;
last;
}
}
}

# if nothing worked, we still have the default in $wanted_language which was set at the beginning

# fix URL
my $url = $q->url(-path_info=>1);

# (some servers give us a url ending with 2 "/", so we need "/+" in the
# regex below instead of just "/".
# Otherwise we may end up with "http://example.com//en/" which doesn't look nice.)
$url =~ s|/+[^/]*$|/$available_languages{$wanted_language}|;

# redirect, and also set the cookie for next time
print $q->redirect ( -uri => $url,
-cookie => cookie( -name=>"lang",
-value=>$wanted_language,
-path=>"/",
-expires=>"+1y",
),
);

The javascript setLang function

Anytime the user switches the language, this small javascript function is called
to update a cookie, so the preference is saved between sessions.

function setLang(l) {
// Called from links which switch the language.
// Sets cookie with "lang=fr" or other language code, valid for 1 year, on all the site
var expire = new Date();
expire.setTime(expire.getTime() + 3600000*24*365);
document.cookie = 'lang='+l+";expires="+expire.toGMTString()+";path=/";
}

The language menu

Since all the pages happened to be in PHP, I did the language menu in PHP,
and added an include() to every page.

<?php include("../common/lang-selector.php") ?>

The PHP lang-selector.php script

This is the PHP script which displays the language menu.

<!-- php lang selector -->
<div id="lang-selector">
<?php
$langs = array(
"fr" => "Français",
"de" => "Deutsch",
"en" => "English",
);
$self=$_SERVER['PHP_SELF'];
$pattern = "{^/[^\/]+/}i";
$links = array();
foreach ($langs as $l => $lang) {
$is_current = ! ( strpos($self, "/$l/") === false );
$url = preg_replace($pattern, "/$l/", $self);
$link = "<a href=\"$url\" class=\"lang-"
. ($is_current ? "current\">" : "other\" onclick=\"setLang('$l')\">")
. " $lang </a> ";
array_push($links, $link);
}
echo " " . implode(" | ", $links) . "\n";
?>
</div>
<!-- end php lang selector -->

The finishing touch: CSS

The look of the language menu is done with CSS. This is what I used for that
particular site:

#lang-selector {
font-size: x-small;
text-align: left;
white-space: nowrap;
padding-bottom: 1em;
color: #CCCCCC;
}

a.lang-current,
a.lang-current:visited,
a.lang-current:hover
{
color: #CCCCCC;
text-decoration: underline;
}

a.lang-other,
a.lang-other:visited
{
color: #333333;
text-decoration: none;
}

a.lang-other:hover
{
text-decoration: underline;
background: #F7EBDB;
}

@media print {
#lang-selector {
display: none;
}
}

By the way, the site for which this was first set up is http://lesouffledudesert.com/.

When we added English to the site, the steps were:

  • Copy the /fr directory to a new /en directory
  • Add "en" => "English", to lang-selector.php
  • Add en => 'en/', to index.cgi
  • And of course, translate all pages, which was the real work and where
    this little multi-language system doesn't help...

Labels: , , , , ,