Switch from libsoup to libcurl, add login and my information support
[beifahrer] / src / adac-mitfahrclub.vala
1 /* This file is part of Beifahrer.
2  *
3  * Copyright (C) 2010 Philipp Zabel
4  *
5  * Beifahrer is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * Beifahrer is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Beifahrer. If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 [Compact]
20 public class City {
21         public int number;
22         public string name;
23         public double latitude;
24         public double longitude;
25         public double north;
26         public double south;
27         public double east;
28         public double west;
29
30         public City (int _number, string _name) {
31                 number = _number;
32                 name = _name;
33                 latitude = 0.0;
34                 longitude = 0.0;
35         }
36
37         internal double bb_area () {
38                 return (north - south) * (east - west);
39         }
40 }
41
42 public enum LiftFlags {
43         SMOKER = 1,
44         NON_SMOKER = 2,
45         ADAC_MEMBER = 4,
46         WOMEN_ONLY = 8
47 }
48
49 public class Lift : Object {
50         public string city_from;
51         public string city_to;
52         public Time time;
53         public int places;
54         public string price;
55         public LiftFlags flags;
56
57         public string href;
58
59         public List<string> city_via;
60         public string name;
61         public string cell;
62         public string phone;
63         public string phone2;
64         public string email;
65         public string email_image_uri;
66         public string description;
67         public string modified;
68
69         public Lift () {
70                 time.hour = -1;
71         }
72 }
73
74 public class MyInformation {
75         public enum Gender {
76                 MALE,
77                 FEMALE
78         }
79
80         public Gender gender;
81         public string title;
82         public string first_name;
83         public string last_name;
84         public Date birthday;
85         public Date registered_since;
86
87         // Address
88         public string street;
89         public string number;
90         public string zip_code;
91         public string city;
92         public string country;
93
94         // Contact
95         public string phone1;
96         public string phone2;
97         public string phone3;
98         public string cell;
99         public string email1;
100         public string email2;
101
102         public bool smoker;
103         public bool adac_member;
104 }
105
106 public class AdacMitfahrclub {
107         const string HTTP_BASE_URI = "http://mitfahrclub.adac.de";
108         const string HTTPS_BASE_URI = "https://mitfahrclub.adac.de";
109
110         Curl.EasyHandle curl;
111         List<City> city_list = null;
112
113         public AdacMitfahrclub () {
114                 curl = new Curl.EasyHandle ();
115                 // FIXME: Fremantle SDK doesn't come with certs
116                 curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
117         //      curl.setopt (Curl.Option.VERBOSE, 1);
118         }
119
120         private string _url = null;
121         private SourceFunc callback = null;
122         void* download_thread () {
123                 curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
124                 curl.setopt (Curl.Option.WRITEDATA, this);
125                 curl.setopt (Curl.Option.URL, _url);
126                 if (aeolus_cookie != null)
127                         curl.setopt (Curl.Option.COOKIE, "MIKINIMEDIA=%s; Quirinus[adacAeolus]=%s;".printf (mikini_cookie, aeolus_cookie));
128                 else if (mikini_cookie != null)
129                         curl.setopt (Curl.Option.COOKIE, "MIKINIMEDIA=%s".printf (mikini_cookie));
130                 var res = curl.perform ();
131
132                 if (callback != null)
133                         Idle.add (callback);
134                 callback = null;
135
136                 return null;
137         }
138
139         StringBuilder result;
140
141         [CCode (instance_pos = -1)]
142         size_t write_callback (void *buffer, size_t size, size_t nmemb) {
143         //      if (cancellable != null && cancellable.is_cancelled ())
144         //              return 0;
145
146                 result.append_len ((string) buffer, (ssize_t) (size * nmemb));
147
148                 return size * nmemb;
149         }
150
151         private async Html.Doc* get_html_document (string url) {
152                 _url = url;
153                 callback = get_html_document.callback;
154                 result = new StringBuilder ();
155                 try {
156                         Thread.create(download_thread, false);
157                 } catch (ThreadError e) {
158                         critical ("Failed to create download thread\n");
159                         return null;
160                 }
161
162                 yield;
163
164                 return Html.Doc.read_memory ((char[]) result.str, (int) result.len,
165                                              url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING);
166         }
167
168         private string username;
169         private string password;
170         private string mikini_cookie;
171         private string aeolus_cookie;
172
173         public void set_cookie (string value) {
174                 aeolus_cookie = value;
175         }
176
177         public void login (string? _username, string? _password) {
178                 username = _username;
179                 password = _password;
180                 if (logged_in)
181                         return;
182                 if (login_callback != null)
183                         return;
184                 login_thread ();
185         }
186
187         public bool logged_in = false;
188         private SourceFunc login_callback = null;
189         public async bool login_async (string? _username, string? _password) {
190                 username = _username;
191                 password = _password;
192                 if (logged_in)
193                         return true;
194                 if (login_callback != null)
195                         return false;
196                 login_callback = login_async.callback;
197                 result = new StringBuilder ();
198                 try {
199                         Thread.create(login_thread, false);
200                 } catch (ThreadError e) {
201                         critical ("Failed to create login thread\n");
202                         return false;
203                 }
204
205                 yield;
206                 login_callback = null;
207
208                 return logged_in;
209         }
210
211         void *login_thread () {
212                 result = new StringBuilder ();
213                 curl.setopt (Curl.Option.URL, HTTP_BASE_URI);
214                 curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
215                 curl.setopt (Curl.Option.WRITEDATA, this);
216                 curl.setopt (Curl.Option.COOKIEFILE, "");
217                 var res = curl.perform ();
218
219                 Curl.SList cookies;
220                 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
221                 unowned Curl.SList cookie = cookies;
222                 while (cookie != null) {
223                         if (cookie.data != null) {
224                                 var c = cookie.data.split ("\t");
225                                 if (c.length > 5)
226                                         print ("%s=%s\n", c[5], c[6]);
227                                         if (c[5] == "MIKINIMEDIA")
228                                                 mikini_cookie = c[6];
229                         }
230                         cookie = cookie.next;
231                 }
232
233                 result = new StringBuilder ();
234                 string postdata = "data[User][continue]=/&data[User][js_allowed]=0&data[User][cookie_allowed]=1&data[User][username]=%s&data[User][password]=%s&data[User][remember_me]=1".printf (username, password);
235                 curl.setopt (Curl.Option.POSTFIELDS, postdata);
236                 curl.setopt (Curl.Option.URL, HTTPS_BASE_URI + "/users/login/");
237                 curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
238                 res = curl.perform ();
239         //      print ("%s\n", result.str);
240
241                 cookies = null;
242                 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
243                 cookie = cookies;
244                 while (cookie != null) {
245                         if (cookie.data != null) {
246                                 var c = cookie.data.split ("\t");
247                                 if (c.length > 5)
248                                         print ("%s=%s\n", c[5], c[6]);
249                                         // "Quirinus[adacAeolus]"
250                                         if (c[5] == "Quirinus[adacAeolus]") {
251                                                 aeolus_cookie = c[6];
252                                                 logged_in = true;
253                                         }
254                         }
255                         cookie = cookie.next;
256                 }
257
258                 if (result.str.contains ("<div id=\"flashMessage\" class=\"message\">Die eingegebenen Zugangsdaten konnten nicht gefunden werden. Bitte versuchen Sie es erneut.</div>")) {
259                         print ("LOGIN FAILED\n");
260                         aeolus_cookie = null;
261                         logged_in = false;
262                 }
263
264                 if (login_callback != null)
265                         Idle.add (login_callback);
266                 return null;
267         }
268
269         private void save_city_list () {
270                 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
271                 if (list_file == null)
272                         return;
273
274                 foreach (unowned City city in city_list) {
275                         if (city.north != 0.0 || city.south != 0.0 || city.east != 0.0 || city.west != 0.0)
276                                 list_file.printf ("%d\t%s\t%f\t%f\t%f\t%f\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude, city.north, city.south, city.east, city.west);
277                         else if (city.latitude != 0.0 || city.longitude != 0.0)
278                                 list_file.printf ("%d\t%s\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude);
279                         else
280                                 list_file.printf ("%d\t%s\n", city.number, city.name);
281                 }
282         }
283
284         private bool load_city_list () {
285                 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "r");
286                 if (list_file == null)
287                         list_file = FileStream.open ("/usr/share/beifahrer/city_list", "r");
288                 if (list_file == null)
289                         return false;
290
291                 city_list = new List<City> ();
292                 string line = list_file.read_line ();
293                 while (line != null) {
294                         var split_line = line.split ("\t");
295                         if (split_line.length < 2)
296                                 continue;
297                         int number = split_line[0].to_int ();
298                         weak string name = split_line[1];
299
300                         var city = new City (number, name);
301                         if (split_line.length >= 4) {
302                                 city.latitude = split_line[2].to_double ();
303                                 city.longitude = split_line[3].to_double ();
304                         }
305                         if (split_line.length >= 8) {
306                                 city.north = split_line[4].to_double ();
307                                 city.south = split_line[5].to_double ();
308                                 city.east = split_line[6].to_double ();
309                                 city.west = split_line[7].to_double ();
310                         }
311                         city_list.append ((owned) city);
312
313                         line = list_file.read_line ();
314                 }
315
316                 return true;
317         }
318
319         public unowned List<City>? get_city_list () {
320                 if (city_list != null)
321                         return city_list;
322
323                 if (load_city_list ())
324                         return city_list;
325
326                 return null;
327         }
328
329         public async unowned List<City>? download_city_list () {
330                 var doc = yield get_html_document (HTTP_BASE_URI);
331                 if (doc == null) {
332                         stderr.printf ("Error: parsing failed\n");
333                         return null;
334                 }
335
336                 var form = search_tag_by_id (doc->children, "form", "search_national_form");
337                 if (form == null) {
338                         stderr.printf ("Error: does not contain search_national_form\n");
339                         return null;
340                 }
341
342                 var select = search_tag_by_name (form->children, "select", "city_from");
343                 if (select == null) {
344                         stderr.printf ("Error: does not contain city_from\n");
345                         return null;
346                 }
347
348                 city_list = new List<City> ();
349                 for (var n = select->children; n != null; n = n->next) {
350                         if (n->name == "option" && n->children != null && n->children->name == "text") {
351                                 int number = n->get_prop ("value").to_int ();
352                                 // Skip 0 "Alle St.dte"
353                                 if (number == 0)
354                                         continue;
355                                 var city = new City(number,
356                                                     n->children->content);
357                                 city_list.append ((owned) city);
358                         }
359                 }
360
361                 // TODO: get coordinates
362
363                 save_city_list ();
364
365                 return city_list;
366         }
367
368         private int get_city_number (string name) {
369                 foreach (unowned City city in city_list) {
370                         if (city.name == name)
371                                 return city.number;
372                 }
373                 return 0;
374         }
375
376         public unowned City find_nearest_city (double latitude, double longitude) {
377                 unowned City result = null;
378                 double min_distance = 0.0;
379                 bool in_result = false;
380
381                 foreach (unowned City city in city_list) {
382                         double lat = latitude - city.latitude;
383                         double lng = longitude - city.longitude;
384                         double distance = lat * lat + lng * lng;
385                         bool in_city = ((city.south <= latitude <= city.north) &&
386                                         (city.west <= longitude <= city.east));
387
388                         if ((result == null) ||
389                             (in_city && !in_result) ||
390                             (in_city && in_result && distance / city.bb_area () < min_distance / result.bb_area ()) ||
391                             (!in_city && !in_result && distance < min_distance)) {
392                                 result = city;
393                                 min_distance = distance;
394                                 in_result = in_city;
395                         }
396                 }
397
398                 return result;
399         }
400
401         public async List<Lift>? get_lift_list (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
402                 if (city_list == null)
403                         get_city_list ();
404
405                 int num_from = get_city_number (city_from);
406                 if (num_from == 0) {
407                         stderr.printf ("Unknown city: %s\n", city_to);
408                         return null;
409                 }
410
411                 int num_to = get_city_number (city_to);
412                 if (num_to == 0) {
413                         stderr.printf ("Unknown city: %s\n", city_to);
414                         return null;
415                 }
416
417                 string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
418                         city_from,
419                         city_to
420                 );
421
422                 url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
423                         num_from,
424                         radius_from,
425                         num_to,
426                         radius_to
427                 );
428
429                 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
430                         date.get_day (),
431                         date.get_month (),
432                         date.get_year (),
433                         tolerance
434                 );
435
436                 var doc = yield get_html_document (url);
437                 if (doc == null) {
438                         stderr.printf ("Error: parsing failed\n");
439                         return null;
440                 }
441
442                 var table = search_tag_by_class (doc->children, "table", "list p_15");
443                 if (table == null) {
444                         stderr.printf ("Error: does not contain list p_15 table\n");
445                         return null;
446                 }
447
448                 var list = new List<Lift> ();
449                 for (var n = table->children; n != null; n = n->next) {
450                         if (n->name == "tr") {
451                                 var lift = parse_lift_row (n->children);
452                                 if (lift.city_from != null) // Skip the title row
453                                         list.append ((owned) lift);
454                         }
455                 }
456
457                 // Error message?
458                 var div = table->next;
459                 if (div != null && div->get_prop ("class") == "error-message") {
460                         if (div->children == null || div->children->content == null ||
461                             !div->children->content.has_prefix ("Es sind leider noch keine Einträge vorhanden.")) {
462                                 stderr.printf ("Got an unknown error message!\n");
463                                 if (div->children != null && div->children->content != null)
464                                         stderr.printf ("\"%s\"\n", div->children->content);
465                         }
466                 }
467
468                 return list;
469         }
470
471         Lift parse_lift_row (Xml.Node* node) {
472                 var lift = new Lift ();
473                 int i = 0;
474                 for (var n = node; n != null; n = n->next) {
475                         if (n->name == "td") {
476                                 var n2 = n->children;
477                                 if (n2 != null) {
478                                         if (n2->name == "a") {
479                                                 var href = n2->get_prop ("href");
480                                                 if (href != null && lift.href == null)
481                                                         lift.href = href;
482                                                 var n3 = n2->children;
483                                                 while (n3 != null) {
484                                                         if (n3->name == "text")
485                                                                 switch (i) {
486                                                                 case 0:
487                                                                         lift.city_from = n3->content;
488                                                                         break;
489                                                                 case 1:
490                                                                         lift.city_to = n3->content;
491                                                                         break;
492                                                                 case 2:
493                                                                         parse_date (n3->content, out lift.time);
494                                                                         break;
495                                                                 case 3:
496                                                                         parse_time (n3->content.strip (), out lift.time);
497                                                                         break;
498                                                                 case 4:
499                                                                         lift.places = n3->content.to_int ();
500                                                                         break;
501                                                                 case 5:
502                                                                         lift.price = n3->content;
503                                                                         break;
504                                                                 default:
505                                                                         print ("TEXT:%s\n", n3->content);
506                                                                         break;
507                                                                 }
508                                                         if (n3->name == "span") {
509                                                                 string class = n3->get_prop ("class");
510                                                                 if (class == "icon_smoker")
511                                                                         lift.flags |= LiftFlags.SMOKER;
512                                                                 else if (class == "icon_non_smoker")
513                                                                         lift.flags |= LiftFlags.NON_SMOKER;
514                                                                 else if (class == "icon_adac")
515                                                                         lift.flags |= LiftFlags.ADAC_MEMBER;
516                                                                 else if (class == "icon_women")
517                                                                         lift.flags |= LiftFlags.WOMEN_ONLY;
518                                                                 else if (class != null)
519                                                                         print ("SPAN %s\n", class);
520                                                         }
521                                                         n3 = n3->next;
522                                                 }
523                                         }
524                                 }
525                                 i++;
526                         }
527                 }
528
529                 return lift;
530         }
531
532         public async bool update_lift_details (Lift lift) {
533                 string url = HTTP_BASE_URI + lift.href;
534
535                 var doc = yield get_html_document (url);
536                 if (doc == null) {
537                         stderr.printf ("Error: parsing failed\n");
538                         return false;
539                 }
540
541                 var table = search_tag_by_class (doc->children, "table", "lift");
542                 if (table == null) {
543                         stderr.printf ("Error: does not contain lift table\n");
544                         return false;
545                 }
546
547                 Xml.Node* n;
548                 for (n = table->children; n != null; n = n->next) {
549                         if (n->name == "tr") {
550                                 var n2 = n->children;
551                                 if (n2 == null || n2->name != "td" ||
552                                     n2->children == null || n2->children->name != "text")
553                                         continue;
554
555                                 string text = n2->children->content;
556
557                                 if (text != "Strecke & Infos" && text != "&nbsp;" && !text.has_prefix ("\xc2\xa0") &&
558                                     text != "Datum" &&
559                                     text != "Freie Pl\xc3\xa4tze" &&
560                                     text != "Name" &&
561                                     text != "Handy" &&
562                                     text != "Telefon" &&
563                                     text != "Telefon 2" &&
564                                     text != "E-Mail 1" &&
565                                     text != "Details" &&
566                                     text != "Beschreibung")
567                                         continue;
568
569                                 n2 = n2->next;
570                                 if (n2 == null)
571                                         continue;
572
573                                 // Skip text between td nodes
574                                 if (n2->name == "text")
575                                         n2 = n2->next;
576
577                                 if (n2 == null || n2->name != "td" || n2->children == null)
578                                         continue;
579
580                                 if (n2->children->name == "img") {
581                                         // FIXME: email image
582                                          lift.email_image_uri = n2->children->get_prop ("src");
583                                         continue;
584                                 }
585
586                                 if (n2->children->name == "div" && text == "Beschreibung") {
587                                         var n3 = n2->children->children;
588                                         lift.description = "";
589                                         while (n3 != null) {
590                                                 if (n3->name == "text")
591                                                         lift.description += n3->content.strip () + "\n";
592                                                 n3 = n3->next;
593                                         }
594                                         continue;
595                                 } else if (n2->children->name != "text") {
596                                         continue;
597                                 }
598
599                                 var text1 = n2->children->content.strip ();
600
601                                 if (text == "Freie Pl\xc3\xa4tze")
602                                         lift.places = text1.to_int ();
603                                 else if (text == "Name")
604                                         lift.name = text1;
605                                 else if (text == "Handy")
606                                         lift.cell = text1;
607                                 else if (text == "Telefon")
608                                         lift.phone = text1;
609                                 else if (text == "Telefon 2")
610                                         lift.phone2 = text1;
611                                 else if (text == "E-Mail 1")
612                                         lift.email = text1;
613                                 else if (text != "Strecke & Infos" && text != "&nbsp;" &&
614                                     !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
615                                     text != "Details")
616                                         continue;
617
618                                 n2 = n2->next;
619                                 if (n2 == null)
620                                         continue;
621
622                                 // Skip text between td nodes
623                                 if (n2->name == "text")
624                                         n2 = n2->next;
625
626                                 if (n2 == null || n2->name != "td" ||
627                                     n2->children == null)
628                                         continue;
629
630                                 if (n2->children->name == "span" &&
631                                     n2->children->get_prop ("class") == "icon_non_smoker") {
632                                         lift.flags |= LiftFlags.NON_SMOKER;
633                                         continue;
634                                 } else if (n2->children->name == "span" &&
635                                     n2->children->get_prop ("class") == "icon_smoker") {
636                                         lift.flags |= LiftFlags.SMOKER;
637                                         continue;
638                                 } else if (n2->children->name != "text")
639                                         continue;
640
641                                 var text2 = n2->children->content.strip ();
642
643                                 if (text1 == "von")
644                                         lift.city_from = text2;
645                                 else if (text1.has_prefix ("\xc3\xbc"))
646                                         lift.city_via.append (text2);
647                                 else if (text1 == "nach")
648                                         lift.city_to = text2;
649                                 else if (text1 == "Datum")
650                                         parse_date (text2, out lift.time);
651                                 else if (text1 == "Uhrzeit")
652                                         parse_time (text2, out lift.time);
653                                 else if (text1 == "Raucher")
654                                         print ("Raucher: %s\n", text2);
655                                 else if (text1 == "Fahrpreis")
656                                         lift.price = text2;
657                                 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
658                                         lift.flags |= LiftFlags.ADAC_MEMBER;
659                         }
660                 }
661
662                 // The paragraph after the table contains the date of last modification
663                 var p = table->next;
664                 for (n = p->children; n != null; n = n->next) {
665                         if (n->name != "text")
666                                 continue;
667
668                         var s = n->content.strip ();
669                         if (s.has_prefix ("Letztmalig aktualisiert am "))
670                                 lift.modified = s.offset (27).dup (); // "Do 15.04.2010 20:32"
671                 }
672
673                 return true;
674         }
675
676         public async MyInformation get_my_information () {
677                 string url = HTTPS_BASE_URI + "/users/view";
678
679                 var doc = yield get_html_document (url);
680                 if (doc == null) {
681                         stderr.printf ("Error: parsing failed\n");
682                         return null;
683                 }
684
685                 var table = search_tag_by_class (doc->children, "table", "user");
686                 if (table == null) {
687                         stderr.printf ("Error: does not contain user table\n");
688                         return null;
689                 }
690
691                 var my_info = new MyInformation ();
692
693                 Xml.Node* n;
694                 for (n = table->children; n != null; n = n->next) {
695                         if (n->name == "tr") {
696                                 var n2 = n->children;
697                                 if (n2 == null || n2->name != "td" ||
698                                     n2->children == null || n2->children->name != "text")
699                                         continue;
700
701                                 string text = n2->children->content;
702
703                                 n2 = n2->next;
704                                 if (n2 != null && n2->name == "text")
705                                         n2 = n2->next;
706                                 if (n2 == null || n2->name != "td" ||
707                                     n2->children == null || n2->children->name != "text")
708                                         continue;
709
710                                 string content = n2->children->content;
711
712                                 switch (text) {
713                                 case "Anrede":
714                                         my_info.gender = (content == "Herr") ? MyInformation.Gender.MALE : MyInformation.Gender.FEMALE;
715                                         continue;
716                                 case "Titel":
717                                         my_info.title = content;
718                                         continue;
719                                 case "Vorname":
720                                         my_info.first_name = content;
721                                         continue;
722                                 case "Name":
723                                         my_info.last_name = content;
724                                         continue;
725                                 case "Geburtsdatum":
726                         //              my_info.birthday = ...
727                                         continue;
728                                 case "registriert seit":
729                         //              my_info.registered_since = content;
730                                         continue;
731                         //      default:
732                         //              print ("\t%s=%s\n", text, content);
733                         //              break;
734                                 }
735
736                                 text = content;
737
738                                 n2 = n2->next;
739                                 if (n2 != null && n2->name == "text")
740                                         n2 = n2->next;
741                                 if (n2 == null || n2->name != "td")
742                                         continue;
743
744                                 if (n2->children != null && n2->children->name != "text")
745                                         content = n2->children->content;
746                                 else
747                                         content = "";
748
749                                 switch (text) {
750                                 case "Straße, Nr.":
751                                         my_info.street = content;
752                                         my_info.number = "";
753                                         continue;
754                                 case "PLZ, Ort":
755                                         my_info.zip_code = "";
756                                         my_info.city = content;
757                                         continue;
758                                 case "Land":
759                                         my_info.country = content;
760                                         continue;
761                                 case "Telefon 1":
762                                         my_info.phone1 = content;
763                                         continue;
764                                 case "Telefon 2":
765                                         my_info.phone2 = content;
766                                         continue;
767                                 case "Telefon 3":
768                                         my_info.phone3 = content;
769                                         continue;
770                                 case "Handy":
771                                         my_info.cell = content;
772                                         continue;
773                                 case "Email 1":
774                                         my_info.email1 = content;
775                                         continue;
776                                 case "Email 2":
777                                         my_info.email2 = content;
778                                         continue;
779                                 case "Raucher":
780                                         // FIXME
781                                         my_info.smoker = false;
782                                         continue;
783                                 case "ADAC-Mitglied":
784                                         // FIXME
785                                         my_info.adac_member = false;
786                                         continue;
787                         //      default:
788                         //              print ("\"%s\"=\"%s\"\n", text, content);
789                         //              break;
790                                 }
791                         }
792                 }
793
794 /*
795                         <tr class="head top">
796                                 <td width="150" class="label">Anrede</td>
797                                 <td width="400">Herr</td>
798                         </tr>
799                         <tr class="head">
800                                 <td class="label">Titel</td>
801
802                                 <td>--</td>
803                         </tr>
804                         ...
805 */
806                 return my_info;
807         }
808
809         Xml.Node* search_tag_by_property (Xml.Node* node, string tag, string prop, string val) requires (node != null) {
810                 for (var n = node; n != null; n = n->next) {
811                         if (n->name == tag && n->get_prop (prop) == val)
812                                 return n;
813                         if (n->children != null) {
814                                 var found = search_tag_by_property (n->children, tag, prop, val);
815                                 if (found != null)
816                                         return found;
817                         }
818                 }
819                 return null;
820         }
821
822         Xml.Node* search_tag_by_id (Xml.Node* node, string tag, string id) requires (node != null) {
823                 return search_tag_by_property (node, tag, "id", id);
824         }
825
826         Xml.Node* search_tag_by_name (Xml.Node* node, string tag, string name) requires (node != null) {
827                 return search_tag_by_property (node, tag, "name", name);
828         }
829
830         Xml.Node* search_tag_by_class (Xml.Node* node, string tag, string @class) requires (node != null) {
831                 return search_tag_by_property (node, tag, "class", @class);
832         }
833
834         void parse_date (string date, out Time time) {
835                 int year;
836                 if (date.length == 11)
837                         date = date.offset (3);
838                 if (date.length != 8)
839                         return;
840                 var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
841                 time.year = year + 2000;
842         }
843
844         void parse_time (string time, out Time result) {
845                 var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);
846         }
847 }