Added printer-friendly version of the HTML (directly from torrent)
[amuse-wiki:kolumbo-sandbox.git] / AmuseWiki / lib / AmuseWiki.pm
1 package AmuseWiki;
2 use strict;
3 use warnings;
4 use utf8;
5 use Encode;
6 use HTML::Entities;
7 use Dancer ':syntax';
8 use Dancer::Plugin::FlashMessage;
9 use Dancer::Plugin::TimeRequests;
10 use Data::Dumper;
11 use Image::ExifTool qw/ImageInfo/;
12 use Text::Muse qw/muse_to_html/;
13 use Text::Muse::Preprocessing qw/typography_filter linkify_filter/;
14 use Text::Muse::Wiki::Storage;
15 use Text::Muse::Wiki::Search qw/xapian_search_db/;
16 use File::Spec;
17 use File::Copy;
18 use File::Temp qw/tempfile/;
19 use File::Basename qw/fileparse/;
20 use Text::Muse::Utils qw/muse_naming_algo
21                          write_to_file
22                          read_file_as_string
23                          read_file_as_array/;
24 use YAML::XS qw/LoadFile Dump/;
25 use AmuseWiki::Helpers qw/process_diff_output build_rss create_tag_cloud/;
26 use AmuseWiki::BookBuilder qw/build_book/;
27 use AmuseWiki::Cleaner qw/html_to_muse/;
28 use Storable qw(lock_store lock_retrieve);
29
30 our $VERSION = '0.1';
31 my $MAXUPLOADFILESIZE = 1024 * 1024 * 3; # 4 Mb?
32
33
34 ################################################################################
35 #                             DIRECTORIES SETUP                                #
36 ################################################################################
37
38 my $locations = directory_setup();
39 debug Dumper($locations);
40
41 # when we start up, we read the db.
42 my ($archiveindex, $archiveindexts, $tagcloud);
43 init_db();
44
45 # repo init
46 my $repo = Text::Muse::Wiki::Storage->new($locations->{repo});
47 my $dirty = Text::Muse::Wiki::Storage->new($locations->{dirty});
48
49
50 ################################################################################
51 #                             SPECIAL BLOCK                                    #
52 ################################################################################
53
54 get '/' => sub {
55   forward '/special/index'
56 };
57
58 get '/special/:page' => sub {
59   if (my $content = _get_special_page(params->{page}, "html")){
60     return template 'special' => {thecontent => $content,
61                                   cloud => $tagcloud,
62                                   pagename => params->{page},
63                                   latests => $archiveindex->{latestpieces},
64                                  };
65   } elsif (session->{user}) {
66     my $page = muse_naming_algo(params->{page});
67     return redirect "/editspecial/$page"
68   }
69   else {
70     status 404;
71     send_file "404.html";
72   }
73 };
74
75 any ['get', 'post'] => '/editspecial/:page' => sub {
76   unless ((session->{authenticated}) &&
77           (session->{authenticated} eq config->{amusewiki_authkey})) {
78     return redirect '/login'
79   }
80   my $destpage = muse_naming_algo(params->{page});
81   if (request->is_get) {
82     params->{maintextarea} = _get_special_page(params->{page})
83   }
84   # filters # duplicated code with the postform
85   if ((params->{maintextarea}) && (! (params->{notypography}))) {
86     params->{maintextarea} = typography_filter("en", params->{maintextarea})
87   }
88   if (params->{linkify}) {
89     params->{maintextarea} = linkify_filter(params->{maintextarea});
90   }
91   my ($body, $directives);
92   if (params->{maintextarea}) {
93     ($body, $directives) = muse_to_html(params->{maintextarea});
94   };
95
96   # we're ready, go.
97   if (params->{commit}
98       && params->{maintextarea}) {
99     manage_special_page($destpage, params->{maintextarea});
100     my $redirect = "/special/" . params->{page};
101     return redirect $redirect;
102   }
103   return template 'editspecial', { wantseditor => 1,
104                                    preview => $body,
105                                  };
106 };
107
108
109 ################################################################################
110 #                             ADMIN BLOCK                                      #
111 ################################################################################
112
113
114 get '/login' => sub {
115   if (session->{authenticated}) {
116     flash alreadyloggedin => 1;
117     return redirect '/';
118   } else {
119     template 'login';
120   }
121 };
122
123 get '/logout' => sub {
124   session->destroy;
125   return redirect '/';
126 };
127
128 post '/login' => sub {
129   my $user = params->{username};
130   my $passwd = params->{password};
131   if ((exists config->{'amusewiki_users'}->{$user}) &&
132       $passwd eq config->{'amusewiki_users'}->{$user}) {
133     session 'authenticated' => config->{amusewiki_authkey};
134     session 'user' => $user;
135     return redirect '/admin';
136   } else {
137     template 'login' => { failed => 1 }
138   }
139 };
140
141 get '/admin' => sub {
142   unless ((session->{authenticated}) &&
143           (session->{authenticated} eq config->{amusewiki_authkey})) {
144     return redirect '/login'
145   }
146   my $gitlog = $dirty->show_diff_between_branch_with_log('master', 'dirty');
147   my $diff;
148   my $pending;
149   my $err = {};
150   unless (param "ok") {
151     if (defined $gitlog) {
152       if ($gitlog ne "") {
153         $diff = process_diff_output(encode_entities($gitlog));
154         $pending = 1;
155       }
156     } else {
157       # this should not happen anymore. With the lock it would just wait.
158       $err->{gitlocked} = 1;
159     }
160   }
161   template 'gitlog' => { commits => $diff,
162                          err => $err,
163                          pending => $pending,
164                        };
165 };
166
167 get config->{wikiprefix} . '/:name/history' => sub {
168   unless ((session->{authenticated}) &&
169           (session->{authenticated} eq config->{amusewiki_authkey})) {
170     return redirect '/login';
171   }
172   my $target = muse_naming_algo(params->{name});
173   my $gitlog = $dirty->show_history_for_page($target);
174   my $diff;
175   my $err = {};
176   if (defined $gitlog) {
177     $diff = process_diff_output(encode_entities($gitlog));
178   } else {
179     $err->{gitlocked} = 1;
180   }
181   template 'gitlog' => { commits => $diff,
182                          err => $err,
183                        };
184 };
185
186 post '/admin' => sub {
187   unless ((session->{authenticated}) &&
188           (session->{authenticated} eq config->{amusewiki_authkey})) {
189     return redirect '/login';
190   }
191   my $output;
192   if (params->{approvequeue}) {
193     $output = $dirty->merge_dirty_branch_in_master('dirty');
194   }
195   template 'gitlog' => { commandout => $output };
196 };
197
198
199 ################################################################################
200 #                             REGULAR ROUTES                                   #
201 ################################################################################
202
203 get '/library' => sub {
204   forward '/titles';
205 };
206
207 foreach my $key (keys %{config->{amusewiki_indexmap}}) {
208   get "/$key" => sub {
209     update_db();
210     template $key => {
211                       sortedlist => $archiveindex->{$key},
212                       pagename => config->{amusewiki_indexmap}->{$key}->{desc},
213                       nav => $key,
214                      }
215   };
216
217   get qr{/$key/(.+?)(.html)?} => sub {
218     my ($name, $ext) = splat;
219     my $hash = $key . '-as-hash';
220     my $realname = muse_naming_algo($name);
221
222     unless (exists $archiveindex->{$hash}->{$realname}) {
223       status 404;
224       return send_file "404.html";
225     }
226     params->{name} = $archiveindex->{$hash}->{$realname}->{name};
227     template "single$key" => {
228                               sortedlist => $archiveindex->{$hash}->{$realname},
229                               nav => $key,
230                              }
231   };
232 };
233
234 prefix config->{wikiprefix};
235
236 # images:
237 get qr{/([^\/]+)(\.(png|jpeg))} => sub {
238   my ($name, $extwithdot, $ext) = splat;
239   # ask the dirty resolver. Dirty is supposed to be ahead of master
240   my $dirtypath = $dirty->resolver($name, $ext);
241   if (-f $dirtypath->{fullpath}) {
242     send_file($dirtypath->{fullpath},
243               system_path => 1)
244   } else {
245     status 404;
246     return "File not found";
247   }
248 };
249
250 get qr{/([^\/]+?)(\.(a4.pdf|lt.pdf|pdf|tex|html|muse|epub))?} => sub {
251   my ($name, $extwithdot, $ext) = splat;
252   $ext = $ext || "html";
253   # normalize
254   my $canonical = muse_naming_algo($name);
255   # it's an "?edit"? good, don't ask the resolver nor the db, just try
256   # to fetch the existing file and serve the form
257   if (exists params->{edit}) {
258     params->{maintextarea} = $dirty->get_muse_file_for($canonical);
259     params->{name} = $canonical;
260     return template 'postform',
261       { upload_url => uri_for(config->{wikiprefix} . '/' . $canonical),
262         wantseditor => 1,
263       },
264   };
265   my $file; # the file to send
266   # first we see if it's in the db, and if it's not, we try to reload
267   # it, maybe it's not synced
268   unless (exists $archiveindex->{meta}->{$canonical}) {
269     debug "$canonical not in the db, trying to reload it";
270     update_db();
271   }
272   if (exists $archiveindex->{meta}->{$canonical}) {
273     $file = File::Spec->catfile($archiveindex->{meta}->{$canonical}->{path},
274                                 $archiveindex->{meta}->{$canonical}->{file}
275                                 . "." . $ext);
276     # check if it's been deleted
277     if (my $deletion = $archiveindex->{meta}->{$canonical}->{DELETED}) {
278       return template 'error' => { deletedpage => $deletion };
279     }
280     unless (-f $file) {
281       return template 'error', { supposedherebutnotfound => 1 }
282     }
283   } else {
284     return redirect (uri_for(config->{wikiprefix} . '/' . $canonical) . "?edit");
285   }
286   
287   if ($ext eq "html") {
288     if (exists params->{print}) {
289       send_file($file,
290                 system_path => 1);
291     } else {
292     # process the HTML, we reuse the layout
293       my $text = generate_html($file);
294       my $directives = $archiveindex->{meta}->{$canonical};
295       return template 'page' => {
296                                  directives => $directives,
297                                  text => $text,
298                                 };
299     }
300   } elsif ($ext eq "epub") {
301     send_file($file,
302               system_path => 1,
303               content_type => 'application/epub+zip');
304   } elsif ($ext eq "tex") {
305     send_file($file,
306               system_path => 1,
307               content_type => 'application/x-tex;charset=UTF-8');
308   } elsif ($ext eq "muse") {
309     send_file($file,
310               system_path => 1,
311               content_type => 'text/plain;charset=UTF-8');
312   } else {
313     send_file($file, system_path => 1);
314   }
315 };
316
317 ################################################################################
318 #                             UPLOAD BLOCK                                     #
319 ################################################################################
320
321 # general form for uploading
322 post '/:name' => sub {
323   # first, important thing, we clean up the name and save it in the params
324   my $canonical = muse_naming_algo(params->{name});
325   params->{name} = $canonical;
326   # here we dump the session and restore it later.
327   my $contrib = session 'contrib' || {};
328   unless (exists $contrib->{$canonical}) {
329     $contrib->{$canonical} = { preview => 0,
330                                ups => [],
331                              };
332   }
333   my $err = {};
334   my $lastupload = {};
335   my $upload_url = uri_for(config->{wikiprefix} . '/' . $canonical);
336   if (params->{hitme}) {
337     return "Go away, spammer"
338   }
339   my $canupload;
340   if ($contrib->{$canonical}->{preview} || params->{preview}) {
341     $canupload = 1;
342   }
343   # see if we have upload. It looks nice
344   if ((my $file = upload('Upload')) && $canupload) {
345     my $safeimage = process_image($file, $canonical);
346     if (! defined $safeimage) {$err->{errorgitlocked} = 1 }
347     elsif      (! $safeimage) { $err->{uploadinvalid} = 1 }
348     elsif        ($safeimage) {
349       # we save the attachments in the session->{contrib}, under {$canonical}->ups
350       push @{$contrib->{$canonical}->{ups}}, $safeimage;
351       $lastupload->{source} = $file->filename;
352       $lastupload->{dest} = $safeimage;
353     }
354   }
355   # filters
356   if ((params->{maintextarea}) && (! (params->{notypography}))) {
357     params->{maintextarea} = typography_filter("en", params->{maintextarea})
358   }
359   if (params->{linkify}) {
360     params->{maintextarea} = linkify_filter(params->{maintextarea});
361   }
362   my ($body, $directives);
363   if (params->{maintextarea}) {
364     ($body, $directives) = muse_to_html(params->{maintextarea});
365   };
366   ### start conditional to see what we can do
367   if (params->{preview} || (! params->{commit})) {
368     unless ($contrib->{$canonical}->{preview}) {
369       $contrib->{$canonical}->{preview} = time;
370     }
371   }
372   elsif (params->{commit}) {
373     # it wants a commit.
374     if (!$contrib->{$canonical}->{preview}) {
375       $err->{nopreview} = 1;
376       $contrib->{$canonical}->{preview} = time;
377     }
378     elsif ((time - $contrib->{$canonical}->{preview}) < 10) { 
379       $err->{toofast} = 1; }
380     elsif (! params->{messagetolibs})  { $err->{nomsg} =  1; }
381     elsif (! params->{usernameforgit}) { $err->{nouser} = 1; }
382     elsif (! params->{maintextarea})   { $err->{notext} = 1; }
383     elsif (((length params->{messagetolibs}) > 500) || # don't write a book
384            ((length params->{usernameforgit}) > 50) || # it's just a name
385            ((length params->{maintextarea}) > $MAXUPLOADFILESIZE)) {
386       $err->{overflow} = 1; }
387     elsif ((length params->{maintextarea}) < 10) { $err->{tooshort} = 1; }
388     elsif (! $body) { $err->{notext} = 1; }
389     elsif (request->is_post &&
390            (! (keys %$err))) {
391       my $commit_result = process_text_upload($dirty,
392                                               $canonical,
393                                               params->{maintextarea},
394                                               $contrib->{$canonical}->{ups},
395                                               params->{usernameforgit},
396                                               params->{messagetolibs});
397       # all ok
398       if (defined $commit_result) {
399         delete $contrib->{$canonical};
400         session 'contrib' => $contrib;
401         flash committed => $commit_result;
402         return redirect '/';
403       }
404       else {
405         $err->{errorgitlocked} = 1;
406       }
407     }
408   }
409   ### end of checking. Return the preview (as the commit has already
410   ### been caught
411   unless (keys %$err) {
412     $err = undef;
413   }
414   session 'contrib' => $contrib; 
415   template 'postform', { preview => $body,
416                          directives => $directives,
417                          canupload => $canupload,
418                          lastupload => $lastupload, # a message
419                          attachments => $contrib->{$canonical}->{ups},
420                          upload_url => $upload_url,
421                          wantseditor => 1,
422                          err => $err }
423 };
424
425
426
427 ################################################################################
428 #                             BOOK BUILDER                                     #
429 ################################################################################
430
431 prefix undef;
432
433 any ["get", "post"] => '/bookbuilder' => sub {
434   my %errs;
435   my %params = params;
436   my $books = session "books";
437   #  debug \%params;
438   unless ($books && (ref($books) eq "ARRAY")) {
439     $books = [];
440   }
441   if (params->{addme}) {
442     push @$books, muse_naming_algo(params->{addme});
443   };
444   # First check if an action is required
445   # this is not fun, but I can't see a better option.
446   foreach my $k (keys %params) {
447     if ($k =~ m/^movedw(\d+)$/) {
448       my $i = $1;
449       if (exists $books->[$i]) {
450         list_movedown($books, $i)
451       }
452     }
453     elsif ($k =~ m/^moveup(\d+)$/) {
454       my $i = $1;
455       if (exists $books->[$i]) {
456         list_moveup($books, $i)
457       }
458     }
459     elsif ($k =~ m/^delete(\d+)$/) {
460       my $i = $1;
461       if (exists $books->[$i]) {
462         splice(@$books, $i, 1);
463       }
464     }
465     elsif ($k =~ m/^deleteall$/) {
466       @$books = ();
467     }
468   }
469   # wash the parameters.
470   my $bboptions = {};
471   # we put the cleaned vars in $bboptions, and we pass that to the builder.
472   # signature checking
473   $bboptions->{minsig} = checksig(param("signmin"));
474   $bboptions->{maxsig} = checksig(param("signmax"));
475   if (($bboptions->{minsig} || $bboptions->{minsig}) &&
476       ($bboptions->{maxsig} - $bboptions->{minsig}) < 30) {
477     $errs{wrongsigrange} = 1;
478   }
479
480   # modes
481   if (param("imposed"))                    { $bboptions->{imposed} = 1   };
482   if (param("tocwanted") || (@$books > 1)) { $bboptions->{tocwanted} = 1 };
483
484   if (param("font"))  { $bboptions->{font}  = checkmode(param("font"))  };
485   if (param("paper")) { $bboptions->{paper} = checkmode(param("paper")) };
486
487   # wash the books
488   my $washed_books = [];
489   foreach my $b (@$books) {
490     my $musepath = $repo->resolver($b, "muse");
491     if (-f $musepath->{fullpath}) {
492       push @$washed_books, $musepath->{fullpath};
493     } else {
494       $errs{notexists} .= $b . " ";
495     }
496   }
497   #  debug $washed_books;
498   if ((params->{build}) && (! params->{collectionname})) {
499     $errs{noname} = 1;
500   }
501   # do it.
502   my $created;
503   if ((params->{build}) &&
504       (params->{collectionname}) &&
505       (@$washed_books) && (! %errs)) {
506     my $outfile = find_file_name_free($locations->{customtexdir},
507                                       params->{collectionname});
508
509     debug $washed_books;
510     debug $bboptions;
511     # the $washed_books is shifted in the build_book functions
512     # AT THIS POINT ALL THE OPTIONS PASSED SHOULD BE CLEANED AND CHECKED
513     session "latestbook" => build_book($outfile,
514                                        $locations->{templatedir},
515                                        $washed_books,
516                                        $bboptions);
517     $created = 1;
518   }
519
520   # save the books in the session or reset it
521   my $wantsform;
522   if (@$washed_books) {
523     session "books" => $books;
524     $wantsform = 1;
525   } else {
526     session "books" => [];
527   }
528   # check the formats
529   my $formats = {};
530   if (my $latest = session->{latestbook}) {
531     #    debug session->{latestbook};
532     my $basenameout = File::Basename::basename($latest);
533     my $pdf = File::Spec->catfile(config->{amusewiki_custom_tex_dir},
534                                   $basenameout . ".pdf");
535     if (-f $pdf) { $formats->{pdf} = $basenameout };
536     my $tex = File::Spec->catfile(config->{amusewiki_custom_tex_dir},
537                                   $basenameout . ".tex");
538     if (-f $tex) { $formats->{tex} = $basenameout };
539     if ($created) {
540       flash 'bookcreated' => $tex;
541       return redirect '/';
542     }
543   }
544
545   # render the template and exit
546   template 'bookbuilder' => { errs => \%errs,
547                               wantsform => $wantsform,
548                               formats => $formats,
549                             };
550 };
551
552 get '/bookbuilder/clearqueue' => sub {
553   if (session->{latestbook}) {
554     session 'latestbook' => undef;
555   };
556   redirect '/';
557 };
558
559 get '/bookbuilder/status' => sub {
560   my $destdir = $locations->{customtex};
561   opendir(my $dh, $destdir)
562     or return "Cannot open the directory with the files! $!\n";
563   my @pdfs = grep {/\.pdf$/ &&
564                      -f File::Spec->catfile($destdir, $_)
565                    } readdir($dh);
566   closedir($dh);
567   @pdfs = sort { -C File::Spec->catfile($destdir, $a) <=>
568                    -C File::Spec->catfile($destdir, $b) } (@pdfs);
569   template 'bookbuilderstatus' => {
570                                    pdfs => \@pdfs
571                                   };
572 };
573
574
575
576 ################################################################################
577 #                             LEGACY BLOCK                                     #
578 ################################################################################
579
580 if (config->{amusewiki_legacyurls}) { # it's actually a file
581   # html
582   get '/HTML/:name' => sub {
583     my $name = params->{name};
584     my $target = config->{wikiprefix} . '/' . params->{name};
585     debug $target;
586     return forward $target;
587   };
588   # pdf
589   get '/pdfs/a4/*_a4.pdf' => sub {
590     my ($basename) = splat;
591     my $target = config->{wikiprefix} . '/' . $basename . ".pdf";
592     return forward $target;
593   };
594   get '/pdfs/letter/*_letter.pdf' => sub {
595     my ($basename) = splat;
596     my $target = config->{wikiprefix} . '/' . $basename . ".pdf";
597     return forward $target;
598   };
599   get '/pdfs/a4_imposed/*_a4_imposed.pdf' => sub {
600     my ($basename) = splat;
601     my $target = config->{wikiprefix} . '/' . $basename . ".a4.pdf";
602     return forward $target;
603   };
604   get '/pdfs/letter_imposed/*_letter_imposed.pdf' => sub {
605     my ($basename) = splat;
606     my $target = config->{wikiprefix} . '/' . $basename . ".lt.pdf";
607     return forward $target
608   };
609   # epub
610   get '/epub/*.epub' => sub {
611     my ($basename) = splat;
612     my $target = config->{wikiprefix} . '/' . $basename . ".epub";
613     return forward $target
614   };
615   get '/print/*.html' => sub {
616     my ($basename) = splat;
617     my $target = config->{wikiprefix} . '/' . $basename . ".html";
618     return forward $target
619   };
620   get '/text-index.html' => sub {
621     return forward '/titles';
622   };
623   get '/author-index.html' => sub {
624     return forward '/authors';
625   };
626   get '/authors.html' => sub {
627     return forward '/authors';
628   };
629   get '/topics.html' => sub {
630     return forward '/topics';
631   };
632
633 }
634
635 ################################################################################
636 #                              SEARCH ENGINE                                   #
637 ################################################################################
638
639 get '/search/:query' => sub {
640   forward '/search';
641 };
642
643
644 any ['get', 'post'] => '/search' => sub {
645   my ($result, $total, $matchany);
646   if (param 'query') {
647     my $config = config->{amusewiki_xapian_index_map};
648     my $querystring = params->{query}; # we just strip the "<>", just to be on the
649                                        # safe side, but probably not needed
650     # just not to pass garbage
651     if (param 'matchany') { $matchany = 1 };
652     $querystring =~ s/(<|>)//g;
653     if ($querystring =~ m/\w/) {
654       debug " Query: $querystring";
655       ($result, $total) = xapian_search_db($locations->{xapian},
656                                            $config,
657                                            $querystring,
658                                            $matchany);
659     }
660   }
661   if (ref($result) eq 'ARRAY') {
662     foreach my $text (@$result) {
663       $text->{directives} = $archiveindex->{meta}->{$text->{pagename}};
664     }
665   }
666   
667   template 'search' => {
668                         results => $result,
669                         total => $total,
670                         nav => "search",
671                        };
672 };
673
674
675 any ['get', 'post'] => '/new' => sub {
676   my %params = params;
677   if (params->{title} &&
678       params->{author} &&
679       params->{SORTtopics} &&
680       params->{source} &&
681       params->{go} &&
682       params->{maineditor}) {
683     my $body;
684     my $destpage = config->{wikiprefix} . "/" .
685       muse_naming_algo(params->{author} . " " . params->{title});
686     foreach my $directive (@{config->{amusewiki_defined_directives}}) {
687       next unless params->{$directive};
688       debug $directive;
689       debug params->{$directive};
690       my $text = html_to_muse(params->{$directive});
691       debug $text;
692       $text =~ s/\r*\n/ /g; # it's a directive, no \n
693       $body .= "#" . $directive . " " . $text . "\n";
694     }
695     $body .= html_to_muse(params->{maineditor});
696     debug $body;
697     return forward $destpage,
698       {maintextarea => $body },
699         { method => 'POST' };
700   };
701   my (@topics, @authors);
702   if (exists $archiveindex->{authors}) {
703     foreach my $item (@{$archiveindex->{authors}}) {
704       push @authors, $item->{name};
705     }
706   }
707   if (exists $archiveindex->{topics}) {
708     foreach my $item (@{$archiveindex->{topics}}) {
709       push @topics, $item->{name};
710     }
711   }
712
713   debug Dumper(\%params);
714   template 'newentry' => {
715                           wantsjquery => 1,
716                           wantsck => 1,
717                           jsontopics => to_json(\@topics),
718                           jsonauthors => to_json(\@authors)
719                          };
720 };
721
722
723 ################################################################################
724 #                             END OF ROUTES, start helpers                     #
725 ################################################################################
726
727
728 sub process_image {
729   my ($danceruploaded, $target) = @_;
730   debug $danceruploaded->size;
731   debug $danceruploaded->tempname;
732   # validate the size
733   return 0 unless (($danceruploaded->size > 0) &&
734                        ($danceruploaded->size < $MAXUPLOADFILESIZE));
735
736   # validate the type
737   my $imginfo = ImageInfo($danceruploaded->tempname, 'FileType');
738   debug Dumper($imginfo);
739   my $ext = lc $imginfo->{FileType};
740   return 0 unless ($ext and (($ext eq "png") or
741                              ($ext eq "jpeg")));
742   
743   my ($fhx, $tmpfile) = tempfile();
744   $danceruploaded->copy_to($tmpfile);
745   # all ok? good, store in the git directory, without overwriting and
746   # without commiting. We return the name.
747   return $dirty->drop_attachment_in_tree($tmpfile, $target, $ext);
748 }
749
750 sub init_db {
751   my $db = $locations->{db};
752   $archiveindexts = get_m_time($db);
753   debug "Loading $db";
754   $archiveindex = lock_retrieve($db);
755   debug "Done";
756
757   my $rss_output = build_rss($archiveindex->{sortedbydate},
758                              config->{amusewiki_rss});
759
760   write_to_file($locations->{rss}, $rss_output->{rss});
761   $archiveindex->{latestpieces} = $rss_output->{latests};
762
763   $tagcloud = create_tag_cloud(prepare_hash_for_cloud($archiveindex),
764                                config->{amusewiki_min_cloud_items});
765   #  debug $tagcloud;
766   # the Sitemap
767   my $sitemap = $locations->{sitemap};
768   if ((time - get_m_time($sitemap)) > (60 * 60 * 12) ||
769       ((-s $sitemap) == 0 )) {
770     update_site_map($sitemap, $archiveindex->{meta});
771   }
772 }
773
774 sub get_m_time {
775   my $file = shift;
776   if (-e $file) {
777     my $time = (stat($file))[9];
778     return $time;
779   } else {
780     return 0;
781   }
782 }
783
784 sub update_db {
785   my $db = $locations->{db};
786   debug "Checking if the db is up-to-date";
787   return unless ((get_m_time($db)) > $archiveindexts);
788   debug "Updating $db";
789   undef %$archiveindex; # clear all, just to be sure
790   $archiveindex = undef; # even the pointer
791   init_db();
792 }
793
794 sub process_text_upload {
795   my ($dirty, $canonical, $text, $attachments, $username, $message) = @_;
796   $text =~ s/\r\n/\n/gs;
797   $text =~ s/\t/    /gs;
798   $message =~ s/\r\n/\n/gs;
799   $message =~ s/^\s+$//;
800   $message =~ s/\n+/\n/;
801   if ($username =~ m/(\w+)/) {
802     $username = $1;
803   } else {
804     $username = "Anonymous";
805   }
806   my $commitmsg =  "*** " . $username . " ***\n\n" . $message .
807     "\n\n" . $canonical . "\n";
808   my @attachs;
809   if ((defined $attachments)
810       && (ref($attachments) eq "ARRAY") &&
811       (@$attachments >= 0)) {
812     debug Dumper($attachments);
813     $commitmsg .= join("\n", @$attachments);
814     @attachs = @$attachments;
815   }
816   $commitmsg .= "\n"; # be sure to end with a newline
817   # dump the things in temp files
818   my ($fh, $commitmsgfile) = tempfile();
819   write_to_file($commitmsgfile, $commitmsg, "\n"); # add a newline
820   my ($fhx, $musetmpfile) = tempfile();
821   write_to_file($musetmpfile, $text, "\n");  # add a newline
822   unless (-T $commitmsgfile && -T $musetmpfile) {
823     warn "Some genius uploaded binary data";
824     return 0;
825   }
826   my $exit = $dirty->dump_muse_file_and_commit($canonical, $musetmpfile, \@attachs,
827                                               $commitmsgfile);
828   return $exit;
829 }
830
831 sub checksig {
832   my $sig = shift;
833   my $washed = 0;
834   if ($sig && $sig =~ m/(\d+)/) {
835     $washed = $1;
836     # if doesn't divide by 4 without modulo, set it to zero.
837     if ($washed % 4) {
838       $washed = 0;
839     }
840   }
841   return $washed;
842 }
843
844 sub checkmode {
845   my $mode = shift;
846   my $washed;
847   if ($mode =~ m/(\w+)/) {
848     $mode = $1;
849     if (config->{amusewiki_bookbuilder_def_modes}->{$mode}) {
850       $washed = $mode;
851     }
852   }
853   return $washed;
854 }
855
856 sub find_file_name_free {
857   my ($dir, $dirtyname) = @_;
858   my $basename = muse_naming_algo($dirtyname);
859   my $number = 1;
860   my $fullname = File::Spec->catfile($dir, $basename . "-" . $number . ".tex");
861   while (-e $fullname) {
862     $number++;
863     $fullname = File::Spec->catfile($dir, $basename . "-" . $number . ".tex");
864   }
865   debug $fullname;
866   write_to_file($fullname, "% Created by AmuseWiki::BookBuilder $$\n");
867   return $fullname;
868 }
869
870 sub swap_element {
871   my ($arrayref, $first, $second) = @_;
872   my $tmp = $arrayref->[$second];
873   $arrayref->[$second] = $arrayref->[$first];
874   $arrayref->[$first] = $tmp;
875 }
876
877 sub list_moveup {
878   my ($booklist, $index) = @_;
879   my $newindex = $index;
880   if ($index > 0) {
881     $newindex--;
882     if ($booklist->[$newindex]) {
883       swap_element($booklist, $index, $newindex);
884     }
885   }
886 }
887
888 sub list_movedown {
889   my ($booklist, $index) = @_;
890   my $newindex = $index;
891   if ($index < (scalar(@$booklist))) {
892     $newindex++;
893     if ($booklist->[$newindex]) {
894       swap_element($booklist, $index, $newindex);
895     }
896   }
897 }
898
899 sub manage_special_page {
900   my ($page, $text) = @_;
901   $text =~ s/\r\n/\n/gs;
902   my $destfile = File::Spec->catfile($locations->{specials},
903                                      muse_naming_algo($page));
904   # save the old one.
905   if (-f $destfile) {
906     copy $destfile, $destfile . "_pre_" . _getsuffix();
907   }
908   # write the muse (source)
909   write_to_file($destfile, $text);
910   my ($body, $directives) = muse_to_html($text);
911   # write the html
912   write_to_file($destfile . ".html", $body);
913   return
914 }
915
916 sub _getsuffix {
917   my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
918     localtime(time);
919   $year = sprintf("%02d", $year % 100);;
920   $mon = sprintf("%02d", $mon + 1);
921   $mday = sprintf("%02d", $mday);
922   $hour = sprintf("%02d", $hour);
923   $min = sprintf("%02d", $min);
924   $sec = sprintf("%02d", $sec);
925   my $result = "-" . $year . "-" . $mon . "-" . $mday . 
926     "-" . $hour . "-" . $min . "-" . $sec;
927   # letter_imposed
928   # 12-05-14-21-28
929   return $result;
930 }
931
932 sub _get_special_page {
933   my ($page, $format) = @_;
934   my $file = File::Spec->catfile($locations->{specials},
935                                  muse_naming_algo(params->{page}));
936   if ($format && ($format eq 'html')) {
937     $file .= ".html";
938   }
939   if (-f $file) {
940     return read_file_as_string($file);
941   }
942 }
943
944 sub update_site_map {
945   my ($file, $targets) = @_;
946   my @output;
947   # first the indices
948   my $prefix = config->{amusewiki_rss}->{link} . "/";
949   foreach my $k (keys %{config->{amusewiki_indexmap}}) {
950     push @output, $prefix . $k . "\n"; 
951   }
952   # next the title
953   $prefix = config->{amusewiki_rss}->{link} . config->{wikiprefix} . "/";
954   foreach my $item (keys %$targets) {
955     push @output, $prefix . $item . "\n";
956   };
957
958   if (my $oldurls = $locations->{legacyurls}) {
959     push @output, read_file_as_array($oldurls);
960   }
961   write_to_file($file, @output);
962 }
963
964 sub generate_html {
965   my $file = shift;
966   my $rawtext = read_file_as_string($file);
967   my $start = index $rawtext, config->{amusewiki_html_markers}->{start};
968   my $stop = index $rawtext, config->{amusewiki_html_markers}->{stop}, $start;
969   return unless (($start >= 0) and  ($stop >= 0));
970   my $length = $stop - $start +
971     length(config->{amusewiki_html_markers}->{stop});
972   return substr $rawtext, $start, $length;
973 }
974
975 sub directory_setup {
976   my %out;
977   $out{repo} = File::Spec->catdir(config->{amusewiki_data_root},
978                                   config->{amusewiki_build_repo});
979
980   $out{dirty} = File::Spec->catdir(config->{amusewiki_data_root},
981                                    config->{amusewiki_dirty_repo});
982
983   $out{xapian} = File::Spec->catdir(config->{amusewiki_data_root},
984                                     config->{amusewiki_xapian_db});
985
986   $out{customtex} = File::Spec->catdir(config->{public},
987                                        "customtex"); # hardcode this one.
988
989   $out{templatedir} = File::Spec->catdir($out{repo},
990                                          config->{amusewiki_repo_templates_dir});
991
992   $out{specials} = File::Spec->catdir(config->{amusewiki_data_root},
993                                       config->{amusewiki_special_cache});
994   # check the directories
995   foreach my $dir (values %out) {
996     die "$dir doesn't exists\n" unless -d $dir;
997   }
998   
999   if (config->{amusewiki_legacyurls}) {
1000     $out{legacyurls} = File::Spec->catfile(config->{amusewiki_data_root},
1001                                            config->{amusewiki_legacyurls})
1002   }
1003   $out{sitemap} = File::Spec->catfile(config->{public},
1004                                       "sitemap.txt");
1005   $out{rss} = File::Spec->catfile(config->{public},
1006                                   "rss.xml");
1007   $out{db} = File::Spec->catfile($out{repo},
1008                                  config->{amusewiki_text_db});
1009   die "Missing db, cannot start\n" unless -f $out{db};
1010   # sanity check
1011   return \%out;
1012 }
1013
1014 sub prepare_hash_for_cloud {
1015   my $meta = shift;
1016   my @hashes_to_scan;
1017   my $mapping = config->{amusewiki_indexmap};
1018   foreach my $k (keys %$mapping) {
1019     if ($mapping->{$k}->{sortwith} =~ m/^SORT/) {
1020       push @hashes_to_scan, $k;
1021     }
1022   }
1023   return unless (@hashes_to_scan);
1024
1025   my %rawcloud;
1026   my $min = config->{amusewiki_min_cloud_items};
1027   foreach my $hash (@hashes_to_scan) {
1028     # check if it exists. It *must* exists
1029     die "No hash with $hash as key in the db"
1030       unless exists $archiveindex->{$hash};
1031     foreach my $item (@{$archiveindex->{$hash}}) {
1032       my $name = $item->{name};
1033       my $cleanurl = $item->{cleanurl};
1034       my $url = "/" . $hash . "/" . $item->{cleanurl};
1035       my $size = scalar @{$item->{titles}};
1036       next if $size < $min;
1037       if (exists $rawcloud{$cleanurl}) {
1038         next if ($rawcloud{$cleanurl}{size} > $size)
1039       }
1040       $rawcloud{$cleanurl} = {
1041                               name => $name,
1042                               url => $url,
1043                               size => $size,
1044                              };
1045     }
1046   }
1047   return \%rawcloud;
1048 }
1049
1050
1051 1;
1052