File Coverage

blib/lib/FAQ/OMatic.pm
Criterion Covered Total %
statement 31 759 4.0
branch 1 312 0.3
condition 1 200 0.5
subroutine 11 81 13.5
pod 0 69 0.0
total 44 1421 3.1


line stmt bran cond sub pod time code
1             ##############################################################################
2             # The Faq-O-Matic is Copyright 1997 by Jon Howell, all rights reserved. #
3             # #
4             # This program is free software; you can redistribute it and/or #
5             # modify it under the terms of the GNU General Public License #
6             # as published by the Free Software Foundation; either version 2 #
7             # of the License, or (at your option) any later version. #
8             # #
9             # This program is distributed in the hope that it will be useful, #
10             # but WITHOUT ANY WARRANTY; without even the implied warranty of #
11             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
12             # GNU General Public License for more details. #
13             # #
14             # You should have received a copy of the GNU General Public License #
15             # along with this program; if not, write to the Free Software #
16             # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.#
17             # #
18             # Jon Howell can be contacted at: #
19             # 6211 Sudikoff Lab, Dartmouth College #
20             # Hanover, NH 03755-3510 #
21             # jonh@cs.dartmouth.edu #
22             # #
23             # An electronic copy of the GPL is available at: #
24             # http://www.gnu.org/copyleft/gpl.html #
25             # #
26             ##############################################################################
27              
28 1     1   10315 use strict;
  1         3  
  1         53  
29              
30             ##
31             ## FAQ::OMatic.pm
32             ##
33             ## This module contains routines common to the various faqomatic cgi-bins.
34             ## It also loads FaqConfig.pm, which also defines variables in the
35             ## FAQ::OMatic:: namespace.
36             ##
37              
38             # THANKS to Andrew W. Nosenko for several patches
39             # for locale, russian translation, and bug fixes. Thanks also to
40             # Andrew for patiently waiting, what, EIGHT MONTHS until I finally
41             # got them plugged into the CVS tree. :v)
42              
43             package FAQ::OMatic;
44              
45 1     1   7 use Fcntl; # for lockFile. Not portable, but then neither is lockFile().
  1         5  
  1         338  
46              
47 1     1   909 use FAQ::OMatic::Item;
  1         5  
  1         46  
48 1     1   701 use FAQ::OMatic::Log;
  1         4  
  1         36  
49 1     1   8 use FAQ::OMatic::Appearance;
  1         1  
  1         1378  
50 1     1   7 use FAQ::OMatic::Bags;
  1         2  
  1         18  
51 1     1   5 use FAQ::OMatic::I18N;
  1         2  
  1         156  
52              
53             use vars # these are mod_perl-safe
54             # effectively constants
55 1         7068 qw($VERSION $USE_MOD_PERL),
56             # variables that get reset on every invocation
57 1     1   6 qw($theParams $theLocals);
  1         2  
