Guest User

data.pm

a guest
Jun 13th, 2014
288
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 125.58 KB | None | 0 0
  1. # This code is a part of Slash, and is released under the GPL.
  2. # Copyright 1997-2005 by Open Source Technology Group. See README
  3. # and COPYING for more information, or see http://slashcode.com/.
  4.  
  5. package Slash::Utility::Data;
  6.  
  7. =head1 NAME
  8.  
  9. Slash::MODULE - SHORT DESCRIPTION for Slash
  10.  
  11.  
  12. =head1 SYNOPSIS
  13.  
  14.     use Slash::Utility;
  15.     # do not use this module directly
  16.  
  17. =head1 DESCRIPTION
  18.  
  19. LONG DESCRIPTION.
  20.  
  21.  
  22. =head1 EXPORTED FUNCTIONS
  23.  
  24. =cut
  25.  
  26. BEGIN {
  27. #   $HTML::TreeBuilder::DEBUG = 2;
  28. }
  29.  
  30. use strict;
  31. use Date::Calc qw(Monday_of_Week);
  32. use Date::Format qw(time2str);
  33. use Date::Language;
  34. use Date::Parse qw(str2time);
  35. use Digest::MD5 qw(md5_hex md5_base64);
  36. #use Encode qw(encode_utf8 decode_utf8 is_utf8);
  37. use Email::Valid;
  38. use HTML::Entities qw(:DEFAULT %char2entity %entity2char);
  39. use HTML::FormatText;
  40. use HTML::Tagset ();
  41. use HTML::TokeParser;
  42. use HTML::TreeBuilder;
  43. use Lingua::Stem;
  44. use Mail::Address;
  45. use POSIX qw(UINT_MAX);
  46. use Safe;
  47. use Slash::Constants qw(:strip);
  48. use Slash::Utility::Environment;
  49. use Slash::Apache::User::PasswordSalt;
  50. use URI;
  51. use XML::Parser;
  52.  
  53. use base 'Exporter';
  54.  
  55. # whitespace regex
  56. our $WS_RE = qr{(?: \s | </? (?:br|p) (?:\ /)?> )*}x;
  57.  
  58. # without this, HTML::TreeBuilder will skip slash
  59. BEGIN {
  60.     $HTML::Tagset::isKnown{slash} = 1;
  61.     $HTML::Tagset::optionalEndTag{slash} = 1;
  62.     $HTML::Tagset::isBodyElement{slash} = 1;
  63.     $HTML::Tagset::isPhraseMarkup{slash} = 1;
  64.     $HTML::Tagset::linkElements{slash} = ['src', 'href'];
  65. }
  66.  
  67. our $VERSION = $Slash::Constants::VERSION;
  68. our @EXPORT  = qw(
  69.     addDomainTags
  70.     createStoryTopicData
  71.     slashizeLinks
  72.     approveCharref
  73.     parseDomainTags
  74.     parseSlashizedLinks
  75.     balanceTags
  76.     changePassword
  77.     chopEntity
  78.     cleanRedirectUrl
  79.     cleanRedirectUrlFromForm
  80.     commify
  81.     comparePassword
  82.     countTotalVisibleKids
  83.     countWords
  84.     createLogToken
  85.     createSid
  86.     decode_entities
  87.     determine_html_format
  88.     ellipsify
  89.     emailValid
  90.     email_to_domain
  91.     encryptPassword
  92.     findWords
  93.     fixStory
  94.     fixHref
  95.     fixint
  96.     fixparam
  97.     fixurl
  98.     fudgeurl
  99.     fullhost_to_domain
  100.     formatDate
  101.     getArmoredEmail
  102.     getDayBreakLevels
  103.     getFormatFromDays
  104.     getRandomWordFromDictFile
  105.     getUrlsFromText
  106.     grepn
  107.     html2text
  108.     issueAge
  109.     nickFix
  110.     nick2matchname
  111.     noFollow
  112.     regexSid
  113.     revertQuote
  114.     parseDayBreakLevel
  115.     prepareQuoteReply
  116.     processSub
  117.     quoteFixIntrotext
  118.     root2abs
  119.     roundrand
  120.     set_rootdir
  121.     sitename2filename
  122.     split_bayes
  123.     strip_anchor
  124.     strip_attribute
  125.     strip_code
  126.     strip_extrans
  127.     strip_html
  128.     strip_literal
  129.     strip_mode
  130.     strip_nohtml
  131.     strip_notags
  132.     strip_plaintext
  133.     strip_paramattr
  134.     strip_paramattr_nonhttp
  135.     strip_urlattr
  136.     submitDomainAllowed
  137.     timeCalc
  138.     titleCaseConvert
  139.     url2html
  140.     url2abs
  141.     urlizeTitle
  142.     urlFromSite
  143.     xmldecode
  144.     xmlencode
  145.     xmlencode_plain
  146.     validUrl
  147.     vislenify
  148. );
  149.  
  150.  
  151. # really, these should not be used externally, but we leave them
  152. # here for reference as to what is in the package
  153. # @EXPORT_OK = qw(
  154. #   approveTag
  155. #   breakHtml
  156. #   processCustomTagsPre
  157. #   processCustomTagsPost
  158. #   stripByMode
  159. # );
  160.  
  161. #========================================================================
  162.  
  163. sub nickFix {
  164.     my($nick) = @_;
  165.     return '' if !$nick;
  166.     my $constants = getCurrentStatic();
  167.     my $nc = $constants->{nick_chars} || join('', 'a' .. 'z');
  168.     my $nr = $constants->{nick_regex} || '^[a-z]$';
  169.     $nick =~ s/\s+/ /g;
  170.     $nick =~ s/[^$nc]+//g;
  171.     $nick = substr($nick, 0, $constants->{nick_maxlen});
  172.     return '' if $nick !~ $nr;
  173.     return $nick;
  174. }
  175.  
  176. #========================================================================
  177.  
  178. sub nick2matchname {
  179.     my($nick) = @_;
  180.     $nick = lc $nick;
  181.     $nick =~ s/[^a-zA-Z0-9]//g;
  182.     return $nick;
  183. }
  184.  
  185. #========================================================================
  186. # If you change createSid() for your site, change regexSid() too.
  187. # Check getOpAndDatFromStatusAndURI also.
  188. # If your site will have multiple formats of sids, you'll want this
  189. # to continue matching the old formats too.
  190. # NOTE: sid is also used for discussion ID (and maybe stoid too?),
  191. # such as in comments.pl, so that's what the \d{1,8} is for. -- pudge
  192. sub regexSid {
  193.     my $anchor = shift;
  194.     my $sid = '(\d{2}/\d{2}/\d{2}/\d{3,8}|\d{1,8})';
  195.     return $anchor ? qr{^$sid$} : qr{\b$sid\b};
  196. }
  197.  
  198. #========================================================================
  199.  
  200. =head2 emailValid(EMAIL)
  201.  
  202. Returns true if email is valid, false otherwise.
  203.  
  204. =over 4
  205.  
  206. =item Parameters
  207.  
  208. =over 4
  209.  
  210. =item EMAIL
  211.  
  212. Email address to check.
  213.  
  214. =back
  215.  
  216. =item Return value
  217.  
  218. True if email is valid, false otherwise.
  219.  
  220. =back
  221.  
  222. =cut
  223.  
  224. sub emailValid {
  225.     my($email) = @_;
  226.     return 0 if !$email;
  227.  
  228.     my $constants = getCurrentStatic();
  229.     return 0 if $constants->{email_domains_invalid}
  230.         && ref($constants->{email_domains_invalid})
  231.         && $email =~ $constants->{email_domains_invalid};
  232.  
  233.     my $valid = Email::Valid->new;
  234.     return 0 unless $valid->rfc822($email);
  235.  
  236.     return 1;
  237. }
  238.  
  239. #========================================================================
  240.  
  241. =head2 issueAge(ISSUE)
  242.  
  243. Returns the "age" in days of an issue, given in issue mode form: yyyymmdd.
  244.  
  245. =over 4
  246.  
  247. =item Parameters
  248.  
  249. =over 4
  250.  
  251. =item ISSUE
  252.  
  253. Which issue, in yyyymmdd form (matches /^\d{8}$/)
  254.  
  255. =back
  256.  
  257. =item Return value
  258.  
  259. Age in days of that issue (a decimal number).  Takes current user's
  260. timezone into account.  Return value of 0 indicates error.
  261.  
  262. =back
  263.  
  264. =cut
  265.  
  266. sub issueAge {
  267.     my($issue) = @_;
  268.     return 0 unless $issue =~ /^\d{8}$/;
  269.     my $user = getCurrentUser();
  270.     my $issue_unix_timestamp = timeCalc("${issue}0000", '%s', -$user->{off_set});
  271.     my $age = (time - $issue_unix_timestamp) / 86400;
  272.     $age = 0.00001 if $age == 0; # don't return 0 on success
  273.     return $age;
  274. }
  275.  
  276. #========================================================================
  277.  
  278. =head2 submitDomainAllowed(DOMAIN)
  279.  
  280. Returns true if domain is allowed, false otherwise.
  281.  
  282. =over 4
  283.  
  284. =item Parameters
  285.  
  286. =over 4
  287.  
  288. =item DOMAIN
  289.  
  290. host domain to check.
  291.  
  292. =back
  293.  
  294. =item Return value
  295.  
  296. True if domain is valid, false otherwise.
  297.  
  298. =back
  299.  
  300. =cut
  301.  
  302. sub submitDomainAllowed {
  303.         my($domain) = @_;
  304.  
  305.         my $constants = getCurrentStatic();
  306.         return 0 if $constants->{submit_domains_invalid}
  307.                 && ref($constants->{submit_domains_invalid})
  308.                 && $domain =~ $constants->{submit_domains_invalid};
  309.  
  310.         return 1;
  311. }
  312. #========================================================================
  313.  
  314. =head2 root2abs()
  315.  
  316. Convert C<rootdir> to its absolute equivalent.  By default, C<rootdir> is
  317. protocol-inspecific (such as "//www.example.com") and for redirects needs
  318. to be converted to its absolute form.  There is an C<absolutedir> var, but
  319. it is protocol-specific, and we want to inherit the protocol.  So if we're
  320. connected over HTTPS, we use HTTPS, else we use HTTP.
  321.  
  322. =over 4
  323.  
  324. =item Return value
  325.  
  326. rootdir variable, converted to absolute with proper protocol.
  327.  
  328. =back
  329.  
  330. =cut
  331.  
  332. sub root2abs {
  333.     my $user = getCurrentUser();
  334.  
  335.     # Under specific cirmstances which at best remain nuboiusly vague
  336.     # we don't get values from getCurrentSkin. There's no clear reason
  337.     # on WHY that is, and no errors in the log to suggest the underlying
  338.     # cause of the problem. So, in the interests of sanity, if we don't
  339.     # get valid information from getCurrentSkin(), we'll default to
  340.     # the site's absolutedir. This MIGHT cause some issues with
  341.     # nexuses, but its better than having the site 500
  342.     # - NCommander
  343.  
  344.     my $constants = getCurrentStatic();
  345.  
  346.     if (apacheConnectionSSL()) {
  347.         my $absolutedir_secure = getCurrentSkin('absolutedir_secure');
  348.         if (!$absolutedir_secure) {
  349.             $absolutedir_secure = $constants->{'absolutedir_secure'};
  350.         }      
  351.         #printf STDERR "Secure: ".$absolutedir_secure."\n";
  352.         return $absolutedir_secure;
  353.     } else {
  354.         my $absolutedir = getCurrentSkin('absolutedir');
  355.         if (!$absolutedir) {
  356.             $absolutedir = $constants->{'absolutedir'};
  357.         }      
  358.         #printf STDERR "Nonsecure: ".$absolutedir."\n";
  359.         return $absolutedir;
  360.     }
  361. }
  362.  
  363. #========================================================================
  364.  
  365. =head2 roundrand()
  366.  
  367. Rounds a real value to an integer value, randomly, with the
  368. two options weighted in linear proportion to the fractional
  369. component.  E.g. 1.3 is 30% likely to round to 1, 70% to 2.
  370. And -4.9 is 90% likely to round to -5, 10% to -4.
  371.  
  372. =over 4
  373.  
  374. =item Return value
  375.  
  376. Input value converted to integer.
  377.  
  378. =back
  379.  
  380. =cut
  381.  
  382. sub roundrand {
  383.     my($real) = @_;
  384.     return 0 if !$real;
  385.     my $i = int($real);
  386.     $i-- if $real < 0;
  387.     my $frac = $real - $i;
  388.     return( (rand(1) >= $frac) ? $i : $i+1 );
  389. }
  390.  
  391. #========================================================================
  392.  
  393. =head2 set_rootdir()
  394.  
  395. Make sure all your rootdirs use the same scheme (even if that scheme is no
  396. scheme), and absolutedir's scheme can still be section-specific, and we don't
  397. need an extra var for rootdir/absolutedir.
  398.  
  399. In the future, even this behavior should perhaps be overridable (so
  400. sites could have http for the main site, and https for sections, for
  401. example).
  402.  
  403. =over 4
  404.  
  405. =item Return value
  406.  
  407. rootdir variable, converted to proper scheme.
  408.  
  409. =back
  410.  
  411. =cut
  412.  
  413. sub set_rootdir {
  414.     my($sectionurl, $rootdir) = @_;
  415.     my $rooturi    = new URI $rootdir, 'http';
  416.     my $sectionuri = new URI $sectionurl, 'http';
  417.  
  418.     $sectionuri->scheme($rooturi->scheme || undef);
  419.     return $sectionuri->as_string;
  420. }
  421.  
  422.  
  423. #========================================================================
  424.  
  425. =head2 cleanRedirectUrl(URL)
  426.  
  427. Clean an untrusted URL for safe redirection.  We do not redirect URLs received
  428. from outside Slash (such as in $form->{returnto}) to arbitrary sites, only
  429. to ourself.
  430.  
  431. =over 4
  432.  
  433. =item Parameters
  434.  
  435. =over 4
  436.  
  437. =item URL
  438.  
  439. URL to clean.
  440.  
  441. =back
  442.  
  443. =item Return value
  444.  
  445. Fixed URL.
  446.  
  447. =back
  448.  
  449. =cut
  450.  
  451. sub cleanRedirectUrl {
  452.     my($redirect) = @_;
  453.     my $gSkin = getCurrentSkin();
  454.  
  455.     if (urlFromSite($redirect)) {
  456.         my $base = root2abs();
  457.         return URI->new_abs($redirect || $gSkin->{rootdir}, $base);
  458.     } else {
  459.         return url2abs($gSkin->{rootdir});
  460.     }
  461. }
  462.  
  463.  
  464. sub urlFromSite {
  465.     my($url) = @_;
  466.     my $constants = getCurrentStatic();
  467.     my $user = getCurrentUser();
  468.     my $gSkin = getCurrentSkin();
  469.  
  470.     # We absolutize the return-to URL to our domain just to
  471.     # be sure nobody can use the site as a redirection service.
  472.     # We decide whether to use the secure homepage or not
  473.     # based on whether the current page is secure.
  474.     my $base = root2abs();
  475.     my $clean = URI->new_abs($url || $gSkin->{rootdir}, $base);
  476.  
  477.     # obviously, file: URLs are local
  478.     if ($clean->scheme eq 'file') {
  479.         return 1;
  480.     }
  481.  
  482.     my @site_domain = split m/\./, $gSkin->{basedomain};
  483.     my $site_domain = @site_domain >= 2 ? join '.', @site_domain[-2, -1] : '';
  484.     $site_domain =~ s/:.+$//;   # strip port, if available
  485.  
  486.     my @host = split m/\./, ($clean->can('host') ? $clean->host : '');
  487.     return 0 if scalar(@host) < 2;
  488.     my $host = join '.', @host[-2, -1];
  489.  
  490.     return $site_domain eq $host;
  491. }
  492.  
  493. #========================================================================
  494.  
  495. sub cleanRedirectUrlFromForm {
  496.     my($redirect_formname) = @_;
  497.     my $constants = getCurrentStatic();
  498.     my $gSkin = getCurrentSkin();
  499.     my $form = getCurrentForm();
  500.  
  501.     my $formname = $redirect_formname ? "returnto_$redirect_formname" : 'returnto';
  502.     my $formname_confirm = "${formname}_confirm";
  503.     my $returnto = $form->{$formname} || '';
  504.     return undef if !$returnto;
  505.  
  506.     my $returnto_confirm = $form->{$formname_confirm} || '';
  507.  
  508.     my $returnto_passwd = $constants->{returnto_passwd};
  509.     my $confirmed = md5_hex("$returnto$returnto_passwd") eq $returnto_confirm;
  510.     if ($confirmed) {
  511.         # The URL and the password have been concatted together
  512.         # and confirmed with the MD5, so we know it comes from a
  513.         # trusted source.  Approve it.
  514.         return $returnto;
  515.     } else {
  516.         # There is no proper MD5, so don't redirect.
  517.         return undef;
  518.     }
  519. }
  520.  
  521. #========================================================================
  522.  
  523. =head2 url2abs(URL [, BASE])
  524.  
  525. Take URL and make it absolute.  It takes a URL,
  526. and adds base to the beginning if necessary, and
  527. adds the protocol to the beginning if necessary, and
  528. then uses URI->new_abs() to get the correct string.
  529.  
  530. =over 4
  531.  
  532. =item Parameters
  533.  
  534. =over 4
  535.  
  536. =item URL
  537.  
  538. URL to make absolute.
  539.  
  540. =item BASE
  541.  
  542. URL base.  If not provided, uses rootdir.
  543.  
  544. =back
  545.  
  546. =item Return value
  547.  
  548. Fixed URL.
  549.  
  550. =back
  551.  
  552. =cut
  553.  
  554. sub url2abs {
  555.     my($url, $base) = @_;
  556.     my $newurl;
  557.  
  558.     # set base only if not already set, and rootdir exists
  559.     $base ||= root2abs();
  560.  
  561.     if ($base) {
  562.         $newurl = URI->new_abs($url, $base)->canonical->as_string;
  563.     } elsif ($url !~ m|^https?://|i) {  # no base or rootdir, best we can do
  564.         $newurl =~ s|^/*|/|;
  565.     }
  566.  
  567.     $newurl =~ s|/$|| if $url !~ m|/$|;
  568.  
  569.     return $newurl;
  570. }
  571.  
  572. #========================================================================
  573.  
  574. =head2 formatDate(DATA [, COLUMN, AS, FORMAT])
  575.  
  576. Converts dates from the database; takes an arrayref of rows.
  577.  
  578. This example would take the 1th element of each arrayref in C<$data>, format it,
  579. and put the result in the 2th element.
  580.  
  581.     formatDate($data, 1, 2);
  582.  
  583. This example would take the "foo" key of each hashref in C<$data>, format it,
  584. and put the result in the "bar" key.
  585.  
  586.     formatDate($data, 'foo', 'bar');
  587.  
  588. The C<timeCalc> function does the formatting.
  589.  
  590. =over 4
  591.  
  592. =item Parameters
  593.  
  594. =over 4
  595.  
  596. =item DATA
  597.  
  598. Data is either an arrayref of arrayrefs, or an arrayref of hashrefs.
  599. Which it is will be determined by whether COLUMN is numeric or not.  If
  600. it is numeric, then DATA will be assumed to be an arrayref of arrayrefs.
  601.  
  602. =item COLUMN
  603.  
  604. The column to take the data from, to be translated.  If numeric, then
  605. DATA will be taken to be an arrayref of arrayrefs.  Otherwise, the value
  606. will be the hashref key.  Default value is "date".
  607.  
  608. =item AS
  609.  
  610. The column where to put the newly formatted data.  If COLUMN is numeric
  611. and AS is not defined, then AS will be the same value as COLUMN.  Otherwise,
  612. the default value of AS is "time".
  613.  
  614. =item FORMAT
  615.  
  616. Optional Date::Format format string.
  617.  
  618. =back
  619.  
  620. =item Return value
  621.  
  622. True if successful, false if not.
  623.  
  624. =item Side effects
  625.  
  626. Changes values in DATA.
  627.  
  628. =item Dependencies
  629.  
  630. The C<timeCalc> function.
  631.  
  632. =back
  633.  
  634. =cut
  635.  
  636. sub formatDate {
  637.     my($data, $col, $as, $format) = @_;
  638.     errorLog('Not arrayref'), return unless ref($data) eq 'ARRAY';
  639.  
  640.     if (defined($col) && $col =~ /^\d+$/) {   # LoL
  641.         $as = defined($as) ? $as : $col;
  642.         for (@$data) {
  643.             errorLog('Not arrayref'), return unless ref eq 'ARRAY';
  644.             $_->[$as] = timeCalc($_->[$col], $format);
  645.         }
  646.     } else {    # LoH
  647.         $col ||= 'date';
  648.         $as  ||= 'time';
  649.         for (@$data) {
  650.             errorLog('Not hashref'), return unless ref eq 'HASH';
  651.             $_->{$as} = timeCalc($_->{$col}, $format);
  652.         }
  653.     }
  654. }
  655.  
  656.  
  657. #========================================================================
  658.  
  659. =head2 timeCalc(DATE [, FORMAT, OFFSET])
  660.  
  661. Format time strings using user's format preference.
  662.  
  663. =over 4
  664.  
  665. =item Parameters
  666.  
  667. =over 4
  668.  
  669. =item DATE
  670.  
  671. Raw date/time to format.
  672. Supply a false value here to get the current date/time.
  673.  
  674. =item FORMAT
  675.  
  676. Optional format to override user's format.
  677.  
  678. =item OFFSET
  679.  
  680. Optional positive or negative integer for offset seconds from GMT,
  681. to override user's offset.
  682.  
  683. =back
  684.  
  685. =item Return value
  686.  
  687. Formatted date string.
  688.  
  689. =item Dependencies
  690.  
  691. The 'atonish' and 'aton' template blocks.
  692.  
  693. =back
  694.  
  695. =cut
  696.  
  697. sub timeCalc {
  698.     # raw mysql date of story
  699.     my($date, $format, $off_set, $options) = @_;
  700.     my $user = getCurrentUser();
  701.     my(@dateformats, $err);
  702.  
  703.     $off_set = $user->{off_set} || 0 if !defined $off_set;
  704.  
  705.     if ($date) {
  706.         # massage data for YYYYMMDDHHmmSS or YYYYMMDDHHmm (with optional TZ)
  707.         $date =~ s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})?( [a-zA-Z]+)?$/"$1-$2-$3 $4:$5:" . ($6 || '00') . ($7 || '')/e;
  708.  
  709.         # find out the user's time based on personal offset in seconds
  710.         $date = str2time($date) + $off_set;
  711.     } else {
  712.         # use current time (plus offset) if no time provided
  713.         $date = time() + $off_set;
  714.     }
  715.  
  716.     # set user's language; we only use this if it is defined,
  717.     # so it's not a performance hit
  718.     my $lang = getCurrentStatic('datelang');
  719.  
  720.     # If no format passed in, default to the current user's.
  721.     $format ||= $user->{'format'};
  722.  
  723.     if ($format =~ /\bIF_OLD\b/) {
  724.         # Split $format into its new half and old half.
  725.         my($format_new, $format_old) = $format =~ /^(.+?)\s*\bIF_OLD\b\s*(.+)$/;
  726.         warn "format cannot be parsed: '$format'" if !defined($format_new);
  727.         # Reassign whichever half we want back to $format.
  728.         $format = $date < time() - 180*86400
  729.             || ($options && $options->{is_old})
  730.             ? $format_old
  731.             : $format_new;
  732.     }
  733.  
  734.     # convert the raw date to pretty formatted date
  735.     if ($lang && $lang ne 'English') {
  736.         my $datelang = Date::Language->new($lang);
  737.         $date = $datelang->time2str($format, $date);
  738.     } else {
  739.         $date = time2str($format, $date);
  740.     }
  741.  
  742.     # return the new pretty date
  743.     return $date;
  744. }
  745.  
  746. sub titleCaseConvert {
  747.     my($title) = @_;
  748.     my @words = split / /, $title;
  749.     my @newwords;
  750.  
  751.     for (my $i = 0; $i < @words; $i++) {
  752.         my $word = $words[$i];
  753.         if ($i == 0) {
  754.             $word = ucfirst $word;
  755.         } elsif ($word =~ m/^a(n|nd)?$|^the$|^of$/i) {
  756.             $word = lcfirst $word;
  757.         } else {
  758.             $word = ucfirst $word;
  759.         }
  760.  
  761.         push @newwords, $word;
  762.     }
  763.  
  764.     $title = join(' ', @newwords);
  765.     return $title;
  766. }
  767.  
  768. sub quoteFixIntrotext {
  769.     my ($text) = @_;
  770.     if ($text =~ m/^[^"]*"[^"]*"[^"]*$/s) {
  771.         $text =~ s/"/'/g;
  772.     }
  773.     return $text;
  774. }
  775.  
  776. sub getFormatFromDays {
  777.     my($days, $options) = @_;
  778.     my $ret_array = [];
  779.     return $ret_array unless $days && ref($days) eq 'ARRAY';
  780.  
  781.     my $label;
  782.     my $which_day;
  783.     my $orig_day = $options->{orig_day} || $days->[0];
  784.     my($db_levels, $db_order) = getDayBreakLevels();
  785.  
  786.     my $slashdb = getCurrentDB();
  787.     my $today = $slashdb->getDay(0, { orig_day => $orig_day });
  788.     my $yesterday = $slashdb->getDay(1, { orig_day => $orig_day });
  789.  
  790.     if ($orig_day =~ $db_levels->{hour}{re}) {
  791.         $which_day = 'hour';
  792.         my $yesterday = $slashdb->getDay(1);
  793.  
  794.         for my $day (@$days) {
  795.             my @arr  = $day   =~ $db_levels->{$which_day}{re};
  796.             my $fmt = '%l:00%P'; # 2:00pm
  797.  
  798.             if ($today =~ /^$arr[0]$arr[1]$arr[2]$arr[3]/) {
  799.                 #$fmt = 'Now';
  800.             } elsif ($yesterday =~ /^$arr[0]$arr[1]$arr[2]/) {
  801.                 $fmt = "Yesterday, $fmt";
  802.             } elsif ($today !~ /^$arr[0]/) {
  803.                 $fmt = "%b. %e, %Y $fmt";
  804.             } elsif ($today !~ /^$arr[0]$arr[1]$arr[2]/) {
  805.                 $fmt = "%B %e, $fmt";
  806.             }
  807.  
  808.             push @$ret_array, [ $day, timeCalc($day . '00', $fmt, 0) ];
  809.         }
  810.  
  811.     } elsif ($orig_day =~ $db_levels->{day}{re}) {
  812.         $which_day = 'day';
  813.  
  814.         my $weekago = $slashdb->getDay(7, { orig_day => $orig_day });
  815.         my($ty, $tm, $td) = $today =~ $db_levels->{$which_day}{re};
  816.  
  817.         for my $day (@$days) {
  818.             my @arr = $day =~ $db_levels->{$which_day}{re};
  819.             if ($day eq $today) {
  820.                 $label = 'Today';
  821.             } elsif ($day eq $yesterday) {
  822.                 $label = 'Yesterday';
  823.             } elsif ($day <= $today && $day >= $weekago) {
  824.                 $label = timeCalc($day, '%A', 0);
  825.             } elsif ($ty == $arr[0]) {
  826.                 $label = timeCalc($day, '%B %e', 0);
  827.             } else {
  828.                 $label = timeCalc($day, '%b. %e, %Y', 0);
  829.             }
  830.             push @$ret_array, [ $day, $label ];
  831.         }
  832.  
  833.     } elsif ($orig_day =~ $db_levels->{week}{re}) {
  834.         $which_day = 'week';
  835.  
  836.         for my $day (@$days) {
  837.             my @arr = $day =~ $db_levels->{$which_day}{re};
  838.  
  839.             if ($day eq $today) {
  840.                 $label = 'This Week';
  841.             } elsif ($day eq $yesterday) {
  842.                 $label = 'Last Week';
  843.             } else {
  844.                 my($y, $m, $d) = Monday_of_Week($arr[1]+1, $arr[0]);
  845.                 my $tmpday = sprintf($db_levels->{day}{sfmt}, $y, $m, $d);
  846.                 my $fmt = 'Week of %B %e';
  847.                 if ($today !~ /^$y/) {
  848.                     $fmt .= ', %Y';
  849.                 }
  850.                 $label = timeCalc($tmpday, $fmt, 0);
  851.             }
  852.  
  853.             push @$ret_array, [ $day, $label ];
  854.         }
  855.  
  856.     } elsif ($orig_day =~ $db_levels->{month}{re}) {
  857.         $which_day = 'month';
  858.         for my $day (@$days) {
  859.             (my $tmpday = $day) =~ s/m$//;
  860.             my $fmt = '%B';
  861.             (my $y = $tmpday) =~ s/\d\d$//;
  862.             if ($today !~ /^$y/) {
  863.                 $fmt .= ' %Y';
  864.             }
  865.             push @$ret_array, [ $day, timeCalc($tmpday . '01', $fmt, 0) ];
  866.         }
  867.  
  868.     } elsif ($orig_day =~ $db_levels->{year}{re}) {
  869.         $which_day = 'year';
  870.         for my $day (@$days) {
  871.             push @$ret_array, [ $day, timeCalc($day . '0101', '%Y', 0) ];
  872.         }
  873.     }
  874.  
  875.  
  876.     errorLog("No format found for $orig_day") unless $which_day;
  877.  
  878.     $_->[1] =~ s/May\./May/ for @$ret_array;
  879.  
  880.     # re-format elements if necessary
  881. #   $_->[0] = sprintf($db_levels->{$which_day}{refmt}, $_->[0]) for @$ret_array;
  882.  
  883.     return $ret_array;
  884. }
  885.  
  886. {
  887.     my @db_levels = (
  888.         hour    => { fmt => '%Y%m%d%H', sfmt => '%04d%02d%02d%02d', refmt => '%s',  re => qr{^(\d{4})(\d{2})(\d{2})(\d{2})$}, timefmt => sub { "$_[0]-$_[1]-$_[2] $_[3]:00:00" } },
  889.         day     => { fmt => '%Y%m%d',   sfmt => '%04d%02d%02d',     refmt => '%s',  re => qr{^(\d{4})-?(\d{2})-?(\d{2})$},    timefmt => sub { "$_[0]-$_[1]-$_[2] 00:00:00" } },
  890.         week    => { fmt => '%Y%Ww',    sfmt => '%04d%02dw',        refmt => '%sw', re => qr{^(\d{4})(\d{1,2})w$},            timefmt => sub { sprintf "%04d-%02d-%02d 00:00:00", Monday_of_Week($_[1]+1,$_[0]) } },
  891.         month   => { fmt => '%Y%mm',    sfmt => '%04d%02dm',        refmt => '%sm', re => qr{^(\d{4})(\d{2})m$},              timefmt => sub { "$_[0]-$_[1]-01 00:00:00" } },
  892.         year    => { fmt => '%Y',       sfmt => '%04d',             refmt => '%s',  re => qr{^(\d{4})$},                      timefmt => sub { "$_[0]-01-01 00:00:00" } },
  893.     );
  894.     my %db_levels = @db_levels;
  895.     my $i = 0;
  896.     my @db_order = grep { ++$i % 2 } @db_levels;
  897.     sub getDayBreakLevels { return(\%db_levels, \@db_order) }
  898. }
  899.  
  900. sub parseDayBreakLevel {
  901.     my($day) = @_;
  902.     my($db_levels, $db_order) = getDayBreakLevels();
  903.     for my $level (@$db_order) {
  904.         return $level if $day =~ $db_levels->{$level}{re};
  905.     }
  906.     return;
  907. }
  908.  
  909. #========================================================================
  910.  
  911. =head2 createLogToken()
  912.  
  913. Return new random 22-character logtoken, composed of \w chars.
  914.  
  915. =over 4
  916.  
  917. =item Return value
  918.  
  919. Return a random password that matches /^\w{22}$/.
  920.  
  921. We're only pulling out 3 chars each time thru this loop, so we only
  922. need (and trust) about 18 bits worth of randomness.  We re-seed srand
  923. periodically to try to get more randomness into the mix ("it uses a
  924. semirandom value supplied by the kernel (if it supports the /dev/urandom
  925. device)", says the Camel book).  I don't think I'm doing anything
  926. mathematically dumb to introduce any predictability into this, so it
  927. should be fine, wasteful of a few microseconds perhaps, ugly perhaps, but
  928. the 22-char value it returns should have very close to 131 bits of
  929. randomness.
  930.  
  931. =back
  932.  
  933. =cut
  934.  
  935. sub createLogToken {
  936.     my $str = '';
  937.     my $need_srand = 0;
  938.     while (length($str) < 22) {
  939.         if ($need_srand) {
  940.             srand();
  941.             $need_srand = 0;
  942.         }
  943.         my $r = rand(UINT_MAX) . ':' . rand(UINT_MAX);
  944.         my $md5 = md5_base64($r);
  945.         $md5 =~ tr/A-Za-z0-9//cd;
  946.         $str .= substr($md5, int(rand 8) + 5, 3);
  947.         $need_srand = 1 if rand() < 0.3;
  948.     }
  949.     return substr($str, 0, 22);
  950. }
  951.  
  952. #========================================================================
  953.  
  954. =head2 changePassword()
  955.  
  956. Return new random 8-character password composed of 0..9, A..Z, a..z
  957. (but not including possibly hard-to-read characters [0O1Iil]).
  958.  
  959. =over 4
  960.  
  961. =item Return value
  962.  
  963. Random password.
  964.  
  965. =back
  966.  
  967. =cut
  968.  
  969. {
  970.     my @chars = grep !/[0O1Iil]/, 0..9, 'A'..'Z', 'a'..'z';
  971.     sub changePassword {
  972.         return join '', map { $chars[rand @chars] } 0 .. 7;
  973.     }
  974. }
  975.  
  976. #========================================================================
  977.  
  978. =head2 encryptPassword(PASSWD)
  979.  
  980. Encrypts given password, using the most recent salt (if any) in
  981. Slash::Apache::User::PasswordSalt for the current virtual user.
  982. Currently uses MD5, but could change in the future, so do not
  983. depend on the implementation.
  984.  
  985. =over 4
  986.  
  987. =item Parameters
  988.  
  989. =over 4
  990.  
  991. =item PASSWD
  992.  
  993. Password to be encrypted.
  994.  
  995. =back
  996.  
  997. =item Return value
  998.  
  999. Encrypted password.
  1000.  
  1001. =back
  1002.  
  1003. =cut
  1004.  
  1005. sub encryptPassword {
  1006.     my($passwd, $uid) = @_;
  1007.     $uid ||= '';
  1008.     my $slashdb = getCurrentDB();
  1009.     my $vu = $slashdb->{virtual_user};
  1010.     my $salt = Slash::Apache::User::PasswordSalt::getCurrentPwSalt($vu);
  1011.     #$passwd = Encode::encode_utf8($passwd) if getCurrentStatic('utf8');
  1012.     return md5_hex("$salt:$uid:$passwd");
  1013. }
  1014.  
  1015. #========================================================================
  1016.  
  1017. =head2 comparePassword(PASSWD, MD5, ISPLAIN, ISENC)
  1018.  
  1019. Given a password and an MD5 hex string, compares the two to see if they
  1020. represent the same value.  To be precise:
  1021.  
  1022. If the password given is equal to the MD5 string, it must already be
  1023. in MD5 format and be correct, so return true
  1024.  
  1025. Otherwise, the password is assumed to be plaintext.  Each possible
  1026. salt-encryption of it (including the encryption with empty salt) is
  1027. compared against the MD5 string.  True is returned if there is any
  1028. match.
  1029.  
  1030. If ISPLAIN is true, PASSWD is assumed to be plaintext, so the
  1031. (trivial equality) test against the encrypted MD5 is not performed.
  1032.  
  1033. If ISENC is true, PASSWD is assumed to be already encrypted, so the
  1034. tests of salting and encrypting it are not performed.
  1035.  
  1036. (If neither is true, all tests are performed.  If both are true, no
  1037. tests are performed and 0 is returned.)
  1038.  
  1039. =over 4
  1040.  
  1041. =item Parameters
  1042.  
  1043. =over 4
  1044.  
  1045. =item PASSWD
  1046.  
  1047. Possibly-correct password, either plaintext or already-MD5's,
  1048. to be checked.
  1049.  
  1050. =item MD5
  1051.  
  1052. Encrypted correct password.
  1053.  
  1054. =back
  1055.  
  1056. =item Return value
  1057.  
  1058. 0 or 1.
  1059.  
  1060. =back
  1061.  
  1062. =cut
  1063.  
  1064. sub comparePassword {
  1065.     my($passwd, $md5, $uid, $is_plain, $is_enc) = @_;
  1066.     if (!$is_plain) {
  1067.         return 1 if $passwd eq $md5;
  1068.     }
  1069.     if (!$is_enc) {
  1070.         # An old way of encrypting a user's password, which we have
  1071.         # to check for reverse compatibility.
  1072.         return 1 if md5_hex($passwd) eq $md5;
  1073.  
  1074.         # No?  OK let's see if it matches any of the salts.
  1075.         my $slashdb = getCurrentDB();
  1076.         my $vu = $slashdb->{virtual_user};
  1077.         my $salt_ar = Slash::Apache::User::PasswordSalt::getPwSalts($vu);
  1078.         unshift @$salt_ar, ''; # always test the case of no salt
  1079.         for my $salt (reverse @$salt_ar) {
  1080.             # The current way of encrypting a user's password.
  1081.             return 1 if md5_hex("$salt:$uid:$passwd") eq $md5;
  1082.             # An older way, which we have to check for reverse
  1083.             # compatibility.
  1084.             return 1 if length($salt) && md5_hex("$salt$passwd") eq $md5;
  1085.         }
  1086.     }
  1087.     return 0;
  1088. }
  1089.  
  1090. sub split_bayes {
  1091.     my($t) = @_;
  1092.     my $constants = getCurrentStatic();
  1093.     my $min_len = $constants->{fhbayes_min_token_len} ||  2;
  1094.     my $max_len = $constants->{fhbayes_max_token_len} || 20;
  1095.  
  1096.     my $urls = getUrlsFromText($t);
  1097.     my(@urls, @domains) = ( );
  1098.     for my $url (@$urls) {
  1099.         push @urls, $url;
  1100.         my $uri = URI->new($url);
  1101.         next unless $uri->can('host');
  1102.         my $domain = fullhost_to_domain($uri->host());
  1103.         next unless $domain;
  1104.         push @domains, $domain;
  1105.     }
  1106.  
  1107.     $t = strip_nohtml($t);
  1108.  
  1109.     $t =~ s/[^A-Za-z0-9&;.']+/ /g;
  1110.     $t =~ s/\s[.']+/ /g;
  1111.     $t =~ s/[.']+\s/ /g;
  1112.  
  1113.     my @tokens = grep { length($_) >= $min_len && length($_) <= $max_len } split ' ', $t;
  1114.  
  1115.     return (@urls, @domains, @tokens);
  1116. }
  1117.  
  1118.  
  1119. #========================================================================
  1120.  
  1121. =head2 stripByMode(STRING [, MODE, NO_WHITESPACE_FIX])
  1122.  
  1123. Private function.  Fixes up a string based on what the mode is.  This
  1124. function is no longer exported, use the C<strip_*> functions instead.
  1125.  
  1126. =over 4
  1127.  
  1128. [ Should this be somewhat templatized, so they can customize
  1129. the little HTML bits? Same goes with related functions. -- pudge ]
  1130.  
  1131. =item Parameters
  1132.  
  1133. =over 4
  1134.  
  1135. =item STRING
  1136.  
  1137. The string to be manipulated.
  1138.  
  1139. =item MODE
  1140.  
  1141. May be one of:
  1142.  
  1143. =item nohtml
  1144.  
  1145. The default.  Just strips out HTML.
  1146.  
  1147. =item literal
  1148.  
  1149. Prints the text verbatim into HTML, which
  1150. means just converting < and > and & to their
  1151. HTML entities.  Also turns on NO_WHITESPACE_FIX.
  1152.  
  1153. =item extrans
  1154.  
  1155. Similarly to 'literal', converts everything
  1156. to its HTML entity, but then formatting is
  1157. preserved by converting spaces to HTML
  1158. space entities, and multiple newlines into BR
  1159. tags.
  1160.  
  1161. =item code
  1162.  
  1163. Just like 'extrans' but wraps in CODE tags.
  1164.  
  1165. =item attribute
  1166.  
  1167. Attempts to format string to fit in as an HTML
  1168. attribute, which means the same thing as 'literal',
  1169. but " marks are also converted to their HTML entity.
  1170.  
  1171. =item plaintext
  1172.  
  1173. Similar to 'extrans', but does not translate < and >
  1174. and & first (so C<stripBadHtml> is called first).
  1175.  
  1176. =item anchor
  1177.  
  1178. Removes ALL whitespace from inside the filter. It's
  1179. is indented for use (but not limited to) the removal
  1180. of white space from in side HREF anchor tags to
  1181. prevent nasty browser artifacts from showing up in
  1182. the display. (Note: the value of NO_WHITESPACE_FIX
  1183. is ignored)
  1184.  
  1185. =item html (or anything else)
  1186.  
  1187. Just runs through C<stripBadHtml>.
  1188.  
  1189.  
  1190. =item NO_WHITESPACE_FIX
  1191.  
  1192. A boolean that, if true, disables fixing of whitespace
  1193. problems.  A common exploit in these things is to
  1194. run a lot of characters together so the page will
  1195. stretch very wide.  If NO_WHITESPACE_FIX is false,
  1196. then space is inserted to prevent this (see C<breakHtml>).
  1197.  
  1198. =back
  1199.  
  1200. =item Return value
  1201.  
  1202. The manipulated string.
  1203.  
  1204.  
  1205. =back
  1206.  
  1207. =cut
  1208.  
  1209. { # closure for stripByMode
  1210.  
  1211. my %ansi_to_ascii = (
  1212.     131 => 'f',
  1213.     133 => '...',
  1214.     138 => 'S',
  1215.     140 => 'OE',
  1216.     142 => 'Z',
  1217.     145 => '\'',
  1218.     146 => '\'',
  1219.     147 => '"',
  1220.     148 => '"',
  1221.     150 => '-',
  1222.     151 => '--',
  1223.     153 => '(TM)',
  1224.     154 => 's',
  1225.     156 => 'oe',
  1226.     158 => 'z',
  1227.     159 => 'Y',
  1228.     166 => '|',
  1229.     169 => '(C)',
  1230.     174 => '(R)',
  1231.     177 => '+/-',
  1232.     188 => '1/4',
  1233.     189 => '1/2',
  1234.     190 => '3/4',
  1235.     192 => 'A',
  1236.     193 => 'A',
  1237.     194 => 'A',
  1238.     195 => 'A',
  1239.     196 => 'A',
  1240.     197 => 'A',
  1241.     198 => 'AE',
  1242.     199 => 'C',
  1243.     200 => 'E',
  1244.     201 => 'E',
  1245.     202 => 'E',
  1246.     203 => 'E',
  1247.     204 => 'I',
  1248.     205 => 'I',
  1249.     206 => 'I',
  1250.     207 => 'I',
  1251.     208 => 'D',
  1252.     209 => 'N',
  1253.     210 => 'O',
  1254.     211 => 'O',
  1255.     212 => 'O',
  1256.     213 => 'O',
  1257.     214 => 'O',
  1258.     215 => 'x',
  1259.     216 => 'O',
  1260.     217 => 'U',
  1261.     218 => 'U',
  1262.     219 => 'U',
  1263.     220 => 'U',
  1264.     221 => 'Y',
  1265.     223 => 'B',
  1266.     224 => 'a',
  1267.     225 => 'a',
  1268.     226 => 'a',
  1269.     227 => 'a',
  1270.     228 => 'a',
  1271.     229 => 'a',
  1272.     230 => 'ae',
  1273.     231 => 'c',
  1274.     232 => 'e',
  1275.     233 => 'e',
  1276.     234 => 'e',
  1277.     235 => 'e',
  1278.     236 => 'i',
  1279.     237 => 'i',
  1280.     238 => 'i',
  1281.     239 => 'i',
  1282.     240 => 'd',
  1283.     241 => 'n',
  1284.     242 => 'o',
  1285.     243 => 'o',
  1286.     244 => 'o',
  1287.     245 => 'o',
  1288.     246 => 'o',
  1289.     247 => '/',
  1290.     248 => 'o',
  1291.     249 => 'u',
  1292.     250 => 'u',
  1293.     251 => 'u',
  1294.     252 => 'u',
  1295.     253 => 'y',
  1296.     255 => 'y',
  1297. );
  1298.  
  1299. my %ansi_to_utf = (
  1300.     128 => 8364,
  1301.     129 => '',
  1302.     130 => 8218,
  1303.     131 => 402,
  1304.     132 => 8222,
  1305.     133 => 8230,
  1306.     134 => 8224,
  1307.     135 => 8225,
  1308.     136 => 710,
  1309.     137 => 8240,
  1310.     138 => 352,
  1311.     139 => 8249,
  1312.     140 => 338,
  1313.     141 => '',
  1314.     142 => 381,
  1315.     143 => '',
  1316.     144 => '',
  1317.     145 => 8216,
  1318.     146 => 8217,
  1319.     147 => 8220,
  1320.     148 => 8221,
  1321.     149 => 8226,
  1322.     150 => 8211,
  1323.     151 => 8212,
  1324.     152 => 732,
  1325.     153 => 8482,
  1326.     154 => 353,
  1327.     155 => 8250,
  1328.     156 => 339,
  1329.     157 => '',
  1330.     158 => 382,
  1331.     159 => 376,
  1332. );
  1333.  
  1334. # protect the hash by just returning it, for external use only
  1335. sub _ansi_to_ascii { %ansi_to_ascii }
  1336. sub _ansi_to_utf   { %ansi_to_utf }
  1337.  
  1338. sub _charsetConvert {
  1339.     my($char, $constants) = @_;
  1340.     $constants ||= getCurrentStatic();
  1341.  
  1342.     my $str = '';
  1343.     if ($constants->{draconian_charset_convert}) {
  1344.         if ($constants->{draconian_charrefs}) {
  1345.             if ($constants->{good_numeric}{$char}) {
  1346.                 $str = sprintf('&#%u;', $char);
  1347.             } else { # see if char is in %good_entity
  1348.                 my $ent = $char2entity{chr $char};
  1349.                 if ($ent) {
  1350.                     (my $data = $ent) =~ s/^&(\w+);$/$1/;
  1351.                     $str = $ent if $constants->{good_entity}{$data};
  1352.                 }
  1353.             }
  1354.         }
  1355.         # fall back
  1356.         $str ||= $ansi_to_ascii{$char};
  1357.     }
  1358.  
  1359.     # fall further back
  1360.     # if the char is a special one we don't recognize in Latin-1,
  1361.     # convert it here.  this does not prevent someone from manually
  1362.     # entering &#147; or some such, if they feel they need to, it is
  1363.     # to help catch it when browsers send non-Latin-1 data even though
  1364.     # they shouldn't
  1365.     $char = $ansi_to_utf{$char} if exists $ansi_to_utf{$char};
  1366.     $str ||= sprintf('&#%u;', $char) if length $char;
  1367.     return $str;
  1368. }
  1369.  
  1370. sub _fixupCharrefs {
  1371.     my $constants = getCurrentStatic();
  1372.  
  1373.     return if $constants->{bad_numeric};
  1374.  
  1375.     # At the moment, unless the "draconian" rule is set, only
  1376.     # entities that change the direction of text are forbidden.
  1377.     # For more information, see
  1378.     # <http://www.w3.org/TR/html4/struct/dirlang.html#bidirection>
  1379.     # and <http://www.htmlhelp.com/reference/html40/special/bdo.html>.
  1380.     $constants->{bad_numeric}  = { map { $_, 1 } @{$constants->{charrefs_bad_numeric}} };
  1381.     $constants->{bad_entity}   = { map { $_, 1 } @{$constants->{charrefs_bad_entity}} };
  1382.  
  1383.     $constants->{good_numeric} = { map { $_, 1 } @{$constants->{charrefs_good_numeric}},
  1384.         grep { $_ < 128 || $_ > 159 } keys %ansi_to_ascii };
  1385.     $constants->{good_entity}  = { map { $_, 1 } @{$constants->{charrefs_good_entity}}, qw(apos quot),
  1386.         grep { s/^&(\w+);$/$1/ } map { $char2entity{chr $_} }
  1387.         grep { $_ < 128 || $_ > 159 } keys %ansi_to_ascii };
  1388. }
  1389.  
  1390. my %action_data = ( );
  1391.  
  1392. my %actions = (
  1393.     newline_to_local => sub {
  1394.             ${$_[0]} =~ s/(?:\015?\012|\015)/\n/g;      },
  1395.     trailing_whitespace => sub {
  1396.             ${$_[0]} =~ s/[\t ]+\n/\n/g;            },
  1397.     encode_html_amp => sub {
  1398.             ${$_[0]} =~ s/&/&amp;/g;            },
  1399.     encode_html_amp_ifnotent => sub {
  1400.             ${$_[0]} =~ s/&(?!#?[a-zA-Z0-9]+;)/&amp;/g; },
  1401.     encode_html_ltgt => sub {
  1402.             ${$_[0]} =~ s/</&lt;/g;
  1403.             ${$_[0]} =~ s/>/&gt;/g;             },
  1404.     encode_html_ltgt_stray => sub {
  1405.             1 while ${$_[0]} =~ s{
  1406.                 ( (?: ^ | > ) [^<]* )
  1407.                 >
  1408.             }{$1&gt;}gx;
  1409.             1 while ${$_[0]} =~ s{
  1410.                 <
  1411.                 ( [^>]* (?: < | $ ) )
  1412.                 >
  1413.             }{&lt;$1}gx;                    },
  1414.     encode_html_quote => sub {
  1415.             ${$_[0]} =~ s/"/&#34;/g;            },
  1416.     breakHtml_ifwhitefix => sub {
  1417.             ${$_[0]} = breakHtml(${$_[0]})
  1418.                 unless $action_data{no_white_fix};  },
  1419.     processCustomTagsPre => sub {
  1420.             ${$_[0]} = processCustomTagsPre(${$_[0]});  },
  1421.     processCustomTagsPost => sub {
  1422.             ${$_[0]} = processCustomTagsPost(${$_[0]}); },
  1423.     approveTags => sub {
  1424.             ${$_[0]} =~ s/<(.*?)>/approveTag($1)/sge;   },
  1425.     url2html => sub {
  1426.             ${$_[0]} = url2html(${$_[0]});          },
  1427.     approveCharrefs => sub {
  1428.             ${$_[0]} =~ s{
  1429.                 &(\#?[a-zA-Z0-9]+);?
  1430.             }{approveCharref($1)}gex;           },
  1431.     space_between_tags => sub {
  1432.             ${$_[0]} =~ s/></> </g;             },
  1433.     whitespace_tagify => sub {
  1434.             ${$_[0]} =~ s/\n/<br>/gi;  # pp breaks
  1435.             ${$_[0]} =~ s/(?:<br>\s*){2,}<br>/<br><br>/gi;
  1436.             # Preserve leading indents / spaces
  1437.             # can mess up internal tabs, oh well
  1438.             ${$_[0]} =~ s/\t/    /g;            },
  1439.     paragraph_wrap => sub {
  1440.             # start off the text with a <p>!
  1441.             ${$_[0]} = '<p>' . ${$_[0]} unless ${$_[0]} =~ /^\s*<p>/s;
  1442.             # this doesn't assume there will be only two BRs,
  1443.             # but it does come after whitespace_tagify, so
  1444.             # chances are, will be only two BRs in a row
  1445.             ${$_[0]} =~ s/(?:<br>){2}/<p>/g;
  1446.             # make sure we don't end with a <br><p> or <br>
  1447.             ${$_[0]} =~ s/<br>(<p>|$)/$1/g;         },
  1448.     whitespace_and_tt => sub {
  1449.             ${$_[0]} =~ s{((?:  )+)(?: (\S))?} {
  1450.                 ("&nbsp; " x (length($1)/2)) .
  1451.                 (defined($2) ? "&nbsp;$2" : "")
  1452.             }eg;
  1453.             ${$_[0]} = "<tt>${$_[0]}</tt>";         },
  1454.     newline_indent => sub {
  1455.             ${$_[0]} =~ s{<br>\n?( +)} {
  1456.                 "<br>\n" . ('&nbsp; ' x length($1))
  1457.             }ieg;                       },
  1458.     remove_tags => sub {
  1459.             ${$_[0]} =~ s/<.*?>//gs;            },
  1460.     remove_ltgt => sub {
  1461.             ${$_[0]} =~ s/<//g;
  1462.             ${$_[0]} =~ s/>//g;             },
  1463.     remove_trailing_lts => sub {
  1464.             ${$_[0]} =~ s/<(?!.*?>)//gs;            },
  1465.     remove_newlines => sub {
  1466.             ${$_[0]} =~ s/\n+//g;               },
  1467.     debugprint => sub {
  1468.             print STDERR "stripByMode debug ($_[1]) '${$_[0]}'\n";  },
  1469. );
  1470.  
  1471. my %mode_actions = (
  1472.     ANCHOR, [qw(
  1473.             newline_to_local
  1474.             remove_newlines         )],
  1475.     NOTAGS, [qw(
  1476.             newline_to_local
  1477.             remove_tags
  1478.             remove_ltgt
  1479.             encode_html_amp_ifnotent )],
  1480.     ATTRIBUTE, [qw(
  1481.             newline_to_local
  1482.             encode_html_amp
  1483.             encode_html_ltgt
  1484.             encode_html_quote       )],
  1485.     LITERAL, [qw(
  1486.             newline_to_local
  1487.             encode_html_amp
  1488.             encode_html_ltgt
  1489.             remove_trailing_lts
  1490.             approveTags
  1491.             space_between_tags
  1492.             encode_html_ltgt_stray      )],
  1493.     NOHTML, [qw(
  1494.             newline_to_local
  1495.             trailing_whitespace
  1496.             remove_tags
  1497.             remove_ltgt
  1498.             encode_html_amp         )],
  1499.     PLAINTEXT, [qw(
  1500.             newline_to_local
  1501.             trailing_whitespace
  1502.             processCustomTagsPre
  1503.             remove_trailing_lts
  1504.             approveTags
  1505.             processCustomTagsPost
  1506.             space_between_tags
  1507.             encode_html_ltgt_stray
  1508.             encode_html_amp_ifnotent
  1509.             whitespace_tagify
  1510.             newline_indent
  1511.             paragraph_wrap          )],
  1512.     HTML, [qw(
  1513.             newline_to_local
  1514.             trailing_whitespace
  1515.             processCustomTagsPre
  1516.             remove_trailing_lts
  1517.             approveTags
  1518.             processCustomTagsPost
  1519.             space_between_tags
  1520.             encode_html_ltgt_stray
  1521.             encode_html_amp_ifnotent )],
  1522.     CODE, [qw(
  1523.             newline_to_local
  1524.             trailing_whitespace
  1525.             encode_html_amp
  1526.             encode_html_ltgt
  1527.             whitespace_tagify
  1528.             whitespace_and_tt )],
  1529.     EXTRANS, [qw(
  1530.             newline_to_local
  1531.             trailing_whitespace
  1532.             encode_html_amp
  1533.             encode_html_ltgt
  1534.             whitespace_tagify
  1535.             newline_indent          )],
  1536. );
  1537.  
  1538. sub stripByMode {
  1539.     my($str, $fmode, $no_white_fix) = @_;
  1540.     $str = '' if !defined($str);
  1541.     $fmode ||= NOHTML;
  1542.     $no_white_fix = 1 if !defined($no_white_fix) && $fmode == LITERAL;
  1543.     $action_data{no_white_fix} = $no_white_fix || 0;
  1544.  
  1545.     my @actions = @{$mode_actions{$fmode}};
  1546. #my $c = 0; print STDERR "stripByMode:start:$c:[{ $str }]\n";
  1547.     for my $action (@actions) {
  1548.         $actions{$action}->(\$str, $fmode);
  1549. #$c++; print STDERR "stripByMode:$action:$c:[{ $str }]\n";
  1550.     }
  1551.     return $str;
  1552. }
  1553.  
  1554. }
  1555.  
  1556. #========================================================================
  1557.  
  1558. =head2 strip_anchor(STRING [, NO_WHITESPACE_FIX])
  1559.  
  1560. =head2 strip_attribute(STRING [, NO_WHITESPACE_FIX])
  1561.  
  1562. =head2 strip_code(STRING [, NO_WHITESPACE_FIX])
  1563.  
  1564. =head2 strip_extrans(STRING [, NO_WHITESPACE_FIX])
  1565.  
  1566. =head2 strip_html(STRING [, NO_WHITESPACE_FIX])
  1567.  
  1568. =head2 strip_literal(STRING [, NO_WHITESPACE_FIX])
  1569.  
  1570. =head2 strip_nohtml(STRING [, NO_WHITESPACE_FIX])
  1571.  
  1572. =head2 strip_notags(STRING [, NO_WHITESPACE_FIX])
  1573.  
  1574. =head2 strip_plaintext(STRING [, NO_WHITESPACE_FIX])
  1575.  
  1576. =head2 strip_mode(STRING [, MODE, NO_WHITESPACE_FIX])
  1577.  
  1578. Wrapper for C<stripByMode>.  C<strip_mode> simply calls C<stripByMode>
  1579. and has the same arguments, but C<strip_mode> will only allow modes
  1580. with values greater than 0, that is, the user-supplied modes.  C<strip_mode>
  1581. is only meant to be used for processing user-supplied modes, to prevent
  1582. the user from accessing other mode types.  For using specific modes instead
  1583. of user-supplied modes, use the function with that mode's name.
  1584.  
  1585. See C<stripByMode> for details.
  1586.  
  1587. =cut
  1588.  
  1589. sub strip_mode {
  1590.     my($string, $mode, @args) = @_;
  1591.     return "" if !$mode || $mode < 1 || $mode > 4;  # user-supplied modes are 1-4
  1592.     return stripByMode($string, $mode, @args);
  1593. }
  1594.  
  1595. sub strip_anchor    { stripByMode($_[0], ANCHOR,    @_[1 .. $#_]) }
  1596. sub strip_attribute { stripByMode($_[0], ATTRIBUTE, @_[1 .. $#_]) }
  1597. sub strip_code      { stripByMode($_[0], CODE,  @_[1 .. $#_]) }
  1598. sub strip_extrans   { stripByMode($_[0], EXTRANS,   @_[1 .. $#_]) }
  1599. sub strip_html      { stripByMode($_[0], HTML,  @_[1 .. $#_]) }
  1600. sub strip_literal   { stripByMode($_[0], LITERAL,   @_[1 .. $#_]) }
  1601. sub strip_nohtml    { stripByMode($_[0], NOHTML,    @_[1 .. $#_]) }
  1602. sub strip_notags    { stripByMode($_[0], NOTAGS,    @_[1 .. $#_]) }
  1603. sub strip_plaintext { stripByMode($_[0], PLAINTEXT, @_[1 .. $#_]) }
  1604.  
  1605. sub determine_html_format {
  1606.     my($html, $user) = @_;
  1607.     my $posttype = PLAINTEXT;
  1608.  
  1609.     my $is_admin = 0;
  1610.     my($match, $admin_text);
  1611.     $user ||= getCurrentUser();
  1612.     $is_admin = isAdmin($user) || $user->{acl}{journal_admin_tags};
  1613.  
  1614.     if ($is_admin) {
  1615.         my $constants = getCurrentStatic();
  1616.         my $cache = getCurrentCache();
  1617.         $match = $cache->{approvedtags_admin_alone};
  1618.         if (!$match) {
  1619.             my %tags = map {$_ => 1} @{$constants->{approvedtags_admin}};
  1620.             delete $tags{$_} for @{$constants->{approvedtags}};
  1621.             $match = join '|', map lc, keys %tags;
  1622.             $cache->{approvedtags_admin_alone} = $match = qr/$match/;
  1623.         }
  1624.     }
  1625.  
  1626.     # first check to see if the post starts with <pre>
  1627.     if ($html =~ /^\s*<pre>/s) {
  1628.         $posttype = CODE;
  1629.         $html =~ s/<\/?pre>//g;
  1630.  
  1631.     # then see if user is an admin, and there's an admin-only tag used
  1632.     } elsif ($is_admin && $html =~ /<(?:$match)\b/) {
  1633.         $posttype = FULLHTML;
  1634.  
  1635.     # finally see if there's a line-breaking tag
  1636.     } elsif ($html =~ /<(?:p|br)\b/) {
  1637.         $posttype = HTML;
  1638.     }
  1639.  
  1640.     # the HTML can be modified, so need to return the HTML too
  1641.     return($html, $posttype);
  1642. }
  1643.  
  1644. #========================================================================
  1645.  
  1646. =head2 strip_paramattr(STRING [, NO_WHITESPACE_FIX])
  1647.  
  1648. =head2 strip_paramattr_nonhttp(STRING [, NO_WHITESPACE_FIX])
  1649.  
  1650. =head2 strip_urlattr(STRING [, NO_WHITESPACE_FIX])
  1651.  
  1652. Wrappers for strip_attribute(fixparam($param), $no_whitespace_fix) and
  1653. strip_attribute(fudgeurl($url), $no_whitespace_fix).
  1654.  
  1655. Note that http is a bit of a special case:  its parameters can be escaped
  1656. with "+" for " ", instead of just "%20".  So strip_paramattr should
  1657. probably be renamed strip_paramattrhttp to best indicate that it is a
  1658. special case.  But because the special case is also the most common case,
  1659. with over 100 occurrences in the code, we leave it named strip_paramattr,
  1660. and create a new function strip_paramattr_nonhttp which must be used for
  1661. URI schemes which do not behave in that way.
  1662.  
  1663. =cut
  1664.  
  1665. sub strip_paramattr     { strip_attribute(fixparam($_[0]), $_[1]) }
  1666. sub strip_paramattr_nonhttp { my $h = strip_attribute(fixparam($_[0]), $_[1]); $h =~ s/\+/%20/g; $h }
  1667. sub strip_urlattr       { strip_attribute(fudgeurl($_[0]), $_[1]) }
  1668.  
  1669.  
  1670. #========================================================================
  1671.  
  1672. =head2 stripBadHtml(STRING)
  1673.  
  1674. Private function.  Strips out "bad" HTML by removing unbalanced HTML
  1675. tags and sending balanced tags through C<approveTag>.  The "unbalanced"
  1676. checker is primitive; no "E<lt>" or "E<gt>" tags will are allowed inside
  1677. tag attributes (such as E<lt>A NAME="E<gt>"E<gt>), that breaks the tag.
  1678. Whitespace is inserted between adjacent tags, so "E<lt>BRE<gt>E<lt>BRE<gt>"
  1679. becomes "E<lt>BRE<gt> E<lt>BRE<gt>".  And character references are routed
  1680. through C<approveCharref>.
  1681.  
  1682. =over 4
  1683.  
  1684. =item Parameters
  1685.  
  1686. =over 4
  1687.  
  1688. =item STRING
  1689.  
  1690. String to be processed.
  1691.  
  1692. =back
  1693.  
  1694. =item Return value
  1695.  
  1696. Processed string.
  1697.  
  1698. =item Dependencies
  1699.  
  1700. C<approveTag> function, C<approveCharref> function.
  1701.  
  1702. =back
  1703.  
  1704. =cut
  1705.  
  1706. sub stripBadHtml {
  1707.     my($str) = @_;
  1708.  
  1709.     $str =~ s/<(?!.*?>)//gs;
  1710.     $str =~ s/<(.*?)>/approveTag($1)/sge;
  1711.     $str =~ s/></> </g;
  1712.  
  1713.     # Encode stray >
  1714.     1 while $str =~ s{
  1715.         (
  1716.             (?: ^ | > ) # either beginning of string,
  1717.                     # or another close bracket
  1718.             [^<]*       # not matching open bracket
  1719.         )
  1720.         >           # close bracket
  1721.     }{$1&gt;}gx;
  1722.  
  1723.     # Encode stray <
  1724.     1 while $str =~ s{
  1725.         <           # open bracket
  1726.         (
  1727.             [^>]*       # not match close bracket
  1728.             (?: < | $ ) # either open bracket, or
  1729.                     # end of string
  1730.         )
  1731.     }{&lt;$1}gx;
  1732.  
  1733.     my $ent = qr/#?[a-zA-Z0-9]+/;
  1734.     $str =~ s/&(?!$ent;)/&amp;/g;
  1735.     $str =~ s/&($ent);?/approveCharref($1)/ge;
  1736.  
  1737.     return $str;
  1738. }
  1739.  
  1740. #========================================================================
  1741.  
  1742. =head2 processCustomTagsPre(STRING)
  1743.  
  1744. =head2 processCustomTagsPost(STRING)
  1745.  
  1746. Private function.  It does processing of special custom tags (in Pre, ECODE;
  1747. in Post, QUOTE).
  1748.  
  1749. =over 4
  1750.  
  1751. =item Parameters
  1752.  
  1753. =over 4
  1754.  
  1755. =item STRING
  1756.  
  1757. String to be processed.
  1758.  
  1759. =back
  1760.  
  1761. =item Return value
  1762.  
  1763. Processed string.
  1764.  
  1765. =item Dependencies
  1766.  
  1767. Pre is meant to be used before C<approveTag> is called; Post after.
  1768. Both are called only from regular posting modes, HTML and PLAINTEXT.
  1769.  
  1770. =back
  1771.  
  1772. =cut
  1773.  
  1774. sub processCustomTagsPre {
  1775.     my($str) = @_;
  1776.     my $constants = getCurrentStatic();
  1777.  
  1778.     ## Deal with special ECODE tag (Embedded Code).  This tag allows
  1779.     ## embedding the Code postmode in plain or HTML modes.  It may be
  1780.     ## of the form:
  1781.     ##    <ECODE>literal text</ECODE>
  1782.     ## or, for the case where "</ECODE>" is needed in the text:
  1783.     ##    <ECODE END="SOMETAG">literal text</SOMETAG>
  1784.     ##
  1785.     ## SOMETAG must match /^\w+$/.
  1786.     ##
  1787.     ##
  1788.     ## Note that we also strip out leading and trailing newlines
  1789.     ## surrounding the tags, because in plain text mode this can
  1790.     ## be hard to manage, so we manage it for the user.
  1791.     ##
  1792.     ## Also note that this won't work if the site disallows TT
  1793.     ## or BLOCKQUOTE tags.
  1794.     ##
  1795.     ## -- pudge
  1796.  
  1797.     # ECODE must be in approvedtags
  1798.     if (grep /^ecode$/i, @{$constants->{approvedtags}}) {
  1799.         $str =~ s|<(/?)literal>|<${1}ecode>|gi;  # we used to accept "literal" too
  1800.         my $ecode   = 'ecode';
  1801.         my $open    = qr[\n* <\s* (?:$ecode) (?: \s+ END="(\w+)")? \s*> \n*]xsio;
  1802.         my $close_1 = qr[($open (.*?) \n* <\s* /\2    \s*> \n*)]xsio;  # if END is used
  1803.         my $close_2 = qr[($open (.*?) \n* <\s* /ECODE \s*> \n*)]xsio;  # if END is not used
  1804.  
  1805.         while ($str =~ m[($open)]g) {
  1806.             my $len = length($1);
  1807.             my $end = $2;
  1808.             my $pos = pos($str) - $len;
  1809.  
  1810.             my $close = $end ? $close_1 : $close_2;
  1811.             my $substr = substr($str, $pos);
  1812.             if ($substr =~ m/^$close/si) {
  1813.                 my $len = length($1);
  1814.                 my $codestr = $3;
  1815.                 # remove these if they were added by url2html; I know
  1816.                 # this is a rather cheesy way to do this, but c'est la vie
  1817.                 # -- pudge
  1818.                 $codestr =~ s{<a href="[^"]+" rel="url2html-$$">(.+?)</a>}{$1}g;
  1819.                 my $code = strip_code($codestr);
  1820.                 my $newstr = "<p><blockquote>$code</blockquote></p>";
  1821.                 substr($str, $pos, $len) = $newstr;
  1822.                 pos($str) = $pos + length($newstr);
  1823.             }
  1824.         }
  1825.     }
  1826.     return $str;
  1827. }
  1828.  
  1829. sub processCustomTagsPost {
  1830.     my($str) = @_;
  1831.     my $constants = getCurrentStatic();
  1832.  
  1833.     # QUOTE must be in approvedtags
  1834.     if (grep /^quote$/i, @{$constants->{approvedtags}}) {
  1835.         my $quote   = 'quote';
  1836.         my $open    = qr[\n* <\s*  $quote \s*> \n*]xsio;
  1837.         my $close   = qr[\n* <\s* /$quote \s*> \n*]xsio;
  1838.  
  1839.         $str =~ s/$open/<p><div class="quote">/g;
  1840.         $str =~ s/$close/<\/div><\/p>/g;
  1841.     }
  1842.  
  1843.     # just fix the whitespace for blockquote to something that looks
  1844.     # universally good
  1845.     if (grep /^blockquote$/i, @{$constants->{approvedtags}}) {
  1846.         my $quote   = 'blockquote';
  1847.         my $open    = qr[\s* <\s*  $quote \s*> \n*]xsio;
  1848.         my $close   = qr[\s* <\s* /$quote \s*> \n*]xsio;
  1849.  
  1850.         $str =~ s/(?<!<p>)$open/<p><$quote>/g;
  1851.     }
  1852.  
  1853.     return $str;
  1854. }
  1855.  
  1856. # revert div class="quote" back to <quote>, handles nesting
  1857. sub revertQuote {
  1858.     my($str) = @_;
  1859.  
  1860.     my $bail = 0;
  1861.     while ($str =~ m|((<p>)?<div class="quote">)(.+)$|sig) {
  1862.         my($found, $p, $rest) = ($1, $2, $3);
  1863.         my $pos = pos($str) - (length($found) + length($rest));
  1864.         substr($str, $pos, length($found)) = '<quote>';
  1865.         pos($str) = $pos + length('<quote>');
  1866.  
  1867.         my $c = 0;
  1868.         $bail = 1;
  1869.         while ($str =~ m|(<(/?)div.*?>(</p>)?)|sig) {
  1870.             my($found, $end, $p2) = ($1, $2, $3);
  1871.             if ($end && !$c) {
  1872.                 $bail = 0;  # if we don't get here, something is wrong
  1873.                 my $len = length($found);
  1874.                 # + 4 is for the </p>
  1875.                 my $pl = $p && $p2 ? 4 : 0;
  1876.                 substr($str, pos($str) - $len, $len + $pl) = '</quote>';
  1877.                 pos($str) = 0;
  1878.                 last;
  1879.             } elsif ($end) {
  1880.                 $c--;
  1881.             } else {
  1882.                 $c++;
  1883.             }
  1884.         }
  1885.  
  1886.         if ($bail) {
  1887.             use Data::Dumper;
  1888.             warn "Stuck in endless loop: " . Dumper({
  1889.                 found   => $found,
  1890.                 p   => $p,
  1891.                 rest    => $rest,
  1892.                 'pos'   => $pos,
  1893.                 str => $str,
  1894.             });
  1895.             last;
  1896.         }
  1897.     }
  1898.     return($str);
  1899. }
  1900.  
  1901.  
  1902. sub prepareQuoteReply {
  1903.     my($reply) = @_;
  1904.     my $pid_reply = $reply->{comment} = parseDomainTags($reply->{comment}, 0, 1, 1);
  1905.     $pid_reply = revertQuote($pid_reply);
  1906.  
  1907.     # prep for JavaScript
  1908.     $pid_reply =~ s|\\|\\\\|g;
  1909.     $pid_reply =~ s|'|\\'|g;
  1910.     $pid_reply =~ s|([\r\n])|\\n|g;
  1911.  
  1912.     $pid_reply =~ s{<nobr> <wbr></nobr>(\s*)} {$1 || ' '}gie;
  1913.     #my $nick = strip_literal($reply->{nickname});
  1914.     #$pid_reply = "<div>$nick ($reply->{uid}) wrote: <quote>$pid_reply</quote></div>";
  1915.     $pid_reply = "<quote>$pid_reply</quote>";
  1916. }
  1917.  
  1918.  
  1919. #========================================================================
  1920.  
  1921. =head2 breakHtml(TEXT, MAX_WORD_LENGTH)
  1922.  
  1923. Private function.  Break up long words in some text.  Will ignore the
  1924. contents of HTML tags.  Called from C<stripByMode> functions -- if
  1925. there are any HTML tags in the text, C<stripBadHtml> will have been
  1926. called first.  Handles spaces before dot-words so as to best work around a
  1927. Microsoft bug.  This code largely contributed by Joe Groff <joe at pknet
  1928. dot com>.
  1929.  
  1930. =over 4
  1931.  
  1932. =item Parameters
  1933.  
  1934. =over 4
  1935.  
  1936. =item TEXT
  1937.  
  1938. The text to be fixed.
  1939.  
  1940. =item MAX_WORD_LENGTH
  1941.  
  1942. The maximum length of a word.  Default is 50 (breakhtml_wordlength in vars).
  1943.  
  1944. =back
  1945.  
  1946. =item Return value
  1947.  
  1948. The text.
  1949.  
  1950. =back
  1951.  
  1952. =cut
  1953.  
  1954. sub breakHtml {
  1955.     my($text, $mwl) = @_;
  1956.     return $text if $Slash::Utility::Data::approveTag::admin
  1957.              && $Slash::Utility::Data::approveTag::admin > 1;
  1958.  
  1959.     my $constants = getCurrentStatic();
  1960.     $mwl = $mwl || $constants->{breakhtml_wordlength} || 50;
  1961.  
  1962.     # Only do the <NOBR> and <WBR> bug workaround if wanted.
  1963.     my $workaround_start = $constants->{comment_startword_workaround}
  1964.         ? "<nobr>" : "";
  1965.     my $workaround_end = $constants->{comment_startword_workaround}
  1966.         ? "<wbr></nobr> " : " ";
  1967.  
  1968.     # These are tags that "break" a word;
  1969.     # a<P>b</P> breaks words, y<B>z</B> does not
  1970.     my $approvedtags_break = $constants->{'approvedtags_break'} || [];
  1971.     my $break_tag = join '|', @$approvedtags_break;
  1972.     $break_tag = qr{(?:$break_tag)}i;
  1973.  
  1974.     # This is the regex that finds a char that, at the start of
  1975.     # a word, will trigger Microsoft's bug.  It's already been
  1976.     # set up for us, it just needs a shorter name.
  1977.     my $nswcr = $constants->{comment_nonstartwordchars_regex};
  1978.  
  1979.     # And we also need a regex that will find an HTML entity or
  1980.     # character references, excluding ones that would break words:
  1981.     # a non-breaking entity.  For now, let's assume *all* entities
  1982.     # are non-breaking (except an encoded space which would be
  1983.     # kinda dumb).
  1984.     my $nbe = qr{ (?:
  1985.         &
  1986.         (?! \# (?:32|x20) )
  1987.         (\#?[a-zA-Z0-9]+)
  1988.         ;
  1989.     ) }xi;
  1990.  
  1991.     # Mark off breaking tags, as we don't want them counted as
  1992.     # part of long words
  1993.     $text =~ s{
  1994.         (</?$break_tag>)
  1995.     }{\x00$1\x00}gsx;
  1996.  
  1997.     # Temporarily hide whitespace inside tags so that the regex below
  1998.     # won't accidentally catch attributes, e.g. the HREF= of an A tag.
  1999.     # (Which I don't think it can do anyway, based on the way the
  2000.     # following regex gobbles <> and the fact that tags should already
  2001.     # be balanced by this point...but this can't hurt - Jamie)
  2002.     1 while $text =~ s{
  2003.         (<[^>\s]*)  # Seek in a tab up to its
  2004.         \s+     # first whitespace
  2005.     }{$1\x00}gsx;       # and replace the space with NUL
  2006.  
  2007.     # Put the <wbr> in front of attempts to exploit MSIE's
  2008.     # half-braindead adherance to Unicode char breaking.
  2009.     $text =~ s{$nswcr}{<nobr> <wbr></nobr>$2$3}gs
  2010.         if $constants->{comment_startword_workaround};
  2011.  
  2012.     # Break up overlong words, treating entities/character references
  2013.     # as single characters and ignoring HTML tags.
  2014.     $text =~ s{(
  2015.         (?:^|\G|\s)     # Must start at a word bound
  2016.         (?:
  2017.             (?>(?:<[^>]+>)*)    # Eat up HTML tags
  2018.             (           # followed by either
  2019.                 $nbe        # an entity (char. ref.)
  2020.             |   (?!$nbe)\S  # or an ordinary char
  2021.             )
  2022.         ){$mwl}         # $mwl non-HTML-tag chars in a row
  2023.     )}{
  2024.         substr($1, 0, -length($2))
  2025.         . $workaround_start
  2026.         . substr($1, -length($2))
  2027.         . $workaround_end
  2028.     }gsex;
  2029.  
  2030.     # Just to be tidy, if we appended that word break at the very end
  2031.     # of the text, eliminate it.
  2032.     $text =~ s{<nobr> <wbr></nobr>\s*$}{}
  2033.         if $constants->{comment_startword_workaround};
  2034.  
  2035.     # Fix breaking tags
  2036.     $text =~ s{
  2037.         \x00
  2038.         (</?$break_tag>)
  2039.         \x00
  2040.     }{$1}gsx;
  2041.    
  2042.     # Change other NULs back to whitespace.
  2043.     $text =~ s{\x00}{ }g;
  2044.  
  2045.     return $text;
  2046. }
  2047.  
  2048. #========================================================================
  2049.  
  2050. =head2 fixHref(URL [, ERROR])
  2051.  
  2052. Take a relative URL and fix it to some predefined set.
  2053.  
  2054. I don't really like this function much, it should be played with.
  2055.  
  2056. =over 4
  2057.  
  2058. =item Parameters
  2059.  
  2060. =over 4
  2061.  
  2062. =item URL
  2063.  
  2064. Relative URL to manipulate.
  2065.  
  2066. =item ERROR
  2067.  
  2068. Boolean whether or not to return error number.
  2069.  
  2070. =back
  2071.  
  2072. =item Return value
  2073.  
  2074. Undef if URL is not handled.  If it is handled and ERROR is false,
  2075. new URL is returned.  If it is handled and ERROR is true, URL
  2076. and the error number are returned.
  2077.  
  2078. =item Dependencies
  2079.  
  2080. The fixhrefs section in the vars table, and some sort of table
  2081. (like 404-main) for determining what the number means.
  2082.  
  2083. =back
  2084.  
  2085. =cut
  2086.  
  2087. sub fixHref {  # I don't like this.  we need to change it. -- pudge
  2088.     my($rel_url, $print_errs) = @_;
  2089.     my $abs_url; # the "fixed" URL
  2090.     my $errnum; # the errnum for 404.pl
  2091.  
  2092.     my $fixhrefs = getCurrentStatic('fixhrefs');
  2093.     for my $qr (@{$fixhrefs}) {
  2094.         if ($rel_url =~ $qr->[0]) {
  2095.             my @ret = $qr->[1]->($rel_url);
  2096.             return $print_errs ? @ret : $ret[0];
  2097.         }
  2098.     }
  2099.  
  2100.     my $gSkin = getCurrentSkin();
  2101.     my $rootdir = $gSkin->{rootdir};
  2102.     if ($rel_url =~ /^www\.\w+/) {
  2103.         # errnum 1
  2104.         $abs_url = "http://$rel_url";
  2105.         return($abs_url, 1) if $print_errs;
  2106.         return $abs_url;
  2107.  
  2108.     } elsif ($rel_url =~ /^ftp\.\w+/) {
  2109.         # errnum 2
  2110.         $abs_url = "ftp://$rel_url";
  2111.         return ($abs_url, 2) if $print_errs;
  2112.         return $abs_url;
  2113.  
  2114.     } elsif ($rel_url =~ /^[\w\-\$\.]+\@\S+/) {
  2115.         # errnum 3
  2116.         $abs_url = "mailto:$rel_url";
  2117.         return ($abs_url, 3) if $print_errs;
  2118.         return $abs_url;
  2119.  
  2120.     } elsif ($rel_url =~ /^articles/ && $rel_url =~ /\.shtml$/) {
  2121.         # errnum 6
  2122.         my @chunks = split m|/|, $rel_url;
  2123.         my $file = pop @chunks;
  2124.  
  2125.         if ($file =~ /^98/ || $file =~ /^0000/) {
  2126.             $rel_url = "$rootdir/articles/older/$file";
  2127.             return ($rel_url, 6) if $print_errs;
  2128.             return $rel_url;
  2129.         } else {
  2130.             return;
  2131.         }
  2132.  
  2133.     } elsif ($rel_url =~ /^features/ && $rel_url =~ /\.shtml$/) {
  2134.         # errnum 7
  2135.         my @chunks = split m|/|, $rel_url;
  2136.         my $file = pop @chunks;
  2137.  
  2138.         if ($file =~ /^98/ || $file =~ /~00000/) {
  2139.             $rel_url = "$rootdir/features/older/$file";
  2140.             return ($rel_url, 7) if $print_errs;
  2141.             return $rel_url;
  2142.         } else {
  2143.             return;
  2144.         }
  2145.  
  2146.     } elsif ($rel_url =~ /^books/ && $rel_url =~ /\.shtml$/) {
  2147.         # errnum 8
  2148.         my @chunks = split m|/|, $rel_url;
  2149.         my $file = pop @chunks;
  2150.  
  2151.         if ($file =~ /^98/ || $file =~ /^00000/) {
  2152.             $rel_url = "$rootdir/books/older/$file";
  2153.             return ($rel_url, 8) if $print_errs;
  2154.             return $rel_url;
  2155.         } else {
  2156.             return;
  2157.         }
  2158.  
  2159.     } elsif ($rel_url =~ /^askslashdot/ && $rel_url =~ /\.shtml$/) {
  2160.         # errnum 9
  2161.         my @chunks = split m|/|, $rel_url;
  2162.         my $file = pop @chunks;
  2163.  
  2164.         if ($file =~ /^98/ || $file =~ /^00000/) {
  2165.             $rel_url = "$rootdir/askslashdot/older/$file";
  2166.             return ($rel_url, 9) if $print_errs;
  2167.             return $rel_url;
  2168.         } else {
  2169.             return;
  2170.         }
  2171.  
  2172.     } else {
  2173.         # if we get here, we don't know what to
  2174.         # $abs_url = $rel_url;
  2175.         return;
  2176.     }
  2177.  
  2178.     # just in case
  2179.     return $abs_url;
  2180. }
  2181.  
  2182. #========================================================================
  2183.  
  2184. =head2 approveTag(TAG)
  2185.  
  2186. Private function.  Checks to see if HTML tag is OK, and adjusts it as necessary.
  2187.  
  2188. =over 4
  2189.  
  2190. =item Parameters
  2191.  
  2192. =over 4
  2193.  
  2194. =item TAG
  2195.  
  2196. Tag to check.
  2197.  
  2198. =back
  2199.  
  2200. =item Return value
  2201.  
  2202. Tag after processing.
  2203.  
  2204. =item Dependencies
  2205.  
  2206. Uses the "approvetags" variable in the vars table.  Passes URLs
  2207. in HREFs through C<fudgeurl>.
  2208.  
  2209. =back
  2210.  
  2211. =cut
  2212.  
  2213. {
  2214.     # here's a simple hardcoded list of replacement tags, ones
  2215.     # we don't really care about, or that are no longer valid.
  2216.     # we just replace them with sane substitutes, if and only if
  2217.     # they are not in approvedtags already
  2218.     my %replace = (
  2219.         em  => 'i',
  2220.         strong  => 'b',
  2221.         dfn => 'i',
  2222.         code    => 'tt',
  2223.         samp    => 'tt',
  2224.         kbd => 'tt',
  2225.         var => 'i',
  2226.         cite    => 'i',
  2227.  
  2228.         address => 'i',
  2229.         lh  => 'li',
  2230.         dir => 'ul',
  2231.     );
  2232.  
  2233. sub approveTag {
  2234.     my($wholetag) = @_;
  2235.     my $constants = getCurrentStatic();
  2236.  
  2237.     $wholetag =~ s/^\s*(.*?)\s*$/$1/; # trim leading and trailing spaces
  2238.  
  2239.     # Take care of URL:foo and other HREFs
  2240.     # Using /s means that the entire variable is treated as a single line
  2241.     # which means \n will not fool it into stopping processing.  fudgeurl()
  2242.     # knows how to handle multi-line URLs (it removes whitespace).
  2243.     if ($wholetag =~ /^URL:(.+)$/is) {
  2244.         my $url = fudgeurl($1);
  2245.         return qq!<a href="$url">$url</a>!;
  2246.     }
  2247.  
  2248.     # Build the hash of approved tags
  2249.     # XXX someday maybe should be an option, not a global var ...
  2250.     my $approvedtags = $Slash::Utility::Data::approveTag::admin && $constants->{approvedtags_admin}
  2251.         ? $constants->{approvedtags_admin}
  2252.         : $constants->{approvedtags};
  2253.     my %approved =
  2254.         map  { (lc, 1)   }
  2255.         grep { !/^ecode$/i }
  2256.         @$approvedtags;
  2257.  
  2258.     # We can do some checks at this point.  $t is the tag minus its
  2259.     # properties, e.g. for "<a href=foo>", $t will be "a".
  2260.     my($taglead, $slash, $t) = $wholetag =~ m{^(\s*(/?)\s*(\w+))};
  2261.     my $t_lc = lc $t;
  2262.     if (!$approved{$t_lc}) {
  2263.         if ($replace{$t_lc} && $approved{ $replace{$t_lc} }) {
  2264.             $t = $t_lc = $replace{$t_lc};
  2265.         } else {
  2266.             if ($constants->{approveTag_debug}) {
  2267.                 $Slash::Utility::Data::approveTag::removed->{$t_lc} ||= 0;
  2268.                 $Slash::Utility::Data::approveTag::removed->{$t_lc}++;
  2269.             }
  2270.             return '';
  2271.         }
  2272.     }
  2273.  
  2274.     # These are now stored in a var approvedtags_attr
  2275.     #
  2276.     # A string in the format below:
  2277.     # a:href_RU img:src_RU,alt_N,width,height,longdesc_U
  2278.     #
  2279.     # Is decoded into the following data structure for attribute
  2280.     # approval
  2281.     #
  2282.     # {
  2283.     #   a =>    { href =>   { ord => 1, req => 1, url => 1 } },
  2284.     #   img =>  { src =>    { ord => 1, req => 1, url => 1 },
  2285.     #         alt =>    { ord => 2, req => 2           },
  2286.     #         width =>  { ord => 3                     },
  2287.     #         height => { ord => 4                     },
  2288.     #         longdesc =>   { ord => 5,           url => 1 }, },
  2289.     # }
  2290.     # this is decoded in Slash/DB/MySQL.pm getSlashConf
  2291.  
  2292.     my $attr = $Slash::Utility::Data::approveTag::admin && $constants->{approvedtags_attr_admin}
  2293.         ? $constants->{approvedtags_attr_admin}
  2294.         : $constants->{approvedtags_attr};
  2295.     $attr ||= {};
  2296.  
  2297.     if ($slash) {
  2298.         # Close-tags ("</A>") never get attributes.
  2299.         $wholetag = "/$t_lc";
  2300.  
  2301.     } elsif ($attr->{$t_lc}) {
  2302.         # This is a tag with attributes, verify them.
  2303.  
  2304.         my %allowed = %{$attr->{$t_lc}};
  2305.         my %required =
  2306.             map  { $_, $allowed{$_}  }
  2307.             grep { $allowed{$_}{req} }
  2308.             keys   %allowed;
  2309.  
  2310.         my $tree = HTML::TreeBuilder->new; #_from_content("<$wholetag>");
  2311.         $tree->attr_encoded(1);
  2312.         $tree->implicit_tags(0);
  2313.         $tree->parse("<$wholetag>");
  2314.         $tree->eof;
  2315.         my $elem = $tree->look_down(_tag => $t_lc);
  2316.         # look_down() can return a string for some kinds of bogus data
  2317.         return "" unless $elem && ref($elem) eq 'HTML::Element';
  2318.         my @attr_order =
  2319.             sort { $allowed{lc $a}{ord} <=> $allowed{lc $b}{ord} }
  2320.             grep { !/^_/ && exists $allowed{lc $_} }
  2321.             $elem->all_attr_names;
  2322.         my %attr_data  = map { ($_, $elem->attr($_)) } @attr_order;
  2323.         my %found;
  2324.         $wholetag = $t_lc;
  2325.  
  2326.         for my $a (@attr_order) {
  2327.             my $a_lc = lc $a;
  2328.             next unless $allowed{$a_lc};
  2329.             my $data = $attr_data{$a_lc} || '';
  2330.             $data = fudgeurl($data) if $allowed{$a_lc}{url};
  2331.             next unless length $data;
  2332.             $wholetag .= qq{ $a_lc="$data"};
  2333.             ++$found{$a_lc} if $required{$a_lc};
  2334.         }
  2335.  
  2336.         # If the required attributes were not all present, the whole
  2337.         # tag is invalid, unless req == 2, in which case we fudge it
  2338.         for my $a (keys %required) {
  2339.             my $a_lc = lc $a;
  2340.             next if $found{$a_lc};
  2341.             if ($required{$a}{req} == 2) {
  2342.                 # is there some better default than "*"?
  2343.                 $wholetag .= qq{ $a_lc="*"};
  2344.             } else {
  2345.                 return '';
  2346.             }
  2347.         }
  2348.  
  2349.     } else {
  2350.         # No attributes allowed.
  2351.         $wholetag = $t_lc;
  2352.     }
  2353.  
  2354.     # If we made it here, the tag is valid.
  2355.     return "<$wholetag>";
  2356. }
  2357. }
  2358.  
  2359. #========================================================================
  2360.  
  2361. =head2 approveCharref(CHARREF)
  2362.  
  2363. Private function.  Checks to see if a character reference (minus the
  2364. leading & and trailing ;) is OK.  If so, returns the whole character
  2365. reference (including & and ;), and if not, returns the empty string.
  2366. See <http://www.w3.org/TR/html4/charset.html#h-5.3> for definitions and
  2367. explanations of character references.
  2368.  
  2369. =over 4
  2370.  
  2371. =item Parameters
  2372.  
  2373. =over 4
  2374.  
  2375. =item CHARREF
  2376.  
  2377. HTML character reference to check.
  2378.  
  2379. =back
  2380.  
  2381. =item Return value
  2382.  
  2383. Character reference after processing.
  2384.  
  2385. =item Dependencies
  2386.  
  2387. None.
  2388.  
  2389. =back
  2390.  
  2391. =cut
  2392.  
  2393. sub approveCharref {
  2394.     my($charref) = @_;
  2395.     my $constants = getCurrentStatic();
  2396.  
  2397.     my $ok = 1; # Everything not forbidden is permitted.
  2398.  
  2399.     _fixupCharrefs();
  2400.     my %ansi_to_ascii = _ansi_to_ascii();
  2401.     my $ansi_to_utf   = _ansi_to_utf();
  2402.     my $decimal = 0;
  2403.  
  2404.     if ($ok == 1 && $charref =~ /^#/) {
  2405.         # Probably a numeric character reference.
  2406.         if ($charref =~ /^#x([0-9a-f]+)$/i) {
  2407.             # Hexadecimal encoding.
  2408.             $charref =~ s/^#X/#x/; # X should work fine, but x is better
  2409.             $decimal = hex($1); # always returns a positive integer
  2410.         } elsif ($charref =~ /^#(\d+)$/) {
  2411.             # Decimal encoding.
  2412.             $decimal = $1;
  2413.         } else {
  2414.             # Unknown, assume flawed.
  2415.             $ok = 0;
  2416.         }
  2417.  
  2418.         # NB: 1114111/10FFFF is highest allowed by Unicode spec,
  2419.         # but 917631/E007F is highest with actual glyph
  2420.         $ok = 0 if $decimal <= 0 || $decimal > 65534; # sanity check
  2421.         if ($constants->{draconian_charrefs}) {
  2422.             if (!$constants->{good_numeric}{$decimal}) {
  2423.                 $ok = $ansi_to_ascii{$decimal} ? 2 : 0;
  2424.             }
  2425.         } else {
  2426.             $ok = 0 if $constants->{bad_numeric}{$decimal};
  2427.         }
  2428.     } elsif ($ok == 1 && $charref =~ /^([a-z0-9]+)$/i) {
  2429.         # Character entity.
  2430. #       my $entity = lc $1;
  2431.         my $entity = $1;  # case matters
  2432.         if ($constants->{draconian_charrefs}) {
  2433.             if (!$constants->{good_entity}{$entity}) {
  2434.                 if (defined $entity2char{$entity}) {
  2435.                     $decimal = ord $entity2char{$entity};
  2436.                     $ok = $ansi_to_ascii{$decimal} ? 2 : 0;
  2437.                 } else {
  2438.                     $ok = 0;
  2439.                 }
  2440.             }
  2441.         } else {
  2442.             $ok = 0 if $constants->{bad_entity}{$entity}
  2443.                 || ($constants->{draconian_charset} && ! exists $entity2char{$entity});
  2444.         }
  2445.     } elsif ($ok == 1) {
  2446.         # Unknown character reference type, assume flawed.
  2447.         $ok = 0;
  2448.     }
  2449.  
  2450.     # special case for old-style broken entities we want to convert to ASCII
  2451.     if ($ok == 2 && $decimal) {
  2452.         return $ansi_to_ascii{$decimal};
  2453.     } elsif ($ok) {
  2454.         return "&$charref;";
  2455.     } else {
  2456.         return '';
  2457.     }
  2458. }
  2459.  
  2460. #========================================================================
  2461.  
  2462. =head2 fixparam(DATA)
  2463.  
  2464. Prepares data to be a parameter in a URL.  Such as:
  2465.  
  2466. =over 4
  2467.  
  2468.     my $url = 'http://example.com/foo.pl?bar=' . fixparam($data);
  2469.  
  2470. =item Parameters
  2471.  
  2472. =over 4
  2473.  
  2474. =item DATA
  2475.  
  2476. The data to be escaped.  B<NOTE>: space characters are encoded as C<+>
  2477. instead of C<%20>.  If you must have C<%20>, perform an C<s/\+/%20/g>
  2478. on the result.  Note that this is designed for HTTP URIs, the most
  2479. common scheme;  for other schemes, refer to the comments documenting
  2480. strip_paramattr and strip_paramattr_nonhttp.
  2481.  
  2482. =back
  2483.  
  2484. =item Return value
  2485.  
  2486. The escaped data.
  2487.  
  2488. =back
  2489.  
  2490. =cut
  2491.  
  2492. sub fixparam {
  2493.     my($url) = @_;
  2494.     $url = encode_utf8($url) if (getCurrentStatic('utf8') && is_utf8($url));
  2495.     $url =~ s/([^$URI::unreserved ])/$URI::Escape::escapes{$1}/og;
  2496.     $url =~ s/ /+/g;
  2497.     return $url;
  2498. }
  2499.  
  2500. #========================================================================
  2501.  
  2502. =head2 fixurl(DATA)
  2503.  
  2504. Prepares data to be a URL or in part of a URL.  Such as:
  2505.  
  2506. =over 4
  2507.  
  2508.     my $url = 'http://example.com/~' . fixurl($data) . '/';
  2509.  
  2510. =item Parameters
  2511.  
  2512. =over 4
  2513.  
  2514. =item DATA
  2515.  
  2516. The data to be escaped.
  2517.  
  2518. =back
  2519.  
  2520. =item Return value
  2521.  
  2522. The escaped data.
  2523.  
  2524. =back
  2525.  
  2526. =cut
  2527.  
  2528. {
  2529. # [] is only allowed for IPV6 (see RFC 2732), and we don't use IPV6 ...
  2530. # in theory others could still create links to them, but we would need
  2531. # better heuristics for it, in another place in the code
  2532. (my $allowed = $URI::uric) =~ s/[\[\]]//g;
  2533. # add '#' to allowed characters, since it is often included
  2534. $allowed .= '#';
  2535. sub fixurl {
  2536.     my($url) = @_;
  2537.     #$url = encode_utf8($url) if (getCurrentStatic('utf8') && is_utf8($url));
  2538.     $url =~ s/([^$allowed])/$URI::Escape::escapes{$1}/og;
  2539.     $url =~ s/%(?![a-fA-F0-9]{2})/%25/g;
  2540.     return $url;
  2541. }
  2542. }
  2543.  
  2544. #========================================================================
  2545.  
  2546. =head2 fudgeurl(DATA)
  2547.  
  2548. Prepares data to be a URL.  Such as:
  2549.  
  2550. =over 4
  2551.  
  2552.     my $url = fudgeurl($someurl);
  2553.  
  2554. =item Parameters
  2555.  
  2556. =over 4
  2557.  
  2558. =item DATA
  2559.  
  2560. The data to be escaped.
  2561.  
  2562. =back
  2563.  
  2564. =item Return value
  2565.  
  2566. The escaped data.
  2567.  
  2568. =back
  2569.  
  2570. =cut
  2571.  
  2572. sub fudgeurl {
  2573.     my($url) = @_;
  2574.  
  2575.     ### should we just escape spaces, quotes, apostrophes, and <> instead
  2576.     ### of removing them? -- pudge
  2577.  
  2578.     # Remove quotes and whitespace (we will expect some at beginning and end,
  2579.     # probably)
  2580.     $url =~ s/["\s]//g;
  2581.     # any < or > char after the first char truncates the URL right there
  2582.     # (we will expect a trailing ">" probably)
  2583.     $url =~ s/^[<>]+//;
  2584.     $url =~ s/[<>].*//;
  2585.     # strip surrounding ' if exists
  2586.     $url =~ s/^'(.+?)'$/$1/g;
  2587.     # escape anything not allowed
  2588.     $url = fixurl($url);
  2589.     # run it through the grungy URL miscellaneous-"fixer"
  2590.     $url = fixHref($url) || $url;
  2591.  
  2592.     my $scheme_regex = _get_scheme_regex();
  2593.  
  2594.     my $uri = new URI $url;
  2595.     my $scheme = undef;
  2596.     $scheme = $uri->scheme if $uri && $uri->can("scheme");
  2597.  
  2598.     # modify scheme:/ to scheme:// for $schemes defined below
  2599.     # need to recreate $uri after doing so to make userinfo
  2600.     # clearing work for something like http:/[email protected]
  2601.     my $schemes_to_mod = { http => 1, https => 1, ftp => 1 };
  2602.     if ($scheme && $schemes_to_mod->{$scheme}) {
  2603.         $url = $uri->canonical->as_string;
  2604.         $url =~ s|^$scheme:/([^/])|$scheme://$1|;
  2605.         $uri = new URI $url;
  2606.     }
  2607.  
  2608.     if ($uri && !$scheme && $uri->can("authority") && $uri->authority) {
  2609.         # The URI has an authority but no scheme, e.g. "//sitename.com/".
  2610.         # URI.pm doesn't always handle this well.  E.g. host() returns
  2611.         # undef.  So give it a scheme.
  2612.         # XXX Rethink this -- it could probably be put lower down, in
  2613.         # the "if" that handles stripping the userinfo.  We don't
  2614.         # really need to add the scheme for most URLs. - Jamie
  2615.  
  2616.         # and we should only add scheme if not a local site URL
  2617.         my($from_site) = urlFromSite($uri->as_string);
  2618.         $uri->scheme('http') unless $from_site;
  2619.     }
  2620.  
  2621.     if (!$uri) {
  2622.  
  2623.         # Nothing we can do with it; manipulate the probably-bogus
  2624.         # $url at the end of this function and return it.
  2625.  
  2626.     } elsif ($scheme && $scheme !~ /^$scheme_regex$/) {
  2627.  
  2628.         $url =~ s/^$scheme://i;
  2629.         $url =~ tr/A-Za-z0-9-//cd; # allow only a few chars, for security
  2630.         $url = "$scheme:$url";
  2631.  
  2632.     } elsif ($uri) {
  2633.  
  2634.         # Strip the authority, if any.
  2635.         # This prevents annoying browser-display-exploits
  2636.         # like "http://cnn.com%20%20%20...%[email protected]".
  2637.         # In future we may set up a package global or a field like
  2638.         # getCurrentUser()->{state}{fixurlauth} that will allow
  2639.         # this behavior to be turned off.
  2640.  
  2641.         if ($uri->can('userinfo') && $uri->userinfo) {
  2642.             $uri->userinfo(undef);
  2643.         }
  2644.         if ($uri->can('host') && $uri->host) {
  2645.             # If this scheme has an authority (which means a
  2646.             # username and/or password and/or host and/or port)
  2647.             # then make sure the host and port are legit, and
  2648.             # zap the port if it's the default port.
  2649.             my $host = $uri->host;
  2650.             # Re the below line, see RFC 1035 and maybe 2396.
  2651.             # Underscore is not recommended and Slash has
  2652.             # disallowed it for some time, but allowing it
  2653.             # is really the right thing to do.
  2654.             $host =~ tr/A-Za-z0-9._-//cd;
  2655.             $uri->host($host);
  2656.             if ($uri->can('authority') && $uri->authority) {
  2657.                 # We don't allow anything in the authority except
  2658.                 # the host and optionally a port.  This shouldn't
  2659.                 # matter since the userinfo portion was zapped
  2660.                 # above.  But this is a bit of double security to
  2661.                 # ensure nothing nasty in the authority.
  2662.                 my $authority = $uri->host;
  2663.                 if ($uri->can('host_port')
  2664.                     && $uri->port != $uri->default_port) {
  2665.                     $authority = $uri->host_port;
  2666.                 }
  2667.                 $uri->authority($authority);
  2668.             }
  2669.         }
  2670.  
  2671.         if ($scheme && $scheme eq 'mailto') {
  2672.             if (my $query = $uri->query) {
  2673.                 $query =~ s/@/%40/g;
  2674.                 $uri->query($query);
  2675.             }
  2676.         }
  2677.  
  2678.         $url = $uri->canonical->as_string;
  2679.  
  2680.         if ($url =~ /#/) {
  2681.             my $token = ':::INSERT__23__HERE:::';
  2682.             # no # is OK, unless ...
  2683.             $url =~ s/#/$token/g;
  2684.             if ($url =~ m|^https?://|i || $url =~ m|^/|) {
  2685.                 # HTTP, in which case the first # is OK
  2686.                 $url =~ s/$token/#/;
  2687.             }
  2688.             $url =~ s/$token/%23/g;
  2689.         }
  2690.     }
  2691.  
  2692.     # These entities can crash browsers and don't belong in URLs.
  2693.     $url =~ s/&#(.+?);//g;
  2694.     # we don't like SCRIPT at the beginning of a URL
  2695.     my $decoded_url = decode_entities($url);
  2696.     $decoded_url =~ s{ &(\#?[a-zA-Z0-9]+);? } { approveCharref($1) }gex;
  2697.     return $decoded_url =~ /^[\s\w]*script\b/i ? undef : $url;
  2698. }
  2699.  
  2700. sub _get_scheme_regex {
  2701.     my $constants = getCurrentStatic();
  2702.     if (! $constants->{approved_url_schemes_regex}) {
  2703.         $constants->{approved_url_schemes_regex} = join('|', map { lc } @{$constants->{approved_url_schemes}});
  2704.         $constants->{approved_url_schemes_regex} = qr{(?:$constants->{approved_url_schemes_regex})};
  2705.     }
  2706.     return $constants->{approved_url_schemes_regex};
  2707. }
  2708.  
  2709. #========================================================================
  2710.  
  2711. =head2 chopEntity(STRING)
  2712.  
  2713. Chops a string to a specified length, without splitting in the middle
  2714. of an HTML entity or HTML tag (so we will err on the short side).
  2715.  
  2716. =over 4
  2717.  
  2718. =item Parameters
  2719.  
  2720. =over 4
  2721.  
  2722. =item STRING
  2723.  
  2724. String to be chomped.
  2725.  
  2726. =back
  2727.  
  2728. =item Return value
  2729.  
  2730. Chomped string.
  2731.  
  2732. =back
  2733.  
  2734. =cut
  2735.  
  2736. sub chopEntity {
  2737.     my($text, $length, $end) = @_;
  2738.     #$text = decode_utf8($text) if (getCurrentStatic('utf8') && !is_utf8($text));
  2739.     if ($length && $end) {
  2740.         $text = substr($text, -$length);
  2741.     } elsif ($length) {
  2742.         $text = substr($text, 0, $length);
  2743.     }  
  2744.     $text =~ s/&#?[a-zA-Z0-9]*$//;
  2745.     $text =~ s/<[^>]*$//;
  2746.     return $text;
  2747. }
  2748.  
  2749.  
  2750. sub url2html {
  2751.     my($text) = @_;
  2752.     return '' if !defined($text) || $text eq '';
  2753.  
  2754.     my $scheme_regex = _get_scheme_regex();
  2755.  
  2756.     # we know this can break real URLs, but probably will
  2757.     # preserve real URLs more often than it will break them
  2758.     # was ['":=>]
  2759.     # should we parse the HTML instead?  problematic ...
  2760.     $text =~  s{(?<!\S)((?:$scheme_regex):/{0,2}[$URI::uric#]+)}{
  2761.         my $url   = fudgeurl($1);
  2762.         my $extra = '';
  2763.         $extra = $1 if $url =~ s/([?!;:.,']+)$//;
  2764.         $extra = ')' . $extra if $url !~ /\(/ && $url =~ s/\)$//;
  2765. print STDERR "url2html s/// url='$url' extra='$extra'\n" if !defined($url) || !defined($extra);
  2766.         qq[<a href="$url" rel="url2html-$$">$url</a>$extra];
  2767.     }ogie;
  2768.     # url2html-$$ is so we can remove the whole thing later for ecode
  2769.  
  2770.     return $text;
  2771. }
  2772.  
  2773. sub urlizeTitle {
  2774.     my($title) = @_;
  2775.     $title = strip_notags($title);
  2776.     $title =~ s/^\s+|\s+$//g;
  2777.     $title =~ s/\s+/-/g;
  2778.     $title =~ s/[^A-Za-z0-9\-]//g;
  2779.     return $title;
  2780. }
  2781.  
  2782.  
  2783. sub noFollow {
  2784.     my($html) = @_;
  2785.     $html =~ s/(<a href=.+?)>/$1 rel="nofollow">/gis;
  2786.     return $html;
  2787. }
  2788.  
  2789.  
  2790.  
  2791. # DOCUMENT after we remove some of this in favor of
  2792. # HTML::Element
  2793.  
  2794. sub html2text {
  2795.     my($html, $col) = @_;
  2796.     my($text, $tree, $form, $refs);
  2797.  
  2798.     my $user      = getCurrentUser();
  2799.     my $gSkin     = getCurrentSkin();
  2800.  
  2801.     $col ||= 74;
  2802.  
  2803.     $tree = new HTML::TreeBuilder;
  2804.     $form = new HTML::FormatText (leftmargin => 0, rightmargin => $col-2);
  2805.     $refs = new HTML::FormatText::AddRefs;
  2806.  
  2807.     #my $was_utf8 = getCurrentStatic('utf8') ? is_utf8($html) : 0;
  2808.     $tree->parse($html);
  2809.     $tree->eof;
  2810.     $refs->parse_refs($tree);
  2811.     $text = $form->format($tree);
  2812.     1 while chomp($text);
  2813.  
  2814.     # restore UTF-8 Flag lost by HTML::TreeBuilder
  2815.     #$text = decode_utf8($text) if ($was_utf8);
  2816.  
  2817.     return $text, $refs->get_refs($gSkin->{absolutedir});
  2818. }
  2819.  
  2820. sub HTML::FormatText::AddRefs::new {
  2821.     bless { HH => {}, HA => [], HS => 0 }, $_[0];
  2822. };
  2823.  
  2824. sub HTML::FormatText::AddRefs::parse_refs {
  2825.     my($ref, $self, $format) = @_;
  2826.     $format ||= '[%d]%s';
  2827.  
  2828.     # find all the HREFs where the tag is "a"
  2829.     if (exists $self->{'href'} && $self->{'_tag'} =~ /^[aA]$/) {
  2830.         my $href = $self->{'href'};
  2831.  
  2832.         # only increment number in hash and add to array if
  2833.         # not already in array/hash
  2834.         if (!exists $ref->{'HH'}{$href}) {
  2835.             $ref->{'HH'}{$href} = $$ref{'HS'}++;
  2836.             push(@{$ref->{'HA'}}, $href);
  2837.         }
  2838.  
  2839.         # get nested elements
  2840.         my $con = $self->{'_content'};
  2841.         while (ref($con->[0]) eq 'HTML::Element') {
  2842.             $con = $con->[0]{'_content'};
  2843.         }
  2844.  
  2845.         # add "footnote" to text
  2846.         $con->[0] = sprintf(
  2847.             $format, $ref->{'HH'}{$href}, $con->[0]
  2848.         ) if defined $con->[0];
  2849.  
  2850.     # get nested elements
  2851.     } elsif (exists $self->{'_content'}) {
  2852.         foreach (@{$self->{'_content'}}) {
  2853.             if (ref($_) eq 'HTML::Element') {
  2854.                 $ref->parse_refs($_);
  2855.             }
  2856.         }
  2857.     }
  2858. }
  2859.  
  2860. sub HTML::FormatText::AddRefs::add_refs {
  2861.     my($ref, $url) = @_;
  2862.  
  2863.     my $count = 0;
  2864.     my $ascii = "\n\nReferences\n";
  2865.     foreach ($ref->get_refs($url, $count)) {
  2866.         $ascii .= sprintf("%4d. %s\n", $count++, $_);
  2867.     }
  2868.     return $ascii;
  2869. }
  2870.  
  2871.  
  2872. sub HTML::FormatText::AddRefs::get_refs {
  2873.     my($ref, $url) = @_;
  2874.  
  2875.     my @refs;
  2876.     foreach (@{$ref->{'HA'}}) {
  2877.         push @refs, URI->new_abs($_, $url);
  2878.     }
  2879.     return @refs;
  2880. }
  2881.  
  2882.  
  2883. #========================================================================
  2884.  
  2885. =head2 balanceTags(HTML [, OPTIONS])
  2886.  
  2887. Balances HTML tags; if tags are not closed, close them; if they are not
  2888. open, remove close tags; if they are in the wrong order, reorder them
  2889. (order of open tags determines order of close tags).
  2890.  
  2891. =over 4
  2892.  
  2893. =item Parameters
  2894.  
  2895. =over 4
  2896.  
  2897. =item HTML
  2898.  
  2899. The HTML to balance.
  2900.  
  2901. =item OPTIONS
  2902.  
  2903. A hashref for various options.
  2904.  
  2905. =over 4
  2906.  
  2907. =item deep_nesting
  2908.  
  2909. Integer for how deep to allow nesting indenting tags, 0 means no limit, 1 means
  2910. to use var (nesting_maxdepth).  Default is 0.
  2911.  
  2912. =item deep_su
  2913.  
  2914. Integer for how deep to allow nesting sup/sub tags, 0 means no limit, 1 means
  2915. to use var (nest_su_maxdepth).  Default is 0.
  2916.  
  2917. =item length
  2918.  
  2919. A maximum length limit for the result.
  2920.  
  2921. =back
  2922.  
  2923. =back
  2924.  
  2925. =item Return value
  2926.  
  2927. The balanced HTML.
  2928.  
  2929. =item Dependencies
  2930.  
  2931. The 'approvedtags' entry in the vars table.
  2932.  
  2933. =back
  2934.  
  2935. =cut
  2936.  
  2937. {
  2938.     # these are the tags we know about.
  2939.     # they are hardcoded because the code must know about each one at
  2940.     # a fairly low level; if you want to add more, then we need to
  2941.     # change the code for them.  in theory we could generalize it more,
  2942.     # using vars for all this, but that is a low priority.
  2943.     my %known_tags  = map { ( lc, 1 ) } qw(
  2944.         b i p br a ol ul li dl dt dd em strong tt blockquote div ecode quote
  2945.         img hr big small sub sup span
  2946.         q dfn code samp kbd var cite address ins del
  2947.         h1 h2 h3 h4 h5 h6
  2948.     );
  2949.     # NB: ECODE is excluded because it is handled elsewhere.
  2950.  
  2951.     # tags that are indented, so we can make sure indentation level is not too great
  2952.     my %is_nesting  = map { ( lc, 1 ) } qw(ol ul dl blockquote quote);
  2953.  
  2954.     # or sub-super level
  2955.     my %is_suscript = map { ( lc, 1 ) } qw(sub sup);
  2956.  
  2957.     # block elements cannot be inside certain other elements; this defines which are which
  2958.     my %is_block    = map { ( lc, 1 ) } qw(p ol ul li dl dt dd blockquote quote div hr address h1 h2 h3 h4 h5 h6);
  2959.     my %no_block    = map { ( lc, 1 ) } qw(b i strong em tt q dfn code samp kbd var cite address ins del big small span p sub sup a h1 h2 h3 h4 h5 h6);
  2960.  
  2961.     # needs a <p> inside it
  2962.     my %needs_p     = map { ( lc, 1 ) } qw(blockquote quote div);
  2963.  
  2964.     # when a style tag is cut off prematurely because of a newly introduced block
  2965.     # element, we want to re-start the style inside the block; it is not perfect,
  2966.     # but that's why we're here, innit?
  2967.     my %is_style    = map { ( lc, 1 ) } qw(b i strong em tt q dfn code samp kbd var cite big small span);
  2968.  
  2969.     # tags that CAN be empty
  2970.     my %empty   = map { ( lc, 1 ) } qw(p br img hr);
  2971.     # tags that HAVE to be empty
  2972.     my %really_empty = %empty;
  2973.     # for now p is the only one ... var?
  2974.     delete $really_empty{'p'};
  2975.  
  2976.  
  2977.     # define the lists, and the content elements in the lists, in both directions
  2978.     my %lists = (
  2979.         dl      => ['dd', 'dt'],
  2980.         ul      => ['li'],
  2981.         ol      => ['li'],
  2982.         # blockquote not a list, but has similar semantics:
  2983.         # everything in a blockquote needs to be in a block element,
  2984.         # so we choose two that would fit the bill
  2985.         blockquote  => ['div'],
  2986.     );
  2987.     my %needs_list = (
  2988.         dd      => qr/dl/,
  2989.         dt      => qr/dl/,
  2990.         li      => qr/ul|ol/,
  2991.     );
  2992.  
  2993.     # regexes to use later
  2994.     my $list_re = join '|', keys %lists;
  2995.     my %lists_re;
  2996.     for my $list (keys %lists) {
  2997.         my $re = join '|', @{$lists{$list}};
  2998.         $lists_re{$list} = qr/$re/;
  2999.     }
  3000.  
  3001.     my $is_block_re = join '|', keys %is_block;
  3002.  
  3003. sub balanceTags {
  3004.     my($html, $options) = @_;
  3005.     return '' if !defined($html) || !length($html);
  3006.     my $orightml = $html;
  3007.     my $constants = getCurrentStatic();
  3008.     my $cache = getCurrentCache();
  3009.  
  3010.     my($max_nest_depth, $max_su_depth) = (0, 0);
  3011.     if (ref $options) {
  3012.         $max_nest_depth = ($options->{deep_nesting} && $options->{deep_nesting} == 1)
  3013.             ? $constants->{nesting_maxdepth}
  3014.             : ($options->{deep_nesting} || 0);
  3015.         $max_su_depth   = ($options->{deep_su} && $options->{deep_su} == 1)
  3016.             ? $constants->{nest_su_maxdepth}
  3017.             : ($options->{deep_su} || 0);
  3018.     } else {
  3019.         # deprecated
  3020.         $max_nest_depth = ($options && $options == 1)
  3021.             ? $constants->{nesting_maxdepth}
  3022.             : ($options || 0);
  3023.     }
  3024.  
  3025.     my(%tags, @stack, $tag, $close, $whole, $both, @list, $nesting_level, $su_level);
  3026.  
  3027.     # cache this regex
  3028.     # if $options->{admin} then allow different regex ... also do in approveTag
  3029.     my $matchname = $options->{admin} ? 'match_admin' : 'match';
  3030.     my $varname   = $options->{admin} && $constants->{approvedtags_admin}
  3031.         ? 'approvedtags_admin'
  3032.         : 'approvedtags';
  3033.     my $match = $cache->{balanceTags}{$matchname};
  3034.     if (!$match) {
  3035.         $match = join '|', grep $known_tags{$_},
  3036.             map lc, @{$constants->{$varname}};
  3037.         $cache->{balanceTags}{$matchname} = $match = qr/$match/;
  3038.     }
  3039.  
  3040.     # easier to do this before we start the loop, and then fix it inside
  3041.     # we need to make sure when a block ends, a new <p> begins
  3042.     $html =~ s|(</(?:$is_block_re)>)|$1<p>|g;
  3043.  
  3044.  
  3045.     ## this is the main loop.  it finds a tag, any tag
  3046.     while ($html =~ /(<(\/?)($match)\b[^>]*?( \/)?>)/sig) { # loop over tags
  3047.         ($tag, $close, $whole, $both) = (lc($3), $2, $1, $4);
  3048. #       printf "DEBUG:%d:%s:%s: %d:%s\n%s\n\n", pos($html), $tag, $whole, scalar(@stack), "@stack", $html;
  3049.  
  3050.         # this is a closing tag (note: not an opening AND closing tag,
  3051.         # like <br /> ... that is handled with opening tags)
  3052.         if ($close) {
  3053.             # we have opened this tag already, handle closing of it
  3054.             if (!$really_empty{$tag} && @stack && $tags{$tag}) {
  3055.                 # the tag is the one on the top of the stack,
  3056.                 # remove from stack and counter, and move on
  3057.                 if ($stack[-1] eq $tag) {
  3058.                     pop @stack;
  3059.                     $tags{$tag}--;
  3060.  
  3061.                     # we keep track of lists in an add'l stack,
  3062.                     # so pop off that one too
  3063.                     if ($lists{$tag}) {
  3064.                         my $pop = pop @list;
  3065.                         # this should always be equal, else why
  3066.                         # would it be bottom of @stack too?
  3067.                         # so warn if it isn't ...
  3068.                         warn "huh?  $tag ne $pop?" if $tag ne $pop;
  3069.                     }
  3070.  
  3071.                 # Close tag somewhere else in stack; add it to the
  3072.                 # text and then loop back to catch it properly
  3073.                 # XXX we could optimize here so we don't need to loop back
  3074.                 } else {
  3075.                     _substitute(\$html, $whole, "</$stack[-1]>", 1, 1);
  3076.                 }
  3077.  
  3078.             # Close tag not on stack; just delete it, since it is
  3079.             # obviously not needed
  3080.             } else {
  3081.                 _substitute(\$html, $whole, '');
  3082.             }
  3083.  
  3084.  
  3085.         # this is an open tag (or combined, like <br />)
  3086.         } else {
  3087.             # the tag nests, and we don't want to nest too deeply,
  3088.             # so just remove it if we are in too deep already
  3089.             if ($is_nesting{$tag} && $max_nest_depth) {
  3090.                 my $cur_depth = 0;
  3091.                 $cur_depth += $tags{$_} || 0 for keys %is_nesting;
  3092.                 if ($cur_depth >= $max_nest_depth) {
  3093.                     _substitute(\$html, $whole, '');
  3094.                     next;
  3095.                 }
  3096.             }
  3097.  
  3098.             # the tag nests, and we don't want to nest too deeply,
  3099.             # so just remove it if we are in too deep already
  3100.             if ($is_suscript{$tag} && $max_su_depth) {
  3101.                 my $cur_depth = 0;
  3102.                 $cur_depth += $tags{$_} for keys %is_suscript;
  3103.                 if ($cur_depth >= $max_su_depth) {
  3104.                     _substitute(\$html, $whole, '');
  3105.                     next;
  3106.                 }
  3107.             }
  3108.  
  3109.             # we are directly inside a list (UL), but this tag must be
  3110.             # a list element (LI)
  3111.             # this comes now because it could include a closing tag
  3112. # this isn't necessary anymore, with _validateLists()
  3113. #           if (@stack && $lists{$stack[-1]} && !(grep { $tag eq $_ } @{$lists{$stack[-1]}}) ) {
  3114. #               my $replace = $lists{$stack[-1]}[0];
  3115. #               _substitute(\$html, $whole, "<$replace>$whole");
  3116. #               $tags{$replace}++;
  3117. #               push @stack, $replace;
  3118. #           }
  3119.  
  3120.             if ($needs_list{$tag}) {
  3121.                 # tag needs a list, like an LI needs a UL or OL, but we
  3122.                 # are not inside one: replace it with a P.  not pretty,
  3123.                 # but you should be more careful about what you put in there!
  3124.                 if (!@list || $list[-1] !~ /^(?:$needs_list{$tag})$/) {
  3125.                     my $replace = @list ? $lists{$list[-1]}[0] : 'p';
  3126.                     _substitute(\$html, $whole, "<$replace>");
  3127.                     pos($html) -= length("<$replace>");
  3128.                     next;  # try again
  3129.  
  3130.                 # we are inside a list (UL), and opening a new list item (LI),
  3131.                 # but a previous one is already open
  3132.                 } else {
  3133.                     for my $check (reverse @stack) {
  3134.                         last if $check =~ /^(?:$needs_list{$tag})/;
  3135.                         if ($needs_list{$check}) {
  3136.                             my $newtag = '';
  3137.                             while (my $pop = pop @stack) {
  3138.                                 $tags{$pop}--;
  3139.                                 $newtag .= "</$pop>";
  3140.                                 last if $needs_list{$pop};
  3141.                             }
  3142.                             _substitute(\$html, $whole, $newtag, 0, 1);
  3143.                             _substitute(\$html, '', $whole);
  3144.                             last;
  3145.                         }
  3146.                     }
  3147.                 }
  3148.             }
  3149.  
  3150.             # if we are opening a block tag, make sure no open no_block
  3151.             # tags are on the stack currently.  if they are, close them
  3152.             # first!
  3153.             if ($is_block{$tag} || $tag eq 'a' || $tag eq 'br') {
  3154.                 # a is a special case for a and br: we do not want a or b tags
  3155.                 # to be included in a tags, even though they are not blocks;
  3156.                 # another var for this special case?
  3157.                 my @no_block = ($tag eq 'a' || $tag eq 'br') ? 'a' : keys %no_block;
  3158.                 my $newtag  = '';  # close no_block tags
  3159.                 my $newtag2 = '';  # re-open closed style tags inside block
  3160.  
  3161.                 while (grep { $tags{$_} } @no_block) {
  3162.                     my $pop = pop @stack;
  3163.                     $tags{$pop}--;
  3164.                     $newtag .= "</$pop>";
  3165.                     if ($is_style{$pop}) {
  3166.                         $newtag2 = "<$pop>" . $newtag2;
  3167.                     }
  3168.                 }
  3169.  
  3170.                 if ($newtag) {
  3171.                     _substitute(\$html, $whole, $newtag . $whole . $newtag2);
  3172.                     # loop back to catch newly added tags properly
  3173.                     # XXX we could optimize here so we don't need to loop back
  3174.                     pos($html) -= length($whole . $newtag2);
  3175.                     next;
  3176.                 }
  3177.             }
  3178.  
  3179.             # the tag must be an empty tag, e.g. <br />; if it has $both, do
  3180.             # nothing, else add the " /".  since we are closing the tag
  3181.             # here, we don't need to add it to the stack
  3182.             if ($really_empty{$tag} || ($empty{$tag} && $both)) {
  3183.                 # this is the only difference we have between
  3184.                 # XHTML and HTML, in this part of the code
  3185.                 if ($constants->{xhtml} && !$both) {
  3186.                     (my $newtag = $whole) =~ s/^<(.+?)>$/<$1 \/>/;
  3187.                     _substitute(\$html, $whole, $newtag);
  3188.                 } elsif (!$constants->{xhtml} && $both) {
  3189.                     (my $newtag = $whole) =~ s/^<(.+?)>$/<$1>/;
  3190.                     _substitute(\$html, $whole, $newtag);
  3191.                 }
  3192.                 next;
  3193.             }
  3194.  
  3195.             # opening a new tag to be added to the stack
  3196.             $tags{$tag}++;
  3197.             push @stack, $tag;
  3198.             if ($needs_p{$tag}) {
  3199.                 _substitute(\$html, '', '<p>', 1);
  3200.             }
  3201.  
  3202.             # we keep track of lists in an add'l stack, for
  3203.             # the immediately above purpose, so push it on here
  3204.             push @list, $tag if $lists{$tag};
  3205.         }
  3206.  
  3207.     }
  3208.  
  3209.     $html =~ s/\s+$//s;
  3210.  
  3211.     # add on any unclosed tags still on stack
  3212.     $html .= join '', map { "</$_>" } grep { !exists $really_empty{$_} } reverse @stack;
  3213.  
  3214.     _validateLists(\$html);
  3215.     _removeEmpty(\$html);
  3216.  
  3217.     # if over limit, do it again
  3218.     if ($options->{length} && $options->{length} < length($html)) {
  3219.         my $limit = delete $options->{length};
  3220.         while ($limit > 0 && length($html) > $limit) {
  3221.             $limit -= 1;
  3222.             $html = balanceTags(chopEntity($orightml, $limit), $options);
  3223.  
  3224.             # until we get wrap fix in CSS
  3225.             my $nobr  = () = $html =~ m|<nobr>|g;
  3226.             my $wbr   = () = $html =~ m|<wbr>|g;
  3227.             my $nobre = () = $html =~ m|</nobr>|g;
  3228.             $html .= '<wbr>'   if $nobr > $wbr;
  3229.             $html .= '</nobr>' if $nobr > $nobre;
  3230.         }
  3231.     }
  3232.  
  3233.     return $html;
  3234. }
  3235.  
  3236. sub _removeEmpty {
  3237.     my($html) = @_;
  3238.     my $p    = getCurrentStatic('xhtml') ? '<p />' : '<p>';
  3239.  
  3240.     # remove consecutive <p> or <p>, <br> tags
  3241.     1 while $$html =~ s{<p> \s* <(?: /?p | br(?:\ /)? )>} {$p}gx;
  3242.     # remove <p> and <br> tags before beginning, or end, of blocks, or end of string
  3243.     1 while $$html =~ s{\s* <(?: p | br(?:\ /)?) > \s*  ( $ | </?(?:$is_block_re)> )} {$1}gx;
  3244.  
  3245.     # remove still-empty tags
  3246.     while ($$html =~ m|<(\w+)>\s*</\1>|) {
  3247.         $$html =~ s|<(\w+)>\s*</\1>\s*||g;
  3248.     }
  3249.  
  3250.     # for now, only remove <br> and <p> as whitespace inside
  3251.     # lists, where we are more likely to mistakenly run into it,
  3252.     # where it will cause more problems
  3253.     for my $re (values %lists_re) {
  3254.         while ($$html =~ m|<($re)>$WS_RE</\1>|) {
  3255.             $$html =~ s|<($re)>$WS_RE</\1>\s*||g;
  3256.         }
  3257.     }
  3258. }
  3259.  
  3260.  
  3261. # validate the structure of lists ... essentially, make sure
  3262. # they are properly nested, that everything in a list is inside
  3263. # a proper li/dt/dd, etc.
  3264.  
  3265. sub _validateLists {
  3266.     my($html) = @_;
  3267.  
  3268.     # each nested list is cleaned up and then stored in the hash,
  3269.     # to be expanded later
  3270.     my %full;
  3271.     # counter for %full
  3272.     my $j = 0;
  3273.    
  3274.     # the main loop finds paired list tags, and what is between them,
  3275.     # like <ul> ... </ul>
  3276.     while ($$html =~ m:(<($list_re)>(.*?)</\2>):sig) {
  3277.         my($whole, $list, $content) = ($1, $2, $3);
  3278.         # if we don't have an innermost list, but there's another
  3279.         # list nested inside this one, increment pos and try again
  3280.         if ($content =~ /<(?:$list_re)>/) {
  3281.             pos($$html) -= length($whole) - length("<$list>");
  3282.             next;
  3283.         }
  3284.  
  3285.         # the default element to use inside the list, for content
  3286.         # that is not inside any proper element
  3287.         my $inside = $lists{$list}[0] || '';
  3288. print STDERR "_validateLists logic error, no entry for list '$list'\n" if !$inside;
  3289.         my $re     = $lists_re{$list};
  3290.  
  3291.         # since we are looking at innermost lists, we do not
  3292.         # need to worry about stacks or nesting, just keep
  3293.         # track of the current element that we are in
  3294.         my $in    = '';
  3295.  
  3296.         # the secondary loop finds either a tag, or text between tags
  3297.         while ($content =~ m!\s*([^<]+|<([^\s>]+).*?>)!sig) {
  3298.             my($whole, $tag) = ($1, $2);
  3299.             next if $whole !~ /\S/;
  3300.             # we only care here if this is one that can be inside a list
  3301.             if ($tag) {
  3302.                 # if open tag ...
  3303.                 if ($tag =~ /^(?:$re)$/) {
  3304.                     # add new close tag if we are current inside a tag
  3305.                     if ($in) {
  3306.                         _substitute(\$content, $whole, "</$in>", 0, 1);
  3307.                         _substitute(\$content, '', $whole);
  3308.                     }
  3309.                     # set new open tag
  3310.                     $in = $tag;
  3311.                     next;
  3312.  
  3313.                 # if close tag ...
  3314.                 } elsif ($tag =~ /^\/(?:$re)$/) {
  3315.                     # remove if we are not already inside a tag
  3316.                     _substitute(\$content, $whole, '') unless $in;
  3317.                     # this should not usually happen, as
  3318.                     # we've already balanced the tags
  3319.                     #warn "huh?  $tag ne /$in?" if $tag ne "/$in";
  3320.                     # set to no open tag
  3321.                     $in = '';
  3322.                     next;
  3323.                 }
  3324.             }
  3325.  
  3326.             # we are NOT an appropriate tag, or inside one, so
  3327.             # create one to be inside of
  3328.             if (!$in) {
  3329.                 $in = $inside;
  3330.                 _substitute(\$content, $whole, "<$inside>$whole");
  3331.             }
  3332.         }
  3333.  
  3334.         # now done with loop, so add rest of $in if there is any
  3335.         $content =~ s|(\s*)$|</$in>$1| if $in;
  3336.  
  3337.         # we have nesting to deal with, so replace this part
  3338.         # with a temporary token and cache the result in the hash
  3339.         $full{$j} = "<$list>$content</$list>";
  3340.         _substitute($html, $whole, "<FULL-$j>");
  3341.         $j++;
  3342.         pos($$html) = 0;  # start over
  3343.     }
  3344.  
  3345.     # expand it all back out
  3346.     while ($j--) {
  3347.         last if $j < 0;
  3348.         $$html =~ s/<FULL-$j>/$full{$j}/;
  3349.     }
  3350.  
  3351.     return 1;
  3352. }
  3353.  
  3354. # put a string into the current position in that string, and update
  3355. # pos() accordingly
  3356. sub _substitute {
  3357.     my($full, $old, $new, $zeropos, $ws_backup) = @_;
  3358.     # zeropos is for when we add a close tag or somesuch, but don't touch
  3359.     # the stack, and just let the code handle it by keeping pos right in
  3360.     # front of the new tag
  3361.  
  3362.     my $len = length $old;
  3363.     my $p = pos($$full) - $len;
  3364.  
  3365.     # back up insert past whitespace
  3366.     if ($ws_backup) {
  3367.         my $o = $p;
  3368.         while (substr($$full, $p-1, 1) =~ /\s/) {
  3369.             # just in case
  3370.             last if $p == 0;
  3371.             $p--;
  3372.             $len++ unless $zeropos;
  3373.         }
  3374.         if (!$zeropos && $p != $o) {
  3375.             $new .= substr($$full, $p, $o-$p);
  3376.         }
  3377.     }
  3378.  
  3379.     substr($$full, $p, ($zeropos ? 0 : $len)) = $new;
  3380.     pos($$full) = $p + ($zeropos ? 0 : length($new));
  3381. }
  3382. }
  3383.  
  3384. #========================================================================
  3385.  
  3386. =head2 parseDomainTags(HTML, RECOMMENDED, NOTAGS)
  3387.  
  3388. To be called before sending the HTML to the user for display.  Takes
  3389. HTML with domain tags (see addDomainTags()) and parses out the tags,
  3390. if necessary.
  3391.  
  3392. =over 4
  3393.  
  3394. =item Parameters
  3395.  
  3396. =over 4
  3397.  
  3398. =item HTML
  3399.  
  3400. The HTML tagged with domains.
  3401.  
  3402. =item RECOMMENDED
  3403.  
  3404. Boolean for whether or not domain tags are recommended.  They are not
  3405. required, the user can choose to leave it up to us.
  3406.  
  3407. =item NOTAGS
  3408.  
  3409. Boolean overriding RECOMMENDED; it strips out all domain tags if true.
  3410.  
  3411. =item NOTITLE
  3412.  
  3413. Boolean which strips out title attributes for links if true
  3414.  
  3415. =back
  3416.  
  3417. =item Return value
  3418.  
  3419. The parsed HTML.
  3420.  
  3421. =back
  3422.  
  3423. =cut
  3424.  
  3425. sub parseDomainTags {
  3426.     my($html, $recommended, $notags, $notitle) = @_;
  3427.     return '' if !defined($html) || $html eq '';
  3428.  
  3429.     my $user = getCurrentUser();
  3430.  
  3431.     # The default is 2 ("always show");  note this default is enforced in
  3432.     # prepareUser().  Note also that if I were being smart I'd use
  3433.     # constants for 0, 1 and 2...
  3434.     my $udt = $user->{domaintags};
  3435.  
  3436.     my $want_tags = 1;          # assume we'll be displaying the [domain.tags]
  3437.     $want_tags = 0 if           # but, don't display them if...
  3438.         $udt == 0           # the user has said they never want the tags
  3439.         || (                # or
  3440.             $udt == 1       # the user leaves it up to us
  3441.             && $recommended     # and we think the poster has earned tagless posting
  3442.         );
  3443.  
  3444.     if ($want_tags && !$notags) {
  3445.         $html =~ s{</a ([^<>]+)>}{</a> [$1]}gi;
  3446.     } else {
  3447.         $html =~ s{</a[^<>]+>}   {</a>}gi;
  3448.     }
  3449.  
  3450.     $html =~ s{<a([^>]*) title="([^"]+")>} {<a$1>}gi if $notitle;
  3451.    
  3452.     return $html;
  3453. }
  3454.  
  3455.  
  3456. #========================================================================
  3457.  
  3458. =head2 parseSlashizedLinks(HTML)
  3459.  
  3460. To be called before sending the HTML to the user for display.  Takes
  3461. HTML with slashized links (see slashizedLinks()) and converts them to
  3462. the appropriate HTML.
  3463.  
  3464. =over 4
  3465.  
  3466. =item Parameters
  3467.  
  3468. =over 4
  3469.  
  3470. =item HTML
  3471.  
  3472. The HTML with slashized links.
  3473.  
  3474. =back
  3475.  
  3476. =item Return value
  3477.  
  3478. The parsed HTML.
  3479.  
  3480. =back
  3481.  
  3482. =cut
  3483.  
  3484. sub parseSlashizedLinks {
  3485.     my($html, $options) = @_;
  3486.     $html = '' if !defined($html);
  3487.     $options = '' if !defined($options);
  3488.     $html =~ s{
  3489.         <a[ ]href="__SLASHLINK__"
  3490.         ([^>]+)
  3491.         >
  3492.     }{
  3493.         _slashlink_to_link($1, $options)
  3494.     }igxe;
  3495.     return $html;
  3496. }
  3497.  
  3498. # This function mirrors the behavior of _link_to_slashlink.
  3499.  
  3500. sub _slashlink_to_link {
  3501.     my($sl, $options) = @_;
  3502.     my $constants = getCurrentStatic();
  3503.     my $ssi = getCurrentForm('ssi') || 0;
  3504.     my $reader = getObject('Slash::DB', { db_type => 'reader' });
  3505.     my %attr = $sl =~ / (\w+)="([^"]+)"/g;
  3506.     # We should probably de-strip-attribute the values of %attr
  3507.     # here, but it really doesn't matter.
  3508.  
  3509.     # Load up special values and delete them from the attribute list.
  3510.     my $sn = delete $attr{sn} || '';
  3511.     my $skin_id = delete $attr{sect} || '';
  3512.  
  3513.     # skin_id could be a name, a skid, or blank, or invalid.
  3514.     # In any case, get its skin hashref and its name.
  3515.     my $skin = undef;
  3516.     $skin = $reader->getSkin($skin_id) if $skin_id;
  3517.     $skin ||= $reader->getSkin($constants->{mainpage_skid});
  3518.     my $skin_name = $skin->{name};
  3519.     my $skin_root = $skin->{rootdir};
  3520.     if ($options && $options->{absolute}) {
  3521.         $skin_root = URI->new_abs($skin_root, $options->{absolute})
  3522.             ->as_string;
  3523.     }
  3524.     my $frag = delete $attr{frag} || '';
  3525.     # Generate the return value.
  3526.     my $url = '';
  3527.     if ($sn eq 'comments') {
  3528.         $url .= qq{$skin_root/comments.pl?};
  3529.         $url .= join('&',
  3530.             map { qq{$_=$attr{$_}} }
  3531.             sort keys %attr);
  3532.         $url .= qq{#$frag} if $frag;
  3533.     } elsif ($sn eq 'article') {
  3534.         # Different behavior here, depending on whether we are
  3535.         # outputting for a dynamic page, or a static one.
  3536.         # This is the main reason for doing slashlinks at all!
  3537.         # Added 2009-04: and now it's mostly obviated :) since
  3538.         # we no longer want to output .shtml but instead will
  3539.         # trust Varnish to cache non-user-specific data, and
  3540.         # will dynamically generate the rest with .pl.
  3541.         # Set article_link_story_dynamic to 2 or greater and
  3542.         # even slashized links will be forced dynamic.
  3543.         my $force_dyn = $constants->{article_link_story_dynamic} > 1 ? 1 : 0;
  3544.         if (!$force_dyn && $ssi) {
  3545.             $url .= qq{$skin_root/};
  3546.             $url .= qq{$skin_name/$attr{sid}.shtml};
  3547.             $url .= qq{?tid=$attr{tid}} if $attr{tid};
  3548.             $url .= qq{#$frag} if $frag;
  3549.         } else {
  3550.             $url .= qq{$skin_root/article.pl?};
  3551.             $url .= join('&',
  3552.                 map { qq{$_=$attr{$_}} }
  3553.                 sort keys %attr);
  3554.             $url .= qq{#$frag} if $frag;
  3555.         }
  3556.     }
  3557.     return q{<a href="} . strip_urlattr($url) . q{">};
  3558. }
  3559.  
  3560. #========================================================================
  3561.  
  3562. =head2 addDomainTags(HTML)
  3563.  
  3564. To be called only after C<balanceTags>, or results are not guaranteed.
  3565. Munges HTML E<lt>/aE<gt> tags into E<lt>/a foo.comE<gt> tags, where
  3566. "foo.com" is the domain name of the link found in the opening E<lt>aE<gt>
  3567. tag.  Note that this is not proper HTML, and that C<dispComment> knows
  3568. how properly to convert it back to proper HTML.
  3569.  
  3570. =over 4
  3571.  
  3572. =item Parameters
  3573.  
  3574. =over 4
  3575.  
  3576. =item HTML
  3577.  
  3578. The HTML to tag with domains.
  3579.  
  3580. =back
  3581.  
  3582. =item Return value
  3583.  
  3584. The tagged HTML.
  3585.  
  3586. =back
  3587.  
  3588. =cut
  3589.  
  3590. sub addDomainTags {
  3591.     my($html) = @_;
  3592.  
  3593.     # First step is to eliminate unclosed <A> tags.
  3594.  
  3595.     my $in_a = 0;
  3596.     $html =~ s
  3597.     {
  3598.         ( < (/?) a \b[^>]* > )
  3599.     }{
  3600.         my $old_in_a = $in_a;
  3601.         my $new_in_a = !$2;
  3602.         $in_a = $new_in_a;
  3603.         (($old_in_a && $new_in_a) ? '</a>' : '') . $1
  3604.     }gixe;
  3605.     $html .= '</a>' if $in_a;
  3606.  
  3607.     # Now, since we know that every <A> has a </A>, this pattern will
  3608.     # match and let the subroutine above do its magic properly.
  3609.     # Note that, since a <A> followed immediately by </A> will not
  3610.     # only fail to appear in a browser, but would also look goofy if
  3611.     # followed by a [domain.tag], in such a case we simply remove the
  3612.     # <A></A> pair entirely.
  3613.  
  3614.     $html =~ s
  3615.     {
  3616.         (<a\s+href="        # $1 is the whole <A HREF...>
  3617.             ([^">]*)    # $2 is the URL (quotes guaranteed to
  3618.                     # be there thanks to approveTag)
  3619.         ">)
  3620.         (.*?)           # $3 is whatever's between <A> and </A>
  3621.         </a\b[^>]*>
  3622.     }{
  3623.         $3  ? _url_to_domain_tag($1, $2, $3)
  3624.             : ''
  3625.     }gisex;
  3626.  
  3627.     # If there were unmatched <A> tags in the original, balanceTags()
  3628.     # would have added the corresponding </A> tags to the end.  These
  3629.     # will stick out now because they won't have domain tags.  We
  3630.     # know we've added enough </A> to make sure everything balances
  3631.     # and doesn't overlap, so now we can just remove the extra ones,
  3632.     # which are easy to tell because they DON'T have domain tags.
  3633.  
  3634.     $html =~ s{</a>}{}gi;
  3635.  
  3636.     return $html;
  3637. }
  3638.  
  3639. sub email_to_domain {
  3640.     my($email) = @_;
  3641.     my $addr = Mail::Address->new('', $email);
  3642.     return '' if !$addr;
  3643.     my $host = $addr->host();
  3644.     return '' if !$host;
  3645.     return fullhost_to_domain($host);
  3646. }
  3647.  
  3648. sub fullhost_to_domain {
  3649.     my($fullhost) = @_;
  3650.     my $info = lc $fullhost;
  3651.     if ($info =~ m/^([\d.]+)\.in-addr\.arpa$/) {
  3652.         $info = join('.', reverse split /\./, $1);
  3653.     }
  3654.     if ($info =~ m/^(\d{1,3}\.){3}\d{1,3}$/) {
  3655.         # leave a numeric IP address alone
  3656.     } elsif ($info =~ m/([\w-]+\.[a-z]{3,4})$/) {
  3657.         # a.b.c.d.com -> d.com
  3658.         $info = $1;
  3659.     } elsif ($info =~ m/([\w-]+\.[a-z]{2,4}\.[a-z]{2})$/) {
  3660.         # a.b.c.d.co.uk -> d.co.uk
  3661.         $info = $1;
  3662.     } elsif ($info =~ m/([\w-]+\.[a-z]{2})$/) {
  3663.         # a.b.c.realdomain.gr -> realdomain.gr
  3664.         $info = $1;
  3665.     } else {
  3666.         # any other a.b.c.d.e -> c.d.e
  3667.         my @info = split /\./, $info;
  3668.         my $num_levels = scalar @info;
  3669.         if ($num_levels >= 3) {
  3670.             $info = join('.', @info[-3..-1]);
  3671.         }
  3672.     }
  3673.     return $info;
  3674. }
  3675.  
  3676. sub _url_to_domain_tag {
  3677.     my($href, $link, $body) = @_;
  3678.     my $absolutedir = getCurrentSkin('absolutedir');
  3679.     my $uri = URI->new_abs($link, $absolutedir);
  3680.     my $uri_str = $uri->as_string;
  3681.  
  3682.     my($info, $scheme) = ('', '');
  3683.     if ($uri->can('host')) {
  3684.         my $host;
  3685.         unless (($host = $uri->host)
  3686.                 &&
  3687.             $uri->can('scheme')
  3688.                 &&
  3689.             ($scheme = $uri->scheme)
  3690.         ) {
  3691.             # If this URL is malformed in a particular
  3692.             # way ("scheme:///host"), treat it the way
  3693.             # that many browsers will (rightly or
  3694.             # wrongly) treat it.
  3695.             if ($uri_str =~ s|$scheme:///+|$scheme://|) {
  3696.                 $uri = URI->new_abs($uri_str, $absolutedir);
  3697.                 $uri_str = $uri->as_string;
  3698.                 $host = $uri->host;
  3699.             }
  3700.         }
  3701.         $info = fullhost_to_domain($host) if $host;
  3702.     }
  3703.  
  3704.     if (!$info && ($scheme || (
  3705.         $uri->can('scheme') && ($scheme = $uri->scheme)
  3706.     ))) {
  3707.         # Most schemes, like ftp or http, have a host.  Some,
  3708.         # most notably mailto and news, do not.  For those,
  3709.         # at least give the user an idea of why not, by
  3710.         # listing the scheme.  Or, if this URL is malformed
  3711.         # in a particular way ("scheme:host/path", missing
  3712.         # the "//"), treat it the way that many browsers will
  3713.         # (rightly or wrongly) treat it.
  3714.         if ($uri_str =~ m{^$scheme:([\w-]+)}) {
  3715.             $uri_str =~ s{^$scheme:}{$scheme://};
  3716.             return _url_to_domain_tag($href, $uri_str, $body);
  3717.         } else {
  3718.             $info = lc $scheme;
  3719.         }
  3720.     }
  3721.  
  3722.     $info =~ tr/A-Za-z0-9.-//cd if $info;
  3723.  
  3724.     if (length($info) == 0) {
  3725.         $info = '?';
  3726.     }
  3727.  
  3728.     # Add a title tag to make this all friendly for those with vision
  3729.     # and similar issues -Brian
  3730.     $href =~ s/>/ title="$info">/ if $info ne '?';
  3731.     return "$href$body</a $info>";
  3732. }
  3733.  
  3734. #========================================================================
  3735.  
  3736. =head2 slashizeLinks(HTML)
  3737.  
  3738. Munges HTML E<lt>aE<gt> tags that point to specific types of links on
  3739. this Slash site (articles.pl, comments.pl, and articles .shtml pages)
  3740. into a special type of E<lt>aE<gt> tag.  Note that this is not proper
  3741. HTML, and that it will be converted back to proper HTML when the
  3742. story is displayed.
  3743.  
  3744. =over 4
  3745.  
  3746. =item Parameters
  3747.  
  3748. =over 4
  3749.  
  3750. =item HTML
  3751.  
  3752. The HTML to slashize links in.
  3753.  
  3754. =back
  3755.  
  3756. =item Return value
  3757.  
  3758. The converted HTML.
  3759.  
  3760. =back
  3761.  
  3762. =cut
  3763.  
  3764. sub slashizeLinks {
  3765.     my($html) = @_;
  3766.     $html =~ s{
  3767.         (<a[^>]+href\s*=\s*"?)
  3768.         ([^"<>]+)
  3769.         ([^>]*>)
  3770.     }{
  3771.         _link_to_slashlink($1, $2, $3)
  3772.     }gxie;
  3773.     return $html;
  3774. }
  3775.  
  3776. # URLs that match a pattern are converted into our special format.
  3777. # Those that don't are passed through.  This function mirrors the
  3778. # behavior of _slashlink_to_link.
  3779. {
  3780. # This closure is here because generating the %urla table is
  3781. # somewhat resource-intensive.
  3782. my %urla;
  3783. sub _link_to_slashlink {
  3784.     my($pre, $url, $post) = @_;
  3785.     my $reader = getObject('Slash::DB', { db_type => 'reader' });
  3786.     my $constants = getCurrentStatic();
  3787.     my $gSkin = getCurrentSkin();
  3788.     my $virtual_user = getCurrentVirtualUser();
  3789.     my $retval = "$pre$url$post";
  3790.     my $abs = $gSkin->{absolutedir};
  3791.     my $skins = $reader->getSkins();
  3792. #print STDERR "_link_to_slashlink begin '$url'\n";
  3793.  
  3794.     if (!defined($urla{$virtual_user})) {
  3795.         # URLs may show up in any skins, which means when absolutized
  3796.         # their host may be either the main one or a sectional one.
  3797.         # We have to allow for any of those possibilities.
  3798.         my @skin_urls = grep { $_ }
  3799.             map { $skins->{$_}{rootdir} }
  3800.             sort keys %$skins;
  3801.         my %all_urls = ( );
  3802.         for my $url ($abs, @skin_urls) {
  3803.             my $new_url = URI->new($url);
  3804.             # Remove the scheme to make it relative (schemeless).
  3805.             # XXXSECTIONTOPICS hey, skin urls should already be schemeless, test this
  3806.             # XXXSKIN - no, urls are not schemeless, rootdirs are
  3807.             # (and they are generated, at this point, from urls)
  3808.             $new_url->scheme(undef);
  3809.             my $new_url_q = quotemeta($new_url->as_string);
  3810.             $all_urls{"(?:https?:)?$new_url"} = 1;
  3811.         }
  3812.         my $any_host = "(?:"
  3813.             . join("|", sort keys %all_urls)
  3814.             . ")";
  3815.         # All possible URLs' arguments, soon to be attributes
  3816.         # in the new tag (thus "urla"). Values are the name
  3817.         # of the script ("sn") and expressions that can pull
  3818.         # those arguments out of a text URL.  (We could use
  3819.         # URI::query_form to pull out the .pl arguments, but that
  3820.         # wouldn't help with the .shtml regex so we might as well
  3821.         # do it this way.)  If we ever want to extend slash-linking
  3822.         # to cover other tags, here's the place to start.
  3823.         %{$urla{$virtual_user}} = (
  3824.             qr{^$any_host/article\.pl\?} =>
  3825.                 { _sn => 'article',
  3826.                   sid => qr{\bsid=([\w/]+)} },
  3827.             qr{^$any_host/\w+/\d+/\d+/\d+/\d+\.shtml\b} =>
  3828.                 { _sn => 'article',
  3829.                   sid => qr{^$any_host/\w+/(\d+/\d+/\d+/\d+)\.shtml\b} },
  3830.             qr{^$any_host/comments\.pl\?} =>
  3831.                 { _sn => 'comments',
  3832.                   sid => qr{\bsid=(\d+)},
  3833.                   cid => qr{\bcid=(\d+)} },
  3834.         );
  3835.     }
  3836.     # Get a reference to the URL argument hash for this
  3837.     # virtual user, thus "urlavu".
  3838.     my $urlavu = $urla{$virtual_user};
  3839.  
  3840.     my $canon_url = URI->new_abs($url, $abs)->canonical;
  3841.     my $frag = $canon_url->fragment() || "";
  3842.  
  3843.     # %attr is the data structure storing the attributes of the <a>
  3844.     # tag that we will use.
  3845.     my %attr = ( );
  3846.     URLA: for my $regex (sort keys %$urlavu) {
  3847.         # This loop only applies to the regex that matches this
  3848.         # URL (if any).
  3849.         next unless $canon_url =~ $regex;
  3850.  
  3851.         # The non-underscore keys are regexes that we need to
  3852.         # pull from the URL.
  3853.         for my $arg (sort grep !/^_/, keys %{$urlavu->{$regex}}) {
  3854.             ($attr{$arg}) = $canon_url =~ $urlavu->{$regex}{$arg};
  3855.             delete $attr{$arg} if !$attr{$arg};
  3856.         }
  3857.         # The _sn key is special, it gets copied into sn.
  3858.         $attr{sn} = $urlavu->{$regex}{_sn};
  3859.         # Section and topic attributes get thrown in too.
  3860.         if ($attr{sn} eq 'comments') {
  3861.             # sid is actually a discussion id!
  3862.             # XXXSECTIONTOPICS
  3863.             my $primaryskid = $reader->getDiscussion(
  3864.                 $attr{sid}, 'primaryskid');
  3865.             $attr{sect} = $skins->{$primaryskid}{name};
  3866.             $attr{tid} = $reader->getDiscussion(
  3867.                 $attr{sid}, 'topic');
  3868.         } else {
  3869.             # sid is a story id
  3870.             # XXXSECTIONTOPICS
  3871.             my $primaryskid = $reader->getStory(
  3872.                 $attr{sid}, 'primaryskid', 1);
  3873.             $attr{sect} = $skins->{$primaryskid}{name};
  3874.             $attr{tid} = $reader->getStory(
  3875.                 $attr{sid}, 'tid', 1);
  3876.         }
  3877.         $attr{frag} = $frag if $frag;
  3878.         # We're done once we match any regex to the link's URL.
  3879.         last URLA;
  3880.     }
  3881.  
  3882.     # If we have something good in %attr, we can go ahead and
  3883.     # use our custom tag.  Concatenate it together.
  3884.     if ($attr{sn}) {
  3885.         $retval = q{<a href="__SLASHLINK__" }
  3886.             . join(" ",
  3887.                 map { qq{$_="} . strip_attribute($attr{$_}) . qq{"} }
  3888.                 sort keys %attr)
  3889.             . q{>};
  3890.     }
  3891.  
  3892.     # Return either the new $retval we just made, or just send the
  3893.     # original text back.
  3894.     return $retval;
  3895. }
  3896. }
  3897.  
  3898.  
  3899. #========================================================================
  3900.  
  3901. =head2 xmlencode_plain(TEXT)
  3902.  
  3903. Same as xmlencode(TEXT), but does not encode for use in HTML.  This is
  3904. currently ONLY for use for E<lt>linkE<gt> elements.
  3905.  
  3906. =over 4
  3907.  
  3908. =item Parameters
  3909.  
  3910. =over 4
  3911.  
  3912. =item TEXT
  3913.  
  3914. Whatever text it is you want to encode.
  3915.  
  3916. =back
  3917.  
  3918. =item Return value
  3919.  
  3920. The encoded string.
  3921.  
  3922. =item Dependencies
  3923.  
  3924. XML::Parser::Expat(3).
  3925.  
  3926. =back
  3927.  
  3928. =cut
  3929.  
  3930. sub xmlencode_plain {
  3931.     xmlencode($_[0], 1);
  3932. }
  3933.  
  3934. #========================================================================
  3935.  
  3936. =head2 xmlencode(TEXT)
  3937.  
  3938. Encodes / escapes a string for putting into XML.
  3939. The text goes through three phases: we first convert
  3940. all "&" that are not part of an entity to "&amp;"; then
  3941. we convert all "&", "<", and ">" to their entities.
  3942. Then all characters that are not printable ASCII characters
  3943. (\040 to \176) are converted to their numeric entities
  3944. (such as "&#192;").
  3945.  
  3946. Note that this is basically encoding a string into valid
  3947. HTML, then escaping it for XML.  When run through regular
  3948. XML unescaping, a valid HTML string should remain
  3949. (that is, the characters will be valid for HTML, while it
  3950. may not be syntactically correct).  You may use something
  3951. like C<HTML::Entities::decode_entities> if you wish to get
  3952. the regular text.
  3953.  
  3954. =over 4
  3955.  
  3956. =item Parameters
  3957.  
  3958. =over 4
  3959.  
  3960. =item TEXT
  3961.  
  3962. Whatever text it is you want to encode.
  3963.  
  3964. =back
  3965.  
  3966. =item Return value
  3967.  
  3968. The encoded string.
  3969.  
  3970. =item Dependencies
  3971.  
  3972. XML::Parser::Expat(3).
  3973.  
  3974. =back
  3975.  
  3976. =cut
  3977.  
  3978. sub xmlencode {
  3979.     my($text, $nohtml) = @_;
  3980.     return '' if !defined($text) || length($text) == 0;
  3981.  
  3982.     # if there is an & that is not part of an entity, convert it
  3983.     # to &amp;
  3984.     $text =~ s/&(?!#?[a-zA-Z0-9]+;)/&amp;/g
  3985.         unless $nohtml;
  3986.  
  3987.     # convert & < > to XML entities
  3988.     $text = XML::Parser::Expat->xml_escape($text, ">");
  3989.  
  3990.     # convert ASCII-non-printable to numeric entities
  3991.     $text =~ s/([^\s\040-\176])/ "&#" . ord($1) . ";" /ge;
  3992.  
  3993.     return $text;
  3994. }
  3995.  
  3996.  
  3997. #========================================================================
  3998.  
  3999. =head2 xmldecode(TEXT)
  4000.  
  4001. Decodes / unescapes an XML string.  It basically just
  4002. decodes the five entities used to encode "<", ">", '"',
  4003. "'", and "&".  "&" is only decoded if it is not the start
  4004. of an entity.
  4005.  
  4006. This will decode the named, decimal numeric, or hex numeric
  4007. versions of the entities.
  4008.  
  4009. Note that while C<xmlencode> will make sure the characters
  4010. in the string are proper HTML characters, C<xmldecode> will
  4011. not take the extra step to get back the original non-HTML
  4012. text; we want to leave the text as OK to put directly into
  4013. HTML.  You may use something like
  4014. C<HTML::Entities::decode_entities> if you wish to get
  4015. the regular text.
  4016.  
  4017. =over 4
  4018.  
  4019. =item Parameters
  4020.  
  4021. =over 4
  4022.  
  4023. =item TEXT
  4024.  
  4025. Whatever text it is you want to decode.
  4026.  
  4027. =back
  4028.  
  4029. =item Return value
  4030.  
  4031. The decoded string.
  4032.  
  4033. =back
  4034.  
  4035. =cut
  4036.  
  4037. {
  4038.     # for all following chars but &, convert entities back to
  4039.     # the actual character
  4040.  
  4041.     # for &, convert &amp; back to &, but only if it is the
  4042.     # beginning of an entity (like "&amp;#32;")
  4043.  
  4044.     # precompile these so we only do it once
  4045.  
  4046.     my %e = qw(< lt > gt " quot ' apos & amp);
  4047.     for my $chr (keys %e) {
  4048.         my $word = $e{$chr};
  4049.         my $ord = ord $chr;
  4050.         my $hex = sprintf "%x", $ord;
  4051.         $hex =~ s/([a-f])/[$1\U$1]/g;
  4052.         my $regex = qq/&(?:$word|#$ord|#[xX]$hex);/;
  4053.         $regex .= qq/(?=#?[a-zA-Z0-9]+;)/ if $chr eq "&";
  4054.         $e{$chr} = qr/$regex/;
  4055.     }
  4056.  
  4057.     sub xmldecode {
  4058.         my($text) = @_;
  4059.  
  4060.         # do & only _after_ the others
  4061.         for my $chr ((grep !/^&$/, keys %e), "&") {
  4062.             $text =~ s/$e{$chr}/$chr/g;
  4063.         }
  4064.  
  4065.         return $text;
  4066.     }
  4067. }
  4068.  
  4069. #========================================================================
  4070.  
  4071. =head2 vislenify (ID_OR_HASHREF [, LEN])
  4072.  
  4073. Given an MD5 string such as an IPID or SubnetID, converts it to
  4074. the length as determined by the id_md5_vislength var.  If passed
  4075. a hashref, looks for any and all of the keys ipid, subnetid, and
  4076. md5id, and if found, adds the same keys with _vis appended and
  4077. shortened values.  If passed an arrayref, it must be an arrayref
  4078. of hashrefs, and does the above for each hashref.
  4079.  
  4080. =over 4
  4081.  
  4082. =item Parameters
  4083.  
  4084. =over 4
  4085.  
  4086. =item ID_OR_HASHREF
  4087.  
  4088. Either a 32-char MD5 ID string, or a hashref as described above.
  4089.  
  4090. =item LEN
  4091.  
  4092. Usually not necessary;  if present, overrides the var id_md5_vislength.
  4093.  
  4094. =back
  4095.  
  4096. =item Return value
  4097.  
  4098. If scalar ID passed in, returns new value.  If hashref passed in,
  4099. it is modified in place.
  4100.  
  4101. =back
  4102.  
  4103. =cut
  4104.  
  4105. sub vislenify {
  4106.     my($id_or_ref, $len) = @_;
  4107.     $len ||= getCurrentStatic('id_md5_vislength') || 32;
  4108.     if (ref $id_or_ref) {
  4109.         if (ref($id_or_ref) eq 'HASH') {
  4110.             my $hr = $id_or_ref;
  4111.             for my $key (qw( ipid ipid2 subnetid md5id )) {
  4112.                 if ($hr->{$key}) {
  4113.                     $hr->{"${key}_vis"} = substr($hr->{$key}, 0, $len);
  4114.                 }
  4115.             }
  4116.         } elsif (ref($id_or_ref) eq 'ARRAY') {
  4117.             for my $item_hr (@$id_or_ref) {
  4118.                 for my $key (qw( ipid ipid2 subnetid md5id )) {
  4119.                     if ($item_hr->{$key}) {
  4120.                         $item_hr->{"${key}_vis"} = substr($item_hr->{$key}, 0, $len);
  4121.                     }
  4122.                 }
  4123.             }
  4124.         }
  4125.     } else {
  4126.         return substr($id_or_ref, 0, $len);
  4127.     }
  4128. }
  4129.  
  4130. #========================================================================
  4131.  
  4132. =head2 ellipsify (TEXT [, LEN])
  4133.  
  4134. Given any text, makes sure it's not too long by shrinking its
  4135. length to at most LEN, putting an ellipse in the middle.  If the
  4136. LEN is too short to allow an ellipse in the middle, it just does
  4137. an ellipse at the end, or in the worst case, a substr.
  4138.  
  4139. =over 4
  4140.  
  4141. =item Parameters
  4142.  
  4143. =over 4
  4144.  
  4145. =item TEXT
  4146.  
  4147. Any text.
  4148.  
  4149. =item LEN
  4150.  
  4151. Usually not necessary;  if present, overrides the var
  4152. comments_max_email_len (email is what this function was designed to
  4153. work on).
  4154.  
  4155. =back
  4156.  
  4157. =item Return value
  4158.  
  4159. New value.
  4160.  
  4161. =back
  4162.  
  4163. =cut
  4164.  
  4165. sub ellipsify {
  4166.     my($text, $len) = @_;
  4167.     $len ||= getCurrentStatic('comments_max_email_len') || 40;
  4168.     if (length($text) > $len) {
  4169.         my $len2 = int(($len-7)/2);
  4170.         if ($len2 >= 4) {
  4171.             $text = chopEntity($text, $len2)
  4172.                 . ' ... '
  4173.                 . chopEntity($text, $len2, 1);
  4174.         } elsif ($len >= 8) {
  4175.             $text = chopEntity($text, $len-4)
  4176.                 . ' ...';
  4177.         } else {
  4178.             $text = chopEntity($text, $len);
  4179.         }
  4180.     }
  4181.     return $text;
  4182. }
  4183.  
  4184. #========================================================================
  4185.  
  4186. =head2 getArmoredEmail (UID)
  4187.  
  4188. Returns a Spam Armored email address for the user associated with the
  4189. given UID.
  4190.  
  4191. This routine DOES NOT save its results back to the user record. This is
  4192. the responsibility of the calling routine.
  4193.  
  4194. =over 4
  4195.  
  4196. =item Parameters
  4197.  
  4198. =over 4
  4199.  
  4200. =item UID
  4201.  
  4202. The user's ID whose email address you wish to randomize.
  4203.  
  4204. =back
  4205.  
  4206. =item Return value
  4207.  
  4208. The email address, if successful.
  4209.  
  4210. =back
  4211.  
  4212. =cut
  4213.  
  4214. sub getArmoredEmail {
  4215.     my($uid, $realemail) = @_;
  4216.     # If the caller knows realemail, pass it in to maybe save a DB query
  4217.     $realemail ||= '';
  4218.  
  4219.     my $reader = getObject('Slash::DB', { db_type => 'reader' });
  4220.     my $armor = $reader->getRandomSpamArmor();
  4221.  
  4222.     # Execute the retrieved code in a Safe compartment. We do this
  4223.     # in an anonymous block to enable local scoping for some variables.
  4224.     {
  4225.         local $_ = $realemail;
  4226.         $_ ||= $reader->getUser($uid, 'realemail');
  4227.  
  4228.         # maybe this should be cached, something like the template
  4229.         # cache in Slash::Display?  it has some significant
  4230.         # overhead -- pudge
  4231.         my $cpt = new Safe;
  4232.  
  4233.         # We only permit basic arithmetic, loop and looping opcodes.
  4234.         # We also explicitly allow join since some code may involve
  4235.         # Separating the address so that obfuscation can be performed
  4236.         # in parts.
  4237.         # NOTE: these opcode classes cannot be in the database etc.,
  4238.         # because that would compromise the security model.  -- pudge
  4239.         $cpt->permit(qw[:base_core :base_loop :base_math join]);
  4240.  
  4241.         # Each compartment should be designed to take input from, and
  4242.         # send output to, $_.
  4243.         $cpt->reval($armor->{code});
  4244.         return $_ unless $@;
  4245.  
  4246.         # If we are here, an error occured in the block. This should be
  4247.         # logged.
  4248.         #
  4249.         # Ideally, this text should be in a template, somewhere
  4250.         # but I hesitate to use Slash::getData() in a module where I
  4251.         # don't see it already in use. - Cliff
  4252.         # it can be used anywhere, since Slash.pm is assumed to
  4253.         # be loaded -- pudge
  4254.         errorLog(<<EOT);
  4255. Error randomizing realemail using armor '$armor->{name}':
  4256. $@
  4257. EOT
  4258.  
  4259.     }
  4260. }
  4261.  
  4262. #========================================================================
  4263.  
  4264. =head2 getRandomWordFromDictFile (FILENAME, OPTIONS)
  4265.  
  4266. Pulls a random word from a dictionary file on disk (e.g. /usr/dict/words)
  4267. based on certain parameters.
  4268.  
  4269. =over 4
  4270.  
  4271. =item Parameters
  4272.  
  4273. =over 4
  4274.  
  4275. =item FILENAME
  4276.  
  4277. The name of the disk file to read from.
  4278.  
  4279. =back
  4280.  
  4281. =item OPTIONS
  4282.  
  4283. min_chars is the word length minimum, or 1 by default.
  4284.  
  4285. max_chars is the word length maximum, or 99 by default.
  4286.  
  4287. word_regex is the regex to match a word; by default this will include
  4288. all words of all-lowercase letters (e.g. no "O'Reilly") between the
  4289. min_chars and max_chars lengths.
  4290.  
  4291. excl_regexes is an arrayref of regular expressions.  If any one of them
  4292. matches a word it will not be returned.
  4293.  
  4294. =item Return value
  4295.  
  4296. The word found.
  4297.  
  4298. =back
  4299.  
  4300. =cut
  4301.  
  4302. sub getRandomWordFromDictFile {
  4303.     my($filename, $options) = @_;
  4304.     my $min_chars = $options->{min_chars} || 1;
  4305.     $min_chars = 1 if $min_chars < 1;
  4306.     my $max_chars = $options->{max_chars} || 99;
  4307.        my $word_regex = $options->{word_regex} || qr{^([a-z]{$min_chars,$max_chars})$};
  4308.     my $excl_regexes = $options->{excl_regexes} || [ ];
  4309.  
  4310.     return '' if !$filename || !-r $filename;
  4311.        my $filesize = -s $filename;
  4312.        return '' if !$filesize;
  4313.        my $word = '';
  4314.  
  4315.        # Start looking in the dictionary at a random location.
  4316.        my $start_seek = int(rand($filesize-$max_chars));
  4317.        my $fh;
  4318.        if (!open($fh, "<", $filename)) {
  4319.                return '';
  4320.        }
  4321.        if (!seek($fh, $start_seek, 0)) {
  4322.                return '';
  4323.        }
  4324.         my $line = <$fh>;       # throw first (likely partial) line away
  4325.         my $reseeks = 0;        # how many times have we moved the seek point?
  4326.         my $bytes_read_total = 0;   # how much have we read in total?
  4327.         my $bytes_read_thisseek = 0;    # how much read since last reseek?
  4328.        LINE: while ($line = <$fh>) {
  4329.                if (!$line) {
  4330.                        # We just hit the end of the file.  Roll around
  4331.                        # to the beginning.
  4332.                        if (!seek($fh, 0, 0)) {
  4333.                                last LINE;
  4334.                        }
  4335.                        ++$reseeks;
  4336.                        next LINE;
  4337.                }
  4338.                $bytes_read_total += length($line);
  4339.                $bytes_read_thisseek += length($line);
  4340.                if ($bytes_read_thisseek >= $filesize * 0.001) {
  4341.                        # If we've had to read through more than 0.1% of
  4342.                         # the dictionary to find a word of the appropriate
  4343.                         # length, we're obviously in a part of the
  4344.                         # dictionary that doesn't have any acceptable words
  4345.                         # (maybe a section with all-capitalized words).
  4346.                         # Try another section.
  4347.                         if (!seek($fh, int(rand($filesize-$max_chars)), 0)) {
  4348.                                 last LINE;
  4349.                         }
  4350.                         $line = <$fh>; # throw likely partial away
  4351.                         ++$reseeks;
  4352.                         $bytes_read_thisseek = 0;
  4353.                 }
  4354.                 if ($bytes_read_total >= $filesize) {
  4355.                         # If we've read a total of more than the complete
  4356.                         # file and haven't found a word, give up.
  4357.                         last LINE;
  4358.                 }
  4359.                 chomp $line;
  4360.                 if ($line =~ $word_regex) {
  4361.                         $word = $1;
  4362.                         for my $r (@$excl_regexes) {
  4363.                                 if ($word =~ /$r/) {
  4364.                                         # Skip this word.
  4365. #print STDERR "word=$word start_seek=$start_seek SKIPPING regex=$r\n";
  4366.                                         $word = '';
  4367.                                         next LINE;
  4368.                                 }
  4369.                         }
  4370.                         last LINE;
  4371.                 }
  4372.         }
  4373.         close $fh;
  4374. #print STDERR "word=$word start_seek=$start_seek bytes_read_thisseek=$bytes_read_thisseek bytes_read_total=$bytes_read_total\n";
  4375.         return $word;
  4376. }
  4377.  
  4378. sub getUrlsFromText {
  4379.     my(@texts) = @_;
  4380.     my %urls = ( );
  4381.     for my $text (@texts) {
  4382.         next unless $text;
  4383.         my $tokens = HTML::TokeParser->new(\$text);
  4384.         next unless $tokens;
  4385.         while (my $token = $tokens->get_tag('a')) {
  4386.             my $linkurl = $token->[1]{href};
  4387.             next unless $linkurl;
  4388.             my $canon = URI->new($linkurl)->canonical()->as_string();
  4389.             $urls{$canon} = 1;
  4390.         }
  4391.     }
  4392.     return [ keys %urls ];
  4393. }
  4394.  
  4395. ########################################################
  4396. # fix parameter input that should be integers
  4397. sub fixint {
  4398.     my($int) = @_;
  4399.     return if !defined($int);
  4400.     $int =~ s/^\+//;
  4401.     $int =~ s/^(-?[\d.]+).*$/$1/s or return;
  4402.     return $int;
  4403. }
  4404.  
  4405. ########################################################
  4406. # Count words in a given scalar will strip HTML tags
  4407. # before counts are made.
  4408. sub countWords {
  4409.     my($body) = @_;
  4410.  
  4411.     # Sanity check.
  4412.     $body = ${$body} if ref $body eq 'SCALAR';
  4413.     return 0 if ref $body;
  4414.  
  4415.     # Get rid of nasty print artifacts that may screw up counts.
  4416.     $body = strip_nohtml($body);
  4417.     $body =~ s/['`"~@#$%^()|\\\/!?.]//g;
  4418.     $body =~ s/&(?:\w+|#(\d+));//g;
  4419.     $body =~ s/[;\-,+=*&]/ /g;
  4420.     $body =~ s/\s\s+/ /g;
  4421.  
  4422.     # count words in $body.
  4423.     my(@words) = ($body =~ /\b/g);
  4424.  
  4425.     # Since we count boundaries, each word has two boundaries, so
  4426.     # we divide by two to get our count. This results in values
  4427.     # *close* to the return from a 'wc -w' on $body (within 1)
  4428.     # so I think this is close enough. ;)
  4429.     # - Cliff
  4430.     return scalar @words / 2;
  4431. }
  4432.  
  4433. ########################################################
  4434. # If you change createSid() for your site, change regexSid() too.
  4435. sub createSid {
  4436.     my($bogus_sid) = @_;
  4437.     # yes, this format is correct, don't change it :-)
  4438.     my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
  4439.     # Create a sid based on the current time.
  4440.     my @lt;
  4441.     my $start_time = time;
  4442.     if ($bogus_sid) {
  4443.         # If we were called being told that there's at
  4444.         # least one sid that is invalid (already taken),
  4445.         # then look backwards in time until we find it,
  4446.         # then go one second further.
  4447.         my $loops = 1000;
  4448.         while (--$loops) {
  4449.             $start_time--;
  4450.             @lt = localtime($start_time);
  4451.             $lt[5] %= 100; $lt[4]++; # year and month
  4452.             last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
  4453.         }
  4454.         if ($loops) {
  4455.             # Found the bogus sid by looking
  4456.             # backwards.  Go one second further.
  4457.             $start_time--;
  4458.         } else {
  4459.             # Something's wrong.  Skip ahead in
  4460.             # time instead of back (not sure what
  4461.             # else to do).
  4462.             $start_time = time + 1;
  4463.         }
  4464.     }
  4465.     @lt = localtime($start_time);
  4466.     $lt[5] %= 100; $lt[4]++; # year and month
  4467.     return sprintf($sidformat, @lt[reverse 0..5]);
  4468. }
  4469.  
  4470. ########################################################
  4471. # A very careful extraction of all the words from HTML text.
  4472. # URLs count as words.  (A different algorithm than countWords
  4473. # because countWords just has to be fast; this has to be
  4474. # precise.  Also, this counts occurrences of each word -- which
  4475. # is different than counting the overall number of words.)
  4476. sub findWords {
  4477.     my($args_hr) = @_;
  4478.     my $constants = getCurrentStatic();
  4479.     my $gSkin = getCurrentSkin();
  4480.     my $use_stemming = $constants->{stem_uncommon_words};
  4481.     my $language = $constants->{rdflanguage} || "EN-US";
  4482.     $language = uc($language);
  4483.     my $stemmer = Lingua::Stem->new(-locale => $language);
  4484.     $stemmer->stem_caching({ -level => 2 });
  4485.     my $text_return_hr = {};
  4486.     my @word_stems;
  4487.  
  4488.  
  4489.     # Return a hashref;  keys are the words, values are hashrefs
  4490.     # with the number of times they appear and so on.
  4491.     my $wordcount = $args_hr->{output_hr} || { };
  4492.  
  4493.     for my $key (keys %$args_hr) {
  4494.  
  4495.         # The default weight for each chunk of text is 1.
  4496.         my $weight_factor = $args_hr->{$key}{weight} || 1;
  4497.  
  4498.         my $text = $args_hr->{$key}{text} || '';
  4499.  
  4500.         # Pull out linked URLs from $text and treat them specially.
  4501.         # We only recognize the two most common types of link.
  4502.         # Actually, we could use HTML::LinkExtor here, which might
  4503.         # be more robust...
  4504.         my @urls_ahref = $text =~ m{
  4505.             <a[^>]+href\s*=\s*"?
  4506.             ([^"<>]+)
  4507.         }gxi;
  4508.         my @urls_imgsrc = $text =~ m{
  4509.             <img[^>]+src\s*=\s*"?
  4510.             ([^"<>]+)
  4511.         }gxi;
  4512.         foreach my $url (@urls_ahref, @urls_imgsrc) {
  4513.             my $uri = URI->new_abs($url, $gSkin->{absolutedir})
  4514.                 ->canonical;
  4515.             $url = $uri->as_string;
  4516.             # Tiny URLs don't count.
  4517.             next unless length($url) > 8;
  4518.             # All URLs get a high weight so they are almost
  4519.             # guaranteed to get into the list.
  4520.             $wordcount->{$url}{weight} += $weight_factor * 10;
  4521.             $wordcount->{$url}{count}++;
  4522.             $wordcount->{$url}{is_url} = 1;
  4523.             $wordcount->{$url}{is_url_with_path} = 1 if length($uri->path) > 2;
  4524.         }
  4525.  
  4526.         # Now remove the text's HTML tags and find and count the
  4527.         # words remaining in the text.  For our purposes, words
  4528.         # can include character references (entities) and the '
  4529.         # and - characters as well as \w.  This regex is a bit
  4530.         # messy.  I've tried to reduce backtracking as much as
  4531.         # possible but it's still a concern.
  4532.         $text = strip_notags($text);
  4533.         my $entity = qr{(?:&(?:(?:#x[0-9a-f]+|\d+)|[a-z0-9]+);)};
  4534.         my @words = $text =~ m{
  4535.             (
  4536.                 # Start with a non-apostrophe, non-dash char.
  4537.                 (?: $entity | \w )
  4538.                 # Followed by, optionally, any valid char.
  4539.                 [\w'-]?
  4540.                 # Followed by zero or more sequence of entities,
  4541.                 # character references, or normal chars.  The
  4542.                 # ' and - must alternate with the other types,
  4543.                 # so '' and -- break words.
  4544.                 (?:
  4545.                     (?: $entity | \w ) ['-]?
  4546.                 )*
  4547.                 # And end with a non-apostrophe, non-dash char.
  4548.                 (?: $entity | \w )
  4549.             )
  4550.         }gxi;
  4551.         for my $word (@words) {
  4552.             my $cap = $word =~ /^[A-Z]/ ? 1 : 0;
  4553.             # Ignore all uncapitalized words less than 4 chars.
  4554.             next if length($word) < 4 && !$cap;
  4555.             # Ignore *all* words less than 3 chars.
  4556.             next if length($word) < 3;
  4557.             my $ww = $weight_factor * ($cap ? 1.3 : 1);
  4558.             my $log_word = $word;
  4559.             if ($use_stemming) {
  4560.                 # For performance reasons we don't want to stem story text for all
  4561.                 # stories we are comparing to in getSimilarStories.
  4562.                 # Instead we make sure the stems we save are substrings of the word
  4563.                 # anchored at the beginning
  4564.                 #
  4565.                 # A breakdown of stem/word comparisons based on /usr/dict/words
  4566.                 # 70%    $stem eq $word
  4567.                 # 93%    $stem is a substring of $word anchored at the beginning
  4568.                 # 100%   $stem w/o its last letter is a substring of $word anchored at the beginning
  4569.                 #
  4570.                 # For now use the stem only if it a substring of the word anchored at the beginning
  4571.                 # otherwise use the complete word.  That way we can do a pattern match to check against
  4572.                 # older stories rather than stemming them for comparison
  4573.                
  4574.  
  4575.                 my $stems = $stemmer->stem($word);
  4576.                 $log_word = $stems->[0];
  4577.                 $log_word = $word if $word!~/^\Q$log_word\E/i;
  4578.                 push @word_stems, $log_word;
  4579.             }
  4580.  
  4581.             $wordcount->{lc $log_word}{weight} += $ww;
  4582.         }
  4583.         my %uniquewords = map { ( lc($_), 1 ) } $use_stemming ? @word_stems: @words;
  4584.         for my $word (keys %uniquewords) {
  4585.             $wordcount->{$word}{count}++;
  4586.         }
  4587.     }
  4588.     $stemmer->clear_stem_cache();
  4589.  
  4590.     return $wordcount;
  4591. }
  4592.  
  4593. #========================================================================
  4594.  
  4595. =head2 commify(NUMBER)
  4596.  
  4597. Returns the number with commas added, so 1234567890 becomes
  4598. 1,234,567,890.
  4599.  
  4600. =over 4
  4601.  
  4602. =item Parameters
  4603.  
  4604. =over 4
  4605.  
  4606. =item NUMBER
  4607.  
  4608. A number.
  4609.  
  4610. =back
  4611.  
  4612. =item Return value
  4613.  
  4614. Commified number.
  4615.  
  4616. =back
  4617.  
  4618. =cut
  4619.  
  4620. sub commify {
  4621.     my($num) = @_;
  4622.     $num =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g;
  4623.     return $num;
  4624. }
  4625.  
  4626. #========================================================================
  4627.  
  4628. =head2 grepn(list, value)
  4629.  
  4630. Returns the 1-based position of the first occurance of $value in @$list.
  4631.  
  4632. [ That is not actually the case at all! ]
  4633.  
  4634. =over 4
  4635.  
  4636. =item Parameters
  4637.  
  4638. =over 4
  4639.  
  4640. =item @$list
  4641.  
  4642. A reference to the list in question.
  4643.  
  4644. =item $value
  4645.  
  4646. The value you wish to search for.
  4647.  
  4648. =back
  4649.  
  4650. =item Return value
  4651.  
  4652. The position in the list of the first occurance of $value or undef if $value
  4653. is not in the list. Please note that the returned list is a 1-based value,
  4654. not a 0-based value, like perl arrays.
  4655.  
  4656. =back
  4657.  
  4658. =cut
  4659.  
  4660. sub grepn {
  4661.     my($list, $value) = @_;
  4662.  
  4663.     my $c = 1;
  4664.     for (@{$list}) {
  4665.         return $c if $_ eq $value;
  4666.         $c++;
  4667.     }
  4668.     return;
  4669. }
  4670.  
  4671. ##################################################################
  4672. sub sitename2filename {
  4673.     my($section) = @_;
  4674.     $section ||= '';
  4675.     my $filename = '';
  4676.  
  4677.     # XXXSKIN - hardcode 'index' for the sake of RSS feeds
  4678.     if ($section eq 'mainpage') {
  4679.         $filename = 'index';
  4680.     } elsif ($section ne 'light') {
  4681.         $filename = $section || lc getCurrentStatic('sitename');
  4682.     } else {
  4683.         $filename = lc getCurrentStatic('sitename');
  4684.     }
  4685.  
  4686.     $filename =~ s/\W+//g;
  4687.  
  4688.     return $filename;
  4689. }
  4690.  
  4691. ##################################################################
  4692. # counts total visible kids for each parent comment
  4693. sub countTotalVisibleKids {
  4694.     my($comments, $pid) = @_;
  4695.  
  4696.     my $constants        = getCurrentStatic();
  4697.     my $total            = 0;
  4698.     my $last_updated     = '';
  4699.     my $last_updated_uid = 0;
  4700.     $pid               ||= 0;
  4701.  
  4702.     $total += $comments->{$pid}{visiblekids} || 0;
  4703.  
  4704.     for my $cid (@{$comments->{$pid}{kids}}) {
  4705.         my($num_kids, $date_test, $uid) =
  4706.             countTotalVisibleKids($comments, $cid);
  4707.         $total += $num_kids;
  4708.     }
  4709.  
  4710.     $comments->{$pid}{totalvisiblekids} = $total;
  4711.  
  4712.     return($total, $last_updated, $last_updated_uid);
  4713. }
  4714.  
  4715. ##################################################################
  4716. # Why is this here and not a method in Slash::DB::MySQL? - Jamie 2003/05/13
  4717. sub createStoryTopicData {
  4718.     my($slashdb, $form) = @_;  
  4719.     $form ||= getCurrentForm();
  4720.  
  4721.     # Probably should not be changing stid, so set up @tids.
  4722.     my @tids = ( );
  4723.     if ($form->{_multi}{stid} && ref($form->{_multi}{stid}) eq 'ARRAY') {
  4724.         @tids = grep { $_ } @{$form->{_multi}{stid}};
  4725.     } elsif ($form->{stid}) {
  4726.         push @tids, $form->{stid};
  4727.     }
  4728.     push @tids, $form->{tid} if $form->{tid};
  4729.  
  4730.     # Store the list of original topic ids, before we generate the
  4731.     # list of all topic ids including parents.
  4732.     my @original = @tids;
  4733.     my %original_seen = map { ($_, 1) } @original;
  4734.  
  4735.     my $topics = $slashdb->getTopics;
  4736.     my %seen = map { ($_, 1) } @tids;
  4737.     for my $tid (@tids) {
  4738.         my $new_tid = $topics->{$tid}{parent_topic};
  4739.         next if !$new_tid || $seen{$new_tid};
  4740.         push @tids, $new_tid;
  4741.         $seen{$new_tid} = 1;
  4742.     }
  4743.  
  4744.     # The hashref that we return has an entry for every topic id
  4745.     # associated with this story, including all parent topic ids.
  4746.     # The value for each topic id is a boolean *string* intended
  4747.     # for the database:  "no" if the id is not a parent and is one
  4748.     # of the listed topic ids for the story, or "yes" if the id is
  4749.     # only in the list because it is the parent id of a listed
  4750.     # topic id.
  4751.     my %tid_ref;
  4752.     for my $tid (@tids) {
  4753.         next unless $tid;
  4754.         $tid_ref{$tid} = $original_seen{$tid} ? 'no' : 'yes' ;
  4755.     }
  4756.  
  4757.     return \%tid_ref;
  4758. }
  4759.  
  4760. # check whether url is correctly formatted and has a scheme that is allowed for bookmarks and submissions
  4761. sub validUrl {
  4762.     my($url) = @_;
  4763.     my $constants = getCurrentStatic();
  4764.     my $fudgedurl = fudgeurl($url);
  4765.    
  4766.     my @allowed_schemes = split(/\|/, $constants->{bookmark_allowed_schemes} || "http|https");
  4767.     my %allowed_schemes = map { $_ => 1 } @allowed_schemes;
  4768.  
  4769.     my $scheme;
  4770.    
  4771.     if ($fudgedurl) {
  4772.         my $uri = new URI $fudgedurl;
  4773.         $scheme = $uri->scheme if $uri && $uri->can("scheme");
  4774.     }      
  4775.     return ($fudgedurl && $scheme && $allowed_schemes{$scheme});
  4776. }
  4777.  
  4778.  
  4779. #################################################################
  4780. sub fixStory {
  4781.     my($str, $opts) = @_;
  4782.  
  4783.     if ($opts->{sub_type} && $opts->{sub_type} eq 'plain') {
  4784.         $str = strip_plaintext(url2html($str));
  4785.     } else {
  4786.         $str = strip_html(url2html($str));
  4787.     }
  4788.  
  4789.     # remove leading and trailing whitespace
  4790.     $str =~ s/^$Slash::Utility::Data::WS_RE+//io;
  4791.     $str =~ s/$Slash::Utility::Data::WS_RE+$//io;
  4792.  
  4793.     # and let's just get rid of these P tags; we don't need them, and they
  4794.     # cause too many problems in submissions
  4795.     unless (getCurrentStatic('submit_keep_p')) {
  4796.         $str =~ s|</p>||g;
  4797.         $str =~ s|<p(?: /)?>|<br><br>|g;
  4798.     }
  4799.  
  4800.     # smart conversion of em dashes to real ones
  4801.     # leave if - has nonwhitespace on either side, otherwise, convert
  4802.     unless (getCurrentStatic('submit_keep_dashes')) {
  4803.         $str =~ s/(\s+-+\s+)/ &mdash; /g;
  4804.     }
  4805.  
  4806.     $str = balanceTags($str, { deep_nesting => 1 });
  4807.  
  4808.     # do it again, just in case balanceTags added more ...
  4809.     $str =~ s/^$Slash::Utility::Data::WS_RE+//io;
  4810.     $str =~ s/$Slash::Utility::Data::WS_RE+$//io;
  4811.  
  4812.     return $str;
  4813. }
  4814.  
  4815. #################################################################
  4816. sub processSub {
  4817.     my($home, $known_to_be) = @_;
  4818.  
  4819.     my $proto = qr[^(?:mailto|http|https|ftp|gopher|telnet):];
  4820.  
  4821.     if  ($home =~ /\@/  && ($known_to_be eq 'mailto' || $home !~ $proto)) {
  4822.         $home = "mailto:$home";
  4823.     } elsif ($home ne ''    && ($known_to_be eq 'http'   || $home !~ $proto)) {
  4824.         $home = "http://$home";
  4825.     }
  4826.  
  4827.     return $home;
  4828. }
  4829.  
  4830.  
  4831.  
  4832.  
  4833.  
  4834.  
  4835. 1;
  4836.  
  4837. __END__
  4838.  
  4839.  
  4840. =head1 SEE ALSO
  4841.  
  4842. Slash(3), Slash::Utility(3).
Add Comment
Please, Sign In to add comment