# Copyright (C) 2013-2015 all contributors <meta@public-inbox.org>
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
+#
+# Used for generating Atom feeds for web-accessible mailing list archives.
package PublicInbox::Feed;
use strict;
use warnings;
use Email::Address;
use Email::MIME;
use Date::Parse qw(strptime);
-use PublicInbox::Hval;
-use PublicInbox::GitCatFile;
+use PublicInbox::Hval qw/ascii_html/;
+use PublicInbox::Git;
use PublicInbox::View;
use PublicInbox::MID qw/mid_clean mid2path/;
use POSIX qw/strftime/;
use constant {
- DATEFMT => '%Y-%m-%dT%H:%M:%SZ', # atom standard
+ DATEFMT => '%Y-%m-%dT%H:%M:%SZ', # Atom standard
MAX_PER_PAGE => 25, # this needs to be tunable
};
-use Encode qw/find_encoding/;
-my $enc_utf8 = find_encoding('UTF-8');
-
# main function
sub generate {
my ($ctx) = @_;
sub title_tag {
my ($title) = @_;
+ $title =~ tr/\t\n / /s; # squeeze spaces
# try to avoid the type attribute in title:
- $title = PublicInbox::Hval->new_oneline($title)->as_html;
+ $title = ascii_html($title);
my $type = index($title, '&') >= 0 ? "\ntype=\"html\"" : '';
"<title$type>$title</title>";
}
qq(<?xml version="1.0" encoding="us-ascii"?>\n) .
qq{<feed\nxmlns="http://www.w3.org/2005/Atom">} .
qq{$title} .
- qq(<link\nhref="$feed_opts->{url}"/>) .
+ qq(<link\nrel="alternate"\ntype="text/html") .
+ qq(\nhref="$feed_opts->{url}"/>) .
qq(<link\nrel="self"\nhref="$feed_opts->{atomurl}"/>) .
qq(<id>mailto:$feed_opts->{id_addr}</id>);
}
my $max = $ctx->{max} || MAX_PER_PAGE;
my $feed_opts = get_feedopts($ctx);
my $x = atom_header($feed_opts);
- my $git = PublicInbox::GitCatFile->new($ctx->{git_dir});
+ my $git = $ctx->{git} ||= PublicInbox::Git->new($ctx->{git_dir});
each_recent_blob($ctx, sub {
my ($path, undef, $ts) = @_;
if (defined $x) {
}
add_to_feed($feed_opts, $fh, $path, $git);
});
- $git = undef; # destroy pipes
end_feed($fh);
}
$feed_opts->{url} = $html_url;
$feed_opts->{emit_header} = 1;
- my $git = PublicInbox::GitCatFile->new($ctx->{git_dir});
+ my $git = $ctx->{git} ||= PublicInbox::Git->new($ctx->{git_dir});
foreach my $msg (@{$res->{msgs}}) {
add_to_feed($feed_opts, $fh, mid2path($msg->mid), $git);
}
- $git = undef; # destroy pipes
end_feed($fh);
}
sub emit_html_index {
- my ($cb, $ctx) = @_;
- my $fh = $cb->([200,['Content-Type'=>'text/html; charset=UTF-8']]);
+ my ($res, $ctx) = @_;
+ my $fh = $res->([200,['Content-Type'=>'text/html; charset=UTF-8']]);
my $max = $ctx->{max} || MAX_PER_PAGE;
my $feed_opts = get_feedopts($ctx);
- my $title = $feed_opts->{description} || '';
- $title = PublicInbox::Hval->new_oneline($title)->as_html;
- my $atom_url = $feed_opts->{atomurl};
+ my $title = ascii_html($feed_opts->{description} || '');
my ($footer, $param, $last);
- my $state = { ctx => $ctx, seen => {}, anchor_idx => 0 };
+ my $state = { ctx => $ctx, seen => {}, anchor_idx => 0, fh => $fh };
my $srch = $ctx->{srch};
- my $top = "<b>$title</b> (<a\nhref=\"$atom_url\">Atom feed</a>)";
+ my $top = "<b>$title</b> (<a\nhref=\"new.atom\">Atom feed</a>)";
if ($srch) {
- $top = qq{<form\naction=""><tt>$top} .
+ $top = qq{<form\naction=""><pre>$top} .
qq{ <input\nname=q\ntype=text />} .
qq{<input\ntype=submit\nvalue=search />} .
- qq{</tt></form>} .
- PublicInbox::View::PRE_WRAP;
+ q{</pre></form><pre>}
} else {
- $top = PublicInbox::View::PRE_WRAP . $top . "\n";
+ $top = '<pre>' . $top . "\n";
}
$fh->write("<html><head><title>$title</title>" .
"<link\nrel=alternate\ntitle=\"Atom feed\"\n".
- "href=\"$atom_url\"\ntype=\"application/atom+xml\"/>" .
+ "href=\"new.atom\"\ntype=\"application/atom+xml\"/>" .
+ PublicInbox::Hval::STYLE .
"</head><body>$top");
# if the 'r' query parameter is given, it is a legacy permalink
my $cgi = $ctx->{cgi};
if ($cgi && !$cgi->param('r') && $srch) {
$state->{srch} = $srch;
- $last = PublicInbox::View::emit_index_topics($state, $fh);
+ $last = PublicInbox::View::emit_index_topics($state);
$param = 'o';
} else {
- $last = emit_index_nosrch($ctx, $state, $fh);
+ $last = emit_index_nosrch($ctx, $state);
$param = 'r';
}
$footer = nav_footer($cgi, $last, $feed_opts, $state, $param);
}
sub emit_index_nosrch {
- my ($ctx, $state, $fh) = @_;
- my $git = PublicInbox::GitCatFile->new($ctx->{git_dir});
+ my ($ctx, $state) = @_;
+ my $git = $ctx->{git} ||= PublicInbox::Git->new($ctx->{git_dir});
my (undef, $last) = each_recent_blob($ctx, sub {
my ($path, $commit, $ts, $u, $subj) = @_;
$state->{first} ||= $commit;
my $mime = do_cat_mail($git, $path) or return 0;
- PublicInbox::View::index_entry($fh, $mime, 0, $state);
+ PublicInbox::View::index_entry($mime, 0, $state);
1;
});
Email::Address->purge_cache;
my $anchor = $state->{anchor_idx};
if ($last) {
- $next = qq!<a\nhref="?$param=$last">next</a>!;
+ $next = qq!<a\nhref="?$param=$last"\nrel=next>next</a>!;
}
if ($old_r) {
$head = $cgi->path_info;
$head = qq!<a\nhref="$head">head</a>!;
}
- my $atom = "<a\nhref=\"$feed_opts->{atomurl}\">atom</a>";
+ my $atom = "<a\nhref=\"$feed_opts->{atomurl}\">Atom feed</a>";
"<a\nname=\"s$anchor\">page:</a> $next $head $atom";
}
# get recent messages
# we could use git log -z, but, we already know ssoma will not
# leave us with filenames with spaces in them..
- my @cmd = ('git', "--git-dir=$ctx->{git_dir}",
- qw/log --no-notes --no-color --raw -r
- --abbrev=16 --abbrev-commit/,
- "--format=%h%x00%ct%x00%an%x00%s%x00");
- push @cmd, $range;
-
- my $pid = open(my $log, '-|', @cmd) or
- die('open `'.join(' ', @cmd) . " pipe failed: $!\n");
+ my $git = $ctx->{git} ||= PublicInbox::Git->new($ctx->{git_dir});
+ my $log = $git->popen(qw/log --no-notes --no-color --raw -r
+ --abbrev=16 --abbrev-commit/,
+ "--format=%h%x00%ct%x00%an%x00%s%x00",
+ $range);
my %deleted; # only an optimization at this point
my $last;
my $nr = 0;
}
}
- close $log; # we may EPIPE here
# for pagination
($first_commit, $last_commit);
}
my %rv;
if (open my $fh, '<', "$ctx->{git_dir}/description") {
chomp($rv{description} = <$fh>);
- close $fh;
} else {
$rv{description} = '($GIT_DIR/description missing)';
}
my $url_base;
if ($cgi) {
- my $base;
- if (ref($cgi) eq 'CGI') {
- $base = $cgi->url(-base);
- } else {
- $base = $cgi->base->as_string;
- $base =~ s!/\z!!;
- }
- $url_base = "$base/$listname";
+ $url_base = $cgi->base->as_string . $listname;
if (my $mid = $ctx->{mid}) { # per-thread feed:
$rv{atomurl} = "$url_base/$mid/t.atom";
} else {
\%rv;
}
-sub mime_header {
- my ($mime, $name) = @_;
- PublicInbox::Hval->new_oneline($mime->header($name))->raw;
-}
-
sub feed_updated {
my ($date, $ts) = @_;
my @t = eval { strptime($date) } if defined $date;
my $midurl = $feed_opts->{midurl};
my $header_obj = $mime->header_obj;
- my $mid = $header_obj->header('Message-ID');
+ my $mid = $header_obj->header_raw('Message-ID');
defined $mid or return 0;
$mid = PublicInbox::Hval->new_msgid($mid);
my $href = $mid->as_href;
- my $content = PublicInbox::View->feed_entry($mime, "$midurl$href/f/");
+ my $content = PublicInbox::View->feed_entry($mime);
defined($content) or return 0;
$mime = undef;
my $date = $header_obj->header('Date');
my $updated = feed_updated($date);
- my $title = mime_header($header_obj, 'Subject') or return 0;
+ my $title = $header_obj->header('Subject');
+ defined $title or return 0;
$title = title_tag($title);
- my $from = mime_header($header_obj, 'From') or return 0;
+ my $from = $header_obj->header('From') or return 0;
my @from = Email::Address->parse($from) or return 0;
- my $name = PublicInbox::Hval->new_oneline($from[0]->name)->as_html;
+ my $name = ascii_html($from[0]->name);
my $email = $from[0]->address;
- $email = PublicInbox::Hval->new_oneline($email)->as_html;
+ $email = ascii_html($email);
if (delete $feed_opts->{emit_header}) {
$fh->write(atom_header($feed_opts, $title) . $updated);