# Copyright 2001-2003 Six Apart. This code cannot be redistributed without
# permission from www.movabletype.org.
#
# $Id: Util.pm,v 1.89 2003/02/16 20:41:30 btrott Exp $
package MT::Util;
use strict;
use MT::ConfigMgr;
use MT::Request;
use Exporter;
@MT::Util::ISA = qw( Exporter );
use vars qw( @EXPORT_OK );
@EXPORT_OK = qw( start_end_day start_end_week start_end_month
html_text_transform encode_html decode_html munge_comment
offset_time offset_time_list first_n_words
archive_file_for format_ts dirify remove_html
days_in wday_from_ts encode_js get_entry spam_protect
is_valid_email encode_php encode_url encode_xml
decode_xml is_valid_url discover_tb convert_high_ascii );
{
my @In_Year = (
[ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ],
[ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 ],
);
sub wday_from_ts {
my($y, $m, $d) = @_;
my $leap = $y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0) ? 1 : 0;
$y--;
## Copied from Date::Calc.
my $days = $y * 365;
$days += $y >>= 2;
$days -= int($y /= 25);
$days += $y >> 2;
$days += $In_Year[$leap][$m-1] + $d;
$days % 7;
}
sub yday_from_ts {
my($y, $m, $d) = @_;
my $leap = $y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0) ? 1 : 0;
$In_Year[$leap][$m-1] + $d;
}
}
use vars qw( %Languages );
sub format_ts {
my($format, $ts, $blog, $lang) = @_;
my %f;
unless ($lang) {
$lang = $blog && $blog->language ? $blog->language : 'en';
}
unless (defined $format) {
$format = $Languages{$lang}[3] || "%B %e, %Y %I:%M %p";
}
my $cache = MT::Request->instance->cache('formats');
unless ($cache) {
MT::Request->instance->cache('formats', $cache = {});
}
if (my $f_ref = $cache->{$ts . $lang}) {
%f = %$f_ref;
} else {
my $L = $Languages{$lang};
my @ts = @f{qw( Y m d H M S )} = unpack 'A4A2A2A2A2A2', $ts;
$f{w} = wday_from_ts(@ts[0..2]);
$f{j} = yday_from_ts(@ts[0..2]);
$f{'y'} = substr $f{Y}, 2;
$f{b} = substr $L->[1][$f{'m'}-1], 0, 3;
$f{B} = $L->[1][$f{'m'}-1];
$f{a} = substr $L->[0][$f{w}], 0, 3;
$f{A} = $L->[0][$f{w}];
($f{e} = $f{d}) =~ s!^0! !;
$f{I} = $f{H};
$f{I} = $f{H};
if ($f{I} > 12) {
$f{I} -= 12;
$f{p} = $L->[2][1];
} elsif ($f{I} == 0) {
$f{I} = 12;
$f{p} = $L->[2][0];
} elsif ($f{I} == 12) {
$f{p} = $L->[2][1];
} else {
$f{p} = $L->[2][0];
}
$f{I} = sprintf "%02d", $f{I};
($f{k} = $f{H}) =~ s!^0! !;
($f{l} = $f{I}) =~ s!^0! !;
$f{j} = sprintf "%03d", $f{j};
$f{Z} = '';
$cache->{$ts . $lang} = \%f;
}
my $date_format = $Languages{$lang}->[4] || "%B %d, %Y";
my $time_format = $Languages{$lang}->[5] || "%I:%M %p";
$format =~ s!%x!$date_format!g;
$format =~ s!%X!$time_format!g;
## This is a dreadful hack. I can't think of a good format specifier
## for "%B %Y" (which is used for monthly archives, for example) so
## I'll just hardcode this, for Japanese dates.
if ($lang eq 'jp') {
$format =~ s!%B %Y!$Languages{$lang}->[6]!g;
}
$format =~ s!%(\w)!$f{$1}!g;
$format;
}
{
my @Days_In = ( -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
sub days_in {
my($m, $y) = @_;
return $Days_In[$m] unless $m == 2;
return $y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0) ?
29 : 28;
}
}
sub start_end_day {
my $day = substr $_[0], 0, 8;
return $day . '000000' unless wantarray;
($day . "000000", $day . "235959");
}
sub start_end_week {
my($ts) = @_;
my($y, $mo, $d, $h, $m, $s) = unpack 'A4A2A2A2A2A2', $ts;
my $wday = wday_from_ts($y, $mo, $d);
my($sd, $sm, $sy) = ($d - $wday, $mo, $y);
if ($sd < 1) {
$sm--;
$sm = 12, $sy-- if $sm < 1;
$sd += days_in($sm, $sy);
}
my $start = sprintf "%04d%02d%02d%s", $sy, $sm, $sd, "000000";
return $start unless wantarray;
my($ed, $em, $ey) = ($d + 6 - $wday, $mo, $y);
if ($ed > days_in($em, $ey)) {
$ed -= days_in($em, $ey);
$em++;
$em = 1, $ey++ if $em > 12;
}
my $end = sprintf "%04d%02d%02d%s", $ey, $em, $ed, "235959";
($start, $end);
}
sub start_end_month {
my($ts) = @_;
my($y, $mo) = unpack 'A4A2', $ts;
my $start = sprintf "%04d%02d01000000", $y, $mo;
return $start unless wantarray;
my $end = sprintf "%04d%02d%02d235959", $y, $mo, days_in($mo, $y);
($start, $end);
}
sub offset_time_list { gmtime offset_time(@_) }
sub offset_time {
my($ts, $blog, $dir) = @_;
my $offset;
if (defined $blog) {
if (!ref($blog)) {
require MT::Blog;
$blog = MT::Blog->load($blog);
}
$offset = $blog && $blog->server_offset ? $blog->server_offset : 0;
} else {
$offset = MT::ConfigMgr->instance->TimeOffset;
}
$offset += 1 if (localtime $ts)[8];
$offset *= -1 if $dir && $dir eq '-';
$ts += $offset * 3600;
$ts;
}
sub html_text_transform {
my $str = shift;
$str ||= '';
my @paras = split /\r?\n\r?\n/, $str;
for my $p (@paras) {
if ($p !~ m/^<(?:table|ol|ul|pre|select|form|blockquote|div)/) {
$p =~ s!\r?\n!
\n!g;
$p = "
$p
"; } } join "\n\n", @paras; } { my %Map = (':' => ':', '@' => '@', '.' => '.'); sub spam_protect { my($str) = @_; my $look = join '', keys %Map; $str =~ s!([$look])!$Map{$1}!g; $str; } } sub encode_js { my($str) = @_; return '' unless defined $str; $str =~ s!(['"])!\\$1!g; $str =~ s!\n!\\n!g; $str =~ s!\f!\\f!g; $str =~ s!\r!\\r!g; $str =~ s!\t!\\t!g; $str; } sub encode_php { my($str, $meth) = @_; return '' unless defined $str; if ($meth eq 'qq') { $str = encode_phphere($str); $str =~ s!"!\\"!g; ## Replace " with \" } elsif (substr($meth, 0, 4) eq 'here') { $str = encode_phphere($str); } else { $str =~ s!\\!\\\\!g; ## Replace \ with \\ $str =~ s!'!\\'!g; ## Replace ' with \' } $str; } sub encode_phphere { my($str) = @_; $str =~ s!\\!\\\\!g; ## Replace \ with \\ $str =~ s!\$!\\\$!g; ## Replace $ with \$ $str =~ s!\n!\\n!g; ## Replace character \n with string \n $str =~ s!\r!\\r!g; ## Replace character \r with string \r $str =~ s!\t!\\t!g; ## Replace character \t with string \t $str; } sub encode_url { my($str) = @_; $str =~ s!([^a-zA-Z0-9_.-])!uc sprintf "%%%02x", ord($1)!eg; $str; } { my $Have_Entities = eval 'use HTML::Entities; 1' ? 1 : 0; sub encode_html { my($html, $can_double_encode) = @_; return '' unless defined $html; $html =~ tr!\cM!!d; if ($Have_Entities && !MT::ConfigMgr->instance->NoHTMLEntities) { $html = HTML::Entities::encode_entities($html); } else { if ($can_double_encode) { $html =~ s!&!&!g; } else { ## Encode any & not followed by something that looks like ## an entity, numeric or otherwise. $html =~ s/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w{1,8});)/&/g; } $html =~ s!"!"!g; $html =~ s!!>!g; } $html; } sub decode_html { my($html) = @_; return '' unless defined $html; $html =~ tr!\cM!!d; if ($Have_Entities && !MT::ConfigMgr->instance->NoHTMLEntities) { $html = HTML::Entities::decode_entities($html); } else { $html =~ s!"!"!g; $html =~ s!<!!g; $html =~ s!&!&!g; } $html; } } { my %Map = ('&' => '&', '"' => '"', '<' => '<', '>' => '>', '\'' => '''); my %Map_Decode = reverse %Map; my $RE = join '|', keys %Map; my $RE_D = join '|', keys %Map_Decode; sub encode_xml { my($str) = @_; if (!MT::ConfigMgr->instance->NoCDATA && $str =~ m/ <[^>]+> ## HTML markup | ## or &(?:(?!(\#([0-9]+)|\#x([0-9a-fA-F]+))).*?); ## something that looks like an HTML entity. /x) { ## If ]]> exists in the string, encode the > to >. $str =~ s/]]>/]]>/g; $str = ''; } else { $str =~ s!($RE)!$Map{$1}!g; } $str; } sub decode_xml { my($str) = @_; if ($str =~ s//$1/g) { ## Decode encoded ]]> $str =~ s/]]&(gt|#62);/]]>/g; } else { $str =~ s!($RE_D)!$Map_Decode{$1}!g; } $str; } } sub remove_html { my($text) = @_; $text =~ s!<[^>]+>!!gs; $text =~ s! 'A', # A` "\xe0" => 'a', # a` "\xc1" => 'A', # A' "\xe1" => 'a', # a' "\xc2" => 'A', # A^ "\xe2" => 'a', # a^ "\xc4" => 'Ae', # A: "\xe4" => 'ae', # a: "\xc3" => 'A', # A~ "\xe3" => 'a', # a~ "\xc8" => 'E', # E` "\xe8" => 'e', # e` "\xc9" => 'E', # E' "\xe9" => 'e', # e' "\xca" => 'E', # E^ "\xea" => 'e', # e^ "\xcb" => 'Ee', # E: "\xeb" => 'ee', # e: "\xcc" => 'I', # I` "\xec" => 'i', # i` "\xcd" => 'I', # I' "\xed" => 'i', # i' "\xce" => 'I', # I^ "\xee" => 'i', # i^ "\xcf" => 'Ie', # I: "\xef" => 'ie', # i: "\xd2" => 'O', # O` "\xf2" => 'o', # o` "\xd3" => 'O', # O' "\xf3" => 'o', # o' "\xd4" => 'O', # O^ "\xf4" => 'o', # o^ "\xd6" => 'Oe', # O: "\xf6" => 'oe', # o: "\xd5" => 'O', # O~ "\xf5" => 'o', # o~ "\xd8" => 'Oe', # O/ "\xf8" => 'oe', # o/ "\xd9" => 'U', # U` "\xf9" => 'u', # u` "\xda" => 'U', # U' "\xfa" => 'u', # u' "\xdb" => 'U', # U^ "\xfb" => 'u', # u^ "\xdc" => 'Ue', # U: "\xfc" => 'ue', # u: "\xc7" => 'C', # ,C "\xe7" => 'c', # ,c "\xd1" => 'N', # N~ "\xf1" => 'n', # n~ "\xdf" => 'ss', ); my $HighASCIIRE = join '|', keys %HighASCII; sub convert_high_ascii { my($s) = @_; $s =~ s/($HighASCIIRE)/$HighASCII{$1}/g; $s; } sub first_n_words { my($text, $n) = @_; $text = remove_html($text); my @words = split /\s+/, $text; my $max = @words > $n ? $n : @words; return join ' ', @words[0..$max-1]; } sub munge_comment { my($text, $blog) = @_; unless ($blog->allow_comment_html) { $text = remove_html($text); if ($blog->autolink_urls) { $text =~ s!(http://\S+)!$1!g; } } $text; } my %DynamicURIs = ( 'Individual' => 'entry/<$MTEntryID$>', 'Weekly' => 'archives/week/<$MTArchiveDate format="%Y/%m/%d"$>', 'Monthly' => 'archives/<$MTArchiveDate format="%Y/%m"$>', 'Daily' => 'archives/<$MTArchiveDate format="%Y/%m/%d"$>', 'Category' => 'section/<$MTCategoryID$>', ); sub archive_file_for { my($entry, $blog, $at, $cat, $map) = @_; return if $at eq 'None'; my $file; if ($blog->is_dynamic) { require MT::TemplateMap; $map = MT::TemplateMap->new; $map->file_template($DynamicURIs{$at}); } unless ($map) { my $cache = MT::Request->instance->cache('maps'); unless ($cache) { MT::Request->instance->cache('maps', $cache = {}); } unless ($map = $cache->{$blog->id . $at}) { require MT::TemplateMap; $map = MT::TemplateMap->load({ blog_id => $blog->id, archive_type => $at, is_preferred => 1 }); $cache->{$blog->id . $at} = $map if $map; } } my $file_tmpl = $map ? $map->file_template : ''; my($ctx); if ($file_tmpl) { require MT::Template::Context; $ctx = MT::Template::Context->new; $ctx->stash('blog', $blog); } local $ctx->{__stash}{category}; if ($at eq 'Individual') { if ($file_tmpl) { $ctx->stash('entry', $entry); $ctx->{current_timestamp} = $entry->created_on; } else { $file = sprintf("%06d", $entry->id); } } elsif ($at eq 'Daily') { if ($file_tmpl) { ($ctx->{current_timestamp}, $ctx->{current_timestamp_end}) = start_end_day($entry->created_on); } else { my $start = start_end_day($entry->created_on); my($year, $mon, $mday) = unpack 'A4A2A2', $start; $file = sprintf("%04d_%02d_%02d", $year, $mon, $mday); } } elsif ($at eq 'Weekly') { if ($file_tmpl) { ($ctx->{current_timestamp}, $ctx->{current_timestamp_end}) = start_end_week($entry->created_on); } else { my $start = start_end_week($entry->created_on); my($year, $mon, $mday) = unpack 'A4A2A2', $start; $file = sprintf("week_%04d_%02d_%02d", $year, $mon, $mday); } } elsif ($at eq 'Monthly') { if ($file_tmpl) { ($ctx->{current_timestamp}, $ctx->{current_timestamp_end}) = start_end_month($entry->created_on); } else { my $start = start_end_month($entry->created_on); my($year, $mon) = unpack 'A4A2', $start; $file = sprintf("%04d_%02d", $year, $mon); } } elsif ($at eq 'Category') { my $this_cat = $cat ? $cat : $entry->category; if ($file_tmpl) { $ctx->stash('archive_category', $this_cat); $ctx->{__stash}{category} = $this_cat; } else { my $label = ''; if ($this_cat) { $label = dirify($this_cat->label); } $file = sprintf("cat_%s", $label); } } else { return $entry->error(MT->translate( "Invalid Archive Type setting '[_1]'", $at )); } if ($file_tmpl) { require MT::Builder; my $build = MT::Builder->new; my $tokens = $build->compile($ctx, $file_tmpl) or return; defined($file = $build->build($ctx, $tokens)) or return; } else { my $ext = $blog->file_extension || 'html'; $file .= '.' . $ext; } $file; } { my %Helpers = ( Monthly => \&start_end_month, Weekly => \&start_end_week, Daily => \&start_end_day, ); sub get_entry { my($ts, $blog_id, $at, $order) = @_; my($start, $end) = $Helpers{$at}->($ts); if ($order eq 'previous') { $order = 'descend'; $ts = $start; } else { $order = 'ascend'; $ts = $end; } my $entry = MT::Entry->load( { blog_id => $blog_id, status => MT::Entry::RELEASE() }, { limit => 1, 'sort' => 'created_on', direction => $order, start_val => $ts }); $entry; } } sub is_valid_email { my($addr) = @_; if ($addr =~ /[ |\t|\r|\n]*\"?([^\"]+\"?@[^ <>\t]+\.[^ <>\t][^ <>\t]+)[ |\t|\r|\n]*/) { return $1; } else { return 0; } } sub is_valid_url { my($url) = @_; if ($url =~ /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/) { $url = (($2) && ($2 eq "http")) ? "$1$3$5" : "http://$5"; $url .= $6 if defined $6; $url .= $8 if defined $8; return $url; } else { return 0; } } sub discover_tb { my($url, $find_all, $contents) = @_; my $c; if ($contents) { $c = $$contents; } else { my $ua = MT->new_ua; ## Wrap this in an eval in case some versions don't support it. eval { $ua->parse_head(0) }; my $req = HTTP::Request->new(GET => $url); my $res = $ua->request($req); return unless $res->is_success; $c = $res->content; } (my $url_no_anchor = $url) =~ s/#.*$//; my(@items); while ($c =~ m!((single-quote interpolation). Cis the default. =head2 spam_protect($email_address) Given an email address I<$email_address>, encodes any characters that will identify it as an email address (C<:>, C<@>, and C<.>) into HTML entities, so that spam harvesters will not see the email address as easily. Returns the transformed address. =head2 is_valid_email($email_address) Checks the email address I<$email_address> for syntax validity; if the address--or part of it--is valid, Ireturns the valid (part of) the email address. Otherwise, it returns C<0>. =head1 AUTHOR & COPYRIGHTS Please see the I manpage for author, copyright, and license information. =cut