fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2011-10-25 09:13 am

[dw-free] move cgi-bin/lj*.pl files into proper modules (in cgi-bin/LJ)

[commit: http://hg.dwscoalition.org/dw-free/rev/7181c14fbe5a]

http://bugs.dwscoalition.org/show_bug.cgi?id=1726

Move ljfeed.pl to LJ/Feed.pm

Patch by [personal profile] kareila.

Files modified:
  • cgi-bin/LJ/Feed.pm
  • cgi-bin/ljfeed.pl
  • cgi-bin/ljprotocol.pl
  • cgi-bin/modperl_subs.pl
  • t/feed-atom.t
--------------------------------------------------------------------------------
diff -r 9457acb16e2d -r 7181c14fbe5a cgi-bin/LJ/Feed.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/LJ/Feed.pm	Tue Oct 25 17:14:09 2011 +0800
@@ -0,0 +1,1028 @@
+#!/usr/bin/perl
+# This code was forked from the LiveJournal project owned and operated
+# by Live Journal, Inc. The code has been modified and expanded by
+# Dreamwidth Studios, LLC. These files were originally licensed under
+# the terms of the license supplied by Live Journal, Inc, which can
+# currently be found at:
+#
+# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
+#
+# In accordance with the original license, this code and all its
+# modifications are provided under the GNU General Public License.
+# A copy of that license can be found in the LICENSE file included as
+# part of this distribution.
+
+
+package LJ::Feed;
+use strict;
+
+use LJ::Entry;
+use XML::Atom::Person;
+use XML::Atom::Feed;
+
+my %feedtypes = (
+    rss         => { handler => \&create_view_rss,  need_items => 1 },
+    atom        => { handler => \&create_view_atom, need_items => 1 },
+    foaf        => { handler => \&create_view_foaf,                 },
+    yadis       => { handler => \&create_view_yadis,                },
+    userpics    => { handler => \&create_view_userpics,             },
+    comments    => { handler => \&create_view_comments,             },
+);
+
+sub make_feed
+{
+    my ($r, $u, $remote, $opts) = @_;
+
+    $opts->{pathextra} =~ s!^/(\w+)!!;
+    my $feedtype = $1;
+    my $viewfunc = $feedtypes{$feedtype};
+
+    unless ( $viewfunc && LJ::isu( $u ) ) {
+        $opts->{'handler_return'} = 404;
+        return undef;
+    }
+
+    $r->note('codepath' => "feed.$feedtype") if $r;
+
+    my $dbr = LJ::get_db_reader();
+
+    my $user = $u->user;
+
+    $u->preload_props( qw/ journaltitle journalsubtitle opt_synlevel / );
+
+    LJ::text_out(\$u->{$_})
+        foreach ("name", "url", "urlname");
+
+    # opt_synlevel will default to 'cut'
+    $u->{opt_synlevel} = 'cut'
+        unless $u->{opt_synlevel} &&
+               $u->{opt_synlevel} =~ /^(?:full|cut|summary|title)$/;
+
+    # some data used throughout the channel
+    my $journalinfo = {
+        u         => $u,
+        link      => $u->journal_base . "/",
+        title     => $u->{journaltitle} || $u->name_raw || $u->user,
+        subtitle  => $u->{journalsubtitle} || $u->name_raw,
+        builddate => LJ::time_to_http(time()),
+    };
+
+    # if we do not want items for this view, just call out
+    $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
+    return $viewfunc->{handler}->($journalinfo, $u, $opts)
+        unless ($viewfunc->{need_items});
+
+    # for syndicated accounts, redirect to the syndication URL
+    # However, we only want to do this if the data we're returning
+    # is similar. (Not FOAF, for example)
+    if ( $u->is_syndicated ) {
+        my $synurl = $dbr->selectrow_array("SELECT synurl FROM syndicated WHERE userid=$u->{'userid'}");
+        unless ($synurl) {
+            return 'No syndication URL available.';
+        }
+        $opts->{'redir'} = $synurl;
+        return undef;
+    }
+
+    my %FORM = LJ::parse_args( $r->query_string );
+
+    ## load the itemids
+    my (@itemids, @items);
+
+    # for consistency, we call ditemids "itemid" in user-facing settings
+    my $ditemid = defined $FORM{itemid} ? $FORM{itemid} + 0 : 0;
+
+    if ($ditemid) {
+        my $entry = LJ::Entry->new($u, ditemid => $ditemid);
+
+        if (! $entry || ! $entry->valid || ! $entry->visible_to($remote)) {
+            $opts->{'handler_return'} = 404;
+            return undef;
+        }
+
+        @itemids = $entry->jitemid;
+
+        push @items, {
+            itemid => $entry->jitemid,
+            anum => $entry->anum,
+            posterid => $entry->poster->id,
+            security => $entry->security,
+            alldatepart => LJ::alldatepart_s2($entry->eventtime_mysql),
+            rlogtime => $LJ::EndOfTime - LJ::mysqldate_to_time( $entry->logtime_mysql, 0 ),
+        };
+    } else {
+        @items = $u->recent_items(
+            clusterid => $u->{clusterid},
+            clustersource => 'slave',
+            remote => $remote,
+            itemshow => 25,
+            order => 'logtime',
+            tagids => $opts->{tagids},
+            tagmode => $opts->{tagmode},
+            itemids => \@itemids,
+            friendsview => 1,           # this returns rlogtimes
+            dateformat => 'S2',         # S2 format time format is easier
+        );
+    }
+
+    $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
+
+    ### load the log properties
+    my %logprops = ();
+    my $logtext;
+    my $logdb = LJ::get_cluster_reader($u);
+    LJ::load_log_props2($logdb, $u->{'userid'}, \@itemids, \%logprops);
+    $logtext = LJ::get_logtext2($u, @itemids);
+
+    # set last-modified header, then let apache figure out
+    # whether we actually need to send the feed.
+    my $lastmod = 0;
+    foreach my $item (@items) {
+        # revtime of the item.
+        my $revtime = $logprops{$item->{itemid}}->{revtime} || 0;
+        $lastmod = $revtime if $revtime > $lastmod;
+
+        # if we don't have a revtime, use the logtime of the item.
+        unless ($revtime) {
+            my $itime = $LJ::EndOfTime - $item->{rlogtime};
+            $lastmod = $itime if $itime > $lastmod;
+        }
+    }
+    $r->set_last_modified($lastmod) if $lastmod;
+
+    # use this $lastmod as the feed's last-modified time
+    # we would've liked to use something like
+    # LJ::get_timeupdate_multi instead, but that only changes
+    # with new updates and doesn't change on edits.
+    $journalinfo->{'modtime'} = $lastmod;
+
+    # regarding $r->set_etag:
+    # http://perl.apache.org/docs/general/correct_headers/correct_headers.html#Entity_Tags
+    # It is strongly recommended that you do not use this method unless you
+    # know what you are doing. set_etag() is expecting to be used in
+    # conjunction with a static request for a file on disk that has been
+    # stat()ed in the course of the current request. It is inappropriate and
+    # "dangerous" to use it for dynamic content.
+
+    # verify that our headers are good; especially check to see if we should
+    # return a 304 (Not Modified) response.
+    if ((my $status = $r->meets_conditions) != $r->OK) {
+        $opts->{handler_return} = $status;
+        return undef;
+    }
+
+    $journalinfo->{email} = $u->email_for_feeds if $u && $u->email_for_feeds;
+
+    # load tags now that we have no chance of jumping out early
+    my $logtags = LJ::Tags::get_logtags($u, \@itemids);
+
+    my %posteru = ();  # map posterids to u objects
+    LJ::load_userids_multiple([map { $_->{'posterid'}, \$posteru{$_->{'posterid'}} } @items], [$u]);
+
+    my @cleanitems;
+    my @entries;     # LJ::Entry objects
+
+  ENTRY:
+    foreach my $it (@items)
+    {
+        # load required data
+        my $itemid  = $it->{'itemid'};
+        my $ditemid = $itemid*256 + $it->{'anum'};
+        my $entry_obj = LJ::Entry->new($u, ditemid => $ditemid);
+
+        next ENTRY if $posteru{$it->{'posterid'}} && $posteru{$it->{'posterid'}}->is_suspended;
+        next ENTRY if $entry_obj && $entry_obj->is_suspended_for($remote);
+
+        if ($LJ::UNICODE && $logprops{$itemid}->{'unknown8bit'}) {
+            LJ::item_toutf8($u, \$logtext->{$itemid}->[0],
+                            \$logtext->{$itemid}->[1], $logprops{$itemid});
+        }
+
+        # see if we have a subject and clean it
+        my $subject = $logtext->{$itemid}->[0];
+        if ($subject) {
+            $subject =~ s/[\r\n]/ /g;
+            LJ::CleanHTML::clean_subject_all(\$subject);
+        }
+
+        # an HTML link to the entry. used if we truncate or summarize
+        my $readmore = "<b>(<a href=\"$journalinfo->{link}$ditemid.html\">Read more ...</a>)</b>";
+
+        # empty string so we don't waste time cleaning an entry that won't be used
+        my $event = $u->{'opt_synlevel'} eq 'title' ? '' : $logtext->{$itemid}->[1];
+
+        # clean the event, if non-empty
+        my $ppid = 0;
+        if ($event) {
+
+            # users without 'full_rss' get their logtext bodies truncated
+            # do this now so that the html cleaner will hopefully fix html we break
+            unless ( $u->can_use_full_rss ) {
+                my $trunc = LJ::text_trim($event, 0, 80);
+                $event = "$trunc $readmore" if $trunc ne $event;
+            }
+
+            LJ::CleanHTML::clean_event(\$event,
+                                       {
+                                        wordlength => 0,
+                                        preformatted => $logprops{$itemid}->{opt_preformatted},
+                                        cuturl => $u->{opt_synlevel} eq 'cut' ? "$journalinfo->{link}$ditemid.html" : "",
+                                        to_external_site => 1,
+                                       });
+            # do this after clean so we don't have to about know whether or not
+            # the event is preformatted
+            if ($u->{'opt_synlevel'} eq 'summary') {
+                $event = LJ::Entry->summarize( $event, $readmore );
+            }
+
+            while ($event =~ /<(?:lj-)?poll-(\d+)>/g) {
+                my $pollid = $1;
+
+                my $name = LJ::Poll->new($pollid)->name;
+                if ($name) {
+                    LJ::Poll->clean_poll(\$name);
+                } else {
+                    $name = "#$pollid";
+                }
+
+                $event =~ s!<(lj-)?poll-$pollid>!<div><a href="$LJ::SITEROOT/poll/?id=$pollid">View Poll: $name</a></div>!g;
+            }
+
+            my %args = LJ::parse_args( $r->query_string );
+            LJ::EmbedModule->expand_entry($u, \$event, expand_full => 1)
+                if %args && $args{'unfold_embed'};
+
+            $ppid = $1
+                if $event =~ m!<lj-phonepost journalid=[\'\"]\d+[\'\"] dpid=[\'\"](\d+)[\'\"]( /)?>!;
+        }
+
+        # include comment count image at bottom of event (for readers
+        # that don't understand the commentcount)
+        $event .= "<br /><br />" . $entry_obj->comment_imgtag . " comments";
+
+        my $mood;
+        if ($logprops{$itemid}->{'current_mood'}) {
+            $mood = $logprops{$itemid}->{'current_mood'};
+        } elsif ($logprops{$itemid}->{'current_moodid'}) {
+            $mood = DW::Mood->mood_name( $logprops{$itemid}->{'current_moodid'}+0 );
+        }
+
+        my $createtime = $LJ::EndOfTime - $it->{rlogtime};
+        my $can_comment = ! defined $logprops{$itemid}->{opt_nocomments} ||
+                          ( $logprops{$itemid}->{opt_nocomments} == 0 );
+        my $cleanitem = {
+            itemid     => $itemid,
+            ditemid    => $ditemid,
+            subject    => $subject,
+            event      => $event,
+            createtime => $createtime,
+            eventtime  => $it->{alldatepart},  # ugly: this is of a different format than the other two times.
+            modtime    => $logprops{$itemid}->{revtime} || $createtime,
+            comments   => $can_comment,
+            music      => $logprops{$itemid}->{'current_music'},
+            mood       => $mood,
+            ppid       => $ppid,
+            tags       => [ values %{$logtags->{$itemid} || {}} ],
+            security   => $it->{security},
+            posterid   => $it->{posterid},
+            replycount => $logprops{$itemid}->{'replycount'},
+        };
+        push @cleanitems, $cleanitem;
+        push @entries,    $entry_obj;
+    }
+
+    # fix up the build date to use entry-time
+    $journalinfo->{'builddate'} = LJ::time_to_http($LJ::EndOfTime - $items[0]->{'rlogtime'}),
+
+    return $viewfunc->{handler}->($journalinfo, $u, $opts, \@cleanitems, \@entries);
+}
+
+# helper method to add a namespace to the root of a feed
+sub _add_feed_namespace {
+    my ( $feed, $ns_prefix, $namespace ) = @_;
+    my $doc = $feed->elem->ownerDocument->getDocumentElement;
+    $doc->setAttribute( "xmlns:$ns_prefix", $namespace );
+}
+
+# helper method for create_view_rss and create_view_comments
+sub _init_talkview {
+    my ( $journalinfo, $u, $opts, $talkview ) = @_;
+    my $hubbub = $talkview eq 'rss' && LJ::is_enabled( 'hubbub' );
+    my $ret;
+
+    # header
+    $ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
+    $ret .= LJ::Hooks::run_hook("bot_director", "<!-- ", " -->") . "\n";
+    $ret .= "<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/' " .
+            "xmlns:atom10='http://www.w3.org/2005/Atom'>\n";
+
+    # channel attributes
+    my $desc = { rss => LJ::exml( "$journalinfo->{title} - $LJ::SITENAME" ),
+                 comments => "Latest comments in "
+                           . LJ::exml( $journalinfo->{title} ) };
+
+    $ret .= "<channel>\n";
+    $ret .= "  <title>" . LJ::exml($journalinfo->{title}) . "</title>\n";
+    $ret .= "  <link>$journalinfo->{link}</link>\n";
+    $ret .= "  <description>" . $desc->{$talkview} . "</description>\n";
+    $ret .= "  <managingEditor>" . LJ::exml($journalinfo->{email}) . "</managingEditor>\n" if $journalinfo->{email};
+    $ret .= "  <lastBuildDate>$journalinfo->{builddate}</lastBuildDate>\n";
+    $ret .= "  <generator>LiveJournal / $LJ::SITENAME</generator>\n";
+    $ret .= "  <lj:journal>" . $u->user . "</lj:journal>\n";
+    $ret .= "  <lj:journaltype>" . $u->journaltype_readable . "</lj:journaltype>\n";
+    # TODO: add 'language' field when user.lang has more useful information
+
+    if ( $hubbub ) {
+        $ret .= "  <atom10:link rel='self' href='" . $u->journal_base . "/data/rss' />\n";
+        foreach my $hub (@LJ::HUBBUB_HUBS) {
+            $ret .= "  <atom10:link rel='hub' href='" . LJ::exml($hub) . "' />\n";
+        }
+    }
+
+    ### image block, returns info for their current userpic
+    if ( $u->{'defaultpicid'} ) {
+        my $icon = $u->userpic;
+        my $url = $icon->url;
+        my ( $width, $height ) = $icon->dimensions;
+
+        $ret .= "  <image>\n";
+        $ret .= "    <url>$url</url>\n";
+        $ret .= "    <title>" . LJ::exml( $journalinfo->{title} ) . "</title>\n";
+        $ret .= "    <link>$journalinfo->{link}</link>\n";
+        $ret .= "    <width>$width</width>\n";
+        $ret .= "    <height>$height</height>\n";
+        $ret .= "  </image>\n\n";
+    }
+
+    return $ret;
+}
+
+sub create_view_rss {
+    my ( $journalinfo, $u, $opts, $cleanitems ) = @_;
+
+    my $ret = _init_talkview( $journalinfo, $u, $opts, 'rss' );
+
+    my %posteru = ();  # map posterids to u objects
+    LJ::load_userids_multiple([map { $_->{'posterid'}, \$posteru{$_->{'posterid'}} } @$cleanitems], [$u]);
+
+    # output individual item blocks
+
+    foreach my $it (@$cleanitems)
+    {
+        my $itemid = $it->{itemid};
+        my $ditemid = $it->{ditemid};
+        my $poster = $posteru{$it->{posterid}};
+
+        $ret .= "<item>\n";
+        $ret .= "  <guid isPermaLink='true'>$journalinfo->{link}$ditemid.html</guid>\n";
+        $ret .= "  <pubDate>" . LJ::time_to_http($it->{createtime}) . "</pubDate>\n";
+        $ret .= "  <title>" . LJ::exml($it->{subject}) . "</title>\n" if $it->{subject};
+        $ret .= "  <author>" . LJ::exml($journalinfo->{email}) . "</author>" if $journalinfo->{email};
+        $ret .= "  <link>$journalinfo->{link}$ditemid.html</link>\n";
+        # omit the description tag if we're only syndicating titles
+        #   note: the $event was also emptied earlier, in make_feed
+        unless ($u->{'opt_synlevel'} eq 'title') {
+            $ret .= "  <description>" . LJ::exml($it->{event}) . "</description>\n";
+        }
+        if ($it->{comments}) {
+            $ret .= "  <comments>$journalinfo->{link}$ditemid.html</comments>\n";
+        }
+        $ret .= "  <category>$_</category>\n" foreach map { LJ::exml($_) } @{$it->{tags} || []};
+        # support 'podcasting' enclosures
+        $ret .= LJ::Hooks::run_hook( "pp_rss_enclosure",
+                { userid => $u->{userid}, ppid => $it->{ppid} }) if $it->{ppid};
+        # TODO: add author field with posterid's email address, respect communities
+        $ret .= "  <lj:music>" . LJ::exml($it->{music}) . "</lj:music>\n" if $it->{music};
+        $ret .= "  <lj:mood>" . LJ::exml($it->{mood}) . "</lj:mood>\n" if $it->{mood};
+        $ret .= "  <lj:security>" . LJ::exml($it->{security}) . "</lj:security>\n" if $it->{security};
+        $ret .= "  <lj:poster>" . LJ::exml($poster->user) . "</lj:poster>\n" unless $u->equals( $poster );
+        $ret .= "  <lj:reply-count>$it->{replycount}</lj:reply-count>\n";
+        $ret .= "</item>\n";
+    }
+
+    $ret .= "</channel>\n";
+    $ret .= "</rss>\n";
+
+    return $ret;
+}
+
+
+# the creator for the Atom view
+# keys of $opts:
+# single_entry - only output an <entry>..</entry> block. off by default
+# apilinks - output AtomAPI links for posting a new entry or
+#            getting/editing/deleting an existing one. off by default
+sub create_view_atom
+{
+    my ( $j, $u, $opts, $cleanitems, $entrylist ) = @_;
+    my ( $feed, $xml, $ns, $site_ns_prefix );
+
+    $site_ns_prefix = lc $LJ::SITENAMEABBREV;
+    $ns = "http://www.w3.org/2005/Atom";
+
+    # AtomAPI interface path
+    my $api = $opts->{'apilinks'} ? $u->atom_service_document :
+                                    $u->journal_base . "/data/atom";
+
+    my $make_link = sub {
+        my ( $rel, $type, $href, $title ) = @_;
+        my $link = XML::Atom::Link->new( Version => 1 );
+        $link->rel($rel);
+        $link->type($type) if $type;
+        $link->href($href);
+        $link->title( $title ) if $title;
+        return $link;
+    };
+
+    my $author = XML::Atom::Person->new( Version => 1 );
+    my $journalu = $j->{u};
+    $author->email( $journalu->email_for_feeds ) if $journalu && $journalu->email_for_feeds;
+    $author->name(  $u->{'name'} );
+
+    # feed information
+    unless ($opts->{'single_entry'}) {
+        $feed = XML::Atom::Feed->new( Version => 1 );
+        $xml  = $feed->elem->ownerDocument;
+
+        if ($u->should_block_robots) {
+            _add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
+            $xml->getDocumentElement->setAttribute( "idx:index", "no" );
+        }
+
+        $xml->insertBefore( $xml->createComment( LJ::Hooks::run_hook("bot_director") ), $xml->documentElement());
+
+        # attributes
+        $feed->id( $u->atomid );
+        $feed->title( $j->{'title'} || $u->{user} );
+        if ( $j->{'subtitle'} ) {
+            $feed->subtitle( $j->{'subtitle'} );
+        }
+
+        $feed->author( $author );
+        $feed->add_link( $make_link->( 'alternate', 'text/html', $j->{'link'} ) );
+        $feed->add_link(
+            $make_link->(
+                'self',
+                $opts->{'apilinks'}
+                ? ( 'application/atom+xml', "$api/entries" )
+                : ( 'text/xml', $api )
+            )
+        );
+        $feed->updated( LJ::time_to_w3c($j->{'modtime'}, 'Z') );
+
+        my $ljinfo = $xml->createElement( "$site_ns_prefix:journal" );
+        $ljinfo->setAttribute( 'username', LJ::exml($u->user) );
+        $ljinfo->setAttribute( 'type', LJ::exml($u->journaltype_readable) );
+        $xml->getDocumentElement->appendChild( $ljinfo );
+
+        if ( LJ::is_enabled( 'hubbub' ) ) {
+            foreach my $hub (@LJ::HUBBUB_HUBS) {
+                $feed->add_link($make_link->('hub', undef, $hub));
+            }
+        }
+    }
+
+    my $posteru = LJ::load_userids( map { $_->{posterid} } @$cleanitems);
+    # output individual item blocks
+    # FIXME: use LJ::Entry->atom_entry?
+    foreach my $it (@$cleanitems)
+    {
+        my $itemid = $it->{itemid};
+        my $ditemid = $it->{ditemid};
+        my $poster = $posteru->{$it->{posterid}};
+
+        my $entry = XML::Atom::Entry->new( Version => 1 );
+        my $entry_xml = $entry->elem->ownerDocument;
+
+        $entry->id( $u->atomid . ":$ditemid" );
+
+        # author isn't required if it is in the main <feed>
+        # only add author if we are in a single entry view, or
+        # the journal entry isn't owned by the journal. (communities)
+        if ( $opts->{single_entry} || ! $journalu->equals( $poster ) ) {
+            my $author = XML::Atom::Person->new( Version => 1 );
+            $author->email( $poster->email_visible ) if $poster && $poster->email_visible;
+            $author->name(  $poster->{name} );
+            $entry->author( $author );
+
+            # and the lj-specific stuff
+            my $postauthor = $entry_xml->createElement( "$site_ns_prefix:poster" );
+            $postauthor->setAttribute( 'user', LJ::exml($poster->user));
+            $entry_xml->getDocumentElement->appendChild( $postauthor );
+        }
+
+        $entry->add_link(
+            $make_link->( 'alternate', 'text/html', "$j->{'link'}$ditemid.html" )
+        );
+        $entry->add_link(
+            $make_link->( 'self', 'text/xml', "$api/?itemid=$ditemid" )
+        );
+
+        $entry->add_link(
+            $make_link->(
+                'edit',      'application/atom+xml',
+                "$api/entries/$itemid", 'Edit this post'
+            )
+          ) if $opts->{'apilinks'};
+
+        my ($year, $mon, $mday, $hour, $min, $sec) = split(/ /, $it->{eventtime});
+        my $event_date = sprintf("%04d-%02d-%02dT%02d:%02d:%02d",
+                                 $year, $mon, $mday, $hour, $min, $sec);
+
+
+        # title can't be blank and can't be absent, so we have to fake some subject
+        $entry->title( $it->{'subject'} ||
+                       "$journalu->{user} \@ $event_date"
+                       );
+
+
+        $entry->published( LJ::time_to_w3c($it->{createtime}, "Z") );
+        $entry->updated(   LJ::time_to_w3c($it->{modtime},    "Z") );
+
+        foreach my $tag ( @{$it->{tags} || []} ) {
+            my $category = XML::Atom::Category->new( Version => 1 );
+            $category->term( $tag );
+            $entry->add_category( $category );
+        }
+
+        my @currents = ( [ 'music'       => $it->{music}      ],
+                         [ 'mood'        => $it->{mood}       ],
+                         [ 'security'    => $it->{security}   ],
+                         [ 'reply-count' => $it->{replycount} ],
+                       );
+
+        foreach ( @currents ) {
+            my ( $key, $val ) = @$_;
+            if ( defined $val ) {
+                my $elem = $entry_xml->createElement( "$site_ns_prefix:$key" );
+                $elem->appendTextNode( $val );
+                $entry_xml->getDocumentElement->appendChild( $elem );
+            }
+        }
+
+        # if syndicating the complete entry
+        #   -print a content tag
+        # elsif syndicating summaries
+        #   -print a summary tag
+         # else (code omitted), we're syndicating title only
+        #   -print neither (the title has already been printed)
+        #   note: the $event was also emptied earlier, in make_feed
+        #
+        # a lack of a content element is allowed,  as long
+        # as we maintain a proper 'alternate' link (above)
+        my $make_content = sub {
+            my $content = $entry_xml->createElement( $_[0] );
+            $content->setAttribute( 'type', 'html' );
+            $content->setNamespace( $ns );
+            $content->appendTextNode( $it->{'event'} );
+            $entry_xml->getDocumentElement->appendChild( $content );
+        };
+        if ( $u->{'opt_synlevel'} eq 'full' || $u->{'opt_synlevel'} eq 'cut' ) {
+            # Do this manually for now, until XML::Atom supports new
+            # content type classifications.
+            $make_content->('content');
+        } elsif ($u->{'opt_synlevel'} eq 'summary') {
+            $make_content->('summary');
+        }
+
+        if ( $opts->{'single_entry'} ) {
+            _add_feed_namespace( $entry, $site_ns_prefix, $LJ::SITEROOT );
+            return $entry->as_xml;
+        }
+        else {
+            $feed->add_entry( $entry );
+        }
+    }
+
+    _add_feed_namespace( $feed, $site_ns_prefix, $LJ::SITEROOT );
+    return $feed->as_xml;
+}
+
+# create a FOAF page for a user
+sub create_view_foaf {
+    my ($journalinfo, $u, $opts) = @_;
+    my $comm = $u->is_community;
+
+    my $ret;
+
+    # return nothing if we're not a user
+    unless ( $u->is_person || $comm ) {
+        $opts->{handler_return} = 404;
+        return undef;
+    }
+
+    # set our content type
+    $opts->{contenttype} = 'application/rdf+xml; charset=' . $opts->{saycharset};
+
+    # setup userprops we will need
+    $u->preload_props( qw{
+        aolim icq yahoo jabber msn icbm url urlname external_foaf_url country city journaltitle
+    } );
+
+    # create bare foaf document, for now
+    $ret = "<?xml version='1.0'?>\n";
+    $ret .= LJ::Hooks::run_hook("bot_director", "<!-- ", " -->");
+    $ret .= "<rdf:RDF\n";
+    $ret .= "   xml:lang=\"en\"\n";
+    $ret .= "   xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n";
+    $ret .= "   xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\"\n";
+    $ret .= "   xmlns:foaf=\"http://xmlns.com/foaf/0.1/\"\n";
+    $ret .= "   xmlns:ya=\"http://blogs.yandex.ru/schema/foaf/\"\n";
+    $ret .= "   xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"\n";
+    $ret .= "   xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
+
+    # precompute some values
+    my $digest = "";
+    my $remote = LJ::get_remote();
+    if ($u->is_validated) {
+        my $email_visible = $u->email_visible($remote);
+        $digest = Digest::SHA1::sha1_hex("mailto:$email_visible") if $email_visible;
+    }
+
+    # channel attributes
+    $ret .= ($comm ? "  <foaf:Group>\n" : "  <foaf:Person>\n");
+    $ret .= "    <foaf:nick>$u->{user}</foaf:nick>\n";
+    $ret .= "    <foaf:name>". LJ::exml($u->{name}) ."</foaf:name>\n";
+    $ret .= "    <foaf:openid rdf:resource=\"" . $u->journal_base . "/\" />\n"
+        unless $comm;
+
+    # user location
+    if ($u->{'country'}) {
+        my $ecountry = LJ::eurl($u->{'country'});
+        $ret .= "    <ya:country dc:title=\"$ecountry\" rdf:resource=\"$LJ::SITEROOT/directorysearch?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry\"/>\n" if $u->can_show_location($remote);
+        if ($u->{'city'}) {
+            my $estate = '';  # FIXME: add state.  Yandex didn't need it.
+            my $ecity = LJ::eurl($u->{'city'});
+            $ret .= "    <ya:city dc:title=\"$ecity\" rdf:resource=\"$LJ::SITEROOT/directorysearch?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry&amp;loc_st=$estate&amp;loc_ci=$ecity\"/>\n" if $u->can_show_location($remote);
+       }
+    }
+
+    if ($u->{bdate} && $u->{bdate} ne "0000-00-00" && !$comm && $u->can_show_full_bday) {
+        $ret .= "    <foaf:dateOfBirth>".$u->bday_string."</foaf:dateOfBirth>\n";
+    }
+    $ret .= "    <foaf:mbox_sha1sum>$digest</foaf:mbox_sha1sum>\n" if $digest;
+
+    # userpic
+    if (my $picid = $u->{'defaultpicid'}) {
+        $ret .= "    <foaf:img rdf:resource=\"$LJ::USERPIC_ROOT/$picid/$u->{userid}\" />\n";
+    }
+
+    $ret .= "    <foaf:page>\n";
+    $ret .= "      <foaf:Document rdf:about=\"" . $u->profile_url . "\">\n";
+    $ret .= "        <dc:title>$LJ::SITENAME Profile</dc:title>\n";
+    $ret .= "        <dc:description>Full $LJ::SITENAME profile, including information such as interests and bio.</dc:description>\n";
+    $ret .= "      </foaf:Document>\n";
+    $ret .= "    </foaf:page>\n";
+
+    # we want to bail out if they have an external foaf file, because
+    # we want them to be able to provide their own information.
+    if ($u->{external_foaf_url}) {
+        $ret .= "    <rdfs:seeAlso rdf:resource=\"" . LJ::eurl($u->{external_foaf_url}) . "\" />\n";
+        $ret .= ($comm ? "  </foaf:Group>\n" : "  </foaf:Person>\n");
+        $ret .= "</rdf:RDF>\n";
+        return $ret;
+    }
+
+    # contact type information
+    my %types = (
+        aolim => 'aimChatID',
+        icq => 'icqChatID',
+        yahoo => 'yahooChatID',
+        msn => 'msnChatID',
+        jabber => 'jabberID',
+    );
+    if ($u->{allow_contactshow} eq 'Y') {
+        foreach my $type (keys %types) {
+            next unless defined $u->{$type};
+            $ret .= "    <foaf:$types{$type}>" . LJ::exml($u->{$type}) . "</foaf:$types{$type}>\n";
+        }
+    }
+
+    # blog activity
+    {
+        my $count = $u->number_of_posts;
+        $ret .= "    <ya:blogActivity>\n";
+        $ret .= "      <ya:Posts>\n";
+        $ret .= "        <ya:feed rdf:resource=\"" . $u->journal_base ."/data/rss\" dc:type=\"application/rss+xml\" />\n";
+        $ret .= "        <ya:posted>$count</ya:posted>\n";
+        $ret .= "      </ya:Posts>\n";
+        $ret .= "    </ya:blogActivity>\n";
+    }
+
+    # include a user's journal page and web site info
+    $ret .= "    <foaf:weblog rdf:resource=\"" . $u->journal_base . "/\"/>\n";
+    if ($u->{url}) {
+        $ret .= "    <foaf:homepage rdf:resource=\"" . LJ::eurl($u->{url});
+        $ret .= "\" dc:title=\"" . LJ::exml($u->{urlname}) . "\" />\n";
+    }
+
+    # user bio
+    if ( $u->has_bio ) {
+        my $bio = $u->bio;
+        LJ::text_out( \$bio );
+        LJ::CleanHTML::clean_userbio( \$bio );
+        $ret .= "    <ya:bio>" . LJ::exml( $bio ) . "</ya:bio>\n";
+    }
+
+    # icbm/location info
+    if ($u->{icbm}) {
+        my @loc = split(",", $u->{icbm});
+        $ret .= "    <foaf:based_near><geo:Point geo:lat='" . $loc[0] . "'" .
+                " geo:long='" . $loc[1] . "' /></foaf:based_near>\n";
+    }
+
+    # interests, please!
+    # arrayref of interests rows: [ intid, intname, intcount ]
+    my $intu = $u->get_interests();
+    foreach my $int (@$intu) {
+        LJ::text_out(\$int->[1]); # 1==interest
+        $ret .= "    <foaf:interest dc:title=\"". LJ::exml($int->[1]) . "\" " .
+                "rdf:resource=\"$LJ::SITEROOT/interests?int=" . LJ::eurl($int->[1]) . "\" />\n";
+    }
+
+    # check if the user has a "FOAF-knows" group
+    my $has_foaf_group = $u->trust_groups( name => 'FOAF-knows' ) ? 1 : 0;
+
+    # now information on who you know, limited to a certain maximum number of users
+    my @ids;
+    if ( $has_foaf_group ) {
+        @ids = keys %{ $u->trust_group_members( name => 'FOAF-knows' ) };
+    } else {
+        @ids = $u->trusted_userids;
+    }
+
+    @ids = splice(@ids, 0, $LJ::MAX_FOAF_FRIENDS) if @ids > $LJ::MAX_FOAF_FRIENDS;
+
+    # now load
+    my $users = LJ::load_userids( @ids );
+
+    # iterate to create data structure
+    foreach my $trustid ( @ids ) {
+        next if $trustid == $u->id;
+        my $fu = $users->{$trustid};
+        next if $fu->is_inactive || ! $fu->is_person;
+
+        my $name = LJ::exml($fu->name_raw);
+        my $tagline = LJ::exml($fu->prop('journaltitle') || '');
+        my $upicurl = $fu->userpic ? $fu->userpic->url : '';
+
+        $ret .= $comm ? "    <foaf:member>\n" : "    <foaf:knows>\n";
+        $ret .= "      <foaf:Person>\n";
+        $ret .= "        <foaf:nick>$fu->{'user'}</foaf:nick>\n";
+        $ret .= "        <foaf:member_name>$name</foaf:member_name>\n";
+        $ret .= "        <foaf:tagLine>$tagline</foaf:tagLine>\n";
+        $ret .= "        <foaf:image>$upicurl</foaf:image>\n" if $upicurl;
+        $ret .= "        <rdfs:seeAlso rdf:resource=\"" . $fu->journal_base ."/data/foaf\" />\n";
+        $ret .= "        <foaf:weblog rdf:resource=\"" . $fu->journal_base . "/\"/>\n";
+        $ret .= "      </foaf:Person>\n";
+        $ret .= $comm ? "    </foaf:member>\n" : "    </foaf:knows>\n";
+    }
+
+    # finish off the document
+    $ret .= $comm ? "    </foaf:Group>\n" : "  </foaf:Person>\n";
+    $ret .= "</rdf:RDF>\n";
+
+    return $ret;
+}
+
+# YADIS capability discovery
+sub create_view_yadis {
+    my ($journalinfo, $u, $opts) = @_;
+    my $person = $u->is_person;
+
+    my $ret = "";
+
+    my $println = sub { $ret .= $_[0]."\n"; };
+
+    $println->('<?xml version="1.0" encoding="UTF-8"?>');
+    $println->('<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"><XRD>');
+
+    local $1;
+    $opts->{pathextra} =~ m!^(/.*)?$!;
+    my $viewchunk = $1;
+
+    my $view;
+    if ($viewchunk eq '') {
+        $view = "recent";
+    }
+    elsif ($viewchunk eq '/read') {
+        $view = "read";
+    }
+    else {
+        $view = '';
+    }
+
+    if ($view eq 'recent') {
+        # Only people (not communities, etc) can be OpenID authenticated
+        if ($person && LJ::OpenID->server_enabled) {
+            $println->('    <Service>');
+            $println->('        <Type>http://openid.net/signon/1.0</Type>');
+            $println->('        <URI>'.LJ::ehtml($LJ::OPENID_SERVER).'</URI>');
+            $println->('    </Service>');
+        }
+    }
+
+    # Local site-specific content
+    # TODO: Give these hooks access to $view somehow?
+    LJ::Hooks::run_hook("yadis_service_descriptors", \$ret);
+
+    $println->('</XRD></xrds:XRDS>');
+    return $ret;
+}
+
+# create a userpic page for a user
+sub create_view_userpics {
+    my ($journalinfo, $u, $opts) = @_;
+    my ( $feed, $xml, $ns );
+
+    $ns = "http://www.w3.org/2005/Atom";
+
+    my $make_link = sub {
+        my ( $rel, $type, $href, $title ) = @_;
+        my $link = XML::Atom::Link->new( Version => 1 );
+        $link->rel($rel);
+        $link->type($type);
+        $link->href($href);
+        $link->title( $title ) if $title;
+        return $link;
+    };
+
+    my $author = XML::Atom::Person->new( Version => 1 );
+    $author->name(  $u->{name} );
+
+    $feed = XML::Atom::Feed->new( Version => 1 );
+    $xml  = $feed->elem->ownerDocument;
+
+    if ($u->should_block_robots) {
+        _add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
+        $xml->getDocumentElement->setAttribute( "idx:index", "no" );
+    }
+
+    my $bot = LJ::Hooks::run_hook("bot_director");
+    $xml->insertBefore( $xml->createComment( $bot ), $xml->documentElement())
+        if $bot;
+
+    $feed->id( $u->atomid . ":userpics" );
+    $feed->title( "$u->{user}'s userpics" );
+
+    $feed->author( $author );
+    $feed->add_link( $make_link->( 'alternate', 'text/html', $u->allpics_base ) );
+    $feed->add_link( $make_link->( 'self', 'text/xml', $u->journal_base() . "/data/userpics" ) );
+
+    # now start building all the userpic data
+    # start up by loading all of our userpic information and creating that part of the feed
+    my $info = $u->get_userpic_info( { load_comments => 1, load_urls => 1, load_descriptions => 1 } );
+
+    my %keywords = ();
+    while (my ($kw, $pic) = each %{$info->{kw}}) {
+        LJ::text_out(\$kw);
+        push @{$keywords{$pic->{picid}}}, LJ::exml($kw);
+    }
+
+    my %comments = ();
+    while (my ($pic, $comment) = each %{$info->{comment}}) {
+        LJ::text_out(\$comment);
+        $comments{$pic} = LJ::strip_html($comment);
+    }
+
+    my %descriptions = ();
+    while ( my( $pic, $description ) = each %{$info->{description}} ) {
+        LJ::text_out(\$description);
+        $descriptions{$pic} = LJ::strip_html($description);
+    }
+
+    my @pics = map { $info->{pic}->{$_} } sort { $a <=> $b }
+               grep { $info->{pic}->{$_}->{state} eq 'N' }
+               keys %{ $info->{pic} };
+
+    # FIXME: It sucks that there are two different methods for aggregating
+    #        the information for a user's set of icons, one of which doesn't
+    #        include keywords and the other of which doesn't include pictime.
+    #        But hey, at least they both use caching.
+
+    my %pictimes = map { $_->picid => $_->pictime }
+                       LJ::Userpic->load_user_userpics( $u );
+
+    my $latest = 0;
+    foreach my $pictime ( values %pictimes ) {
+        $latest = ($latest < $pictime) ? $pictime : $latest;
+    }
+
+    $feed->updated( LJ::time_to_w3c($latest, 'Z') );
+
+    foreach my $pic (@pics) {
+        my $entry = XML::Atom::Entry->new( Version => 1 );
+        my $entry_xml = $entry->elem->ownerDocument;
+
+        $entry->id( $u->atomid . ":userpics:$pic->{picid}" );
+
+        my $title = ($pic->{picid} == $u->{defaultpicid}) ? "default userpic" : "userpic";
+        $entry->title( $title );
+
+        $entry->updated( LJ::time_to_w3c( $pictimes{ $pic->{picid} }, 'Z') );
+
+        my $content;
+        $content = $entry_xml->createElement( "content" );
+        $content->setAttribute( 'src', "$LJ::USERPIC_ROOT/$pic->{picid}/$u->{userid}" );
+        $content->setNamespace( $ns );
+        $entry_xml->getDocumentElement->appendChild( $content );
+
+        foreach my $kw (@{$keywords{$pic->{picid}}}) {
+            my $category = $entry_xml->createElement( 'category' );
+            $category->setAttribute( 'term', $kw );
+            $category->setNamespace( $ns );
+            $entry_xml->getDocumentElement->appendChild( $category );
+        }
+
+        if ( $descriptions{$pic->{picid}} ) {
+            my $content = $entry_xml->createElement( 'title' );
+            $content->setNamespace( $ns );
+            $content->appendTextNode( $descriptions{$pic->{picid}} );
+            $entry_xml->getDocumentElement->appendChild( $content );
+        };
+
+        if($comments{$pic->{picid}}) {
+            my $content = $entry_xml->createElement( "summary" );
+            $content->setNamespace( $ns );
+            $content->appendTextNode( $comments{$pic->{picid}} );
+            $entry_xml->getDocumentElement->appendChild( $content );
+        };
+
+        $feed->add_entry( $entry );
+    }
+
+    return $feed->as_xml;
+}
+
+
+sub create_view_comments {
+    my ( $journalinfo, $u, $opts ) = @_;
+
+    unless ( LJ::is_enabled('latest_comments_rss', $u) ) {
+        $opts->{handler_return} = 404;
+        return 404;
+    }
+
+    unless ( $u->can_use_latest_comments_rss ) {
+        $opts->{handler_return} = 403;
+        return;
+    }
+
+    my $ret = _init_talkview( $journalinfo, $u, $opts, 'comments' );
+
+    my @comments = $u->get_recent_talkitems(25);
+    foreach my $r (@comments)
+    {
+        my $c = LJ::Comment->new($u, jtalkid => $r->{jtalkid});
+        my $thread_url = $c->thread_url;
+        my $subject = $c->subject_raw;
+        LJ::CleanHTML::clean_subject_all(\$subject);
+
+        $ret .= "<item>\n";
+        $ret .= "  <guid isPermaLink='true'>$thread_url</guid>\n";
+        $ret .= "  <pubDate>" . LJ::time_to_http($r->{datepostunix}) . "</pubDate>\n";
+        $ret .= "  <title>" . LJ::exml($subject) . "</title>\n" if $subject;
+        $ret .= "  <link>$thread_url</link>\n";
+        # omit the description tag if we're only syndicating titles
+        unless ($u->{'opt_synlevel'} eq 'title') {
+            my $body = $c->body_raw;
+            LJ::CleanHTML::clean_subject_all(\$body);
+            $ret .= "  <description>" . LJ::exml($body) . "</description>\n";
+        }
+        $ret .= "</item>\n";
+    }
+
+    $ret .= "</channel>\n";
+    $ret .= "</rss>\n";
+
+
+    return $ret;
+}
+
+sub generate_hubbub_jobs {
+    my ( $u, $joblist ) = @_;
+
+    return unless LJ::is_enabled( 'hubbub' );
+
+    foreach my $hub ( @LJ::HUBBUB_HUBS ) {
+        my $make_hubbub_job = sub {
+            my $type = shift;
+
+            my $topic_url = $u->journal_base . "/data/$type";
+            return TheSchwartz::Job->new(
+                funcname => 'TheSchwartz::Worker::PubSubHubbubPublish',
+                arg => {
+                    hub => $hub,
+                    topic_url => $topic_url,
+                },
+                coalesce => $hub,
+            );
+        };
+
+        push @$joblist, $make_hubbub_job->("rss");
+        push @$joblist, $make_hubbub_job->("atom");
+    }
+}
+
+
+1;
diff -r 9457acb16e2d -r 7181c14fbe5a cgi-bin/ljfeed.pl
--- a/cgi-bin/ljfeed.pl	Mon Oct 24 19:56:39 2011 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1028 +0,0 @@
-#!/usr/bin/perl
-# This code was forked from the LiveJournal project owned and operated
-# by Live Journal, Inc. The code has been modified and expanded by
-# Dreamwidth Studios, LLC. These files were originally licensed under
-# the terms of the license supplied by Live Journal, Inc, which can
-# currently be found at:
-#
-# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
-#
-# In accordance with the original license, this code and all its
-# modifications are provided under the GNU General Public License.
-# A copy of that license can be found in the LICENSE file included as
-# part of this distribution.
-
-
-package LJ::Feed;
-use strict;
-
-use LJ::Entry;
-use XML::Atom::Person;
-use XML::Atom::Feed;
-
-my %feedtypes = (
-    rss         => { handler => \&create_view_rss,  need_items => 1 },
-    atom        => { handler => \&create_view_atom, need_items => 1 },
-    foaf        => { handler => \&create_view_foaf,                 },
-    yadis       => { handler => \&create_view_yadis,                },
-    userpics    => { handler => \&create_view_userpics,             },
-    comments    => { handler => \&create_view_comments,             },
-);
-
-sub make_feed
-{
-    my ($r, $u, $remote, $opts) = @_;
-
-    $opts->{pathextra} =~ s!^/(\w+)!!;
-    my $feedtype = $1;
-    my $viewfunc = $feedtypes{$feedtype};
-
-    unless ( $viewfunc && LJ::isu( $u ) ) {
-        $opts->{'handler_return'} = 404;
-        return undef;
-    }
-
-    $r->note('codepath' => "feed.$feedtype") if $r;
-
-    my $dbr = LJ::get_db_reader();
-
-    my $user = $u->user;
-
-    $u->preload_props( qw/ journaltitle journalsubtitle opt_synlevel / );
-
-    LJ::text_out(\$u->{$_})
-        foreach ("name", "url", "urlname");
-
-    # opt_synlevel will default to 'cut'
-    $u->{opt_synlevel} = 'cut'
-        unless $u->{opt_synlevel} &&
-               $u->{opt_synlevel} =~ /^(?:full|cut|summary|title)$/;
-
-    # some data used throughout the channel
-    my $journalinfo = {
-        u         => $u,
-        link      => $u->journal_base . "/",
-        title     => $u->{journaltitle} || $u->name_raw || $u->user,
-        subtitle  => $u->{journalsubtitle} || $u->name_raw,
-        builddate => LJ::time_to_http(time()),
-    };
-
-    # if we do not want items for this view, just call out
-    $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
-    return $viewfunc->{handler}->($journalinfo, $u, $opts)
-        unless ($viewfunc->{need_items});
-
-    # for syndicated accounts, redirect to the syndication URL
-    # However, we only want to do this if the data we're returning
-    # is similar. (Not FOAF, for example)
-    if ( $u->is_syndicated ) {
-        my $synurl = $dbr->selectrow_array("SELECT synurl FROM syndicated WHERE userid=$u->{'userid'}");
-        unless ($synurl) {
-            return 'No syndication URL available.';
-        }
-        $opts->{'redir'} = $synurl;
-        return undef;
-    }
-
-    my %FORM = LJ::parse_args( $r->query_string );
-
-    ## load the itemids
-    my (@itemids, @items);
-
-    # for consistency, we call ditemids "itemid" in user-facing settings
-    my $ditemid = defined $FORM{itemid} ? $FORM{itemid} + 0 : 0;
-
-    if ($ditemid) {
-        my $entry = LJ::Entry->new($u, ditemid => $ditemid);
-
-        if (! $entry || ! $entry->valid || ! $entry->visible_to($remote)) {
-            $opts->{'handler_return'} = 404;
-            return undef;
-        }
-
-        @itemids = $entry->jitemid;
-
-        push @items, {
-            itemid => $entry->jitemid,
-            anum => $entry->anum,
-            posterid => $entry->poster->id,
-            security => $entry->security,
-            alldatepart => LJ::alldatepart_s2($entry->eventtime_mysql),
-            rlogtime => $LJ::EndOfTime - LJ::mysqldate_to_time( $entry->logtime_mysql, 0 ),
-        };
-    } else {
-        @items = $u->recent_items(
-            clusterid => $u->{clusterid},
-            clustersource => 'slave',
-            remote => $remote,
-            itemshow => 25,
-            order => 'logtime',
-            tagids => $opts->{tagids},
-            tagmode => $opts->{tagmode},
-            itemids => \@itemids,
-            friendsview => 1,           # this returns rlogtimes
-            dateformat => 'S2',         # S2 format time format is easier
-        );
-    }
-
-    $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
-
-    ### load the log properties
-    my %logprops = ();
-    my $logtext;
-    my $logdb = LJ::get_cluster_reader($u);
-    LJ::load_log_props2($logdb, $u->{'userid'}, \@itemids, \%logprops);
-    $logtext = LJ::get_logtext2($u, @itemids);
-
-    # set last-modified header, then let apache figure out
-    # whether we actually need to send the feed.
-    my $lastmod = 0;
-    foreach my $item (@items) {
-        # revtime of the item.
-        my $revtime = $logprops{$item->{itemid}}->{revtime} || 0;
-        $lastmod = $revtime if $revtime > $lastmod;
-
-        # if we don't have a revtime, use the logtime of the item.
-        unless ($revtime) {
-            my $itime = $LJ::EndOfTime - $item->{rlogtime};
-            $lastmod = $itime if $itime > $lastmod;
-        }
-    }
-    $r->set_last_modified($lastmod) if $lastmod;
-
-    # use this $lastmod as the feed's last-modified time
-    # we would've liked to use something like
-    # LJ::get_timeupdate_multi instead, but that only changes
-    # with new updates and doesn't change on edits.
-    $journalinfo->{'modtime'} = $lastmod;
-
-    # regarding $r->set_etag:
-    # http://perl.apache.org/docs/general/correct_headers/correct_headers.html#Entity_Tags
-    # It is strongly recommended that you do not use this method unless you
-    # know what you are doing. set_etag() is expecting to be used in
-    # conjunction with a static request for a file on disk that has been
-    # stat()ed in the course of the current request. It is inappropriate and
-    # "dangerous" to use it for dynamic content.
-
-    # verify that our headers are good; especially check to see if we should
-    # return a 304 (Not Modified) response.
-    if ((my $status = $r->meets_conditions) != $r->OK) {
-        $opts->{handler_return} = $status;
-        return undef;
-    }
-
-    $journalinfo->{email} = $u->email_for_feeds if $u && $u->email_for_feeds;
-
-    # load tags now that we have no chance of jumping out early
-    my $logtags = LJ::Tags::get_logtags($u, \@itemids);
-
-    my %posteru = ();  # map posterids to u objects
-    LJ::load_userids_multiple([map { $_->{'posterid'}, \$posteru{$_->{'posterid'}} } @items], [$u]);
-
-    my @cleanitems;
-    my @entries;     # LJ::Entry objects
-
-  ENTRY:
-    foreach my $it (@items)
-    {
-        # load required data
-        my $itemid  = $it->{'itemid'};
-        my $ditemid = $itemid*256 + $it->{'anum'};
-        my $entry_obj = LJ::Entry->new($u, ditemid => $ditemid);
-
-        next ENTRY if $posteru{$it->{'posterid'}} && $posteru{$it->{'posterid'}}->is_suspended;
-        next ENTRY if $entry_obj && $entry_obj->is_suspended_for($remote);
-
-        if ($LJ::UNICODE && $logprops{$itemid}->{'unknown8bit'}) {
-            LJ::item_toutf8($u, \$logtext->{$itemid}->[0],
-                            \$logtext->{$itemid}->[1], $logprops{$itemid});
-        }
-
-        # see if we have a subject and clean it
-        my $subject = $logtext->{$itemid}->[0];
-        if ($subject) {
-            $subject =~ s/[\r\n]/ /g;
-            LJ::CleanHTML::clean_subject_all(\$subject);
-        }
-
-        # an HTML link to the entry. used if we truncate or summarize
-        my $readmore = "<b>(<a href=\"$journalinfo->{link}$ditemid.html\">Read more ...</a>)</b>";
-
-        # empty string so we don't waste time cleaning an entry that won't be used
-        my $event = $u->{'opt_synlevel'} eq 'title' ? '' : $logtext->{$itemid}->[1];
-
-        # clean the event, if non-empty
-        my $ppid = 0;
-        if ($event) {
-
-            # users without 'full_rss' get their logtext bodies truncated
-            # do this now so that the html cleaner will hopefully fix html we break
-            unless ( $u->can_use_full_rss ) {
-                my $trunc = LJ::text_trim($event, 0, 80);
-                $event = "$trunc $readmore" if $trunc ne $event;
-            }
-
-            LJ::CleanHTML::clean_event(\$event,
-                                       {
-                                        wordlength => 0,
-                                        preformatted => $logprops{$itemid}->{opt_preformatted},
-                                        cuturl => $u->{opt_synlevel} eq 'cut' ? "$journalinfo->{link}$ditemid.html" : "",
-                                        to_external_site => 1,
-                                       });
-            # do this after clean so we don't have to about know whether or not
-            # the event is preformatted
-            if ($u->{'opt_synlevel'} eq 'summary') {
-                $event = LJ::Entry->summarize( $event, $readmore );
-            }
-
-            while ($event =~ /<(?:lj-)?poll-(\d+)>/g) {
-                my $pollid = $1;
-
-                my $name = LJ::Poll->new($pollid)->name;
-                if ($name) {
-                    LJ::Poll->clean_poll(\$name);
-                } else {
-                    $name = "#$pollid";
-                }
-
-                $event =~ s!<(lj-)?poll-$pollid>!<div><a href="$LJ::SITEROOT/poll/?id=$pollid">View Poll: $name</a></div>!g;
-            }
-
-            my %args = LJ::parse_args( $r->query_string );
-            LJ::EmbedModule->expand_entry($u, \$event, expand_full => 1)
-                if %args && $args{'unfold_embed'};
-
-            $ppid = $1
-                if $event =~ m!<lj-phonepost journalid=[\'\"]\d+[\'\"] dpid=[\'\"](\d+)[\'\"]( /)?>!;
-        }
-
-        # include comment count image at bottom of event (for readers
-        # that don't understand the commentcount)
-        $event .= "<br /><br />" . $entry_obj->comment_imgtag . " comments";
-
-        my $mood;
-        if ($logprops{$itemid}->{'current_mood'}) {
-            $mood = $logprops{$itemid}->{'current_mood'};
-        } elsif ($logprops{$itemid}->{'current_moodid'}) {
-            $mood = DW::Mood->mood_name( $logprops{$itemid}->{'current_moodid'}+0 );
-        }
-
-        my $createtime = $LJ::EndOfTime - $it->{rlogtime};
-        my $can_comment = ! defined $logprops{$itemid}->{opt_nocomments} ||
-                          ( $logprops{$itemid}->{opt_nocomments} == 0 );
-        my $cleanitem = {
-            itemid     => $itemid,
-            ditemid    => $ditemid,
-            subject    => $subject,
-            event      => $event,
-            createtime => $createtime,
-            eventtime  => $it->{alldatepart},  # ugly: this is of a different format than the other two times.
-            modtime    => $logprops{$itemid}->{revtime} || $createtime,
-            comments   => $can_comment,
-            music      => $logprops{$itemid}->{'current_music'},
-            mood       => $mood,
-            ppid       => $ppid,
-            tags       => [ values %{$logtags->{$itemid} || {}} ],
-            security   => $it->{security},
-            posterid   => $it->{posterid},
-            replycount => $logprops{$itemid}->{'replycount'},
-        };
-        push @cleanitems, $cleanitem;
-        push @entries,    $entry_obj;
-    }
-
-    # fix up the build date to use entry-time
-    $journalinfo->{'builddate'} = LJ::time_to_http($LJ::EndOfTime - $items[0]->{'rlogtime'}),
-
-    return $viewfunc->{handler}->($journalinfo, $u, $opts, \@cleanitems, \@entries);
-}
-
-# helper method to add a namespace to the root of a feed
-sub _add_feed_namespace {
-    my ( $feed, $ns_prefix, $namespace ) = @_;
-    my $doc = $feed->elem->ownerDocument->getDocumentElement;
-    $doc->setAttribute( "xmlns:$ns_prefix", $namespace );
-}
-
-# helper method for create_view_rss and create_view_comments
-sub _init_talkview {
-    my ( $journalinfo, $u, $opts, $talkview ) = @_;
-    my $hubbub = $talkview eq 'rss' && LJ::is_enabled( 'hubbub' );
-    my $ret;
-
-    # header
-    $ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
-    $ret .= LJ::Hooks::run_hook("bot_director", "<!-- ", " -->") . "\n";
-    $ret .= "<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/' " .
-            "xmlns:atom10='http://www.w3.org/2005/Atom'>\n";
-
-    # channel attributes
-    my $desc = { rss => LJ::exml( "$journalinfo->{title} - $LJ::SITENAME" ),
-                 comments => "Latest comments in "
-                           . LJ::exml( $journalinfo->{title} ) };
-
-    $ret .= "<channel>\n";
-    $ret .= "  <title>" . LJ::exml($journalinfo->{title}) . "</title>\n";
-    $ret .= "  <link>$journalinfo->{link}</link>\n";
-    $ret .= "  <description>" . $desc->{$talkview} . "</description>\n";
-    $ret .= "  <managingEditor>" . LJ::exml($journalinfo->{email}) . "</managingEditor>\n" if $journalinfo->{email};
-    $ret .= "  <lastBuildDate>$journalinfo->{builddate}</lastBuildDate>\n";
-    $ret .= "  <generator>LiveJournal / $LJ::SITENAME</generator>\n";
-    $ret .= "  <lj:journal>" . $u->user . "</lj:journal>\n";
-    $ret .= "  <lj:journaltype>" . $u->journaltype_readable . "</lj:journaltype>\n";
-    # TODO: add 'language' field when user.lang has more useful information
-
-    if ( $hubbub ) {
-        $ret .= "  <atom10:link rel='self' href='" . $u->journal_base . "/data/rss' />\n";
-        foreach my $hub (@LJ::HUBBUB_HUBS) {
-            $ret .= "  <atom10:link rel='hub' href='" . LJ::exml($hub) . "' />\n";
-        }
-    }
-
-    ### image block, returns info for their current userpic
-    if ( $u->{'defaultpicid'} ) {
-        my $icon = $u->userpic;
-        my $url = $icon->url;
-        my ( $width, $height ) = $icon->dimensions;
-
-        $ret .= "  <image>\n";
-        $ret .= "    <url>$url</url>\n";
-        $ret .= "    <title>" . LJ::exml( $journalinfo->{title} ) . "</title>\n";
-        $ret .= "    <link>$journalinfo->{link}</link>\n";
-        $ret .= "    <width>$width</width>\n";
-        $ret .= "    <height>$height</height>\n";
-        $ret .= "  </image>\n\n";
-    }
-
-    return $ret;
-}
-
-sub create_view_rss {
-    my ( $journalinfo, $u, $opts, $cleanitems ) = @_;
-
-    my $ret = _init_talkview( $journalinfo, $u, $opts, 'rss' );
-
-    my %posteru = ();  # map posterids to u objects
-    LJ::load_userids_multiple([map { $_->{'posterid'}, \$posteru{$_->{'posterid'}} } @$cleanitems], [$u]);
-
-    # output individual item blocks
-
-    foreach my $it (@$cleanitems)
-    {
-        my $itemid = $it->{itemid};
-        my $ditemid = $it->{ditemid};
-        my $poster = $posteru{$it->{posterid}};
-
-        $ret .= "<item>\n";
-        $ret .= "  <guid isPermaLink='true'>$journalinfo->{link}$ditemid.html</guid>\n";
-        $ret .= "  <pubDate>" . LJ::time_to_http($it->{createtime}) . "</pubDate>\n";
-        $ret .= "  <title>" . LJ::exml($it->{subject}) . "</title>\n" if $it->{subject};
-        $ret .= "  <author>" . LJ::exml($journalinfo->{email}) . "</author>" if $journalinfo->{email};
-        $ret .= "  <link>$journalinfo->{link}$ditemid.html</link>\n";
-        # omit the description tag if we're only syndicating titles
-        #   note: the $event was also emptied earlier, in make_feed
-        unless ($u->{'opt_synlevel'} eq 'title') {
-            $ret .= "  <description>" . LJ::exml($it->{event}) . "</description>\n";
-        }
-        if ($it->{comments}) {
-            $ret .= "  <comments>$journalinfo->{link}$ditemid.html</comments>\n";
-        }
-        $ret .= "  <category>$_</category>\n" foreach map { LJ::exml($_) } @{$it->{tags} || []};
-        # support 'podcasting' enclosures
-        $ret .= LJ::Hooks::run_hook( "pp_rss_enclosure",
-                { userid => $u->{userid}, ppid => $it->{ppid} }) if $it->{ppid};
-        # TODO: add author field with posterid's email address, respect communities
-        $ret .= "  <lj:music>" . LJ::exml($it->{music}) . "</lj:music>\n" if $it->{music};
-        $ret .= "  <lj:mood>" . LJ::exml($it->{mood}) . "</lj:mood>\n" if $it->{mood};
-        $ret .= "  <lj:security>" . LJ::exml($it->{security}) . "</lj:security>\n" if $it->{security};
-        $ret .= "  <lj:poster>" . LJ::exml($poster->user) . "</lj:poster>\n" unless $u->equals( $poster );
-        $ret .= "  <lj:reply-count>$it->{replycount}</lj:reply-count>\n";
-        $ret .= "</item>\n";
-    }
-
-    $ret .= "</channel>\n";
-    $ret .= "</rss>\n";
-
-    return $ret;
-}
-
-
-# the creator for the Atom view
-# keys of $opts:
-# single_entry - only output an <entry>..</entry> block. off by default
-# apilinks - output AtomAPI links for posting a new entry or
-#            getting/editing/deleting an existing one. off by default
-sub create_view_atom
-{
-    my ( $j, $u, $opts, $cleanitems, $entrylist ) = @_;
-    my ( $feed, $xml, $ns, $site_ns_prefix );
-
-    $site_ns_prefix = lc $LJ::SITENAMEABBREV;
-    $ns = "http://www.w3.org/2005/Atom";
-
-    # AtomAPI interface path
-    my $api = $opts->{'apilinks'} ? $u->atom_service_document :
-                                    $u->journal_base . "/data/atom";
-
-    my $make_link = sub {
-        my ( $rel, $type, $href, $title ) = @_;
-        my $link = XML::Atom::Link->new( Version => 1 );
-        $link->rel($rel);
-        $link->type($type) if $type;
-        $link->href($href);
-        $link->title( $title ) if $title;
-        return $link;
-    };
-
-    my $author = XML::Atom::Person->new( Version => 1 );
-    my $journalu = $j->{u};
-    $author->email( $journalu->email_for_feeds ) if $journalu && $journalu->email_for_feeds;
-    $author->name(  $u->{'name'} );
-
-    # feed information
-    unless ($opts->{'single_entry'}) {
-        $feed = XML::Atom::Feed->new( Version => 1 );
-        $xml  = $feed->elem->ownerDocument;
-
-        if ($u->should_block_robots) {
-            _add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
-            $xml->getDocumentElement->setAttribute( "idx:index", "no" );
-        }
-
-        $xml->insertBefore( $xml->createComment( LJ::Hooks::run_hook("bot_director") ), $xml->documentElement());
-
-        # attributes
-        $feed->id( $u->atomid );
-        $feed->title( $j->{'title'} || $u->{user} );
-        if ( $j->{'subtitle'} ) {
-            $feed->subtitle( $j->{'subtitle'} );
-        }
-
-        $feed->author( $author );
-        $feed->add_link( $make_link->( 'alternate', 'text/html', $j->{'link'} ) );
-        $feed->add_link(
-            $make_link->(
-                'self',
-                $opts->{'apilinks'}
-                ? ( 'application/atom+xml', "$api/entries" )
-                : ( 'text/xml', $api )
-            )
-        );
-        $feed->updated( LJ::time_to_w3c($j->{'modtime'}, 'Z') );
-
-        my $ljinfo = $xml->createElement( "$site_ns_prefix:journal" );
-        $ljinfo->setAttribute( 'username', LJ::exml($u->user) );
-        $ljinfo->setAttribute( 'type', LJ::exml($u->journaltype_readable) );
-        $xml->getDocumentElement->appendChild( $ljinfo );
-
-        if ( LJ::is_enabled( 'hubbub' ) ) {
-            foreach my $hub (@LJ::HUBBUB_HUBS) {
-                $feed->add_link($make_link->('hub', undef, $hub));
-            }
-        }
-    }
-
-    my $posteru = LJ::load_userids( map { $_->{posterid} } @$cleanitems);
-    # output individual item blocks
-    # FIXME: use LJ::Entry->atom_entry?
-    foreach my $it (@$cleanitems)
-    {
-        my $itemid = $it->{itemid};
-        my $ditemid = $it->{ditemid};
-        my $poster = $posteru->{$it->{posterid}};
-
-        my $entry = XML::Atom::Entry->new( Version => 1 );
-        my $entry_xml = $entry->elem->ownerDocument;
-
-        $entry->id( $u->atomid . ":$ditemid" );
-
-        # author isn't required if it is in the main <feed>
-        # only add author if we are in a single entry view, or
-        # the journal entry isn't owned by the journal. (communities)
-        if ( $opts->{single_entry} || ! $journalu->equals( $poster ) ) {
-            my $author = XML::Atom::Person->new( Version => 1 );
-            $author->email( $poster->email_visible ) if $poster && $poster->email_visible;
-            $author->name(  $poster->{name} );
-            $entry->author( $author );
-
-            # and the lj-specific stuff
-            my $postauthor = $entry_xml->createElement( "$site_ns_prefix:poster" );
-            $postauthor->setAttribute( 'user', LJ::exml($poster->user));
-            $entry_xml->getDocumentElement->appendChild( $postauthor );
-        }
-
-        $entry->add_link(
-            $make_link->( 'alternate', 'text/html', "$j->{'link'}$ditemid.html" )
-        );
-        $entry->add_link(
-            $make_link->( 'self', 'text/xml', "$api/?itemid=$ditemid" )
-        );
-
-        $entry->add_link(
-            $make_link->(
-                'edit',      'application/atom+xml',
-                "$api/entries/$itemid", 'Edit this post'
-            )
-          ) if $opts->{'apilinks'};
-
-        my ($year, $mon, $mday, $hour, $min, $sec) = split(/ /, $it->{eventtime});
-        my $event_date = sprintf("%04d-%02d-%02dT%02d:%02d:%02d",
-                                 $year, $mon, $mday, $hour, $min, $sec);
-
-
-        # title can't be blank and can't be absent, so we have to fake some subject
-        $entry->title( $it->{'subject'} ||
-                       "$journalu->{user} \@ $event_date"
-                       );
-
-
-        $entry->published( LJ::time_to_w3c($it->{createtime}, "Z") );
-        $entry->updated(   LJ::time_to_w3c($it->{modtime},    "Z") );
-
-        foreach my $tag ( @{$it->{tags} || []} ) {
-            my $category = XML::Atom::Category->new( Version => 1 );
-            $category->term( $tag );
-            $entry->add_category( $category );
-        }
-
-        my @currents = ( [ 'music'       => $it->{music}      ],
-                         [ 'mood'        => $it->{mood}       ],
-                         [ 'security'    => $it->{security}   ],
-                         [ 'reply-count' => $it->{replycount} ],
-                       );
-
-        foreach ( @currents ) {
-            my ( $key, $val ) = @$_;
-            if ( defined $val ) {
-                my $elem = $entry_xml->createElement( "$site_ns_prefix:$key" );
-                $elem->appendTextNode( $val );
-                $entry_xml->getDocumentElement->appendChild( $elem );
-            }
-        }
-
-        # if syndicating the complete entry
-        #   -print a content tag
-        # elsif syndicating summaries
-        #   -print a summary tag
-         # else (code omitted), we're syndicating title only
-        #   -print neither (the title has already been printed)
-        #   note: the $event was also emptied earlier, in make_feed
-        #
-        # a lack of a content element is allowed,  as long
-        # as we maintain a proper 'alternate' link (above)
-        my $make_content = sub {
-            my $content = $entry_xml->createElement( $_[0] );
-            $content->setAttribute( 'type', 'html' );
-            $content->setNamespace( $ns );
-            $content->appendTextNode( $it->{'event'} );
-            $entry_xml->getDocumentElement->appendChild( $content );
-        };
-        if ( $u->{'opt_synlevel'} eq 'full' || $u->{'opt_synlevel'} eq 'cut' ) {
-            # Do this manually for now, until XML::Atom supports new
-            # content type classifications.
-            $make_content->('content');
-        } elsif ($u->{'opt_synlevel'} eq 'summary') {
-            $make_content->('summary');
-        }
-
-        if ( $opts->{'single_entry'} ) {
-            _add_feed_namespace( $entry, $site_ns_prefix, $LJ::SITEROOT );
-            return $entry->as_xml;
-        }
-        else {
-            $feed->add_entry( $entry );
-        }
-    }
-
-    _add_feed_namespace( $feed, $site_ns_prefix, $LJ::SITEROOT );
-    return $feed->as_xml;
-}
-
-# create a FOAF page for a user
-sub create_view_foaf {
-    my ($journalinfo, $u, $opts) = @_;
-    my $comm = $u->is_community;
-
-    my $ret;
-
-    # return nothing if we're not a user
-    unless ( $u->is_person || $comm ) {
-        $opts->{handler_return} = 404;
-        return undef;
-    }
-
-    # set our content type
-    $opts->{contenttype} = 'application/rdf+xml; charset=' . $opts->{saycharset};
-
-    # setup userprops we will need
-    $u->preload_props( qw{
-        aolim icq yahoo jabber msn icbm url urlname external_foaf_url country city journaltitle
-    } );
-
-    # create bare foaf document, for now
-    $ret = "<?xml version='1.0'?>\n";
-    $ret .= LJ::Hooks::run_hook("bot_director", "<!-- ", " -->");
-    $ret .= "<rdf:RDF\n";
-    $ret .= "   xml:lang=\"en\"\n";
-    $ret .= "   xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n";
-    $ret .= "   xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\"\n";
-    $ret .= "   xmlns:foaf=\"http://xmlns.com/foaf/0.1/\"\n";
-    $ret .= "   xmlns:ya=\"http://blogs.yandex.ru/schema/foaf/\"\n";
-    $ret .= "   xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"\n";
-    $ret .= "   xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
-
-    # precompute some values
-    my $digest = "";
-    my $remote = LJ::get_remote();
-    if ($u->is_validated) {
-        my $email_visible = $u->email_visible($remote);
-        $digest = Digest::SHA1::sha1_hex("mailto:$email_visible") if $email_visible;
-    }
-
-    # channel attributes
-    $ret .= ($comm ? "  <foaf:Group>\n" : "  <foaf:Person>\n");
-    $ret .= "    <foaf:nick>$u->{user}</foaf:nick>\n";
-    $ret .= "    <foaf:name>". LJ::exml($u->{name}) ."</foaf:name>\n";
-    $ret .= "    <foaf:openid rdf:resource=\"" . $u->journal_base . "/\" />\n"
-        unless $comm;
-
-    # user location
-    if ($u->{'country'}) {
-        my $ecountry = LJ::eurl($u->{'country'});
-        $ret .= "    <ya:country dc:title=\"$ecountry\" rdf:resource=\"$LJ::SITEROOT/directorysearch?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry\"/>\n" if $u->can_show_location($remote);
-        if ($u->{'city'}) {
-            my $estate = '';  # FIXME: add state.  Yandex didn't need it.
-            my $ecity = LJ::eurl($u->{'city'});
-            $ret .= "    <ya:city dc:title=\"$ecity\" rdf:resource=\"$LJ::SITEROOT/directorysearch?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry&amp;loc_st=$estate&amp;loc_ci=$ecity\"/>\n" if $u->can_show_location($remote);
-       }
-    }
-
-    if ($u->{bdate} && $u->{bdate} ne "0000-00-00" && !$comm && $u->can_show_full_bday) {
-        $ret .= "    <foaf:dateOfBirth>".$u->bday_string."</foaf:dateOfBirth>\n";
-    }
-    $ret .= "    <foaf:mbox_sha1sum>$digest</foaf:mbox_sha1sum>\n" if $digest;
-
-    # userpic
-    if (my $picid = $u->{'defaultpicid'}) {
-        $ret .= "    <foaf:img rdf:resource=\"$LJ::USERPIC_ROOT/$picid/$u->{userid}\" />\n";
-    }
-
-    $ret .= "    <foaf:page>\n";
-    $ret .= "      <foaf:Document rdf:about=\"" . $u->profile_url . "\">\n";
-    $ret .= "        <dc:title>$LJ::SITENAME Profile</dc:title>\n";
-    $ret .= "        <dc:description>Full $LJ::SITENAME profile, including information such as interests and bio.</dc:description>\n";
-    $ret .= "      </foaf:Document>\n";
-    $ret .= "    </foaf:page>\n";
-
-    # we want to bail out if they have an external foaf file, because
-    # we want them to be able to provide their own information.
-    if ($u->{external_foaf_url}) {
-        $ret .= "    <rdfs:seeAlso rdf:resource=\"" . LJ::eurl($u->{external_foaf_url}) . "\" />\n";
-        $ret .= ($comm ? "  </foaf:Group>\n" : "  </foaf:Person>\n");
-        $ret .= "</rdf:RDF>\n";
-        return $ret;
-    }
-
-    # contact type information
-    my %types = (
-        aolim => 'aimChatID',
-        icq => 'icqChatID',
-        yahoo => 'yahooChatID',
-        msn => 'msnChatID',
-        jabber => 'jabberID',
-    );
-    if ($u->{allow_contactshow} eq 'Y') {
-        foreach my $type (keys %types) {
-            next unless defined $u->{$type};
-            $ret .= "    <foaf:$types{$type}>" . LJ::exml($u->{$type}) . "</foaf:$types{$type}>\n";
-        }
-    }
-
-    # blog activity
-    {
-        my $count = $u->number_of_posts;
-        $ret .= "    <ya:blogActivity>\n";
-        $ret .= "      <ya:Posts>\n";
-        $ret .= "        <ya:feed rdf:resource=\"" . $u->journal_base ."/data/rss\" dc:type=\"application/rss+xml\" />\n";
-        $ret .= "        <ya:posted>$count</ya:posted>\n";
-        $ret .= "      </ya:Posts>\n";
-        $ret .= "    </ya:blogActivity>\n";
-    }
-
-    # include a user's journal page and web site info
-    $ret .= "    <foaf:weblog rdf:resource=\"" . $u->journal_base . "/\"/>\n";
-    if ($u->{url}) {
-        $ret .= "    <foaf:homepage rdf:resource=\"" . LJ::eurl($u->{url});
-        $ret .= "\" dc:title=\"" . LJ::exml($u->{urlname}) . "\" />\n";
-    }
-
-    # user bio
-    if ( $u->has_bio ) {
-        my $bio = $u->bio;
-        LJ::text_out( \$bio );
-        LJ::CleanHTML::clean_userbio( \$bio );
-        $ret .= "    <ya:bio>" . LJ::exml( $bio ) . "</ya:bio>\n";
-    }
-
-    # icbm/location info
-    if ($u->{icbm}) {
-        my @loc = split(",", $u->{icbm});
-        $ret .= "    <foaf:based_near><geo:Point geo:lat='" . $loc[0] . "'" .
-                " geo:long='" . $loc[1] . "' /></foaf:based_near>\n";
-    }
-
-    # interests, please!
-    # arrayref of interests rows: [ intid, intname, intcount ]
-    my $intu = $u->get_interests();
-    foreach my $int (@$intu) {
-        LJ::text_out(\$int->[1]); # 1==interest
-        $ret .= "    <foaf:interest dc:title=\"". LJ::exml($int->[1]) . "\" " .
-                "rdf:resource=\"$LJ::SITEROOT/interests?int=" . LJ::eurl($int->[1]) . "\" />\n";
-    }
-
-    # check if the user has a "FOAF-knows" group
-    my $has_foaf_group = $u->trust_groups( name => 'FOAF-knows' ) ? 1 : 0;
-
-    # now information on who you know, limited to a certain maximum number of users
-    my @ids;
-    if ( $has_foaf_group ) {
-        @ids = keys %{ $u->trust_group_members( name => 'FOAF-knows' ) };
-    } else {
-        @ids = $u->trusted_userids;
-    }
-
-    @ids = splice(@ids, 0, $LJ::MAX_FOAF_FRIENDS) if @ids > $LJ::MAX_FOAF_FRIENDS;
-
-    # now load
-    my $users = LJ::load_userids( @ids );
-
-    # iterate to create data structure
-    foreach my $trustid ( @ids ) {
-        next if $trustid == $u->id;
-        my $fu = $users->{$trustid};
-        next if $fu->is_inactive || ! $fu->is_person;
-
-        my $name = LJ::exml($fu->name_raw);
-        my $tagline = LJ::exml($fu->prop('journaltitle') || '');
-        my $upicurl = $fu->userpic ? $fu->userpic->url : '';
-
-        $ret .= $comm ? "    <foaf:member>\n" : "    <foaf:knows>\n";
-        $ret .= "      <foaf:Person>\n";
-        $ret .= "        <foaf:nick>$fu->{'user'}</foaf:nick>\n";
-        $ret .= "        <foaf:member_name>$name</foaf:member_name>\n";
-        $ret .= "        <foaf:tagLine>$tagline</foaf:tagLine>\n";
-        $ret .= "        <foaf:image>$upicurl</foaf:image>\n" if $upicurl;
-        $ret .= "        <rdfs:seeAlso rdf:resource=\"" . $fu->journal_base ."/data/foaf\" />\n";
-        $ret .= "        <foaf:weblog rdf:resource=\"" . $fu->journal_base . "/\"/>\n";
-        $ret .= "      </foaf:Person>\n";
-        $ret .= $comm ? "    </foaf:member>\n" : "    </foaf:knows>\n";
-    }
-
-    # finish off the document
-    $ret .= $comm ? "    </foaf:Group>\n" : "  </foaf:Person>\n";
-    $ret .= "</rdf:RDF>\n";
-
-    return $ret;
-}
-
-# YADIS capability discovery
-sub create_view_yadis {
-    my ($journalinfo, $u, $opts) = @_;
-    my $person = $u->is_person;
-
-    my $ret = "";
-
-    my $println = sub { $ret .= $_[0]."\n"; };
-
-    $println->('<?xml version="1.0" encoding="UTF-8"?>');
-    $println->('<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"><XRD>');
-
-    local $1;
-    $opts->{pathextra} =~ m!^(/.*)?$!;
-    my $viewchunk = $1;
-
-    my $view;
-    if ($viewchunk eq '') {
-        $view = "recent";
-    }
-    elsif ($viewchunk eq '/read') {
-        $view = "read";
-    }
-    else {
-        $view = '';
-    }
-
-    if ($view eq 'recent') {
-        # Only people (not communities, etc) can be OpenID authenticated
-        if ($person && LJ::OpenID->server_enabled) {
-            $println->('    <Service>');
-            $println->('        <Type>http://openid.net/signon/1.0</Type>');
-            $println->('        <URI>'.LJ::ehtml($LJ::OPENID_SERVER).'</URI>');
-            $println->('    </Service>');
-        }
-    }
-
-    # Local site-specific content
-    # TODO: Give these hooks access to $view somehow?
-    LJ::Hooks::run_hook("yadis_service_descriptors", \$ret);
-
-    $println->('</XRD></xrds:XRDS>');
-    return $ret;
-}
-
-# create a userpic page for a user
-sub create_view_userpics {
-    my ($journalinfo, $u, $opts) = @_;
-    my ( $feed, $xml, $ns );
-
-    $ns = "http://www.w3.org/2005/Atom";
-
-    my $make_link = sub {
-        my ( $rel, $type, $href, $title ) = @_;
-        my $link = XML::Atom::Link->new( Version => 1 );
-        $link->rel($rel);
-        $link->type($type);
-        $link->href($href);
-        $link->title( $title ) if $title;
-        return $link;
-    };
-
-    my $author = XML::Atom::Person->new( Version => 1 );
-    $author->name(  $u->{name} );
-
-    $feed = XML::Atom::Feed->new( Version => 1 );
-    $xml  = $feed->elem->ownerDocument;
-
-    if ($u->should_block_robots) {
-        _add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
-        $xml->getDocumentElement->setAttribute( "idx:index", "no" );
-    }
-
-    my $bot = LJ::Hooks::run_hook("bot_director");
-    $xml->insertBefore( $xml->createComment( $bot ), $xml->documentElement())
-        if $bot;
-
-    $feed->id( $u->atomid . ":userpics" );
-    $feed->title( "$u->{user}'s userpics" );
-
-    $feed->author( $author );
-    $feed->add_link( $make_link->( 'alternate', 'text/html', $u->allpics_base ) );
-    $feed->add_link( $make_link->( 'self', 'text/xml', $u->journal_base() . "/data/userpics" ) );
-
-    # now start building all the userpic data
-    # start up by loading all of our userpic information and creating that part of the feed
-    my $info = $u->get_userpic_info( { load_comments => 1, load_urls => 1, load_descriptions => 1 } );
-
-    my %keywords = ();
-    while (my ($kw, $pic) = each %{$info->{kw}}) {
-        LJ::text_out(\$kw);
-        push @{$keywords{$pic->{picid}}}, LJ::exml($kw);
-    }
-
-    my %comments = ();
-    while (my ($pic, $comment) = each %{$info->{comment}}) {
-        LJ::text_out(\$comment);
-        $comments{$pic} = LJ::strip_html($comment);
-    }
-
-    my %descriptions = ();
-    while ( my( $pic, $description ) = each %{$info->{description}} ) {
-        LJ::text_out(\$description);
-        $descriptions{$pic} = LJ::strip_html($description);
-    }
-
-    my @pics = map { $info->{pic}->{$_} } sort { $a <=> $b }
-               grep { $info->{pic}->{$_}->{state} eq 'N' }
-               keys %{ $info->{pic} };
-
-    # FIXME: It sucks that there are two different methods for aggregating
-    #        the information for a user's set of icons, one of which doesn't
-    #        include keywords and the other of which doesn't include pictime.
-    #        But hey, at least they both use caching.
-
-    my %pictimes = map { $_->picid => $_->pictime }
-                       LJ::Userpic->load_user_userpics( $u );
-
-    my $latest = 0;
-    foreach my $pictime ( values %pictimes ) {
-        $latest = ($latest < $pictime) ? $pictime : $latest;
-    }
-
-    $feed->updated( LJ::time_to_w3c($latest, 'Z') );
-
-    foreach my $pic (@pics) {
-        my $entry = XML::Atom::Entry->new( Version => 1 );
-        my $entry_xml = $entry->elem->ownerDocument;
-
-        $entry->id( $u->atomid . ":userpics:$pic->{picid}" );
-
-        my $title = ($pic->{picid} == $u->{defaultpicid}) ? "default userpic" : "userpic";
-        $entry->title( $title );
-
-        $entry->updated( LJ::time_to_w3c( $pictimes{ $pic->{picid} }, 'Z') );
-
-        my $content;
-        $content = $entry_xml->createElement( "content" );
-        $content->setAttribute( 'src', "$LJ::USERPIC_ROOT/$pic->{picid}/$u->{userid}" );
-        $content->setNamespace( $ns );
-        $entry_xml->getDocumentElement->appendChild( $content );
-
-        foreach my $kw (@{$keywords{$pic->{picid}}}) {
-            my $category = $entry_xml->createElement( 'category' );
-            $category->setAttribute( 'term', $kw );
-            $category->setNamespace( $ns );
-            $entry_xml->getDocumentElement->appendChild( $category );
-        }
-
-        if ( $descriptions{$pic->{picid}} ) {
-            my $content = $entry_xml->createElement( 'title' );
-            $content->setNamespace( $ns );
-            $content->appendTextNode( $descriptions{$pic->{picid}} );
-            $entry_xml->getDocumentElement->appendChild( $content );
-        };
-
-        if($comments{$pic->{picid}}) {
-            my $content = $entry_xml->createElement( "summary" );
-            $content->setNamespace( $ns );
-            $content->appendTextNode( $comments{$pic->{picid}} );
-            $entry_xml->getDocumentElement->appendChild( $content );
-        };
-
-        $feed->add_entry( $entry );
-    }
-
-    return $feed->as_xml;
-}
-
-
-sub create_view_comments {
-    my ( $journalinfo, $u, $opts ) = @_;
-
-    unless ( LJ::is_enabled('latest_comments_rss', $u) ) {
-        $opts->{handler_return} = 404;
-        return 404;
-    }
-
-    unless ( $u->can_use_latest_comments_rss ) {
-        $opts->{handler_return} = 403;
-        return;
-    }
-
-    my $ret = _init_talkview( $journalinfo, $u, $opts, 'comments' );
-
-    my @comments = $u->get_recent_talkitems(25);
-    foreach my $r (@comments)
-    {
-        my $c = LJ::Comment->new($u, jtalkid => $r->{jtalkid});
-        my $thread_url = $c->thread_url;
-        my $subject = $c->subject_raw;
-        LJ::CleanHTML::clean_subject_all(\$subject);
-
-        $ret .= "<item>\n";
-        $ret .= "  <guid isPermaLink='true'>$thread_url</guid>\n";
-        $ret .= "  <pubDate>" . LJ::time_to_http($r->{datepostunix}) . "</pubDate>\n";
-        $ret .= "  <title>" . LJ::exml($subject) . "</title>\n" if $subject;
-        $ret .= "  <link>$thread_url</link>\n";
-        # omit the description tag if we're only syndicating titles
-        unless ($u->{'opt_synlevel'} eq 'title') {
-            my $body = $c->body_raw;
-            LJ::CleanHTML::clean_subject_all(\$body);
-            $ret .= "  <description>" . LJ::exml($body) . "</description>\n";
-        }
-        $ret .= "</item>\n";
-    }
-
-    $ret .= "</channel>\n";
-    $ret .= "</rss>\n";
-
-
-    return $ret;
-}
-
-sub generate_hubbub_jobs {
-    my ( $u, $joblist ) = @_;
-
-    return unless LJ::is_enabled( 'hubbub' );
-
-    foreach my $hub ( @LJ::HUBBUB_HUBS ) {
-        my $make_hubbub_job = sub {
-            my $type = shift;
-
-            my $topic_url = $u->journal_base . "/data/$type";
-            return TheSchwartz::Job->new(
-                funcname => 'TheSchwartz::Worker::PubSubHubbubPublish',
-                arg => {
-                    hub => $hub,
-                    topic_url => $topic_url,
-                },
-                coalesce => $hub,
-            );
-        };
-
-        push @$joblist, $make_hubbub_job->("rss");
-        push @$joblist, $make_hubbub_job->("atom");
-    }
-}
-
-
-1;
diff -r 9457acb16e2d -r 7181c14fbe5a cgi-bin/ljprotocol.pl
--- a/cgi-bin/ljprotocol.pl	Mon Oct 24 19:56:39 2011 +0800
+++ b/cgi-bin/ljprotocol.pl	Tue Oct 25 17:14:09 2011 +0800
@@ -30,10 +30,8 @@
 
 LJ::Config->load;
 
-use lib "$LJ::HOME/cgi-bin";
-
 use LJ::Tags;
-require "ljfeed.pl";
+use LJ::Feed;
 use LJ::EmbedModule;
 
 #### New interface (meta handler) ... other handlers should call into this.
diff -r 9457acb16e2d -r 7181c14fbe5a cgi-bin/modperl_subs.pl
--- a/cgi-bin/modperl_subs.pl	Mon Oct 24 19:56:39 2011 +0800
+++ b/cgi-bin/modperl_subs.pl	Tue Oct 25 17:14:09 2011 +0800
@@ -86,7 +86,7 @@
 use LJ::Support;
 use LJ::CleanHTML;
 use LJ::Talk;
-require "ljfeed.pl";
+use LJ::Feed;
 use LJ::Memories;
 use LJ::Sendmail;
 use LJ::Sysban;
diff -r 9457acb16e2d -r 7181c14fbe5a t/feed-atom.t
--- a/t/feed-atom.t	Mon Oct 24 19:56:39 2011 +0800
+++ b/t/feed-atom.t	Tue Oct 25 17:14:09 2011 +0800
@@ -7,7 +7,7 @@
 require 'ljlib.pl';
 use LJ::Test qw( temp_user );
 
-require 'ljfeed.pl';
+use LJ::Feed;
 use LJ::ParseFeed;
 
 my $u = temp_user();
--------------------------------------------------------------------------------

Post a comment in response:

This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org