Adac Mitfahrclub: Split URL generation into separate functions
[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 set_credentials (string _username, string _password) {
178                 username = _username;
179                 password = _password;
180         }
181
182         public void login (string? _username, string? _password) {
183                 set_credentials (_username, _password);
184                 if (logged_in)
185                         return;
186                 if (login_callback != null)
187                         return;
188                 login_thread ();
189         }
190
191         public bool logged_in = false;
192         private SourceFunc login_callback = null;
193         public async bool login_async () {
194                 if (logged_in)
195                         return true;
196                 if (login_callback != null || username == null || password == null)
197                         return false;
198                 login_callback = login_async.callback;
199                 result = new StringBuilder ();
200                 try {
201                         Thread.create(login_thread, false);
202                 } catch (ThreadError e) {
203                         critical ("Failed to create login thread\n");
204                         return false;
205                 }
206
207                 yield;
208                 login_callback = null;
209
210                 return logged_in;
211         }
212
213         void *login_thread () {
214                 result = new StringBuilder ();
215                 curl.setopt (Curl.Option.URL, HTTP_BASE_URI);
216                 curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
217                 curl.setopt (Curl.Option.WRITEDATA, this);
218                 curl.setopt (Curl.Option.COOKIEFILE, "");
219                 var res = curl.perform ();
220
221                 Curl.SList cookies;
222                 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
223                 unowned Curl.SList cookie = cookies;
224                 while (cookie != null) {
225                         if (cookie.data != null) {
226                                 var c = cookie.data.split ("\t");
227                                 if (c.length > 5)
228                                         print ("%s=%s\n", c[5], c[6]);
229                                         if (c[5] == "MIKINIMEDIA")
230                                                 mikini_cookie = c[6];
231                         }
232                         cookie = cookie.next;
233                 }
234
235                 result = new StringBuilder ();
236                 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);
237                 curl.setopt (Curl.Option.POSTFIELDS, postdata);
238                 curl.setopt (Curl.Option.URL, HTTPS_BASE_URI + "/users/login/");
239                 curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
240                 res = curl.perform ();
241         //      print ("%s\n", result.str);
242
243                 cookies = null;
244                 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
245                 cookie = cookies;
246                 while (cookie != null) {
247                         if (cookie.data != null) {
248                                 var c = cookie.data.split ("\t");
249                                 if (c.length > 5)
250                                         print ("%s=%s\n", c[5], c[6]);
251                                         // "Quirinus[adacAeolus]"
252                                         if (c[5] == "Quirinus[adacAeolus]") {
253                                                 aeolus_cookie = c[6];
254                                                 logged_in = true;
255                                         }
256                         }
257                         cookie = cookie.next;
258                 }
259
260                 if (result.str.contains ("<div id=\"flashMessage\" class=\"message\">Die eingegebenen Zugangsdaten konnten nicht gefunden werden. Bitte versuchen Sie es erneut.</div>")) {
261                         print ("LOGIN FAILED\n");
262                         aeolus_cookie = null;
263                         logged_in = false;
264                 }
265
266                 if (login_callback != null)
267                         Idle.add (login_callback);
268                 return null;
269         }
270
271         private void save_city_list () {
272                 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
273                 if (list_file == null)
274                         return;
275
276                 foreach (unowned City city in city_list) {
277                         if (city.north != 0.0 || city.south != 0.0 || city.east != 0.0 || city.west != 0.0)
278                                 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);
279                         else if (city.latitude != 0.0 || city.longitude != 0.0)
280                                 list_file.printf ("%d\t%s\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude);
281                         else
282                                 list_file.printf ("%d\t%s\n", city.number, city.name);
283                 }
284         }
285
286         private bool load_city_list () {
287                 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "r");
288                 if (list_file == null)
289                         list_file = FileStream.open ("/usr/share/beifahrer/city_list", "r");
290                 if (list_file == null)
291                         return false;
292
293                 city_list = new List<City> ();
294                 string line = list_file.read_line ();
295                 while (line != null) {
296                         var split_line = line.split ("\t");
297                         if (split_line.length < 2)
298                                 continue;
299                         int number = split_line[0].to_int ();
300                         weak string name = split_line[1];
301
302                         var city = new City (number, name);
303                         if (split_line.length >= 4) {
304                                 city.latitude = split_line[2].to_double ();
305                                 city.longitude = split_line[3].to_double ();
306                         }
307                         if (split_line.length >= 8) {
308                                 city.north = split_line[4].to_double ();
309                                 city.south = split_line[5].to_double ();
310                                 city.east = split_line[6].to_double ();
311                                 city.west = split_line[7].to_double ();
312                         }
313                         city_list.append ((owned) city);
314
315                         line = list_file.read_line ();
316                 }
317
318                 return true;
319         }
320
321         public unowned List<City>? get_city_list () {
322                 if (city_list != null)
323                         return city_list;
324
325                 if (load_city_list ())
326                         return city_list;
327
328                 return null;
329         }
330
331         public async unowned List<City>? download_city_list () {
332                 var doc = yield get_html_document (HTTP_BASE_URI);
333                 if (doc == null) {
334                         stderr.printf ("Error: parsing failed\n");
335                         return null;
336                 }
337
338                 var form = search_tag_by_id (doc->children, "form", "search_national_form");
339                 if (form == null) {
340                         stderr.printf ("Error: does not contain search_national_form\n");
341                         return null;
342                 }
343
344                 var select = search_tag_by_name (form->children, "select", "city_from");
345                 if (select == null) {
346                         stderr.printf ("Error: does not contain city_from\n");
347                         return null;
348                 }
349
350                 city_list = new List<City> ();
351                 for (var n = select->children; n != null; n = n->next) {
352                         if (n->name == "option" && n->children != null && n->children->name == "text") {
353                                 int number = n->get_prop ("value").to_int ();
354                                 // Skip 0 "Alle St.dte"
355                                 if (number == 0)
356                                         continue;
357                                 var city = new City(number,
358                                                     n->children->content);
359                                 city_list.append ((owned) city);
360                         }
361                 }
362
363                 // TODO: get coordinates
364
365                 save_city_list ();
366
367                 return city_list;
368         }
369
370         private int get_city_number (string name) {
371                 foreach (unowned City city in city_list) {
372                         if (city.name == name)
373                                 return city.number;
374                 }
375                 return 0;
376         }
377
378         public unowned City find_nearest_city (double latitude, double longitude) {
379                 unowned City result = null;
380                 double min_distance = 0.0;
381                 bool in_result = false;
382
383                 foreach (unowned City city in city_list) {
384                         double lat = latitude - city.latitude;
385                         double lng = longitude - city.longitude;
386                         double distance = lat * lat + lng * lng;
387                         bool in_city = ((city.south <= latitude <= city.north) &&
388                                         (city.west <= longitude <= city.east));
389
390                         if ((result == null) ||
391                             (in_city && !in_result) ||
392                             (in_city && in_result && distance / city.bb_area () < min_distance / result.bb_area ()) ||
393                             (!in_city && !in_result && distance < min_distance)) {
394                                 result = city;
395                                 min_distance = distance;
396                                 in_result = in_city;
397                         }
398                 }
399
400                 return result;
401         }
402
403         public string? get_lift_list_url (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
404                 if (city_list == null)
405                         get_city_list ();
406
407                 int num_from = get_city_number (city_from);
408                 if (num_from == 0)
409                         return null;
410
411                 int num_to = get_city_number (city_to);
412                 if (num_to == 0)
413                         return null;
414
415                 string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
416                         city_from,
417                         city_to
418                 );
419
420                 url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
421                         num_from,
422                         radius_from,
423                         num_to,
424                         radius_to
425                 );
426
427                 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
428                         date.get_day (),
429                         date.get_month (),
430                         date.get_year (),
431                         tolerance
432                 );
433
434                 return url;
435         }
436
437         public async List<Lift>? get_lift_list (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
438                 var doc = yield get_html_document (get_lift_list_url (city_from, radius_from, city_to, radius_to, date, tolerance));
439                 if (doc == null) {
440                         stderr.printf ("Error: parsing failed\n");
441                         return null;
442                 }
443
444                 var table = search_tag_by_class (doc->children, "table", "list p_15");
445                 if (table == null) {
446                         stderr.printf ("Error: does not contain list p_15 table\n");
447                         return null;
448                 }
449
450                 var list = new List<Lift> ();
451                 for (var n = table->children; n != null; n = n->next) {
452                         if (n->name == "tr") {
453                                 var lift = parse_lift_row (n->children);
454                                 if (lift.city_from != null) // Skip the title row
455                                         list.append ((owned) lift);
456                         }
457                 }
458
459                 // Error message?
460                 var div = table->next;
461                 if (div != null && div->get_prop ("class") == "error-message") {
462                         if (div->children == null || div->children->content == null ||
463                             !div->children->content.has_prefix ("Es sind leider noch keine Einträge vorhanden.")) {
464                                 stderr.printf ("Got an unknown error message!\n");
465                                 if (div->children != null && div->children->content != null)
466                                         stderr.printf ("\"%s\"\n", div->children->content);
467                         }
468                 }
469
470                 return list;
471         }
472
473         Lift parse_lift_row (Xml.Node* node) {
474                 var lift = new Lift ();
475                 int i = 0;
476                 for (var n = node; n != null; n = n->next) {
477                         if (n->name == "td") {
478                                 var n2 = n->children;
479                                 if (n2 != null) {
480                                         if (n2->name == "a") {
481                                                 var href = n2->get_prop ("href");
482                                                 if (href != null && lift.href == null)
483                                                         lift.href = href;
484                                                 var n3 = n2->children;
485                                                 while (n3 != null) {
486                                                         if (n3->name == "text")
487                                                                 switch (i) {
488                                                                 case 0:
489                                                                         lift.city_from = n3->content;
490                                                                         break;
491                                                                 case 1:
492                                                                         lift.city_to = n3->content;
493                                                                         break;
494                                                                 case 2:
495                                                                         parse_date (n3->content, out lift.time);
496                                                                         break;
497                                                                 case 3:
498                                                                         parse_time (n3->content.strip (), out lift.time);
499                                                                         break;
500                                                                 case 4:
501                                                                         lift.places = n3->content.to_int ();
502                                                                         break;
503                                                                 case 5:
504                                                                         lift.price = n3->content;
505                                                                         break;
506                                                                 default:
507                                                                         print ("TEXT:%s\n", n3->content);
508                                                                         break;
509                                                                 }
510                                                         if (n3->name == "span") {
511                                                                 string class = n3->get_prop ("class");
512                                                                 if (class == "icon_smoker")
513                                                                         lift.flags |= LiftFlags.SMOKER;
514                                                                 else if (class == "icon_non_smoker")
515                                                                         lift.flags |= LiftFlags.NON_SMOKER;
516                                                                 else if (class == "icon_adac")
517                                                                         lift.flags |= LiftFlags.ADAC_MEMBER;
518                                                                 else if (class == "icon_women")
519                                                                         lift.flags |= LiftFlags.WOMEN_ONLY;
520                                                                 else if (class != null)
521                                                                         print ("SPAN %s\n", class);
522                                                         }
523                                                         n3 = n3->next;
524                                                 }
525                                         }
526                                 }
527                                 i++;
528                         }
529                 }
530
531                 return lift;
532         }
533
534         public string get_lift_details_url (Lift lift) {
535                 return HTTP_BASE_URI + lift.href;
536         }
537
538         public async bool update_lift_details (Lift lift) {
539                 var doc = yield get_html_document (get_lift_details_url (lift));
540                 if (doc == null) {
541                         stderr.printf ("Error: parsing failed\n");
542                         return false;
543                 }
544
545                 var table = search_tag_by_class (doc->children, "table", "lift");
546                 if (table == null) {
547                         stderr.printf ("Error: does not contain lift table\n");
548                         return false;
549                 }
550
551                 Xml.Node* n;
552                 for (n = table->children; n != null; n = n->next) {
553                         if (n->name == "tr") {
554                                 var n2 = n->children;
555                                 if (n2 == null || n2->name != "td" ||
556                                     n2->children == null || n2->children->name != "text")
557                                         continue;
558
559                                 string text = n2->children->content;
560
561                                 if (text != "Strecke & Infos" && text != "&nbsp;" && !text.has_prefix ("\xc2\xa0") &&
562                                     text != "Datum" &&
563                                     text != "Freie Pl\xc3\xa4tze" &&
564                                     text != "Name" &&
565                                     text != "Handy" &&
566                                     text != "Telefon" &&
567                                     text != "Telefon 2" &&
568                                     text != "E-Mail 1" &&
569                                     text != "Details" &&
570                                     text != "Beschreibung")
571                                         continue;
572
573                                 n2 = n2->next;
574                                 if (n2 == null)
575                                         continue;
576
577                                 // Skip text between td nodes
578                                 if (n2->name == "text")
579                                         n2 = n2->next;
580
581                                 if (n2 == null || n2->name != "td" || n2->children == null)
582                                         continue;
583
584                                 if (n2->children->name == "img") {
585                                         // FIXME: email image
586                                          lift.email_image_uri = n2->children->get_prop ("src");
587                                         continue;
588                                 }
589
590                                 if (n2->children->name == "div" && text == "Beschreibung") {
591                                         var n3 = n2->children->children;
592                                         lift.description = "";
593                                         while (n3 != null) {
594                                                 if (n3->name == "text")
595                                                         lift.description += n3->content.strip () + "\n";
596                                                 n3 = n3->next;
597                                         }
598                                         continue;
599                                 } else if (n2->children->name != "text") {
600                                         continue;
601                                 }
602
603                                 var text1 = n2->children->content.strip ();
604
605                                 if (text == "Freie Pl\xc3\xa4tze")
606                                         lift.places = text1.to_int ();
607                                 else if (text == "Name")
608                                         lift.name = text1;
609                                 else if (text == "Handy")
610                                         lift.cell = text1;
611                                 else if (text == "Telefon")
612                                         lift.phone = text1;
613                                 else if (text == "Telefon 2")
614                                         lift.phone2 = text1;
615                                 else if (text == "E-Mail 1")
616                                         lift.email = text1;
617                                 else if (text != "Strecke & Infos" && text != "&nbsp;" &&
618                                     !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
619                                     text != "Details")
620                                         continue;
621
622                                 n2 = n2->next;
623                                 if (n2 == null)
624                                         continue;
625
626                                 // Skip text between td nodes
627                                 if (n2->name == "text")
628                                         n2 = n2->next;
629
630                                 if (n2 == null || n2->name != "td" ||
631                                     n2->children == null)
632                                         continue;
633
634                                 if (n2->children->name == "span" &&
635                                     n2->children->get_prop ("class") == "icon_non_smoker") {
636                                         lift.flags |= LiftFlags.NON_SMOKER;
637                                         continue;
638                                 } else if (n2->children->name == "span" &&
639                                     n2->children->get_prop ("class") == "icon_smoker") {
640                                         lift.flags |= LiftFlags.SMOKER;
641                                         continue;
642                                 } else if (n2->children->name != "text")
643                                         continue;
644
645                                 var text2 = n2->children->content.strip ();
646
647                                 if (text1 == "von")
648                                         lift.city_from = text2;
649                                 else if (text1.has_prefix ("\xc3\xbc"))
650                                         lift.city_via.append (text2);
651                                 else if (text1 == "nach")
652                                         lift.city_to = text2;
653                                 else if (text1 == "Datum")
654                                         parse_date (text2, out lift.time);
655                                 else if (text1 == "Uhrzeit")
656                                         parse_time (text2, out lift.time);
657                                 else if (text1 == "Raucher")
658                                         print ("Raucher: %s\n", text2);
659                                 else if (text1 == "Fahrpreis")
660                                         lift.price = text2;
661                                 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
662                                         lift.flags |= LiftFlags.ADAC_MEMBER;
663                         }
664                 }
665
666                 // The paragraph after the table contains the date of last modification
667                 var p = table->next;
668                 for (n = p->children; n != null; n = n->next) {
669                         if (n->name != "text")
670                                 continue;
671
672                         var s = n->content.strip ();
673                         if (s.has_prefix ("Letztmalig aktualisiert am "))
674                                 lift.modified = s.offset (27).dup (); // "Do 15.04.2010 20:32"
675                 }
676
677                 return true;
678         }
679
680         public string get_my_information_url () {
681                 return HTTPS_BASE_URI + "/users/view";
682         }
683
684         public async MyInformation get_my_information () {
685                 var doc = yield get_html_document (get_my_information_url ());
686                 if (doc == null) {
687                         stderr.printf ("Error: parsing failed\n");
688                         return null;
689                 }
690
691                 var table = search_tag_by_class (doc->children, "table", "user");
692                 if (table == null) {
693                         stderr.printf ("Error: does not contain user table\n");
694                         return null;
695                 }
696
697                 var my_info = new MyInformation ();
698
699                 Xml.Node* n;
700                 for (n = table->children; n != null; n = n->next) {
701                         if (n->name == "tr") {
702                                 var n2 = n->children;
703                                 if (n2 == null || n2->name != "td" ||
704                                     n2->children == null || n2->children->name != "text")
705                                         continue;
706
707                                 string text = n2->children->content;
708
709                                 n2 = n2->next;
710                                 if (n2 != null && n2->name == "text")
711                                         n2 = n2->next;
712                                 if (n2 == null || n2->name != "td" ||
713                                     n2->children == null || n2->children->name != "text")
714                                         continue;
715
716                                 string content = n2->children->content;
717
718                                 switch (text) {
719                                 case "Anrede":
720                                         my_info.gender = (content == "Herr") ? MyInformation.Gender.MALE : MyInformation.Gender.FEMALE;
721                                         continue;
722                                 case "Titel":
723                                         my_info.title = content;
724                                         continue;
725                                 case "Vorname":
726                                         my_info.first_name = content;
727                                         continue;
728                                 case "Name":
729                                         my_info.last_name = content;
730                                         continue;
731                                 case "Geburtsdatum":
732                         //              my_info.birthday = ...
733                                         continue;
734                                 case "registriert seit":
735                         //              my_info.registered_since = content;
736                                         continue;
737                         //      default:
738                         //              print ("\t%s=%s\n", text, content);
739                         //              break;
740                                 }
741
742                                 text = content;
743
744                                 n2 = n2->next;
745                                 if (n2 != null && n2->name == "text")
746                                         n2 = n2->next;
747                                 if (n2 == null || n2->name != "td")
748                                         continue;
749
750                                 if (n2->children != null && n2->children->name != "text")
751                                         content = n2->children->content;
752                                 else
753                                         content = "";
754
755                                 switch (text) {
756                                 case "Straße, Nr.":
757                                         my_info.street = content;
758                                         my_info.number = "";
759                                         continue;
760                                 case "PLZ, Ort":
761                                         my_info.zip_code = "";
762                                         my_info.city = content;
763                                         continue;
764                                 case "Land":
765                                         my_info.country = content;
766                                         continue;
767                                 case "Telefon 1":
768                                         my_info.phone1 = content;
769                                         continue;
770                                 case "Telefon 2":
771                                         my_info.phone2 = content;
772                                         continue;
773                                 case "Telefon 3":
774                                         my_info.phone3 = content;
775                                         continue;
776                                 case "Handy":
777                                         my_info.cell = content;
778                                         continue;
779                                 case "Email 1":
780                                         my_info.email1 = content;
781                                         continue;
782                                 case "Email 2":
783                                         my_info.email2 = content;
784                                         continue;
785                                 case "Raucher":
786                                         // FIXME
787                                         my_info.smoker = false;
788                                         continue;
789                                 case "ADAC-Mitglied":
790                                         // FIXME
791                                         my_info.adac_member = false;
792                                         continue;
793                         //      default:
794                         //              print ("\"%s\"=\"%s\"\n", text, content);
795                         //              break;
796                                 }
797                         }
798                 }
799
800 /*
801                         <tr class="head top">
802                                 <td width="150" class="label">Anrede</td>
803                                 <td width="400">Herr</td>
804                         </tr>
805                         <tr class="head">
806                                 <td class="label">Titel</td>
807
808                                 <td>--</td>
809                         </tr>
810                         ...
811 */
812                 return my_info;
813         }
814
815         Xml.Node* search_tag_by_property (Xml.Node* node, string tag, string prop, string val) requires (node != null) {
816                 for (var n = node; n != null; n = n->next) {
817                         if (n->name == tag && n->get_prop (prop) == val)
818                                 return n;
819                         if (n->children != null) {
820                                 var found = search_tag_by_property (n->children, tag, prop, val);
821                                 if (found != null)
822                                         return found;
823                         }
824                 }
825                 return null;
826         }
827
828         Xml.Node* search_tag_by_id (Xml.Node* node, string tag, string id) requires (node != null) {
829                 return search_tag_by_property (node, tag, "id", id);
830         }
831
832         Xml.Node* search_tag_by_name (Xml.Node* node, string tag, string name) requires (node != null) {
833                 return search_tag_by_property (node, tag, "name", name);
834         }
835
836         Xml.Node* search_tag_by_class (Xml.Node* node, string tag, string @class) requires (node != null) {
837                 return search_tag_by_property (node, tag, "class", @class);
838         }
839
840         void parse_date (string date, out Time time) {
841                 int year;
842                 if (date.length == 11)
843                         date = date.offset (3);
844                 if (date.length != 8)
845                         return;
846                 var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
847                 time.year = year + 2000;
848         }
849
850         void parse_time (string time, out Time result) {
851                 var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);
852         }
853 }