[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
kareila.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=1726
Move ljfeed.pl to LJ/Feed.pm
Patch by
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&s_loc=1&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&s_loc=1&loc_cn=$ecountry&loc_st=$estate&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&s_loc=1&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&s_loc=1&loc_cn=$ecountry&loc_st=$estate&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();
--------------------------------------------------------------------------------
