| # Copyright (C) all contributors <meta@public-inbox.org> |
| # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> |
| # |
| # Atom body stream for HTTP responses |
| # See PublicInbox::GzipFilter for details. |
| package PublicInbox::WwwAtomStream; |
| use strict; |
| use parent 'PublicInbox::GzipFilter'; |
| |
| use POSIX qw(strftime); |
| use PublicInbox::SHA qw(sha1_hex); |
| use PublicInbox::Address; |
| use PublicInbox::Hval qw(ascii_html mid_href); |
| use PublicInbox::MsgTime qw(msg_timestamp); |
| |
| sub new { |
| my ($class, $ctx, $cb) = @_; |
| $ctx->{feed_base_url} = $ctx->{ibx}->base_url($ctx->{env}); |
| $ctx->{-spfx} = $ctx->{feed_base_url} if $ctx->{ibx}->{coderepo}; |
| $ctx->{cb} = $cb || \&PublicInbox::GzipFilter::close; |
| $ctx->{emit_header} = 1; |
| bless $ctx, $class; |
| } |
| |
| sub async_next ($) { |
| my ($http) = @_; # PublicInbox::HTTP |
| my $ctx = $http->{forward} or return; |
| eval { |
| if (my $smsg = $ctx->{smsg} = $ctx->{cb}->($ctx)) { |
| $ctx->smsg_blob($smsg); |
| } else { |
| $ctx->write('</feed>'); |
| $ctx->close; |
| } |
| }; |
| warn "E: $@" if $@; |
| } |
| |
| sub async_eml { # for async_blob_cb |
| my ($ctx, $eml) = @_; |
| my $smsg = delete $ctx->{smsg}; |
| $smsg->{mid} // $smsg->populate($eml); |
| $ctx->write(feed_entry($ctx, $smsg, $eml)); |
| } |
| |
| sub response { |
| my ($class, $ctx, $cb) = @_; |
| my $res_hdr = [ 'Content-Type' => 'application/atom+xml' ]; |
| $class->new($ctx, $cb); |
| $ctx->psgi_response(200, $res_hdr); |
| } |
| |
| # called once for each message by PSGI server |
| sub getline { |
| my ($self) = @_; |
| my $cb = $self->{cb} or return; |
| while (my $smsg = $cb->($self)) { |
| my $eml = $self->{ibx}->smsg_eml($smsg) or next; |
| return $self->translate(feed_entry($self, $smsg, $eml)); |
| } |
| delete $self->{cb}; |
| $self->zflush('</feed>'); |
| } |
| |
| # private |
| |
| sub title_tag { |
| my ($title) = @_; |
| $title =~ tr/\t\n / /s; # squeeze spaces |
| # try to avoid the type attribute in title: |
| $title = ascii_html($title); |
| my $type = index($title, '&') >= 0 ? "\ntype=\"html\"" : ''; |
| "<title$type>$title</title>"; |
| } |
| |
| sub to_uuid ($) { |
| my ($any) = @_; |
| utf8::encode($any); # really screwed up In-Reply-To fields exist |
| $any = sha1_hex($any); |
| my $h = '[a-f0-9]'; |
| my (@uuid5) = ($any =~ m!\A($h{8})($h{4})($h{4})($h{4})($h{12})!o); |
| 'urn:uuid:' . join('-', @uuid5); |
| } |
| |
| sub atom_header { |
| my ($ctx, $title) = @_; |
| my $ibx = $ctx->{ibx}; |
| my $base_url = $ctx->{feed_base_url}; |
| my $search_q = $ctx->{search_query}; |
| my $self_url = $base_url; |
| my $mid = $ctx->{mid}; |
| my $page_id; |
| if (defined $mid) { # per-thread |
| $self_url .= mid_href($mid).'/t.atom'; |
| $page_id = to_uuid("t\n".$mid) |
| } elsif (defined $search_q) { |
| my $query = $search_q->{'q'}; |
| $title = title_tag("$query - search results"); |
| $base_url .= '?' . $search_q->qs_html(x => undef); |
| $self_url .= '?' . $search_q->qs_html; |
| $page_id = to_uuid("q\n".$query); |
| } elsif (defined(my $cat = $ctx->{topic_category})) { |
| $title = title_tag("$cat topics - ".$ibx->description); |
| $self_url .= "topics_$cat.atom"; |
| } else { |
| $title = title_tag($ibx->description); |
| $self_url .= 'new.atom'; |
| my $addr = $ibx->{-primary_address}; |
| $page_id = "mailto:$addr" if defined $addr; |
| } |
| $page_id //= to_uuid($self_url); |
| qq(<?xml version="1.0" encoding="us-ascii"?>\n) . |
| qq(<feed\nxmlns="http://www.w3.org/2005/Atom"\n) . |
| qq(xmlns:thr="http://purl.org/syndication/thread/1.0">) . |
| qq{$title} . |
| qq(<link\nrel="alternate"\ntype="text/html") . |
| qq(\nhref="$base_url"/>) . |
| qq(<link\nrel="self"\nhref="$self_url"/>) . |
| qq(<id>$page_id</id>) . |
| feed_updated($ibx->modified); |
| } |
| |
| # returns undef or string |
| sub feed_entry { |
| my ($ctx, $smsg, $eml) = @_; |
| my $mid = $smsg->{mid}; |
| my $irt = PublicInbox::View::in_reply_to($eml); |
| my $uuid = to_uuid($mid); |
| my $base = $ctx->{feed_base_url}; |
| if (defined $irt) { |
| my $irt_uuid = to_uuid($irt); |
| $irt = mid_href($irt); |
| $irt = qq(<thr:in-reply-to\nref="$irt_uuid"\n). |
| qq(href="$base$irt/"/>); |
| } else { |
| $irt = ''; |
| } |
| my $href = $base . mid_href($mid) . '/'; |
| my $updated = feed_updated(msg_timestamp($eml)); |
| |
| my $title = $eml->header('Subject'); |
| $title = '(no subject)' unless defined $title && $title ne ''; |
| $title = title_tag($title); |
| |
| my $from = $eml->header('From') // $eml->header('Sender') // |
| $ctx->{ibx}->{-primary_address}; |
| my ($email) = PublicInbox::Address::emails($from); |
| my $name = ascii_html(join(', ', PublicInbox::Address::names($from))); |
| $email = ascii_html($email // $ctx->{ibx}->{-primary_address}); |
| |
| print { $ctx->zfh } |
| (delete($ctx->{emit_header}) ? atom_header($ctx, $title) : ''), |
| "<entry><author><name>$name</name><email>$email</email>" . |
| "</author>$title$updated" . |
| qq(<link\nhref="$href"/>) . |
| "<id>$uuid</id>$irt" . |
| qq{<content\ntype="xhtml">} . |
| qq{<div\nxmlns="http://www.w3.org/1999/xhtml">} . |
| qq(<pre\nstyle="white-space:pre-wrap">); |
| $ctx->{mhref} = $href; |
| $ctx->{changed_href} = "${href}#related"; |
| $eml->each_part(\&PublicInbox::View::add_text_body, $ctx, 1); |
| '</pre></div></content></entry>'; |
| } |
| |
| sub feed_updated { |
| my ($t) = @_; |
| '<updated>' . strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($t)) . '</updated>'; |
| } |
| |
| 1; |