4805ddaa1eb2c93e438991c3ff1d561d3afdaa77
[licom.git] / book.cgi
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use lib (qw(lib));
6
7 use CGI (':cgi');
8 use CGI::Carp (qw(fatalsToBrowser));
9 use URI::Escape;
10 use Data::Dumper;
11
12 use Person;
13
14 our $Debug = 0;
15 our %Config = ();
16
17 our @MultiFields = (qw(address homephone cellphone officephone fax mail uri group));
18
19 our %FieldNames = 
20 (
21         address         => 'Address',
22         homephone       => 'Home Phone',
23         cellphone       => 'Cell Phone',
24         officephone     => 'Office Phone',
25         fax             => 'FAX',
26         mail            => 'E-Mail',
27         uri             => 'URI (Homepage)',
28         group           => 'Group'
29 );
30
31 our $MySelf = $ENV{'SCRIPT_NAME'};
32
33 our $Action = param ('action');
34 $Action ||= 'default';
35
36 our %Actions =
37 (
38         browse  => [\&html_start, \&action_browse,  \&html_end],
39         default => [\&html_start, \&action_browse,  \&html_end],
40         detail  => [\&html_start, \&action_detail,  \&html_end],
41         edit    => [\&html_start, \&action_edit,    \&html_end],
42         save    => [\&html_start, \&action_save,    \&html_end],
43         search  => [\&html_start, \&action_search,  \&html_end],
44         verify  => [\&html_start, \&action_verify,  \&html_end],
45         vcard   => \&action_vcard
46 );
47
48 read_config ();
49
50 # make sure AuthLDAPRemoteUserIsDN is enabled.
51 die unless ($ENV{'REMOTE_USER'});
52 $Config{'base_dn'} = $ENV{'REMOTE_USER'};
53
54 Person->connect
55 (
56         uri     => $Config{'uri'},
57         base_dn => $Config{'base_dn'},
58         bind_dn => $Config{'bind_dn'},
59         password => $Config{'password'}
60 ) or die;
61
62 our ($UserCN, $UserID) = Person->get_user ($Config{'base_dn'});
63
64 if (!$UserID and $Action ne 'save')
65 {
66         $Action = 'edit';
67 }
68
69 if (!$UserCN)
70 {
71         die;
72 }
73
74 if (!defined ($Actions{$Action}))
75 {
76         die;
77 }
78
79 if (ref ($Actions{$Action}) eq 'CODE')
80 {
81         $Actions{$Action}->();
82 }
83 elsif (ref ($Actions{$Action}) eq 'ARRAY')
84 {
85         for (@{$Actions{$Action}})
86         {
87                 $_->();
88         }
89 }
90
91 #print qq#<div>Authenticated as ($UserCN, $UserID, #, $Config{'base_dn'}, qq#)</div>\n#;
92
93 Person->disconnect ();
94
95 exit (0);
96
97 ###
98
99 sub action_browse
100 {
101         my $group = param ('group');
102         $group = shift if (@_);
103         $group ||= '*';
104
105         my @all = Person->search ([[group => $group]]);
106
107         if ($group eq '*')
108         {
109                 my %groups = ();
110                 for (@all)
111                 {
112                         my $person = $_;
113                         my @g = $person->get ('group');
114
115                         $groups{$_} = (defined ($groups{$_}) ? $groups{$_} + 1 : 1) for (@g);
116                 }
117
118                 print qq(\t\t<h2>Contact Groups</h2>\n\t\t<ul class="groups">\n);
119                 for (sort (keys (%groups)))
120                 {
121                         my $group = $_;
122                         my $group_esc = uri_escape ($group);
123                         my $num = $groups{$group};
124
125                         print qq(\t\t\t<li><a href="$MySelf?action=browse&group=$group_esc">$group</a> ($num)</li>\n);
126                 }
127                 if (!%groups)
128                 {
129                         print qq(\t\t\t<li class="empty">There are no groups yet.</li>\n);
130                 }
131                 print qq(\t\t</ul>\n\n);
132         }
133
134         if ($group eq '*')
135         {
136                 print qq(\t\t<h2>All Contacts</h2>\n);
137         }
138         else
139         {
140                 print qq(\t\t<h2>Contact Group &quot;$group&quot;</h2>\n);
141         }
142
143         print qq(\t\t<ul class="results">\n);
144         for (@all)
145         {
146                 my $person = $_;
147                 my $cn = $person->name ();
148                 my $cn_esc = uri_escape ($cn);
149
150                 print qq(\t\t\t<li><a href="$MySelf?action=detail&cn=$cn_esc">$cn</a></li>\n);
151         }
152         print qq(\t\t</ul>\n\n);
153 }
154
155 sub action_detail
156 {
157         my $cn = param ('cn');
158         $cn = shift if (@_);
159         die unless ($cn);
160
161         my $person = Person->load ($cn);
162         if (!$person)
163         {
164                 print qq(\t<div>Entry &quot;$cn&quot; could not be loaded from DB.</div>\n);
165                 return;
166         }
167
168         print qq(\t<h2>Details for $cn</h2>\n);
169
170         my $cn_esc = uri_escape ($cn);
171
172         print <<EOF;
173         <table class="detail">
174                 <tr>
175                         <th>Name</th>
176                         <td>$cn</td>
177                 </tr>
178 EOF
179         for (@MultiFields)
180         {
181                 my $field = $_;
182                 my $values = $person->get ($field);
183                 my $num = scalar (@$values);
184                 my $print = defined ($FieldNames{$field}) ? $FieldNames{$field} : $field;
185
186                 next unless ($num);
187
188                 print "\t\t<tr>\n";
189                 if ($num > 1)
190                 {
191                         print qq(\t\t\t<th rowspan="$num">$print</th>\n);
192                 }
193                 else
194                 {
195                         print qq(\t\t\t<th>$print</th>\n);
196                 }
197
198                 for (my $i = 0; $i < $num; $i++)
199                 {
200                         my $val = $values->[$i];
201                         print "\t\t<tr>\n" if ($i);
202                         print "\t\t\t<td>$val</td>\n",
203                         "\t\t</tr>\n";
204                 }
205         }
206         print <<EOF;
207                 <th colspan="2" class="menu">
208                         [<a href="$MySelf?action=verify&cn=$cn_esc">Verify</a>]
209                         [<a href="$MySelf?action=vcard&cn=$cn_esc">vCard</a>]
210                         [<a href="$MySelf?action=edit&cn=$cn_esc">Edit</a>]
211                 </th>
212         </table>
213 EOF
214 }
215
216 sub action_search
217 {
218         my $search = param ('search');
219
220         $search ||= '';
221         $search =~ s/[^\s\w]//g;
222
223         if (!$search)
224         {
225                 print qq(\t<div class="error">Sorry, the empty search is not allowed.</div>\n);
226                 action_default ();
227                 return;
228         }
229
230         my @patterns = split (m/\s+/, $search);
231         my @filter = ();
232
233         for (@patterns)
234         {
235                 my $pattern = "$_*";
236                 push (@filter, [[lastname => $pattern], [firstname => $pattern]]);
237         }
238
239         my @matches = Person->search (@filter);
240
241         if (!@matches)
242         {
243                 print qq(\t<div>No entries matched your search.</div>\n);
244                 return;
245         }
246
247         if (scalar (@matches) == 1)
248         {
249                 my $person = shift (@matches);
250                 my $cn = $person->name ();
251                 action_detail ($cn);
252                 return;
253         }
254
255         print qq(\t<ul class="result">\n);
256         for (@matches)
257         {
258                 my $person = $_;
259                 my $cn = $person->name ();
260                 my $cn_esc = uri_escape ($cn);
261
262                 print qq(\t\t<li><a href="$MySelf?action=detail&cn=$cn_esc">$cn</a></li>\n);
263         }
264         print qq(\t</ul>\n);
265 }
266
267 sub action_edit
268 {
269         my %opts = @_;
270
271         my $cn = param ('cn');
272
273         $cn = $opts{'cn'} if (defined ($opts{'cn'}));
274         $cn ||= '';
275
276         if (!$UserID)
277         {
278                 $cn = $UserCN;
279         }
280
281         my $person;
282
283         my $lastname;
284         my $firstname;
285
286         my $contacts = {};
287         $contacts->{$_} = [] for (@MultiFields);
288
289         if ($cn)
290         {
291                 $person = Person->load ($cn);
292
293                 if (!$person)
294                 {
295                         print qq(\t<div class="error">Unable to load CN &quot;$cn&quot;. Sorry.</div>\n);
296                         return;
297                 }
298         
299                 $lastname    = $person->lastname ();
300                 $firstname   = $person->firstname ();
301
302                 for (@MultiFields)
303                 {
304                         $contacts->{$_} = $person->get ($_);
305                 }
306         }
307
308         $lastname    = param ('lastname')    if (param ('lastname')  and $UserID);
309         $firstname   = param ('firstname')   if (param ('firstname') and $UserID);
310
311         for (@MultiFields)
312         {
313                 my $field = $_;
314                 my @values = grep { $_ } (param ($field));
315                 $contacts->{$field} = [@values] if (@values);
316         }
317         
318         $lastname    =   $opts{'lastname'}     if (defined ($opts{'lastname'}));
319         $firstname   =   $opts{'firstname'}    if (defined ($opts{'firstname'}));
320         for (@MultiFields)
321         {
322                 my $field = $_;
323                 @{$contacts->{$field}} = @{$opts{$field}} if (defined ($opts{$field}));
324         }
325
326         if ($cn)
327         {
328                 print "\t\t<h2>Edit contact $cn</h2>\n";
329         }
330         else
331         {
332                 print "\t\t<h2>Create new contact</h2>\n";
333         }
334
335         print <<EOF;
336                 <form action="$MySelf" method="post">
337                 <input type="hidden" name="action" value="save" />
338                 <input type="hidden" name="cn" value="$cn" />
339                 <table class="edit">
340                         <tr>
341                                 <th>Lastname</th>
342 EOF
343         if ($UserID)
344         {
345                 print qq(\t\t\t\t<td><input type="text" name="lastname" value="$lastname" /></td>\n);
346         }
347         else
348         {
349                 print qq(\t\t\t\t<td>$lastname</td>\n);
350         }
351         print <<EOF;
352                         </tr>
353                         <tr>
354                                 <th>Firstname</th>
355 EOF
356         if ($UserID)
357         {
358                 print qq(\t\t\t\t<td><input type="text" name="firstname" value="$firstname" /></td>\n);
359         }
360         else
361         {
362                 print qq(\t\t\t\t<td>$firstname</td>\n);
363         }
364         
365         print "\t\t\t</tr>\n";
366
367         for (@MultiFields)
368         {
369                 my $field = $_;
370                 my $print = defined ($FieldNames{$field}) ? $FieldNames{$field} : $field;
371                 my @values = @{$contacts->{$field}};
372
373                 push (@values, '');
374                 
375                 for (@values)
376                 {
377                         my $value = $_;
378
379                         print <<EOF;
380                         <tr>
381                                 <th>$print</th>
382                                 <td><input type="text" name="$field" value="$value" /></td>
383                         </tr>
384 EOF
385                 }
386         }
387
388         print <<EOF;
389                         <tr>
390                                 <th colspan="2" class="menu">
391 EOF
392         if ($UserID)
393         {
394                 print <<EOF;
395                                         <input type="submit" name="button" value="Cancel" />
396                                         <input type="submit" name="button" value="Apply" />
397 EOF
398         }
399         print <<EOF;
400                                         <input type="submit" name="button" value="Save" />
401                                 </th>
402                         </tr>
403                 </table>
404                 </form>
405 EOF
406 }
407
408 sub action_save
409 {
410         my $cn = $UserID ? param ('cn') : $UserCN;
411
412         if ($cn)
413         {
414                 action_update ();
415                 return;
416         }
417
418         die unless ($UserID);
419
420         my $button = lc (param ('button'));
421         $button ||= 'save';
422
423         if ($button eq 'cancel')
424         {
425                 action_browse ();
426                 return;
427         }
428
429         if (!param ('lastname') or !param ('firstname'))
430         {
431                 print qq(\t<div class="error">You have to give both, first and lastname, to identify this record.</div>\n);
432                 action_edit (cn => '');
433                 return;
434         }
435
436         my $lastname  = param ('lastname');
437         my $firstname = param ('firstname');
438
439         my $contacts = {};
440         for (@MultiFields)
441         {
442                 my $field = $_;
443                 my @values = grep { $_ } (param ($field));
444                 $contacts->{$field} = [@values] if (@values);
445         }
446
447         my $person = Person->create (lastname => $lastname, firstname => $firstname, %$contacts);
448
449         if (!$person)
450         {
451                 print qq(\t<div class="error">Unable to save entry. Sorry.</div>\n);
452                 return;
453         }
454         
455         $cn = $person->name ();
456
457         if ($button eq 'apply')
458         {
459                 action_edit (cn => $cn);
460         }
461         else
462         {
463                 action_detail ($cn);
464         }
465 }
466
467 sub action_update
468 {
469         my $cn = $UserID ? param ('cn') : $UserCN;
470         my $person = Person->load ($cn);
471
472         die unless ($person);
473
474         my $button = lc (param ('button'));
475         $button ||= 'save';
476
477         if ($UserID and $button eq 'cancel')
478         {
479                 action_detail ($cn);
480                 return;
481         }
482
483         if ($UserID)
484         {
485                 my $lastname  = param ('lastname');
486                 my $firstname = param ('firstname');
487
488                 $person->lastname  ($lastname)  if ($lastname  and $lastname  ne $person->lastname ());
489                 $person->firstname ($firstname) if ($firstname and $firstname ne $person->firstname ());
490
491                 $cn = $person->name ();
492         }
493
494         my $contacts = {};
495         for (@MultiFields)
496         {
497                 my $field = $_;
498                 my @values = grep { $_ } (param ($field));
499                 $contacts->{$field} = [@values] if (@values);
500         }
501
502         for (@MultiFields)
503         {
504                 my $field = $_;
505                 
506                 if (defined ($contacts->{$field}))
507                 {
508                         my $values = $contacts->{$field};
509                         $person->set ($field, $values);
510                 }
511                 else
512                 {
513                         $person->set ($field, []);
514                 }
515         }
516
517         if ($button eq 'apply' or !$UserID)
518         {
519                 action_edit (cn => $cn);
520         }
521         else
522         {
523                 action_detail ($cn);
524         }
525 }
526
527 sub action_vcard
528 {
529         my $cn = param ('cn');
530         $cn = shift if (@_);
531         die unless ($cn);
532
533         my $person = Person->load ($cn);
534         die unless ($person);
535
536         my %vcard_types =
537         (
538                 homephone       => 'TEL;TYPE=home,voice',
539                 cellphone       => 'TEL;TYPE=cell',
540                 officephone     => 'TEL;TYPE=work,voice',
541                 fax             => 'TEL;TYPE=fax',
542                 mail            => 'EMAIL',
543                 uri             => 'URL',
544                 group           => 'ORG'
545         );
546
547         my $sn = $person->lastname ();
548         my $gn = $person->firstname ();
549         my $cn_esc = uri_escape ($cn);
550
551         print <<EOF;
552 Content-Type: text/x-vcard
553 Content-Disposition: attachment; filename="$cn.vcf"
554
555 BEGIN:VCARD
556 VERSION:3.0
557 FN: $cn
558 N: $sn;$gn
559 EOF
560
561         for (@MultiFields)
562         {
563                 my $field = $_;
564                 my $vc_fld = $vcard_types{$field};
565                 my $values = $person->get ($field);
566
567                 for (@$values)
568                 {
569                         my $value = $_;
570                         print "$vc_fld:$value\n";
571                 }
572         }
573         print "END:VCARD\n";
574 }
575
576 sub action_verify
577 {
578         my $cn = param ('cn');
579         $cn = shift if (@_);
580         die unless ($cn);
581
582         my $person = Person->load ($cn);
583         die unless ($person);
584
585         my ($mail) = $person->get ('mail');
586         $mail ||= '';
587
588         my $message;
589         my $password = $person->password ();
590
591         if (!$password)
592         {
593                 $password = pwgen ();
594                 $person->password ($password);
595         }
596
597         $message = qq(The password for the record &quot;$cn&quot; is &quot;$password&quot;.);
598
599         if ($mail)
600         {
601                 action_verify_send_mail ($person);
602                 $message .= qq( A request for verification has been sent to $mail.);
603         }
604         else
605         {
606                 $message .= q( There was no e-mail address, thus no verification request could be sent.);
607         }
608
609         print qq(\t\t<div class="message">$message</div>\n);
610
611         action_detail ($cn);
612 }
613
614 sub action_verify_send_mail
615 {
616         my $person = shift;
617         my $owner = Person->load ($UserCN);
618         my $smh;
619
620         my $max_width = 0;
621         for (keys %FieldNames)
622         {
623                 $max_width = length $FieldNames{$_} if ($max_width < length $FieldNames{$_});
624         }
625         $max_width++;
626
627         my $person_name = $person->name ();
628         my ($person_mail) = $person->get ('mail');
629         my $person_gn = $person->firstname ();
630         my $password = $person->password ();
631
632         my $owner_name = $owner->name ();
633         my ($owner_mail) = $owner->get ('mail');
634         $owner_mail ||= $ENV{'SERVER_ADMIN'};
635
636         my $host = $ENV{'HTTP_HOST'};
637         my $url = 'http://' . $host . $MySelf;
638         
639         open ($smh, '| /usr/sbin/sendmail -t') or die ("open pipe to sendmail: $!");
640         print $smh <<EOM;
641 To: $person_name <$person_mail>
642 From: $owner_name <$owner_mail>
643 Subject: Please verify our entry in my address book
644
645 Hello $person_gn,
646
647 the following is your entry in my address book:
648 EOM
649         for (@MultiFields)
650         {
651                 my $field = $_;
652                 my $print = defined ($FieldNames{$field}) ? $FieldNames{$field} : $field;
653                 my @values = $person->get ($field);
654
655                 for (@values)
656                 {
657                         printf $smh ('%'.$max_width."s: %-s\n", $print, $_);
658                 }
659         }
660         print $smh <<EOM;
661
662 If this entry is outdated or incomplete, please take a minute and correct it.
663   Address:  $url
664  Username: $person_name
665  Password: $password
666
667 Thank you very much :) Regards,
668 $owner_name
669 EOM
670         close ($smh);
671 }
672
673 sub html_start
674 {
675         my $title = shift;
676         $title = q(Lightweight Contact Manager) unless ($title);
677
678         print <<EOF;
679 Content-Type: text/html; charset=UTF-8
680
681 <html>
682         <head>
683                 <title>$title</title>
684                 <style type="text/css">
685                 <!--
686 a
687 {
688         color: blue;
689         background-color: inherit;
690         text-decoration: none;
691 }
692
693 a:hover
694 {
695         text-decoration: underline;
696 }
697
698 a:visited
699 {
700         color: navy;
701         background-color: inherit;
702 }
703
704 body
705 {
706         color: black;
707         background-color: white;
708 }
709
710 div.error
711 {
712         color: red;
713         background-color: yellow;
714         
715         font-weight: bold;
716         padding: 1ex;
717         border: 2px solid red;
718 }
719
720 div.foot
721 {
722         color: gray;
723         background-color: white;
724
725         position: absolute;
726         top: auto;
727         right: 0px;
728         bottom: 0px;
729         left: 0px;
730         
731         font-size: x-small;
732         text-align: right;
733         border-top: 1px solid black;
734         width: 100%;
735 }
736
737 div.foot a
738 {
739         color: black;
740         background-color: inherit;
741         text-decoration: none;
742 }
743
744 div.foot a:hover
745 {
746         text-decoration: underline;
747 }
748
749 div.menu form
750 {
751         display: inline;
752         margin-right: 5ex;
753 }
754
755 img
756 {
757         border: none;
758 }
759
760 td
761 {
762         color: black;
763         background-color: #cccccc;
764 }
765
766 th
767 {
768         color: black;
769         background-color: #999999;
770         padding: 0.3ex;
771         text-align: left;
772         vertical-align: top;
773 }
774
775 th.menu
776 {
777         text-align: right;
778 }
779
780 th.menu a
781 {
782         color: blue;
783         background-color: transparent;
784 }
785                 //-->
786                 </style>
787         </head>
788
789         <body>
790 EOF
791         if ($UserID)
792         {
793                 my $search = param ('search') || '';
794                 print <<EOF;
795                 <div class="menu">
796                         <form action="$MySelf" method="post">
797                                 <input type="hidden" name="action" value="browse" />
798                                 <input type="submit" name="button" value="Browse" />
799                         </form>
800                         <form action="$MySelf" method="post">
801                                 <input type="hidden" name="action" value="search" />
802                                 <input type="text" name="search" value="$search" />
803                                 <input type="submit" name="button" value="Search" />
804                         </form>
805                         <form action="$MySelf" method="post">
806                                 <input type="hidden" name="action" value="edit" />
807                                 <input type="hidden" name="dn" value="" />
808                                 <input type="submit" name="button" value="Add New" />
809                         </form>
810                 </div>
811                 <hr />
812 EOF
813         }
814         print "\t\t<h1>$title</h1>\n";
815 }
816
817 sub html_end
818 {
819         print <<EOF;
820                 <div class="foot">
821                         &quot;Lightweight Contact Manager&quot;,
822                         written 2005 by <a href="http://verplant.org/">Florian octo Forster</a>
823                         &lt;octo at verplant.org&gt;
824                 </div>
825         </body>
826 </html>
827 EOF
828 }
829
830 sub read_config
831 {
832         my $file = '/var/www/html/cgi.verplant.org/address/book.conf';
833         my $fh;
834
835         open ($fh, "< $file") or die ("open ($file): $!");
836         for (<$fh>)
837         {
838                 chomp;
839                 my $line = $_;
840
841                 if ($line =~ m/^(\w+):\s*"(.+)"\s*$/)
842                 {
843                         my $key = lc ($1);
844                         my $val = $2;
845
846                         $Config{$key} = $val;
847                 }
848         }
849
850         close ($fh);
851
852         for (qw(uri bind_dn password))
853         {
854                 die ("Not defined: $_") unless (defined ($Config{$_}));
855         }
856 }
857
858 sub pwgen
859 {
860         my $len = @_ ? shift : 6;
861         my $retval = '';
862
863         while (!$retval)
864         {
865                 my $numbers = 0;
866                 my $lchars  = 0;
867                 my $uchars  = 0;
868                 
869                 while (length ($retval) < $len)
870                 {
871                         my $chr = int (rand (128));
872
873                         if ($chr >= 48 and $chr < 58)
874                         {
875                                 $numbers++;
876                         }
877                         elsif ($chr >= 65 and $chr < 91)
878                         {
879                                 $uchars++;
880                         }
881                         elsif ($chr >= 97 and $chr < 123)
882                         {
883                                 $lchars++;
884                         }
885                         else
886                         {
887                                 next;
888                         }
889                         $retval .= chr ($chr);
890                 }
891
892                 $retval = '' if (!$numbers or !$lchars or !$uchars);
893         }
894
895         return ($retval);
896 }