58              
59             $VERSION = '2.719';
60              
61             # can't figure out how to get file-scoped variables in mod_perl, so
62             # we ensure that they're all file scoped by reseting them in dispatch.
63             sub reset {
64 0     0 0   $theParams = {};
65 0           $theLocals = {};
66             }
67              
68             sub getLocal {
69 0     0 0   my $localname = shift;
70 0           return $theLocals->{$localname};
71             }
72              
73             sub setLocal {
74 0     0 0   my $localname = shift;
75 0           my $localvalue = shift;
76 0           $theLocals->{$localname} = $localvalue;
77             }
78              
79             sub pageHeader {
80 0   0 0 0   my $params = shift || $theParams;
81 0           my $showLinks = shift;
82 0           my $suppressType = shift;
83              
84 0           return FAQ::OMatic::Appearance::cPageHeader($params,
85             $showLinks, $suppressType);
86             }
87              
88             sub pageFooter {
89 0     0 0   my $params = shift; # arg passed to Apperance::cPageFooter
90 0   0       my $showLinks = shift || []; # arg passed to Apperance::cPageFooter
91 0   0       my $isCached = shift || ''; # don't put gripes in the cached copies
92              
93 0           my $page = '';
94 0   0       my $userGripes = getLocal('userGripes') || '';
95 0 0 0       if (not $isCached and $userGripes ne '') {
96 0           $page.="

".gettext("Warnings:")."

\n".$userGripes."
\n";
97             }
98 0           push @{$showLinks}, 'faqomatic-home';
  0            
99 0           $page.=FAQ::OMatic::Appearance::cPageFooter($params, $showLinks);
100 0           return $page;
101             }
102              
103             # the name of the entire FAQ
104             sub fomTitle {
105 0     0 0   my $topitem = new FAQ::OMatic::Item('1');
106 0           my $title = $topitem->getTitle('undefokay');
107 0 0         if (not $title) {
108 0 0         if (FAQ::OMatic::Versions::getVersion('Items')) {
109             # (don't gripe if FAQ not installed yet)
110 0           FAQ::OMatic::gripe('note',
111             gettext("Your Faq-O-Matic would have a title if it had an item 1, which it will when you've run the installer.")
112             );
113             }
114 0           $title = gettext("Untitled Faq-O-Matic");
115             }
116 0           return $title;
117             }
118              
119             # a description of the page we're on right now
120             sub pageDesc {
121 0     0 0   my $params = shift;
122 0           my $cmd = commandName($params);
123 0           my $rt;
124              
125 0 0 0       $cmd = 'insertItem'
126             if (($cmd eq 'editItem') and ($params->{'_insert'}));
127 0 0 0       $cmd = 'insertPart'
128             if (($cmd eq 'editPart') and ($params->{'_insertpart'}));
129              
130 0   0       my $file = $params->{'file'} || '1';
131 0   0       my $item = new FAQ::OMatic::Item($params->{'file'}||'1');
132 0           my $title = $item->getTitle();
133 0           my $whatAmI = gettext($item->whatAmI());
134              
135 0           my $pageDescs = {
136             'authenticate' => gettext_noop("Log In"),
137             'changePass' => gettext_noop("Change Password"),
138             'editItem' => gettext_noop("Edit Title of %0 %1"),
139             'insertItem' => gettext_noop("New %0"), # special case -- varies editItem
140             'editPart' => gettext_noop("Edit Part in %0 %1"),
141             'insertPart' => gettext_noop("Insert Part in %0 %1"),
142             'moveItem' => gettext_noop("Move %0 %1"),
143             'search' => gettext_noop("Search"),
144             'stats' => gettext_noop("Access Statistics"),
145             'submitPass' => gettext_noop("Validate"),
146             'editModOptions' => gettext_noop("%0 Permissions for %1"),
147             'editBag' => gettext_noop("Upload bag for %0 %1")
148             };
149              
150 0   0       my $pd = $pageDescs->{$cmd} || '';
151 0 0         if ($cmd eq 'faq') {
    0          
152 0 0         $rt = $file eq "1" ? "" : $title;
153             } elsif ($pd) {
154 0           $rt = gettexta($pd, $whatAmI, $title);
155             } else {
156 0           $rt = "$cmd page";
157             }
158              
159 0 0         return $rt ? ": $rt" : "";
160             }
161              
162             sub keyValue {
163 0     0 0   my ($line) = shift;
164 0           my ($key,$value) = ($line =~ m/([A-Za-z0-9\-]*): (.*)$/);
165 0           return ($key,$value);
166             }
167              
168             # returns the name of the currently executing command module (was CGI)
169             sub commandName {
170 0   0 0 0   my $params = shift || $theParams;
171 0   0       return ($params->{'cmd'} || 'faq');
172             }
173              
174             sub shortdate {
175 0     0 0   my (@date) = localtime(time());
176 0           return sprintf("%02d/%02d/%02d %02d:%02d:%02d",
177             $date[5], $date[4], $date[3], $date[2], $date[1], $date[0]);
178             }
179              
180             # TODO we now have two stacktrace-collectors. Clean this up.
181             sub collectStackBacktrace {
182 0     0 0   my @stack_backtrace;
183 0           my $i = 0;
184 0           my ($package, $filename, $line, $subroutine);
185 0           my @a;
186 0           for ($i=0; ; ++$i)
187             {
188 0           @a = caller($i);
189 0 0         last if (!@a);
190 0           ($package, $filename, $line)= @a;
191 0           (undef, undef, undef, $subroutine) = caller($i+1);
192 0 0         if (!defined($subroutine))
193             {
194 0           $subroutine = '';
195             }
196 0           push(@stack_backtrace,
197             { 'package' => $package,
198             'filename' => $filename,
199             'line' => $line,
200             'subroutine' => $subroutine });
201             }
202 0           return @stack_backtrace;
203             }
204              
205             #
206             # sub gripe($severity, $msg, $is_show_stack_backtrace)
207             #
208             # Parameters:
209             # $severity Severity of message
210             # interesting severity values:
211             # 'note' appends msg to log
212             # 'debug' appends to log, tells user
213             # 'error' appends to log, tells user, aborts CGI
214             # 'problem' mails msg to $faqAdmin, appends to log, tells user
215             # 'abort' mails msg to $faqAdmin, appends to log, tells
216             # user, aborts CGI
217             # 'panic' mails trouble to $faqAdmin, $faqAuthor, appends to
218             # log, tells user, and aborts the CGI
219             # $msg Message itself
220             # $options->{'stack'}
221             # Is showing of stack backtrace needed? Boolean.
222             # $options->{'noentify'}
223             # Boolean. Gripe contains no user text, so it's not vulnerable
224             # to CSS, and we want the user to see some real HTML tags.
225             #
226             sub gripe {
227 0   0 0 0   my $severity = shift || 'problem';
228 0   0       my $msg = shift || '[gripe with no msg: '.join(':',caller()).']';
229 0   0       my $options = shift || {};
230              
231 0   0       my $is_show_stack_backtrace = $options->{'stack'} || '';
232 0   0       my $noentify = $options->{'noentify'} || '';
233              
234 0           my @stack_backtrace;
235 0           my $mailguys = '';
236 0   0       my $id = $FAQ::OMatic::Auth::trustedID || $theParams->{'id'} || '(noID)';
237              
238             # mail someone
239 0 0 0       if ($severity eq 'panic') {
    0          
240             # mail admin & author
241 0           $mailguys = $FAQ::OMatic::Config::adminEmail." ".$FAQ::OMatic::Config::authorEmail;
242             } elsif ($severity eq 'problem' or $severity eq 'abort') {
243             # mail admin
244 0           $mailguys = $FAQ::OMatic::Config::adminEmail;
245             }
246              
247 0 0         if ($is_show_stack_backtrace) {
248 0           @stack_backtrace = collectStackBacktrace();
249             }
250              
251 0 0         if ($mailguys ne '') {
252 0           my $message = "The \"".fomTitle()."\" Faq-O-Matic (v. $VERSION)\n";
253 0           $message.="maintained by $FAQ::OMatic::Config::adminEmail\n";
254 0           $message.="had a $severity situation.\n\n";
255 0           $message.="The command was: \"".commandName()."\"\n";
256 0           $message.="The message is: \"$msg\".\n";
257              
258             # TODO there are three backtrace-formatters in this function.
259             # factor them out into one named, parameterized function.
260 0 0         if ($is_show_stack_backtrace)
261             {
262 0           $message.="The stack backtrace:\n";
263 0 0         if (@stack_backtrace)
264             {
265 0           my $i;
266 0           for ($i=0; $i < @stack_backtrace; ++$i)
267             {
268 0           $message .= sprintf("\t%u: %s at %s line %u\n",
269             $i+1,
270             $stack_backtrace[$i]->{'subroutine'},
271             $stack_backtrace[$i]->{'filename'},
272             $stack_backtrace[$i]->{'line'});
273             }
274             }
275             else
276             {
277 0           $message .= "\t(unavailable)\n";
278             }
279             }
280              
281 0           $message.="The process number is: $$\n";
282 0           $message.="The user had given this ID: <$id>\n";
283 0   0       $message.="The browser was: <".($ENV{'HTTP_USER_AGENT'}||'undefined')
284             .">\n";
285 0           sendEmail($mailguys,
286             "Faq-O-Matic $severity Mail",
287             $message);
288             }
289              
290             # tell user
291 0 0         if ($severity ne 'note') {
292 0           my $userGripes = getLocal('userGripes');
293             # since we're submitting the msg to a web browser,
294             # and the messages often include things like
295             # "this input was weird: ", we
296             # need to sanitize the text (with entify) to avoid
297             # a cross-site scripting attack.
298 0 0         my $safeMsg = $noentify ? $msg : entify($msg);
299 0           $userGripes .= "
  • $safeMsg\n";
  • 300            
    301 0 0         if ($is_show_stack_backtrace)
    302             {
    303 0           $userGripes .= "

    The stack backtrace:\n";

    304 0 0         if (@stack_backtrace)
    305             {
    306 0           my $i;
    307 0           $userGripes .= "
      \n";
    308 0           for ($i = 0; $i < @stack_backtrace; ++$i)
    309             {
    310 0           $userGripes .=
    311             sprintf("\t
  • %s at %s line %u
  • \n",
    312             $stack_backtrace[$i]->{'subroutine'},
    313             $stack_backtrace[$i]->{'filename'},
    314             $stack_backtrace[$i]->{'line'});
    315             }
    316 0           $userGripes .= "\n"
    317             }
    318             else
    319             {
    320 0           $userGripes .= "\t(unavailable)\n";
    321             }
    322             }
    323            
    324 0           setLocal('userGripes', $userGripes);
    325             }
    326              
    327             # log to file
    328 0           open ERRORFILE, ">>$FAQ::OMatic::Config::metaDir/errors";
    329 0           print ERRORFILE FAQ::OMatic::Log::numericDate()
    330             ." $FAQ::OMatic::VERSION $severity "
    331             .commandName()
    332             ." $$ <$id> $msg";
    333              
    334 0 0         if ($is_show_stack_backtrace)
    335             {
    336 0           print(ERRORFILE '[Stack backtrace: ');
    337 0 0         if (@stack_backtrace)
    338             {
    339 0           my $i;
    340 0           for ($i=0; $i < @stack_backtrace; ++$i)
    341             {
    342 0 0         if ($i != 0)
    343             {
    344 0           print(ERRORFILE '; ');
    345             }
    346 0           printf(ERRORFILE
    347             "[%u] %s at %s line %u",
    348             $i+1,
    349             $stack_backtrace[$i]->{'subroutine'},
    350             $stack_backtrace[$i]->{'filename'},
    351             $stack_backtrace[$i]->{'line'});
    352             }
    353             }
    354             else
    355             {
    356 0           print("(unavailable)");
    357             }
    358 0           print(ERRORFILE ']');
    359             }
    360 0           print(ERRORFILE "\n");
    361              
    362 0           close ERRORFILE;
    363              
    364             # abort
    365 0 0 0       if ($severity eq 'error' or $severity eq 'panic' or $severity eq 'abort') {
          0        
    366 0 0         if (getParam($theParams, 'isapi')) {
    367             # client expects easy-to-parse data
    368 0   0       my $userGripes = getLocal('userGripes') || '';
    369 0           my $cgi = FAQ::OMatic::dispatch::cgi();
    370 0           print FAQ::OMatic::header($cgi, '-type'=>'text/plain')
    371             ."isapi=1\n"
    372             ."errors=".CGI::escape($userGripes)."\n";
    373             } else {
    374 0           print FAQ::OMatic::pageHeader();
    375 0           print FAQ::OMatic::pageFooter();
    376             }
    377 0           myExit(0);
    378             }
    379             }
    380              
    381             sub lockFile {
    382 0     0 0   my $filename = shift;
    383 0           my $lockname = $filename;
    384 0           $lockname =~ s#/#-#gs;
    385 0           $lockname =~ m#^(.*)$#;
    386 0           $lockname = "$FAQ::OMatic::Config::metaDir/$1.lck";
    387             # if (-e $lockname) {
    388             # sleep 10;
    389             # if (-e $lockname) {
    390             # gripe 'problem', "Lockfile $lockname for $filename has "
    391             # ."been there 10 seconds. Failing.";
    392             # return 0;
    393             # }
    394             # }
    395             # open (LOCK, ">$lockname") or
    396             # gripe('abort', "Can't create lockfile $lockname ($!)");
    397             # print LOCK $$;
    398             # close LOCK;
    399             # return $lockname;
    400              
    401             # THANKS to A.Flavell@physics.gla.ac.uk for working on finding
    402             # how broken my old locking code was.
    403 0           my $retries = 0;
    404 0           while (1) {
    405 0 0         if (++$retries >= 10) {
    406 0           gripe('abort', "waited too long to get lock... ($!, $lockname)");
    407             }
    408 0 0         if (sysopen(LOCK, $lockname, O_CREAT|O_WRONLY, 0444)) {
    409             # success!
    410 0           print LOCK $$;
    411 0           close LOCK;
    412 0           return $lockname;
    413             }
    414             # can't get the lockfile -- wait a little and retry
    415 0           sleep (2);
    416             }
    417             }
    418              
    419             sub unlockFile {
    420 0     0 0   my $lockname = shift;
    421 0 0         if (-e $lockname) {
    422 0           unlink $lockname;
    423 0           return 1;
    424             }
    425 0           gripe 'abort', "$lockname didn't exist -- uh oh, is the locking system broken?";
    426 0           return 0;
    427             }
    428              
    429             # turns faqomatic:file references into HTML links with pleasant titles.
    430             sub faqomaticReference {
    431 0     0 0   my $params = $_[0];
    432 0 0 0       if (($params->{'render'}||'') eq 'text') {
    433 0           return faqomaticReferenceText(@_);
    434             } else {
    435 0           return faqomaticReferenceRich(@_);
    436             }
    437             }
    438              
    439             sub faqomaticReferenceRich {
    440 0     0 0   my $params = shift;
    441 0           my $filename = shift;
    442 0   0       my $which = shift || '-small';
    443             # '-small' (children) or '-also' (see-also links)
    444              
    445 0           my $item = new FAQ::OMatic::Item($filename);
    446 0           my $title = FAQ::OMatic::ImageRef::getImageRefCA($which,
    447             'border=0', $item->isCategory(), $params)
    448             .$item->getTitle();
    449              
    450 0           return (makeAref('-command'=>'faq',
    451             '-refType'=>'url',
    452             '-params'=>$params,
    453             '-changedParams'=>{"file"=>$filename}),
    454             $title);
    455             }
    456              
    457             sub faqomaticReferenceText {
    458 0     0 0   my $params = shift;
    459 0           my $filename = shift;
    460              
    461 0           my $item = new FAQ::OMatic::Item($filename);
    462 0           return ('',$item->getTitle());
    463             }
    464              
    465             sub baginlineReference {
    466 0     0 0   my $params = shift;
    467 0           my $filename = shift;
    468              
    469 0 0         if (not -f $FAQ::OMatic::Config::bagsDir.$filename) {
    470 0           return "[no bag '$filename' on server]";
    471             }
    472              
    473 0           my $sw = FAQ::OMatic::Bags::getBagProperty($filename, 'SizeWidth', '');
    474 0 0         $sw = " width=$sw" if ($sw ne '');
    475 0           my $sh = FAQ::OMatic::Bags::getBagProperty($filename, 'SizeHeight', '');
    476 0 0         $sh = " height=$sh" if ($sh ne '');
    477              
    478             # should point directly to bags dir
    479             # TODO: deal with this correctly when handling all the variations on
    480             # TODO: urls.
    481 0           my $bagUrl = makeBagRef($filename, $params);
    482 0           return "\"($filename)\"";
    483             }
    484              
    485             sub baglinkReference {
    486 0     0 0   my $params = shift;
    487 0           my $filename = shift;
    488              
    489 0 0         if (not -f $FAQ::OMatic::Config::bagsDir.$filename) {
    490 0           return ('',"[no bag '$filename' on server]");
    491             }
    492              
    493 0           my $bagDesc = new FAQ::OMatic::Item($filename.".desc",
    494             $FAQ::OMatic::Config::bagsDir);
    495 0   0       my $size = $bagDesc->{'SizeBytes'} || '';
    496 0 0         if ($size ne '') {
    497 0           $size = " ".describeSize($size);
    498             }
    499              
    500             # should point directly to bags dir
    501             # TODO: deal with this correctly when handling all the variations on
    502             # TODO: urls.
    503 0           my $bagUrl = makeBagRef($filename, $params);
    504 0           return ($bagUrl,
    505             FAQ::OMatic::ImageRef::getImageRef('baglink', 'border=0', $params)
    506             .$filename
    507             .$size);
    508             }
    509              
    510             # The web server passes this information in on every call, but
    511             # it sometimes comes in broken (broken clients, or users typing
    512             # in abbreviated host names which won't work if used as part of a URL
    513             # that's later clicked on by a distant user). So we now let the admin
    514             # configure these fields; but compute them dynamically until the admin
    515             # cements the right ones in place.
    516             sub serverBase {
    517 0 0 0 0 0   if (defined($FAQ::OMatic::Config::serverBase)
    518             && $FAQ::OMatic::Config::serverBase ne '') {
    519 0           return $FAQ::OMatic::Config::serverBase;
    520             }
    521 0           return (hostAndPath())[0];
    522             }
    523              
    524             sub cgiURL {
    525 0 0 0 0 0   if (defined($FAQ::OMatic::Config::cgiURL)
    526             && $FAQ::OMatic::Config::cgiURL ne '') {
    527 0           return $FAQ::OMatic::Config::cgiURL;
    528             }
    529 0           return (hostAndPath())[1];
    530             }
    531              
    532             # compute serverBase and cgiURL dynamically
    533             # (old code -- the cache isn't nearly as necessary now. :v)
    534             sub hostAndPath {
    535 0 0   0 0   if (defined getLocal('hapCache')) {
    536 0           return @{getLocal('hapCache')};
      0            
    537             }
    538              
    539 0           my $cgi = FAQ::OMatic::dispatch::cgi();
    540 0           my $cgiUrl = $cgi->url();
    541 0           my ($urlRoot,$urlPath) = $cgiUrl =~ m#^(https?://[^/]+)(/.*)$#;
    542 0 0 0       if (not defined $urlRoot or not defined $urlPath) {
    543 0 0         if (not $cgi->protocol() =~ m/^http/i) {
    544 0           FAQ::OMatic::gripe('error', "The server protocol ("
    545             .$cgi->protocol()
    546             .") seems wrong. The author has seen this happen when "
    547             ."broken browsers don't escape a space in the GET URL. "
    548             ."(KDE Konqueror 1.0 is known broken; upgrade to "
    549             ."Konquerer 1.1.) "
    550             ."\n\n

    \nThe URL (as CGI.pm saw it) was:\n"

    551             .$ENV{'QUERY_STRING'}
    552             ."\n\n
    The REQUEST_URI was:\n"
    553             .$ENV{'REQUEST_URI'}
    554             ."\n\n
    The SERVER_PROTOCOL was:\n"
    555             .$ENV{'SERVER_PROTOCOL'}
    556             ."\n\n
    The browser was:\n"
    557             .$ENV{'HTTP_USER_AGENT'}."\n"
    558             ."\n\n

    If you are confused, please ask "

    559             ."$FAQ::OMatic::Config::adminEmail.\n"
    560             );
    561             # This seems to happen when you search on two words,
    562             # then get an with a %20 in the _highlightWords
    563             # field. Turns out KDE's integrated Konquerer browser
    564             # version 1.0 has this problem; version 1.1 fixes it.
    565             }
    566 0           FAQ::OMatic::gripe('problem', "Can't parse my own URL: $cgiUrl");
    567             }
    568 0           my @hap = ($urlRoot, $urlPath);
    569 0           setLocal('hapCache', \@hap);
    570 0           return @hap;
    571             }
    572              
    573             sub relativeReference {
    574 0     0 0   my $params = shift;
    575 0           my $url = shift;
    576              
    577 0 0         if ($url =~ m#^/#) {
    578 0           return FAQ::OMatic::serverBase().$url;
    579             }
    580              
    581             # Else url is relative to current directory.
    582             # Deal with ..'s. We would leave this to the browser, but we
    583             # want to return an URL that works everywhere, not just from the
    584             # CGI. (So it works from a cached file or a mirrored file.)
    585 0           my @urlPath = split('/', FAQ::OMatic::cgiURL());
    586 0           shift @urlPath; # shift off first element ('')
    587 0           pop @urlPath; # pop off last element (CGI name)
    588 0   0       while (($url =~ m#^../(.*)$#) and (scalar(@urlPath)>0)) {
    589 0           $url = $1; # strip ../ component...
    590 0           pop @urlPath; # ...and in exchange, explicitly remove path element
    591             }
    592 0           push @urlPath, $url;
    593 0           return FAQ::OMatic::serverBase().'/'.join("/",@urlPath);
    594             }
    595              
    596             # THANKS: to steevATtiredDOTcom for suggesting the ability to mangle
    597             # or disable attributions to reduce the potential for spam address harvesting.
    598             sub mailtoReference {
    599 0   0 0 0   my $params = shift||{};
    600 0   0       my $addr = shift || '';
    601 0   0       my $wantarray = shift || '';
    602              
    603 0           my $isText = getParam($params, 'render') eq 'text';
    604              
    605 0           $addr =~ s/^mailto://; # strip off mailto prefix if it's there
    606 0           $addr = entify($addr);
    607 0   0       my $how = $FAQ::OMatic::Config::antiSpam || 'off';
    608              
    609 0 0         if ($how eq 'cheesy') {
        0          
        0          
    610 0           $addr =~ s#\@#AT#g;
    611 0           $addr =~ s#\.#DOT#g;
    612             } elsif ($how eq 'nameonly') {
    613             # THANKS: to "Alan J. Flavell" for
    614             # sending a patch to implement 'nameonly' address munging
    615 0           $addr =~ s#\@.*##;
    616             } elsif ($how eq 'hide') {
    617 0           $addr = 'address-suppressed';
    618             }
    619             # THANKS to Peter Lawler for suggesting
    620             # that we provide the FAQ-O-Matic's title as the subject line of
    621             # mailto: links.
    622 0           my $subject = "subject=".CGI::escape(fomTitle());
    623 0 0         if ($isText) {
    624 0           return $addr;
    625             }
    626 0           my $target = '';
    627 0 0         if ($how eq 'off') {
    628 0           $target = "mailto:${addr}?${subject}";
    629             }
    630 0 0         if ($wantarray) {
    631             # when urlReference calls this func, it wants the link label split
    632             # from the link target. If $target is empty, it does the right thing
    633             # by not creating an tag.
    634 0           return ($target, $addr);
    635             } else {
    636 0 0         if ($target ne '') {
    637 0           return "$addr";
    638             } else {
    639 0           return $addr;
    640             }
    641             }
    642             }
    643              
    644             # turns link-looking things into actual HTML links, but also turns
    645             # <, > and & into entities to prevent them getting interpreted as HTML.
    646             sub insertLinks {
    647 0     0 0   my $params = shift;
    648 0           my $arg = shift;
    649 0   0       my $ishtml = shift || 0;
    650 0   0       my $isdirectory = shift || 0;
    651              
    652 0 0         if (not $ishtml) {
    653             # look for <>-delimited URLs; THANKS to Hal Wine for pointing out
    654             # , which
    655             # proposes this as a 'standard' way of embedding URLs in non-marked-up
    656             # text for automatic readers:
    657 0           my @pieces = split(/<([^\s<>]+)>/, $arg);
    658             # the result of the previous split() operation is an odd-length
    659             # array; odd-numbered indices contain that matched
    660             # the angle-bracket regex; even numbered things contain the
    661             # rest of the text.
    662 0           my $rt = '';
    663 0           my $i;
    664 0           for ($i=0; $i
    665 0 0         if ($i&1) { # odd index -- a -looking thingamadoo
    666 0           $rt .= urlReference($params,$isdirectory,$pieces[$i]);
    667             } else { # even index -- some body text
    668 0           my $tmp = entify($pieces[$i]);
    669             # entifying first is bad, because it entifies URLs,
    670             # which is wrong. But this is only to preserve the
    671             # old behavior; if you want it right, use the new <>
    672             # syntax and turn off fuzzy matching.
    673             # THANKS: to jon * for reporting
    674             # an instance of entified URLs.
    675             # TODO: make fuzzyMatch disable-able.
    676 0           $tmp = fuzzyMatch($params,$ishtml,$isdirectory,$tmp);
    677 0           $rt .= $tmp;
    678             }
    679             }
    680 0           $arg = $rt;
    681             } else {
    682             # HTML code gets far less mangling. It's not entified, and
    683             # only my made-up URLs get translated into real ones; other
    684             # urls are left untouched.
    685 0           $arg = fuzzyMatch($params,$ishtml,$isdirectory,$arg);
    686             }
    687 0           return $arg;
    688             }
    689              
    690             sub urlReference {
    691             # take an URL from the middle of some text, and wrap it with some
    692             # tags to make it a link. How to do that depends on the type of
    693             # URL.
    694 0     0 0   my $params = shift;
    695 0           my $isdirectory = shift;
    696 0           my $arg = shift; #URL to wrap
    697              
    698 0 0         my $sa = $isdirectory ? '-small' : '-also';
    699            
    700             # unless we can do better, both the label and the target of the URL
    701             # will be whatever we got passed (whatever matched in the text body)
    702 0   0       my $target = $arg||'';
    703 0   0       my $label = $arg||'';
    704              
    705 0           my ($prefix,$rest) = ($arg =~ m/^([^:]+):(.*)$/);
    706 0 0 0       if (not defined $prefix) {
        0 0        
        0 0        
        0 0        
        0          
        0          
        0          
    707             # match didn't work; this is some sort of link we don't understand.
    708             } elsif ($prefix eq 'http' or $prefix eq 'https') {
    709             # it's an http-ish URL.
    710             # It could be absolute (starts with // and includes hostname),
    711             # in which case we should leave it untouched.
    712             # It could be server-relative (starts with /)
    713             # in which case we insert our hostname in case this URL makes it
    714             # a long way away.
    715             # It could be path-relative,
    716             # in which case we have to adjust it against our known path
    717             # to become absolute (again in case the URL makes it away from here).
    718 0 0         if ($rest =~ m#^//#) {
    719 0           $target = $arg;
    720             } else {
    721 0           $target = relativeReference($params, $rest);
    722             }
    723             } elsif ($prefix eq 'ftp'
    724             or $prefix eq 'gopher'
    725             or $prefix eq 'telnet'
    726             or $prefix eq 'news') {
    727 0           $target = $arg;
    728             } elsif ($prefix eq 'mailto') {
    729 0           ($target,$label) = mailtoReference($params, $rest, 'wantarray');
    730             } elsif ($prefix eq 'faqomatic') {
    731             # a local reference defined in terms of a FAQ item #,
    732             # not a web server path (so that it's meaningful on other mirrors
    733             # of this FAQ, for example)
    734 0           ($target,$label) = faqomaticReference($params,$rest,$sa);
    735             } elsif ($prefix eq 'baginline') {
    736 0           $target = '';
    737 0           $label = baginlineReference($params,$rest);
    738             } elsif ($prefix eq 'baglink') {
    739 0           ($target,$label) = baglinkReference($params,$rest);
    740             }
    741              
    742             # A tough choice: should the readable text of the link be what the
    743             # user originally typed (to convey the meaning of a relative link,
    744             # for example), or should it be absolute, so that a printed copy of
    745             # the FAQ is worth something? I have been choosing the latter, so I'll
    746             # stick with it.
    747             # I escape() the target here because (a) it's HTML spec, and (b) then
    748             # it doesn't have any characters that get 'entified' which (rightfully)
    749             # some browsers pass back verbatim to the webserver and everything
    750             # breaks. (jon@clearink.com reported an instance of this, but I didn't
    751             # track it down until now.)
    752             # hthielen@users.sourceforge.net sent the following patch to prevent
    753             # us from linkifying anything without a ':'. This heuristic allows
    754             # usage examples: cat > , which would otherwise
    755             # become link because the contents have no whitespace.
    756             # Arrgh. Vile escaping. :v)
    757 0           my $result;
    758 0 0         if (defined $prefix) {
    759 0 0         if ($target ne '') {
    760 0           $result = "$label";
    761             } else {
    762             # this is for e.g. "baginline:" references
    763 0           $result = $label;
    764             }
    765             } else {
    766             # just return the original text including the already
    767             # removed "<" and ">" signs
    768 0           $result = "<" . $label . ">";
    769             }
    770 0           return $result;
    771             }
    772              
    773             sub fuzzyMatch {
    774             # In 2.707 and older FOMs, any text in the body of a text part that
    775             # looked remotely like a URL got linkified. The rules for finding
    776             # such links (and more importantly, figuring out where they end) were
    777             # clumsy and unreliable, so the new prefered method is to put what
    778             # you want to get linked in . This fuzzy matching
    779             # code is retained for admins of older FAQs who don't want their
    780             # older-style "magically recognized" links to lose their magic.
    781 0     0 0   my $params = shift;
    782 0           my $ishtml = shift;
    783 0           my $isdir = shift;
    784 0           my $arg = shift; # text to fuzzy-match for URLS
    785              
    786 0 0         if (not $ishtml) {
    787 0           $arg =~ s#(https?:[^\s"]*[^\s.,)\?!])#urlReference($params,$isdir,$1)#sge;
      0            
    788 0           $arg =~ s#(ftp://[^\s"]*[^\s.,)\?!])#urlReference($params,$isdir,$1)#sge;
      0            
    789 0           $arg =~ s#(gopher://[^\s"]*[^\s.,)\?!])#urlReference($params,$isdir,$1)#sge;
      0            
    790 0           $arg =~ s#(telnet://[^\s"]*[^\s.,)\?!])#urlReference($params,$isdir,$1)#sge;
      0            
    791 0           $arg =~ s#(mailto:\S+@\S*[^\s.,)\?!])#urlReference($params,$isdir,$1)#sge;
      0            
    792 0           $arg =~ s#(news:[^\s"]*[^\s.,)\?!])#urlReference($params,$isdir,$1)#sge;
      0            
    793             # THANKS: njl25@cam.ac.uk for pointing out the absence of the news: regex
    794             }
    795              
    796             # These get parsed even in HTML text. They're "value added." :v)
    797 0           $arg =~ s#])>?#urlReference($params,$isdir,$1)#sge;
      0            
    798 0           $arg =~ s#])>?#urlReference($params,$isdir,$1)#sge;
      0            
    799 0           $arg =~ s#])>?#urlReference($params,$isdir,$1)#sge;
      0            
    800              
    801 0           return $arg;
    802             }
    803              
    804             # no entifying; only faqomatic: and mailto: links are massaged.
    805             sub insertLinksText {
    806 0     0 0   my $params = shift;
    807 0           my $arg = shift;
    808 0   0       my $ishtml = shift || 0;
    809 0   0       my $isdirectory = shift || 0;
    810              
    811 0           $arg =~ s#faqomatic:(\S*[^\s.,)\?!])#"(*) ".faqomaticReferenceText($params,$1)#sge;
      0            
    812             # TODO: baginlines could map to the stored "alt" tag, if we start
    813             # storing one. :v)
    814 0           $arg =~ s#(mailto:\S+@\S*[^\s.,)\?!])#"(*) ".mailtoReference($params,$1)#sge;
      0            
    815              
    816 0           return $arg;
    817             }
    818              
    819             sub entify {
    820 0     0 0   my $arg = shift;
    821 0           $arg =~ s/&/&/sg;
    822 0           $arg =~ s/
    823 0           $arg =~ s/>/>/sg;
    824 0           $arg =~ s/"/"/sg;
    825 0           return $arg;
    826             }
    827              
    828             # returns ref to %theParams
    829             sub getParams {
    830 0 0   0 0   if (not defined $_[0]) {
    831 0           return $theParams;
    832             }
    833              
    834 0           my $cgi = shift;
    835 0           my $dontLog = shift; # so statgraph requests don't count as hits
    836 0           my $i;
    837              
    838 0           foreach $i ($cgi->param()) {
    839 0           $theParams->{$i} = $cgi->param($i);
    840             }
    841              
    842             # Log this access
    843 0 0         FAQ::OMatic::Log::logEvent($theParams) if (not $dontLog);
    844              
    845             # set up DIEs to panic and WARNs to note in log.
    846             # grep log for "Perl" to see if this is happening.
    847             # We only do this in getParams so that command-line utils
    848             # don't get confused.
    849 0     0     $SIG{__WARN__} = sub { gripe('note', "Perl warning: ".$_[0]); };
      0            
    850             # so it turns out SIGs are the wrong way to catch die()s. Evals
    851             # are the right way.
    852             # $SIG{__DIE__} = sub { gripe('panic', "Perl died: ".$_[0]); };
    853              
    854 0           return $theParams;
    855             }
    856              
    857             # if a param is equal to the default interpretation, we can just
    858             # delete the param. This keeps urls short, and helps us identify
    859             # when the user can be sent over to the cache for faster service.
    860             # Plus, it lets admins configure site defaults that override the
    861             # shipped defaults.
    862              
    863             sub defaultParams {
    864             # This is a local, not a constant, so that mod_perl admins aren't
    865             # confused when they rewrite the *Default admin parameters (this
    866             # way they don't get stuck in the mod_perl cache).
    867 0     0 0   my $defaultParams = getLocal('defaultParams');
    868 0 0         if (not defined $defaultParams) {
    869 0   0       $defaultParams = {
          0        
          0        
          0        
          0        
          0        
    870             'cmd' => 'faq',
    871             'render' =>
    872             $FAQ::OMatic::Config::renderDefault || 'tables',
    873             'editCmds' =>
    874             $FAQ::OMatic::Config::editCmdsDefault || 'hide',
    875             'showModerator' =>
    876             $FAQ::OMatic::Config::showModeratorDefault || 'hide',
    877             'showLastModified' =>
    878             $FAQ::OMatic::Config::showLastModifiedDefault || 'hide',
    879             'showAttributions' =>
    880             $FAQ::OMatic::Config::showAttributionsDefault || 'default',
    881             'textCmds' =>
    882             $FAQ::OMatic::Config::textCmdsDefault || 'hide',
    883             };
    884 0           setLocal('defaultParams', $defaultParams);
    885             }
    886 0           return $defaultParams;
    887             }
    888              
    889             sub getParam {
    890 0     0 0   my $params = shift;
    891 0           my $key = shift;
    892 0 0         if (not ref $params) { FAQ::OMatic::gripe('debug', stackTrace('html')); };
      0            
    893 0 0         return $params->{$key} if defined($params->{$key});
    894 0 0         return defaultParams()->{$key} if defined(defaultParams()->{$key});
    895 0           return '';
    896             }
    897              
    898             sub makeAref {
    899 0     0 0   my $command = 'faq';
    900 0           my $changedParams = {};
    901 0           my $refType = '';
    902 0           my $saveTransients = '';
    903 0           my $blastAll = '';
    904 0           my $params = $theParams; # default to global params (not preferred, tho)
    905 0           my $target = ''; # tag
    906 0           my $thisDocIs = ''; # prevent conversion to a cache URL
    907 0           my $urlBase = ''; # use included params, but specified urlBase
    908 0           my $multipart = ''; # tell browser to reply with a multipart POST
    909              
    910 0 0         if ($_[0] =~ m/^\-/) {
    911             # named-parameter style
    912 0           while (scalar(@_)>=2) {
    913 0           my ($argName, $argVal) = splice(@_,0,2);
    914 0 0         if ($argName =~ m/\-command$/i) {
        0          
        0          
        0          
        0          
        0          
        0          
        0          
        0          
        0          
    915 0           $command = $argVal;
    916             } elsif ($argName =~ m/\-changedParams$/i) {
    917 0           $changedParams = $argVal;
    918             } elsif ($argName =~ m/\-refType$/i) {
    919 0           $refType = $argVal;
    920             } elsif ($argName =~ m/\-saveTransients$/i) {
    921 0           $saveTransients = $argVal;
    922             } elsif ($argName =~ m/\-blastAll$/i) {
    923 0           $blastAll = $argVal;
    924             } elsif ($argName =~ m/\-params$/i) {
    925 0           $params = $argVal;
    926             } elsif ($argName =~ m/\-target$/i) {
    927 0           $target = $argVal;
    928             } elsif ($argName =~ m/\-thisDocIs$/i) {
    929 0           $thisDocIs = $argVal;
    930             } elsif ($argName =~ m/\-urlBase$/i) {
    931 0           $urlBase = $argVal;
    932             } elsif ($argName =~ m/\-multipart$/i) {
    933 0           $multipart = $argVal;
    934             }
    935             }
    936 0 0         if (scalar(@_)) {
    937 0           gripe('problem', "Odd number of args to makeAref()");
    938             }
    939             } else {
    940 0           $command = shift;
    941 0   0       $changedParams = shift || {};
    942             # hash ref to new params
    943 0   0       $refType = shift || '';
    944             # '' =>
    945             # 'POST' =>
    946             # 'GET' =>
    947             # 'url' => just the GET url
    948 0   0       $saveTransients = shift || '';
    949             # true => don't zap the _params, since
    950             # they're only passing through an interposing
    951             # script (authentication script, for example)
    952 0   0       $blastAll = shift || '';
    953             # true => zap all params, then use
    954             # changedParams as only new ones.
    955 0 0         $params = shift if (defined($_[0]));
    956             # given params instead of using icky global
    957             # ones.
    958             }
    959              
    960 0           my %newParams;
    961 0 0         if ($blastAll) {
    962 0           %newParams = (); # blast all existing params
    963             } else {
    964 0           %newParams = %{$params};
      0            
    965             }
    966              
    967             # parameters with a _ prefix are defined to be "transient" -- they
    968             # never make it into a new Aref. That way we can introduce new
    969             # transient parameters, and they automatically get deleted here.
    970 0 0         if (not $saveTransients) {
    971 0           my $i;
    972 0           foreach $i (keys %newParams) {
    973 0 0         delete $newParams{$i} if ($i =~ m/^_/);
    974             }
    975             }
    976              
    977             # change the requested parameters
    978 0           my $i;
    979 0           foreach $i (keys %{ $changedParams }) {
      0            
    980 0 0 0       if (not defined($changedParams->{$i})
    981             or ($changedParams->{$i} eq '')) {
    982 0           delete $newParams{$i};
    983             } else {
    984 0           $newParams{$i} = $changedParams->{$i};
    985             }
    986             }
    987 0           $newParams{'cmd'} = $command;
    988              
    989             # delete keys where values are equal to defaults
    990 0           foreach $i (sort keys %newParams) {
    991 0 0 0       if (defined(defaultParams()->{$i})
    992             and ($newParams{$i} eq defaultParams()->{$i})) {
    993 0           delete $newParams{$i};
    994             }
    995             }
    996              
    997             # So why ever bother generating local references when
    998             # pointing at the CGI? (That's how faqomatic <= 2.605 worked.)
    999             # Generating absolute ones means
    1000             # the same links work in the cache, or when the cache file
    1001             # is copied for use elsewhere. It also means that pointing
    1002             # at a mirror version of the CGI should be a minor tweak.
    1003             # Answer: (V2.610) people like
    1004             # THANKS: Mark Nagel
    1005             # need server-relative references, because
    1006             # absolute references won't work -- at their site, servers are
    1007             # accessed through a ssh forwarder. (Why not just use https?)
    1008              
    1009 0           my $cgiName;
    1010 0 0 0       if ($urlBase ne '') {
        0 0        
    1011 0           $cgiName = $urlBase;
    1012             } elsif (not $thisDocIs and
    1013             ($FAQ::OMatic::Config::useServerRelativeRefs || 0)) {
    1014             # return a server-relative path (starts with /)
    1015             #$cgiName = FAQ::OMatic::dispatch::cgi()->script_name();
    1016 0           $cgiName = FAQ::OMatic::cgiURL();
    1017             } else {
    1018             # return an absolute URL (including protocol and server name)
    1019             #$cgiName = FAQ::OMatic::dispatch::cgi()->url();
    1020 0           $cgiName = FAQ::OMatic::serverBase().FAQ::OMatic::cgiURL();
    1021             }
    1022              
    1023             # collect args in $rt in appropriate form -- hidden fields for
    1024             # forms, or key=value pairs for URLs.
    1025 0           my $rt = "";
    1026 0           foreach $i (sort keys %newParams) {
    1027 0           my $value = $newParams{$i};
    1028 0 0         if (not defined($value)) { $value = ''; }
      0            
    1029              
    1030 0 0 0       if ($refType eq 'POST' or $refType eq 'GET') {
    1031             # GET or POST form. stash args in hidden fields.
    1032 0           $rt .= "
    1033             .entify($value)."\">\n";
    1034             # wow, when that entify (analogous to the CGI::escape in the
    1035             # regular GET case below) was missing, it made for awfully
    1036             # subtle bugs! If one of the old params has a " in it (such as
    1037             # would happen if leaving the define-config page and being asked
    1038             # to stop off at the login page), it didn't get escaped, so the
    1039             # browser quietly truncated the value, which made us save a bogus
    1040             # value into the config file. Ouch!
    1041             } else {
    1042             # regular GET, not GET. URL-style key=val&key=val
    1043 0           $rt.="&".CGI::escape($i)."=".CGI::escape($value);
    1044             }
    1045             }
    1046 0 0 0       if (($refType eq 'POST') or ($refType eq 'GET')) {
    1047 0           my $encoding = '';
    1048 0 0         if ($refType eq 'POST') {
    1049 0 0         if ($multipart) {
    1050             # THANKS: charlie buckheit for discovering
    1051             # THANKS: this bug, which only shows up in MSIE.
    1052 0           $encoding = " ENCTYPE=\"multipart/form-data\""
    1053             ." ENCODING";
    1054             }
    1055             }
    1056 0           return "
    1057             ."method=\"$refType\""
    1058             ."$encoding>\n$rt";
    1059             }
    1060              
    1061 0           $rt =~ s/^\&/\?/; # turn initial & into ?
    1062 0           my $url = $cgiName.$rt;
    1063              
    1064             # see if url can be converted to point to local cache instead of CGI.
    1065 0 0         if (not $thisDocIs) {
    1066             # $thisDocIs indicates that this URL is going to appear to the
    1067             # user in the "This document is:" line. So it should be a
    1068             # fully-qualified URL, and it should not point to the cache.
    1069             # Otherwise, see if the reference can be resolved in the cache to
    1070             # save one or more future CGI accesses.
    1071 0   0       $url = getCacheUrl(\%newParams, $params) || $url;
    1072             }
    1073              
    1074 0 0         if ($refType eq 'url') {
    1075 0           return $url;
    1076             } else {
    1077 0 0         my $targetTag = $target ? " target=\"$target\"" : '';
    1078 0           return "";
    1079             }
    1080             }
    1081              
    1082             # This function examines $params and if they refer to a page that's
    1083             # statically cached, returns a ready-to-eat URL to that page.
    1084             # Otherwise it returns ''.
    1085             sub getCacheUrl {
    1086 0     0 0   my $paramsForUrl = shift;
    1087 0           my $paramsForMe = shift;
    1088              
    1089             # Sometimes we can do *better* than the cache -- a link
    1090             # can point inside this very document! That's true when
    1091             # the document is the result of a "show this entire category."
    1092             # We require the linkee to be a child of the root of this display
    1093             # (i.e., the linked item must appear on this page :v), and the
    1094             # desired URL must have cmd=='' (i.e., looking at the FAQ, not
    1095             # editing it or otherwise). Any other params I think should be
    1096             # appearance-related, and therefore would be the same as the top
    1097             # item being displayed.
    1098 0 0 0       if ($paramsForMe->{'_recurseRoot'}
    1099             and not defined($paramsForUrl->{'cmd'})) {
    1100 0   0       my $linkFile = $paramsForUrl->{'file'} || '1';
    1101 0           my $linkItem = new FAQ::OMatic::Item($linkFile);
    1102 0           my $topFile = $paramsForMe->{'_recurseRoot'};
    1103              
    1104 0 0         if ($linkItem->hasParent($paramsForMe->{'_recurseRoot'})) {
    1105 0           return "#file_".$linkFile;
    1106             }
    1107             }
    1108              
    1109 0 0 0       if ($FAQ::OMatic::Config::cacheDir
      0            
    1110 0           and (not grep {not m/^file$/} keys(%{$paramsForUrl}))
    1111             ) {
    1112 0 0         if ($paramsForMe->{'_fromCache'}) {
    1113             # We have a link from the cache to the cache.
    1114             # If we let it be relative, then the cache files
    1115             # can be picked up and taken elsewhere, and they still
    1116             # work, even without a webserver!
    1117 0           return $paramsForUrl->{'file'}
    1118             .".html";
    1119             } else {
    1120             # pointer into the cache from elsewhere (the CGI) -- use a full URL
    1121             # to get them to our cache.
    1122              
    1123             # clean up the 'file' input so CSS attack can't play games with the
    1124             # resulting URL by faking the file value.
    1125 0           return FAQ::OMatic::serverBase()
    1126             .$FAQ::OMatic::Config::cacheURL
    1127             .cleanFile($paramsForUrl->{'file'})
    1128             .".html";
    1129             }
    1130             }
    1131 0           return '';
    1132             }
    1133              
    1134             # ensure that a file spec is "clean". Let's say the items
    1135             # can only be named things alphanumerics and .-_.
    1136             sub cleanFile {
    1137 0   0 0 0   my $file = shift || '';
    1138 0 0         if ($file =~ m/[^a-zA-Z0-9\.\-\_]/s) {
    1139 0           return '1';
    1140             }
    1141 0           return $file;
    1142             }
    1143              
    1144             sub makeBagRef {
    1145             # Not nearly as tricky as makeAref; this only returns a URL.
    1146              
    1147 0     0 0   my $bagName = shift;
    1148 0           my $params = shift;
    1149              
    1150 0 0         if ($params->{'_fromCache'}) {
        0          
    1151             # from cache to bags -- can use a local reference; this
    1152             # will allow us to transplant the cache and bags directories
    1153             # from this server to a CD or otherwise portable hierarchy.
    1154             #
    1155             # Notice that we rely here on bags/ and cache/ being in the
    1156             # same parent directory. The presence of separate $bagsURL and
    1157             # $cacheURL configuration items might seem to imply that they're
    1158             # independent paths, but they're not. (So that the previous
    1159             # comment about a 'portable hierarchy' is true.)
    1160 0           return "../bags/$bagName";
    1161             } elsif (not defined($FAQ::OMatic::Config::bagsURL)) {
    1162             # put a bad URL in the link to make it obviously fail
    1163 0           return "x:";
    1164             } else {
    1165 0           return FAQ::OMatic::serverBase()
    1166             .$FAQ::OMatic::Config::bagsURL
    1167             .$bagName;
    1168             }
    1169             }
    1170              
    1171             # takes an a href and a button label, and makes a button.
    1172             sub button {
    1173 0     0 0   my $ahref = shift;
    1174 0           my $label = shift;
    1175 0   0       my $image = shift || '';
    1176 0   0       my $params = shift || {}; # needed to get correct image refs from cache
    1177              
    1178             #$label =~ s/ /\ /g;
    1179 0 0 0       if ($FAQ::OMatic::Config::showEditIcons
    1180             and ($image ne '')) {
    1181 0 0 0       if (($FAQ::OMatic::Config::showEditIcons||'') eq 'icons-only') {
        0          
    1182 0           $label = '';
    1183             } elsif ($label ne '') {
    1184 0           $label = "
    $label";
    1185             }
    1186 0           return "$ahref"
    1187             .FAQ::OMatic::ImageRef::getImageRef($image, 'border=0', $params)
    1188             ."$label\n";
    1189             } else {
    1190 0           return "[$ahref$label]";
    1191             }
    1192             }
    1193              
    1194             sub getAllItemNames {
    1195 0   0 0 0   my $dir = shift || $FAQ::OMatic::Config::itemDir;
    1196              
    1197 0           my @allfiles;
    1198              
    1199 0 0         opendir DATADIR, $dir or
    1200             FAQ::OMatic::gripe('problem', "Can't open data directory $dir.");
    1201 0           while (defined($_ = readdir DATADIR)) {
    1202 0 0         next if (m/^\./);
    1203 0 0         next if (not -f $dir."/".$_);
    1204             # not sure what the above test is good for. Avoid subdirectories?
    1205 0           push @allfiles, $_;
    1206             }
    1207 0           close DATADIR;
    1208 0           return @allfiles;
    1209             }
    1210              
    1211             sub lotsOfApostrophes {
    1212 0     0 0   my $word = shift;
    1213 0           $word =~ s/(.)/$1'*/go;
    1214 0           return $word;
    1215             }
    1216              
    1217              
    1218             # Using of locale pragma for entire file can have taint-check fails as
    1219             # result. But search-hits highlighting should be locale dependent.
    1220             # Because of this, locale pragma is used for highlightWords() function
    1221             # only.
    1222 1     1   18 use locale;
      1         3  
      1         13  
    1223              
    1224             sub highlightWords {
    1225 0     0 0   my $text = shift;
    1226 0           my $params = shift;
    1227            
    1228 0           my @hw;
    1229 0 0         if ($params->{'_highlightWords'}) {
        0          
    1230 0           @hw = split(' ', $params->{'_highlightWords'});
    1231             } elsif ($params->{'_searchArray'}) {
    1232 0           @hw = @{ $params->{'_searchArray'} };
      0            
    1233             }
    1234 0 0         if (@hw) {
    1235 0           my $rt = '';
    1236 0           @hw = map { lotsOfApostrophes($_) } @hw;
      0            
    1237              
    1238             # we'll use this to split the text into not-matches and
    1239             # "delimiters" (matches). Split returns a list item for every
    1240             # pair of parens, so we need to know how many parens we
    1241             # ended up with. Then we can reassemble the text my taking
    1242             # the zeroth item, which didn't match at all, the first item,
    1243             # which matched the first set of parens (the anti-HTML-bashing
    1244             # set), the fourth item which actually matched the word, then
    1245             # continue with the zero+$numparens+1 item, which is the next
    1246             # "split-ee."
    1247             # see Camel ed. 2 p. 221
    1248 0           my $matchstr = '((^|>)([^<]*[^\w<&])?)(('.join(')|(',@hw).'))';
    1249 0           my $numparens = scalar(@hw)+4;
    1250 0           my @pieces = split(/$matchstr/i, $text);
    1251              
    1252             # reassemble the split pieces according to the description above
    1253 0           my $i;
    1254 0           $rt = '';
    1255 0           for ($i=0; $i<@pieces; $i+=$numparens+1) {
    1256 0           $rt .= $pieces[$i+0];
    1257 0 0         $rt .= $pieces[$i+1] if ($i+1<@pieces);
    1258 0 0         $rt .= $FAQ::OMatic::Appearance::highlightStart
    1259             .$pieces[$i+4]
    1260             .$FAQ::OMatic::Appearance::highlightEnd if ($i+4 < @pieces);
    1261             }
    1262 0           $text = $rt;
    1263             }
    1264 0           return $text;
    1265             }
    1266              
    1267             # Turn off locale pragma. See comment about `use locale' near to begin
    1268             # of highlightWords() function for reason of this.
    1269 1     1   305 no locale;
      1         4  
      1         7  
    1270              
    1271             sub unallocatedItemName {
    1272 0   0 0 0   my $filename= shift || 1;
    1273              
    1274             # Things under 'trash' should get allocated in the numerical space.
    1275             # I'm not sure when an item would get created under the trash,
    1276             # but I've seen it happen, and they got called 'trasi'
    1277             # and 'trasj' ... :v)
    1278             # (I've done it deliberately with API.pm to test emptyTrash, though.)
    1279 0 0         if ($filename eq 'trash') {
    1280 0           $filename = 1;
    1281             }
    1282              
    1283             # If the user is looking for a numeric filename (i.e. supplied no
    1284             # argument), use hint to skip forward to biggest existing file number.
    1285 0           my $useHint = ($filename =~ m/^\d*$/);
    1286 0 0 0       if ($useHint and
    1287             open HINT, "<$FAQ::OMatic::Config::metaDir/biggestFileHint") {
    1288 0           $filename = int();
    1289 0 0         $filename = 1 if ($filename<1);
    1290 0           close HINT;
    1291 0 0         if (not -e "$FAQ::OMatic::Config::itemDir/$filename") {
    1292             # make sure the hint's valid; else rewind to get earliest empty
    1293             # file
    1294 0           $filename = 1;
    1295             }
    1296             }
    1297 0           while (-e "$FAQ::OMatic::Config::itemDir/$filename") {
    1298 0           $filename++;
    1299             }
    1300 0 0 0       if ($useHint and
    1301             open HINT, ">$FAQ::OMatic::Config::metaDir/biggestFileHint") {
    1302 0           print HINT "$filename\n";
    1303 0           close HINT;
    1304             }
    1305 0           return $filename;
    1306             }
    1307              
    1308             sub notACGI {
    1309 0 0   0 0   return if (not defined $ENV{'QUERY_STRING'});
    1310              
    1311 0           print "Content-type: text/plain\n\n";
    1312 0           print "This script (".commandName().") may not be run as a CGI.\n";
    1313 0           myExit(0);
    1314             }
    1315              
    1316             sub binpath {
    1317 0     0 0   my $binpath = $0;
    1318 0           $binpath =~ s#[^/]*$##;
    1319 0 0         $binpath = "." if (not $binpath);
    1320 0           return $binpath;
    1321             }
    1322              
    1323             sub validEmail {
    1324             # returns true (and the untainted address)
    1325             # if the argument looks like an email address
    1326 0     0 0   my $arg = shift;
    1327 0           my $cnt = ($arg =~ /^([\w\-.+]+\@[\w\-.+]+)$/);
    1328 0 0         return ($cnt == 1) ? $1 : undef;
    1329             }
    1330              
    1331             # sends email; returns true if there was a problem.
    1332             sub sendEmail {
    1333 0     0 0   my $to = shift; # array ref or scalar
    1334 0           my $subj = shift;
    1335 0           my $mesg = shift;
    1336              
    1337 0           my $encode_lang = FAQ::OMatic::I18N::language();
    1338 0 0         if($encode_lang eq "ja_JP.EUC") {
        0          
    1339 0           require Jcode; import Jcode;
      0            
    1340 0           require NKF; import NKF;
      0            
    1341 0           $subj = jcode($subj)->mime_encode;
    1342 0           $mesg = nkf('-j',$mesg);
    1343             } elsif ($encode_lang ne "en") {
    1344 0           require MIME::Words; import MIME::Words qw(:all);
      0            
    1345 0           $subj = encode_mimeword($subj,"B");
    1346             }
    1347              
    1348 0 0         return if (not $FAQ::OMatic::Config::mailCommand);
    1349              
    1350             # untaint $to address
    1351 0 0         if (ref $to) {
    1352 0 0         $to = join(" ", map {validEmail($_)||''} @{$to});
      0            
      0            
    1353             } else {
    1354 0   0       $to = validEmail($to)||'';
    1355             }
    1356 0 0         return 'problem' if ($to =~ m/^\s*$/);
    1357             # found no valid email addresses
    1358              
    1359             # THANKS Jason R .
    1360             # need $PATH to be untainted.
    1361 0           my $pathSave = $ENV{'PATH'};
    1362 0           $ENV{'PATH'} = '/bin';
    1363              
    1364             # X-URL is used to help user to know which FAQ has sent this mail.
    1365             # THANKS suggested by Akiko Takano
    1366             # TODO in the case of moderator mail, we probably want this
    1367             # URL to indicate the correct file name, rather than the top of the
    1368             # FAQ. Make it an optional argument to this sub?
    1369 0           my $xurl = FAQ::OMatic::makeAref('-command'=>'faq',
    1370             '-params'=>{},
    1371             '-thisDocIs'=>1,
    1372             '-refType'=>'url');
    1373              
    1374 0 0         if ($FAQ::OMatic::Config::mailCommand =~ m/sendmail/) {
    1375 0           my $to2 = $to;
    1376 0           $to2 =~ s/ /, /g;
    1377 0 0         if (not open (MAILX, "|$FAQ::OMatic::Config::mailCommand $to 2>&1 "
    1378             .">>$FAQ::OMatic::Config::metaDir/errors")) {
    1379 0           return 'problem';
    1380             }
    1381              
    1382              
    1383 0           print MAILX "X-URL: $xurl\n";
    1384              
    1385 0           print MAILX "To: $to2\n";
    1386 0           print MAILX "Subject: $subj\n";
    1387 0           print MAILX "From: $FAQ::OMatic::Config::adminEmail\n";
    1388 0           print MAILX "\n";
    1389 0           print MAILX $mesg;
    1390 0           close MAILX;
    1391             } else {
    1392 0 0         if (not open (MAILX, "|$FAQ::OMatic::Config::mailCommand -s '$subj' $to")) {
    1393 0           return 'problem';
    1394             }
    1395             # TODO non-sendmail mailers won't get X-URL in the header.
    1396 0           print MAILX "X-URL: $xurl\n\n";
    1397 0           print MAILX $mesg;
    1398 0           close MAILX;
    1399             }
    1400 0           $ENV{'PATH'} = $pathSave; # not sure if it's crucial to hang onto this
    1401 0           return 0; # no problem
    1402             }
    1403              
    1404             # this is a taint-safe glob. It's not as "flexible" as the real glob,
    1405             # but safer and probably anything flexible would be not as portable, since
    1406             # it would depend on csh idiosyncracies.
    1407             sub safeGlob {
    1408 0     0 0   my $dir = shift;
    1409 0           my $match = shift; # perl regexp
    1410              
    1411 0 0         return () if (not opendir(GLOBDIR, $dir));
    1412              
    1413 0           my @firstlist = map { m/^(.*)$/; $1 } readdir(GLOBDIR);
      0            
      0            
    1414             # untaint data -- we can hopefully trust the operating system
    1415             # to provide a valid list of files!
    1416 0           my @filelist = map { "$dir/$_" } (grep { m/$match/ } @firstlist);
      0            
      0            
    1417 0           closedir GLOBDIR;
    1418              
    1419 0           return @filelist;
    1420             }
    1421              
    1422             # for debugging -T
    1423             sub isTainted {
    1424 0     0 0   my $x;
    1425 0           not eval {
    1426 0           $x = join("",@_), kill 0;
    1427 0           1;
    1428             };
    1429             }
    1430              
    1431             # the crummy "require 'flush.pl';" is not acting reliably for me.
    1432             # this is the same routine [made strict], but copied into this package. Grr.
    1433             sub flush {
    1434 0     0 0   my $old = select(shift);
    1435 0           $| = 1;
    1436 0           print "";
    1437 0           $| = 0;
    1438 0           select($old);
    1439             }
    1440              
    1441             sub canonDir {
    1442             # canonicalize a directory path:
    1443             # make sure dir ends with one /, and has no // sequences in it
    1444 0     0 0   my $dir = shift;
    1445 0           $dir =~ s#$#/#; # add an extra / on end
    1446 0           $dir =~ s#//#/#g; # strip any //'s, including the one we possibly
    1447             # put on the end.
    1448 0           return $dir;
    1449             }
    1450              
    1451             sub concatDir {
    1452 0     0 0   my $dir1 = shift;
    1453 0           my $dir2 = shift;
    1454              
    1455 0           return canonDir(canonDir($dir1).canonDir($dir2));
    1456             }
    1457              
    1458             sub cardinal_en {
    1459 0     0 0   my $num = shift;
    1460 0           my %numsuffix=('0'=>'th', '1'=>'st', '2'=>'nd', '3'=>'rd', '4'=>'th',
    1461             '5'=>'th', '6'=>'th', '7'=>'th', '8'=>'th', '9'=>'th');
    1462 0 0 0       my $suffix = ($num>=11 and $num<=19) ? 'th' : $numsuffix{substr($num,-1,1)};
    1463 0           return $num."".$suffix."";
    1464             }
    1465              
    1466             sub cardinal {
    1467 0     0 0   my $num = shift;
    1468 0           return $num.".";
    1469             }
    1470              
    1471             sub describeSize {
    1472 0     0 0   my $num = shift;
    1473              
    1474 0 0         if ($num > 524288) {
        0          
    1475 0           return sprintf("(%3.1f M)", $num/1048576); # megabytess
    1476             } elsif ($num > 512) {
    1477 0           return sprintf("(%3.1f K)", $num/1024); # kilobytes
    1478             } else {
    1479 0           return "($num bytes)";
    1480             }
    1481             }
    1482              
    1483             # This is a variation on system().
    1484             # If it succeeds, you get an empty list ().
    1485             # If it fails (nonzero result code), you get a list containing the
    1486             # exit() value, the signal that stopped the process, the $! translation
    1487             # of the exit() value, and all of the text the child sent to stdout and
    1488             # stderr.
    1489             sub mySystem {
    1490 0     0 0   my $cmd = shift;
    1491 0   0       my $alwaysWantReply = shift || 0;
    1492              
    1493 0           my $count = 0;
    1494 0           my $pid;
    1495              
    1496             # flush now, lest data in a buffer get flushed on close() in every stinking
    1497             # child process.
    1498 0           flush(\*STDOUT);
    1499 0           flush(\*STDERR);
    1500              
    1501 0 0         pipe READPIPE, WRITEPIPE or die "getting pipes";
    1502             # "bulletproof fork" from camel book, 2ed, page 167
    1503 0           FORK: {
    1504 0           $count++;
    1505 0 0 0       if ($pid = fork()) {
        0          
        0          
    1506             # parent here; child in $pid
    1507 0           close WRITEPIPE;
    1508             # (drop out of conditional to parent code below to wait for child)
    1509             } elsif (defined $pid) {
    1510             # child here
    1511              
    1512             # set real uid = effective uid,
    1513             # real gid = effective gid.
    1514             # this keeps RCS from choking in suid situations.
    1515             # RCS has really weird rules about how it uses real and effective
    1516             # uids which probably make a lot of sense when multiple users
    1517             # are competing for the same RCS store.
    1518 0           $< = $>;
    1519 0           $( = $);
    1520              
    1521 0           close READPIPE; # close our fd to the other end of the pipe
    1522 0           close STDOUT; # redirect stderr, stdout into the pipe
    1523 0           open STDOUT, ">&WRITEPIPE";
    1524 0           close STDERR;
    1525 0           open STDERR, ">&WRITEPIPE";
    1526 0           close STDIN; # don't let child dangle on stdin
    1527 0           $ENV{'PATH'} = '/bin'; # THANKS Jason R .
    1528 0           exec $cmd;
    1529 0           die "mySystem($cmd) failed: $!\n";
    1530 0           CORE::exit(-1); # be sure child exits; don't go back
    1531             # and try to be a web server again (in the
    1532             # mod_perl case).
    1533             # TODO: the preceding die will probably result in myExit()
    1534             # getting called, and hence mod_perl continuing to run. Hmmph.
    1535             } elsif (($count < 5) && $! =~ /No more process/) {
    1536             # EAGAIN, supposedly recoverable fork error
    1537 0           sleep(5);
    1538 0           redo FORK;
    1539             } else {
    1540 0           die "Can't fork: $! (tried $count times)\n";
    1541             }
    1542             }
    1543              
    1544 0           my @stdout = ; # read child output in its entirety
    1545 0           close READPIPE;
    1546             # THANKS nobody/anonymous (at sourceforge) submitted this bug fix
    1547             # (#508199); s/he said:
    1548             # "The current code generates a failure code if waitpid
    1549             # finds no child process to wait
    1550             # for ($? == -1) but this is reported as a failure of the
    1551             # mySystem call. The following
    1552             # patch changes the pickup of the $statusword value to
    1553             # look at the pipe close event
    1554             # instead."
    1555 0           my $statusword = $?;
    1556              
    1557 0           my $stdout = join('', @stdout);
    1558 0           my $wrc = waitpid($pid, 0); # just in case
    1559              
    1560 0           my $signal = $statusword & 0x0ff;
    1561 0           my $exitstatus = ($statusword >> 8) & 0x0ff;
    1562 0 0 0       if ($exitstatus == 0 and not $alwaysWantReply) {
    1563 0           return ();
    1564             } else {
    1565 0           return ($exitstatus,$signal,$!,$stdout,\@stdout,"pid=$pid","wrc=$wrc");
    1566             }
    1567             }
    1568              
    1569             # TODO we now have two stacktrace-collectors. Clean this up.
    1570             sub stackTrace {
    1571 0     0 0   my $html = shift;
    1572 0 0         my $linesep = ($html)
    1573             ? '
    '
    1574             : '';
    1575              
    1576 0           my $rt = '';
    1577 0           my $i=0;
    1578 0           while (my ($pack, $file, $line) = caller($i++)) {
    1579 0           $rt .= "$pack $file ${line}${linesep}\n";
    1580             }
    1581 0           return $rt;
    1582             }
    1583              
    1584             sub mirrorsCantEdit {
    1585 0     0 0   my $cgi = shift;
    1586 0           my $params = shift;
    1587              
    1588 0 0         if ($FAQ::OMatic::Config::mirrorURL) {
    1589             # whoah -- we're a mirror site, and the user wants to
    1590             # edit! Send them to the original site.
    1591 0           my $url = makeAref('-command' => commandName(),
    1592             '-urlBase'=>$FAQ::OMatic::Config::mirrorURL,
    1593             '-refType'=>'url');
    1594 0           FAQ::OMatic::redirect($cgi, $url);
    1595             }
    1596             }
    1597              
    1598             sub authorList {
    1599 0     0 0   my $params = shift;
    1600 0           my $listRef = shift;
    1601 0           my $render = getParam($params, 'render');
    1602              
    1603 0           my $rt = '';
    1604 0 0         if ($render ne 'text') {
    1605 0           $rt .= "";
    1606             } else {
    1607 0           $rt .= "[";
    1608             }
    1609 0           $rt .= join(", ", map { FAQ::OMatic::mailtoReference($params, $_) }
      0            
    1610 0           @{$listRef});
    1611 0 0         if ($render ne 'text') {
    1612 0           $rt .= "
    ";
    1613             } else {
    1614 0           $rt .= "]";
    1615             }
    1616 0           $rt .= "\n";
    1617 0           return $rt;
    1618             }
    1619              
    1620             # inspired by mod_perl docs: dynamically detect mod_perl and adjust
    1621             # exit() strategy.
    1622             BEGIN {
    1623             # Auto-detect if we are running under mod_perl or CGI.
    1624 1 50 33 1   4455 $USE_MOD_PERL = ( (exists $ENV{'GATEWAY_INTERFACE'}
    1625             and $ENV{'GATEWAY_INTERFACE'} =~ /CGI-Perl/)
    1626             or exists $ENV{'MOD_PERL'} ) ? 1 : 0;
    1627             }
    1628              
    1629             sub myExit {
    1630             # "Select the correct exit way"
    1631 0     0 0   my $arg = shift;
    1632 0 0         if ($USE_MOD_PERL) {
    1633             # Apache::exit(-2) will cause the server to exit gracefully,
    1634             # once logging happens and protocol, etc (-2 == Apache::Constants::DONE)
    1635             # in any case, I don't think we want it.
    1636 0           Apache::exit(0);
    1637             } else {
    1638 0           CORE::exit($arg);
    1639             }
    1640             }
    1641              
    1642             sub nonce {
    1643             # return a string that's "pretty unique". We do this by returning
    1644             # the time concatenated with the process ID. That's unlikely to repeat.
    1645             # It would require a single process (say a mod_perl apache child proc
    1646             # serving two requests) calling this function twice in a second.
    1647             # TODO: that's not really that unreasonable. It would be better if we
    1648             # could add some other source of uniqueness here.
    1649 0     0 0   return time().'p'.$$;
    1650             }
    1651              
    1652             sub stripnph {
    1653 0     0 0   my $hdr = shift;
    1654              
    1655             # strip off the HTTP/1.0 header line, because we're not
    1656             # really an nph script
    1657 0           $hdr =~ s#^HTTP/[^\n]*\n##s;
    1658 0           return $hdr;
    1659             }
    1660              
    1661             sub header {
    1662 0     0 0   my $cgi = shift;
    1663 0           my $charset = gettext("http-charset");
    1664 0           my $hdr = stripnph($cgi->header((@_,'-charset'=>$charset), '-nph'=>1));
    1665 0           return $hdr;
    1666             }
    1667              
    1668             sub redirect {
    1669 0     0 0   my $cgi = shift;
    1670 0   0       my $url = shift || die 'no argument to redirect';
    1671 0   0       my $asString = shift || '';
    1672              
    1673             # pretend to be nph to work around what I think is a bug in CGI.pm
    1674             # wherein if we're not nph, it sends the header immediately rather
    1675             # than returning it.
    1676 0           my $rd = stripnph($cgi->redirect('-url'=>$url, '-nph'=>1));
    1677             # -nph is true to prevent mod_perl version of CGI from attempting
    1678             # to squirt out the header itself. (CGI.pm 2.49)
    1679              
    1680 0 0         if ($asString) {
    1681 0           return $rd;
    1682             } else {
    1683 0           print $rd;
    1684 0           flush('STDOUT');
    1685 0           myExit(0);
    1686             }
    1687             }
    1688              
    1689             sub rearrange {
    1690             # inspired by CGI.pm
    1691 0     0 0   my ($order, @p) = @_;
    1692              
    1693 0 0 0       if (defined $p[0]
    1694             and substr($p[0],0,1) eq '-') {
    1695 0           my %posh = ();
    1696 0           my @outary = ();
    1697 0           for (my $i=0; $i<@{$order}; $i++) {
      0            
    1698 0           $posh{$order->[$i]} = $i;
    1699             }
    1700 0           while (@p) {
    1701 0           my $k = shift @p;
    1702 0           my $v = shift @p;
    1703 0 0         if (not defined $v) {
    1704 0           die "key $k with no value";
    1705             }
    1706 0           $k =~ s/^\-//;
    1707 0 0         if (exists $posh{$k}) {
    1708 0           $outary[$posh{$k}] = $v;
    1709             } else {
    1710 0           gripe('abort', "unexpected key ($k) received in rearrange");
    1711             }
    1712             }
    1713 0           return @outary;
    1714             } else {
    1715 0           return @p;
    1716             }
    1717             }
    1718              
    1719             sub quoteText {
    1720 0     0 0   my $text = shift;
    1721 0           my $prefix = shift;
    1722              
    1723             # not sure why s/^/> /mg gives a "Substitution loop" error from some Perls.
    1724             # this is a workaround.
    1725              
    1726 0           return join('', map { $prefix.$_."\n" } split(/\n/, $text));
      0            
    1727             }
    1728              
    1729             sub untaintFilename {
    1730             # strips out most chars but 'A-Za-z0-9_-.' A little overly restrictive,
    1731             # but good for when you want to read a file but don't want
    1732             # user sneaking in '../', metachars, shell IFS, or anything
    1733             # sneaky like that.
    1734 0     0 0   my $name = shift;
    1735 0 0         if ($name =~ m/^([A-Za-z0-9\_\-\.]+)$/) {
    1736 0           return $1;
    1737             } else {
    1738 0           return '';
    1739             }
    1740             }
    1741              
    1742             sub cat {
    1743 0     0 0   my $filename = untaintFilename(shift()); # must be in metaDir
    1744              
    1745 0 0         if ($filename eq '') {
    1746 0           return "['$filename' has funny characters]";
    1747             }
    1748              
    1749 0 0         open (CATFILE, "<$FAQ::OMatic::Config::metaDir/$filename")
    1750             or return "[can't open '$filename': $!]";
    1751 0           my @lines = ;
    1752 0           close CATFILE;
    1753              
    1754 0           return join('', @lines);
    1755             }
    1756              
    1757             # returns true to enable original DBM-based search database code.
    1758             # (in false mode, search is linear scans of files. Slow, but robust.)
    1759             sub usedbm {
    1760 0   0 0 0   return $FAQ::OMatic::Config::useDBMSearch || '';
    1761             }
    1762              
    1763             sub checkLoadAverage {
    1764 0     0 0   if (1) {
    1765             # this cobbled feature has no install-page hook; turn it off for now.
    1766 0           return;
    1767             }
    1768 0           my $uptime = `uptime`;
    1769 0           $uptime =~ m/load average: ([\d\.]+)/;
    1770 0           my $load = $1;
    1771 0 0         if ($load > 4) {
    1772 0           FAQ::OMatic::gripe('abort',
    1773             "I'm too busy for that now. (I'm kind of a crummy PC.)");
    1774             }
    1775             }
    1776              
    1777             # Return the integer prefix to this string, or 0.
    1778             # Used to fix "argument isn't numeric" warnings.
    1779             sub stripInt {
    1780 0     0 0   my $str = shift;
    1781 0 0         if (not defined $str) {
    1782 0           return 0;
    1783             }
    1784 0 0         if (not $str =~ m/^([\d\-]+)/) {
    1785 0           return 0;
    1786             }
    1787 0           return $1;
    1788             }
    1789              
    1790             'true';