]> git.p6c8.net - selfforum.git/blob - selfforum-cgi/shared/Posting/_lib.pm
4ca15145af43edb19ccbe1afdb6f593b54b2f61e
[selfforum.git] / selfforum-cgi / shared / Posting / _lib.pm
1 package Posting::_lib;
2
3 ################################################################################
4 # #
5 # File: shared/Posting/_lib.pm #
6 # #
7 # Authors: André Malo <nd@o3media.de>, 2001-06-11 #
8 # Frank Schoenmann <fs@tower.de>, 2001-06-04 #
9 # #
10 # Description: Message access interface, time format routines #
11 # #
12 ################################################################################
13
14 use strict;
15 use vars qw(
16 @EXPORT_OK
17 $VERSION
18 );
19
20 use Encode::Plain; $Encode::Plain::utf8 = 1;
21
22 use Time::German;
23 use XML::DOM;
24
25 ################################################################################
26 #
27 # Version check
28 #
29 $VERSION = do { my @r =(q$Revision$ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
30
31 ################################################################################
32 #
33 # Export
34 #
35 use constant SORT_ASCENT => 0; # (latest postings first)
36 use constant SORT_DESCENT => 1;
37 use constant KEEP_DELETED => 1;
38 use constant KILL_DELETED => 0;
39
40 use base qw(Exporter);
41 @EXPORT_OK = qw(
42 get_message_header
43 get_message_body
44 get_message_node
45 get_body_node
46 parse_single_thread
47 parse_xml_file
48 create_new_thread
49 create_message
50
51 hr_time
52 short_hr_time
53 long_hr_time
54 very_short_hr_time
55 month
56
57 get_all_threads
58 create_forum_xml_string
59
60 save_file
61
62 SORT_ASCENT
63 SORT_DESCENT
64 KEEP_DELETED
65 KILL_DELETED
66 );
67
68 ################################################################################
69 #
70 # Access via XML::DOM
71 #
72
73 ### sub create_message ($$) ####################################################
74 #
75 # create the 'Message' subtree
76 #
77 # Params: $xml - XML::DOM::Document object
78 # $par - hash reference
79 # (msg, ip, name, email, home, image, category, subject, time)
80 #
81 # Return: XML::DOM::Element object
82 #
83 sub create_message ($$) {
84 my ($xml,$par) = @_;
85
86 my $message = $xml -> createElement ('Message');
87 $message -> setAttribute ('id' => $par -> {msg});
88 $message -> setAttribute ('ip' => $par -> {ip});
89
90 my $header = $xml -> createElement ('Header');
91 my $author = $xml -> createElement ('Author');
92 $header -> appendChild ($author);
93
94 my @may = (
95 ['name' => 'Name' => $author],
96 ['email' => 'Email' => $author],
97 ['home' => 'HomepageUrl' => $author],
98 ['image' => 'ImageUrl' => $author],
99 ['category' => 'Category' => $header],
100 ['subject' => 'Subject' => $header]
101 );# key => element name => superior
102
103 for (@may) {
104
105 # create element
106 my $obj = $xml -> createElement ($_->[1]);
107
108 # insert content
109 $obj -> addText (
110 defined $par -> {$_->[0]}
111 ? $par -> {$_->[0]}
112 : ''
113 );
114
115 # link to superior element
116 $_ -> [2] -> appendChild ($obj);
117 }
118
119 my $date = $xml -> createElement ('Date');
120 $date -> setAttribute ('longSec'=> $par -> {time});
121
122 $header -> appendChild ($date);
123 $message -> appendChild ($header);
124
125 # return
126 #
127 $message;
128 }
129
130 ### sub create_new_thread ($) ##################################################
131 #
132 # create a XML::DOM::Document object of a thread containing one posting
133 #
134 # Params: hash reference
135 # (dtd, thread, msg, body, ip, name, email, home,
136 # image, category, subject, time)
137 #
138 # Return: XML::DOM::Document object
139 #
140 sub create_new_thread ($) {
141 my $par = shift;
142
143 # new document
144 #
145 my $xml = new XML::DOM::Document;
146
147 # xml declaration
148 #
149 my $decl = new XML::DOM::XMLDecl;
150 $decl -> setVersion ('1.0');
151 $decl -> setEncoding ('UTF-8');
152 $xml -> setXMLDecl ($decl);
153
154 # set doctype
155 #
156 my $dtd = $xml -> createDocumentType ('Forum' => $par -> {dtd});
157 $xml -> setDoctype ($dtd);
158
159 # create root element 'Forum'
160 # create element 'Thread'
161 # create 'Message' subtree
162 # create element 'ContentList'
163 # create 'MessageContent' subtree
164 #
165 my $forum = $xml -> createElement ('Forum');
166 my $thread = $xml -> createElement ('Thread');
167 $thread -> setAttribute ('id' => $par -> {thread});
168 my $message = create_message ($xml,$par);
169 my $content = $xml -> createElement ('ContentList');
170 my $mcontent = $xml -> createElement ('MessageContent');
171 $mcontent -> setAttribute ('mid' => $par -> {msg});
172 $mcontent -> appendChild (
173 $xml -> createCDATASection (${$par -> {body}})
174 );
175
176 # link all the nodes to
177 # their superior elements
178 #
179 $thread -> appendChild ($message);
180 $forum -> appendChild ($thread);
181 $content -> appendChild ($mcontent);
182 $forum -> appendChild ($content);
183 $xml -> appendChild ($forum);
184
185 # return
186 #
187 $xml;
188 }
189
190 ### get_message_header () ######################################################
191 #
192 # Read message header, return as a hash
193 #
194 # Params: $node - XML message node
195 # Return: hash reference (name, category, subject, email, home, image, time)
196 #
197 sub get_message_header ($)
198 {
199 my $node = shift;
200 my %conf;
201
202 my $header = $node -> getElementsByTagName ('Header' , 0) -> item (0);
203 my $author = $header -> getElementsByTagName ('Author' , 0) -> item (0);
204 my $name = $author -> getElementsByTagName ('Name' , 0) -> item (0);
205 my $email = $author -> getElementsByTagName ('Email' , 0) -> item (0);
206 my $home = $author -> getElementsByTagName ('HomepageUrl', 0) -> item (0);
207 my $image = $author -> getElementsByTagName ('ImageUrl' , 0) -> item (0);
208 my $cat = $header -> getElementsByTagName ('Category' , 0) -> item (0);
209 my $subject = $header -> getElementsByTagName ('Subject' , 0) -> item (0);
210 my $date = $header -> getElementsByTagName ('Date' , 0) -> item (0);
211
212 %conf = (
213 name => ($name -> hasChildNodes)?$name -> getFirstChild -> getData:undef,
214 category => ($cat -> hasChildNodes)?$cat -> getFirstChild -> getData:undef,
215 subject => ($subject -> hasChildNodes)?$subject -> getFirstChild -> getData:undef,
216 email => (defined ($email) and $email -> hasChildNodes)?$email -> getFirstChild -> getData:undef,
217 home => (defined ($home) and $home -> hasChildNodes)?$home -> getFirstChild -> getData:undef,
218 image => (defined ($image) and $image -> hasChildNodes)?$image -> getFirstChild -> getData:undef,
219 time => $date -> getAttribute ('longSec')
220 );
221
222 \%conf;
223 }
224
225 ### get_body_node () ########################################################
226 #
227 # Search a specific message body in a XML tree
228 #
229 # Params: $xml XML::DOM::Document Object (Document Node)
230 # $mid Message ID
231 #
232 # Return: MessageContent XML node (or -none-)
233 #
234 sub get_body_node ($$)
235 {
236 my ($xml, $mid) = @_;
237
238 for ($xml -> getElementsByTagName ('ContentList', 1) -> item (0) -> getElementsByTagName ('MessageContent', 0)) {
239 return $_ if ($_ -> getAttribute ('mid') eq $mid);
240 }
241
242 return;
243 }
244
245 ### get_message_body () ########################################################
246 #
247 # Read message body
248 #
249 # Params: $xml XML::DOM::Document Object (Document Node)
250 # $mid Message ID
251 #
252 # Return: Scalar reference
253 #
254 sub get_message_body ($$)
255 {
256 my $cnode = get_body_node ($_[0], $_[1]);
257 my $body;
258
259 $body = ($cnode -> hasChildNodes)?$cnode -> getFirstChild -> getData:'' if $cnode;
260
261 \$body;
262 }
263
264 ### get_message_node () ########################################################
265 #
266 # Search a specific message in a XML tree
267 #
268 # Params: $xml XML::DOM::Document Object (Document Node)
269 # $tid Thread ID
270 # $mid Message ID
271 #
272 # Return: Message XML node, Thread XML node (or -none-)
273 #
274 sub get_message_node ($$$)
275 {
276 my ($xml, $tid, $mid) = @_;
277 my ($mnode, $tnode);
278
279 for ($xml->getElementsByTagName ('Thread')) {
280 if ($_->getAttribute ('id') eq $tid) {
281 $tnode = $_;
282
283 for ($tnode -> getElementsByTagName ('Message')) {
284 if ($_ -> getAttribute ('id') eq $mid) {
285 $mnode = $_;
286 last;
287 }
288 }
289 last;
290 }
291 }
292
293 wantarray
294 ? ($mnode, $tnode)
295 : $mnode;
296 }
297
298 ### sub parse_xml_file ($) #####################################################
299 #
300 # load the specified XML-File and create the DOM tree
301 # this sub is only to avoid errors and to centralize the parse process
302 #
303 # Params: $file filename
304 #
305 # Return: XML::DOM::Document Object (Document Node) or false
306 #
307 sub parse_xml_file ($) {
308 my $file = shift;
309
310 my $xml = eval {
311 local $SIG{__DIE__}; # CGI::Carp works unreliable ;-(
312 new XML::DOM::Parser(KeepCDATA => 1)->parsefile ($file);
313 };
314
315 return if ($@);
316
317 $xml;
318 }
319
320 ### parse_single_thread () #####################################################
321 #
322 # parse a thread file
323 #
324 # Params: $tnode - Thread element node
325 # $deleted - keep deleted (boolean)
326 # $sorted - sorting order
327 #
328 # Return: arrayref
329 #
330 sub parse_single_thread ($$;$) {
331 my ($tnode, $deleted, $sorted) = @_;
332 my ($header, @msg, %mno);
333
334 for ($tnode -> getElementsByTagName ('Message')) {
335 $header = get_message_header ($_);
336
337 push @msg => {
338 mid => ($_ -> getAttribute ('id') =~ /(\d+)/)[0],
339 ip => $_ -> getAttribute ('ip'),
340 kids => [$_ -> getElementsByTagName ('Message', 0)],
341 answers => $_ -> getElementsByTagName ('Message') -> getLength,
342 deleted => $_ -> getAttribute ('invisible'),
343 archive => $_ -> getAttribute ('archive'),
344 name => plain($header -> {name}),
345 cat => plain($header -> {category} or ''),
346 subject => plain($header -> {subject}),
347 time => plain($header -> {time})
348 };
349 $mno{$_} = $#msg;
350 }
351
352 my $level;
353 $msg[0] -> {level} = 0;
354 for (@msg) {
355 $level = $_ -> {level} + 1;
356 @{$_ -> {kids}} = map {$msg[$mno{$_}] -> {level} = $level; $mno{$_}} @{$_ -> {kids}};
357 }
358
359 # sort and process deleted files
360 #
361 my $smsg = sort_thread (\@msg, $sorted);
362 delete_messages ($smsg) unless ($deleted);
363
364 $smsg;
365 }
366
367 ################################################################################
368 #
369 # Access via regexps and native perl ;)
370 #
371
372 ### sort_thread () #############################################################
373 #
374 # sort the message array
375 #
376 # Params: $msg - arrayref
377 # $sorted - sorting order
378 #
379 # Return: sorted arrayref
380 #
381 sub sort_thread ($$) {
382 my ($msg, $sorted) = @_;
383
384 my ($z, %mhash) = (0);
385
386 if ($sorted) { # oldest first
387 for (@$msg) {
388 @$msg[@{$_ -> {kids}}] = sort {$a -> {mid} <=> $b -> {mid}} @$msg[@{$_ -> {kids}}] if (@{$_ -> {kids}} > 1);
389 $mhash{$_ -> {mid}} = [@$msg[@{$_ -> {kids}}]];}}
390
391 else { # latest first
392 for (@$msg) {
393 @$msg[@{$_ -> {kids}}] = sort {$b -> {mid} <=> $a -> {mid}} @$msg[@{$_ -> {kids}}] if (@{$_ -> {kids}} > 1);
394 $mhash{$_ -> {mid}} = [@$msg[@{$_ -> {kids}}]];}}
395
396 # sort the children
397 #
398 my @smsg = ($msg -> [0]);
399 for (@smsg) {
400 ++$z;
401 splice @smsg,$z,0,@{$mhash{$_ -> {mid}}} if ($_ -> {answers});
402 delete $_ -> {kids};}
403
404 # return
405 \@smsg;
406 }
407
408 ### delete_messages () #########################################################
409 #
410 # Filter out deleted messages
411 #
412 # Params: $smsg Reference of array of references of hashs
413 # Return: -none-
414 #
415 sub delete_messages ($) {
416 my $smsg = shift;
417 my ($z, $oldlevel, @path) = (0,0,0);
418
419 while ($z <= $#{$smsg}) {
420
421 if ($smsg -> [$z] -> {level} > $oldlevel) {
422 push @path => $z;
423 $oldlevel = $smsg -> [$z] -> {level};
424 }
425 elsif ($smsg -> [$z] -> {level} < $oldlevel) {
426 splice @path, $smsg -> [$z] -> {level};
427 push @path => $z;
428 $oldlevel = $smsg -> [$z] -> {'level'};
429 }
430 else {
431 $path[-1] = $z;
432 }
433
434 if ($smsg -> [$z] -> {deleted}) {
435 my $n = $smsg -> [$z] -> {answers} + 1;
436 $smsg -> [$_] -> {answers} -= $n for (@path);
437 splice @$smsg, $z, $n;
438 }
439 else {
440 $z++;
441 }
442 }
443
444 return;
445 }
446
447 ### get_all_threads () #########################################################
448 #
449 # Read and Parse the main file (without any XML-module, they are too slow)
450 #
451 # Params: $file - /path/to/filename of the main file
452 # $deleted - hold deleted (invisible) messages in result (1) oder not (0)
453 # $sorted - direction of message sort: descending (0) (default) or ascending (1)
454 #
455 # Return: scalar context: hash reference (\%threads)
456 # list context: list (\%threads, $last_thread, $last_message, $dtd, \@unids)
457 #
458 sub get_all_threads ($$;$) {
459 my ($file, $deleted, $sorted) = @_;
460 my ($last_thread, $last_message, $dtd, @unids, %threads);
461 local (*FILE, $/);
462
463 open FILE,"< $file" or return;
464 my $xml = join '', <FILE>;
465 close(FILE) or return;
466
467 if (wantarray) {
468 ($dtd) = $xml =~ /<!DOCTYPE\s+\S+\s+SYSTEM\s+"([^"]+)">/;
469 ($last_thread) = map {/(\d+)/} $xml =~ /<Forum.+?lastThread="([^"]+)"[^>]*>/;
470 ($last_message) = map {/(\d+)/} $xml =~ /<Forum.+?lastMessage="([^"]+)"[^>]*>/;
471 }
472
473 my $reg_msg = qr~(?:</Message>
474 |<Message\s+id="m(\d+)"\s+unid="([^"]*)"(?:\s+invisible="([^"]*)")?(?:\s+archive="([^"]*)")?[^>]*>\s*
475 <Header>[^<]*(?:<(?!Name>)[^<]*)*
476 <Name>([^<]+)</Name>[^<]*(?:<(?!Category>)[^<]*)*
477 <Category>([^<]*)</Category>\s*
478 <Subject>([^<]+)</Subject>\s*
479 <Date\s+longSec="(\d+)"[^>]*>\s*
480 </Header>\s*(?:(<)/Message>|(?=(<)Message\s*)))~sx;
481
482 while ($xml =~ /<Thread id="t(\d+)">([^<]*(?:<(?!\/Thread>)[^<]*)*)<\/Thread>/g)
483 {
484 my ($tid, $thread) = ($1, $2);
485 my ($level, $cmno, @msg, @stack) = (0);
486
487 while ($thread =~ m;$reg_msg;g)
488 {
489 if (defined($10))
490 {
491 push @stack,$cmno if (defined $cmno);
492 push @msg, {
493 mid => $1,
494 unid => $2,
495 deleted => $3 || 0,
496 archive => $4 || 0,
497 name => $5,
498 cat => $6,
499 subject => $7,
500 time => $8,
501 level => $level++,
502 unids => [],
503 kids => [],
504 answers => 0
505 };
506
507 if (defined $cmno)
508 {
509 push @{$msg[$cmno] -> {kids}} => $#msg;
510 push @{$msg[$cmno] -> {unids}} => $2;
511 }
512 else
513 {
514 push @unids => $2;
515 }
516
517 $msg[$_] -> {answers}++ for (@stack);
518
519 $cmno=$#msg;
520
521 $msg[-1] -> {name} =~ s/&amp;/&/g;
522 $msg[-1] -> {cat} =~ s/&amp;/&/g;
523 $msg[-1] -> {subject} =~ s/&amp;/&/g;
524
525 }
526 elsif (defined ($9))
527 {
528 push @msg, {
529 mid => $1,
530 unid => $2,
531 deleted => $3 || 0,
532 archive => $4 || 0,
533 name => $5,
534 cat => $6,
535 subject => $7,
536 time => $8,
537 level => $level,
538 unids => [],
539 kids => [],
540 answers => 0
541 };
542
543 if (defined $cmno)
544 {
545 push @{$msg[$cmno] -> {kids}} => $#msg;
546 push @{$msg[$cmno] -> {unids}} => $2;
547 $msg[$cmno] -> {answers}++;
548 }
549 else
550 {
551 push @unids => $2;
552 }
553
554 $msg[$_] -> {answers}++ for (@stack);
555
556 $msg[-1] -> {name} =~ s/&amp;/&/g;
557 $msg[-1] -> {cat} =~ s/&amp;/&/g;
558 $msg[-1] -> {subject} =~ s/&amp;/&/g;
559 }
560 else
561 {
562 $cmno = pop @stack; $level--;
563 }
564 }
565
566 my $smsg = sort_thread (\@msg, $sorted); # sort messages
567 delete_messages ($smsg) unless ($deleted); # remove invisible messages
568
569 $threads{$tid} = $smsg if (@$smsg);
570 }
571
572 wantarray
573 ? (\%threads, $last_thread, $last_message, $dtd, \@unids)
574 : \%threads;
575 }
576
577 ### create_forum_xml_string () #################################################
578 #
579 # compose main file xml string
580 #
581 # Params: $threads - parsed threads (see also 'get_all_threads')
582 # $params - hashref (see doc for details)
583 #
584 # Return: scalarref of the xml string
585 #
586 sub create_forum_xml_string ($$) {
587 my ($threads, $param) = @_;
588 my ($level, $thread, $msg);
589
590 my $xml =
591 '<?xml version="1.0" encoding="UTF-8"?>'."\n"
592 . '<!DOCTYPE Forum SYSTEM "'.$param -> {dtd}.'">'."\n"
593 . '<Forum lastMessage="'.$param -> {lastMessage}.'" lastThread="'.$param -> {lastThread}.'">';
594
595 for $thread (sort {$b <=> $a} keys %$threads) {
596 $xml .= '<Thread id="t'.$thread.'">';
597 $level = -1;
598
599 for $msg (@{$threads -> {$thread}}) {
600 $xml .= '</Message>' x ($level - $msg -> {level} + 1) if ($msg -> {level} <= $level);
601
602 $level = $msg -> {level};
603 $xml .=
604 '<Message id="m'.$msg -> {mid}.'"'
605 . ' unid="'.$msg -> {unid}.'"'
606 . (($msg -> {deleted})?' invisible="1"':'')
607 . (($msg -> {archive})?' archive="1"':'')
608 . '>'
609 . '<Header>'
610 . '<Author>'
611 . '<Name>'
612 . plain($msg -> {name})
613 . '</Name>'
614 . '<Email />'
615 . '</Author>'
616 . '<Category>'
617 . ((length $msg -> {cat})?plain($msg -> {cat}):'')
618 . '</Category>'
619 . '<Subject>'
620 . plain($msg -> {subject})
621 . '</Subject>'
622 . '<Date longSec="'
623 . $msg -> {time}
624 . '"/>'
625 . '</Header>';
626 }
627
628 $xml .= '</Message>' x ($level + 1);
629 $xml .= '</Thread>';}
630
631 $xml.='</Forum>';
632
633 \$xml;
634 }
635
636 ### save_file () ###############################################################
637 #
638 # Save a file
639 #
640 # Params: $filename Filename
641 # $content File content as scalar reference
642 # Return: Status (1 - ok, 0 - error)
643 #
644 sub save_file ($$)
645 {
646 my ($filename, $content) = @_;
647 local *FILE;
648
649 open FILE, ">$filename.temp" or return;
650
651 unless (print FILE $$content)
652 {
653 close FILE;
654 return;
655 }
656
657 close FILE or return;
658
659 rename "$filename.temp", $filename or return;
660
661 1;
662 }
663
664 ################################################################################
665 #
666 # several time formatting routines
667 #
668 # hr_time
669 # 02. Januar 2001, 12:02 Uhr
670 #
671 # short_hr_time
672 # 02. 01. 2001, 12:02 Uhr
673 #
674 # long_hr_time
675 # Dienstag, 02. Januar 2001, 12:02:01 Uhr
676 #
677 # very_short_hr_time
678 # 02. 01. 2001
679 #
680 sub month($) {
681 (qw(Januar Februar), "M\303\244rz", qw(April Mai Juni Juli August September Oktober November Dezember))[shift (@_) - 1];
682 # ^^^^^^^^ - UTF8 #
683 }
684
685 sub hr_time ($) {
686 my (undef, $min, $hour, $day, $mon, $year) = germantime (shift);
687
688 sprintf ('%02d. %s %04d, %02d:%02d Uhr', $day, month($mon+1), $year+1900, $hour, $min);
689 }
690
691 sub short_hr_time ($) {
692 my (undef, $min, $hour, $day, $mon, $year) = germantime (shift);
693
694 sprintf ('%02d. %02d. %04d, %02d:%02d Uhr', $day, $mon+1, $year+1900, $hour, $min);
695 }
696
697 sub long_hr_time ($) {
698 my @wday = qw(Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag);
699 my ($sek, $min, $hour, $day, $mon, $year, $wday) = germantime (shift);
700
701 sprintf ('%s, %02d. %s %04d, %02d:%02d:%02d Uhr', $wday[$wday], $day, month($mon+1), $year+1900, $hour, $min, $sek);
702 }
703
704 sub very_short_hr_time($) {
705 my (undef, $min, $hour, $day, $mon, $year) = germantime (shift);
706
707 sprintf ('%02d. %02d. %04d', $day, $mon+1, $year+1900);
708 }
709
710 # keep 'require' happy
711 1;
712
713 #
714 #
715 ### end of Posting::_lib #######################################################

patrick-canterino.de