#!/usr/bin/perl
package LJ::Feed;
use strict;
no warnings 'uninitialized';
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) {
$opts->{'handler_return'} = 404;
return undef;
}
$r->notes('codepath' => "feed.$feedtype") if $r;
my $dbr = LJ::get_db_reader();
my $user = $u->{'user'};
LJ::load_user_props($u, qw/ journaltitle journalsubtitle opt_synlevel /);
LJ::text_out(\$u->{$_})
foreach ("name", "url", "urlname");
# opt_synlevel will default to 'full'
$u->{'opt_synlevel'} = 'full'
unless $u->{'opt_synlevel'} =~ /^(?:full|summary|title)$/;
# some data used throughout the channel
my $journalinfo = {
u => $u,
link => LJ::journal_base($u) . "/",
title => $u->{journaltitle} || $u->{name} || $u->{user},
subtitle => $u->{journalsubtitle} || $u->{name},
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->{'journaltype'} eq 'Y') {
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 = $r->args;
## load the itemids
my (@itemids, @items);
# for consistency, we call ditemids "itemid" in user-facing settings
my $ditemid = $FORM{itemid}+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),
};
} else {
@items = LJ::get_recent_items({
'clusterid' => $u->{'clusterid'},
'clustersource' => 'slave',
'remote' => $remote,
'userid' => $u->{'userid'},
'itemshow' => 25,
'order' => "logtime",
'tagids' => $opts->{tagids},
'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};
$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.
if ((my $status = $r->meets_conditions) != Apache::Constants::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'}}->{'statusvis'} eq 'S';
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 = "({link}$ditemid.html\">Read more ... ) ";
# 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 (LJ::get_cap($u, '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'} });
# 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') {
# assume the first paragraph is terminated by two or a
# valid XML tags should be handled, even though it makes an uglier regex
if ($event =~ m!(( ()?\s*){2})|()!i) {
# everything before the matched tag + the tag itself
# + a link to read more
$event = $` . $& . $readmore;
}
}
while ($event =~ //g) {
my $pollid = $1;
my $name = LJ::Poll->new($pollid)->name;
if ($name) {
LJ::Poll->clean_poll(\$name);
} else {
$name = "#$pollid";
}
$event =~ s!!!g;
}
my %args = $r->args;
LJ::EmbedModule->expand_entry($u, \$event, expand_full => 1)
if %args && $args{'unfold_embed'};
$ppid = $1
if $event =~ m!!;
}
my $mood;
if ($logprops{$itemid}->{'current_mood'}) {
$mood = $logprops{$itemid}->{'current_mood'};
} elsif ($logprops{$itemid}->{'current_moodid'}) {
$mood = LJ::mood_name($logprops{$itemid}->{'current_moodid'}+0);
}
my $createtime = $LJ::EndOfTime - $it->{rlogtime};
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 => ($logprops{$itemid}->{'opt_nocomments'} == 0),
music => $logprops{$itemid}->{'current_music'},
mood => $mood,
ppid => $ppid,
tags => [ values %{$logtags->{$itemid} || {}} ],
security => $it->{security},
posterid => $it->{posterid},
};
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);
}
# the creator for the RSS XML syndication view
sub create_view_rss
{
my ($journalinfo, $u, $opts, $cleanitems) = @_;
my $ret;
# For Yandex ( http://blogs.yandex.ru/faq.xml?id=542563 )
# if 'copyright' tag contains 'noindex', this rss will not be indexed.
my $copyright = $u->should_block_robots ? 'NOINDEX' : '';
# header
$ret .= "{'saycharset'}' ?>\n";
$ret .= LJ::run_hook("bot_director", "") . "\n";
$ret .= "\n";
# channel attributes
$ret .= "\n";
$ret .= " " . LJ::exml($journalinfo->{title}) . " \n";
$ret .= " $journalinfo->{link}\n";
$ret .= " " . LJ::exml("$journalinfo->{title} - $LJ::SITENAME") . " \n";
$ret .= " " . LJ::exml($journalinfo->{email}) . " \n" if $journalinfo->{email};
$ret .= " $journalinfo->{builddate} \n";
$ret .= " LiveJournal / $LJ::SITENAME \n";
$ret .= " " . $u->user . " \n";
$ret .= " " . $u->journaltype_readable . " \n";
$ret .= " " . $copyright . " \n" if $copyright;
# TODO: add 'language' field when user.lang has more useful information
### image block, returns info for their current userpic
if ($u->{'defaultpicid'}) {
my $pic = {};
LJ::load_userpics($pic, [ $u, $u->{'defaultpicid'} ]);
$pic = $pic->{$u->{'defaultpicid'}}; # flatten
$ret .= " \n";
$ret .= " $LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'} \n";
$ret .= " " . LJ::exml($journalinfo->{title}) . " \n";
$ret .= " $journalinfo->{link}\n";
$ret .= " $pic->{'width'} \n";
$ret .= " $pic->{'height'} \n";
$ret .= " \n\n";
}
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 .= "- \n";
$ret .= "
$journalinfo->{link}$ditemid.html \n";
$ret .= " " . LJ::time_to_http($it->{createtime}) . " \n";
$ret .= " " . LJ::exml($it->{subject}) . " \n" if $it->{subject};
$ret .= " " . LJ::exml($journalinfo->{email}) . " " if $journalinfo->{email};
$ret .= " $journalinfo->{link}$ditemid.html\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 .= " " . LJ::exml($it->{event}) . " \n";
}
if ($it->{comments}) {
$ret .= " $journalinfo->{link}$ditemid.html \n";
}
$ret .= " $_ \n" foreach map { LJ::exml($_) } @{$it->{tags} || []};
# support 'podcasting' enclosures
$ret .= LJ::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::exml($it->{music}) . " \n" if $it->{music};
$ret .= " " . LJ::exml($it->{mood}) . " \n" if $it->{mood};
$ret .= " " . LJ::exml($it->{security}) . " \n" if $it->{security};
$ret .= " " . LJ::exml($poster->user) . " \n" unless LJ::u_equals($u, $poster);
$ret .= " \n";
}
$ret .= " \n";
$ret .= " \n";
return $ret;
}
# the creator for the Atom view
# keys of $opts:
# single_entry - only output an .. block. off by default
# apilinks - output AtomAPI links for posting a new entry or
# getting/editing/deleting an existing one. off by default
# TODO: define and use an 'lj:' namespace
#
# TODO: Remove lines marked with 'COMPAT' - they are only present
# to allow backwards compatibility with atom parsers that are pre 0.6-draft.
# We create tags valid for 1.1-draft, but we want to be nice during
# atom's (and atom users) continuing transition. 1.0 parsers, according
# to spec, should NOT barf on unknown tags.
# * Where we can't be compatible, we use Atom 1.0. *
# http://www.ietf.org/internet-drafts/draft-ietf-atompub-format-11.txt
#
sub create_view_atom
{
my ( $j, $u, $opts, $cleanitems, $entrylist ) = @_;
my ( $feed, $xml, $ns );
$ns = "http://www.w3.org/2005/Atom";
# Strip namespace from child tags. Set default namespace, let
# child tags inherit from it. So ghetto that we even have to do this
# and LibXML can't on its own.
my $normalize_ns = sub {
my $str = shift;
$str =~ s/(<\w+)\s+xmlns="\Q$ns\E"/$1/og;
$str =~ s/// if $opts->{'single_entry'};
return $str;
};
# AtomAPI interface path
my $api = $opts->{'apilinks'} ? "$LJ::SITEROOT/interface/atom" :
$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);
$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->{doc};
if ($u->should_block_robots) {
$xml->getDocumentElement->setAttribute( "xmlns:idx", "urn:atom-extension:indexing" );
$xml->getDocumentElement->setAttribute( "idx:index", "no" );
}
$xml->insertBefore( $xml->createComment( LJ::run_hook("bot_director") ), $xml->documentElement());
# attributes
$feed->id( "urn:lj:$LJ::DOMAIN:atom1:$u->{user}" );
$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/x.atom+xml', "$api/feed" )
: ( 'text/xml', $api )
)
);
$feed->updated( LJ::time_to_w3c($j->{'modtime'}, 'Z') );
my $ljinfo = $xml->createElement( 'lj:journal' );
$ljinfo->setAttribute( 'username', LJ::exml($u->user) );
$ljinfo->setAttribute( 'type', LJ::exml($u->journaltype_readable) );
$xml->getDocumentElement->appendChild( $ljinfo );
# link to the AtomAPI version of this feed
$feed->add_link(
$make_link->(
'service.feed',
'application/x.atom+xml',
( $opts->{'apilinks'} ? "$api/feed" : $api ),
$j->{'title'}
)
);
$feed->add_link(
$make_link->(
'service.post',
'application/x.atom+xml',
"$api/post",
'Create a new entry'
)
) if $opts->{'apilinks'};
}
my $posteru = LJ::load_userids( map { $_->{posterid} } @$cleanitems);
# output individual item blocks
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->{doc};
$entry->id("urn:lj:$LJ::DOMAIN:atom1:$u->{user}:$ditemid");
# author isn't required if it is in the main
# only add author if we are in a single entry view, or
# the journal entry isn't owned by the journal owner. (communities)
if ( $opts->{'single_entry'} || $journalu->email_raw ne $poster->email_raw ) {
my $author = XML::Atom::Person->new( Version => 1 );
$author->email( $poster->email_visible ) if $poster->email_visible;
$author->name( $poster->{name} );
$entry->author( $author );
# and the lj-specific stuff
my $postauthor = $entry_xml->createElement( 'lj: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->(
'service.edit', 'application/x.atom+xml',
"$api/edit/$itemid", 'Edit this post'
)
) if $opts->{'apilinks'};
# NOTE: Atom 0.3 allowed for "issued", where we put the time the
# user says it was. There's no equivalent in later versions of
# Atom, though. And Atom 0.3 is deprecated. Oh well.
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") );
# XML::Atom 0.13 doesn't support categories. Maybe later?
foreach my $tag ( @{$it->{tags} || []} ) {
$tag = LJ::exml( $tag );
my $category = $entry_xml->createElement( 'category' );
$category->setAttribute( 'term', $tag );
$category->setNamespace( $ns );
$entry_xml->getDocumentElement->appendChild( $category );
}
if ($it->{'music'}) {
my $music = $entry_xml->createElement( 'lj:music' );
$music->appendTextNode( $it->{'music'} );
$entry_xml->getDocumentElement->appendChild( $music );
}
# 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') {
# 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'} ) {
return $normalize_ns->( $entry->as_xml() );
}
else {
$feed->add_entry( $entry );
}
}
return $normalize_ns->( $feed->as_xml() );
}
# create a FOAF page for a user
sub create_view_foaf {
my ($journalinfo, $u, $opts) = @_;
my $comm = ($u->{journaltype} eq 'C');
my $ret;
# return nothing if we're not a user
unless ($u->{journaltype} eq 'P' || $comm) {
$opts->{handler_return} = 404;
return undef;
}
# set our content type
$opts->{contenttype} = 'application/rdf+xml; charset=' . $opts->{saycharset};
# setup userprops we will need
LJ::load_user_props($u, qw{
aolim icq yahoo jabber msn icbm url urlname external_foaf_url country city journaltitle
});
# create bare foaf document, for now
$ret = "\n";
$ret .= LJ::run_hook("bot_director", "");
$ret .= "\n";
# precompute some values
my $digest = "";
if ($u->is_validated) {
my $remote = LJ::get_remote();
my $email_visible = $u->email_visible($remote);
$digest = Digest::SHA1::sha1_hex("mailto:$email_visible") if $email_visible;
}
# channel attributes
$ret .= ($comm ? " \n" : " \n");
$ret .= " $u->{user} \n";
$ret .= " ". LJ::exml($u->{name}) ." \n";
$ret .= " journal_base . "/\" />\n"
unless $comm;
# user location
if ($u->{'country'}) {
my $ecountry = LJ::eurl($u->{'country'});
$ret .= " \n";
if ($u->{'city'}) {
my $estate = ''; # FIXME: add state. Yandex didn't need it.
my $ecity = LJ::eurl($u->{'city'});
$ret .= " \n";
}
}
if ($u->{bdate} && $u->{bdate} ne "0000-00-00" && !$comm && $u->can_show_full_bday) {
$ret .= " ".$u->bday_string." \n";
}
$ret .= " $digest \n" if $digest;
# userpic
if (my $picid = $u->{'defaultpicid'}) {
$ret .= " {userid}\" />\n";
}
$ret .= " \n";
$ret .= " profile_url . "\">\n";
$ret .= " $LJ::SITENAME Profile \n";
$ret .= " Full $LJ::SITENAME profile, including information such as interests and bio. \n";
$ret .= " \n";
$ret .= " \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 .= " {external_foaf_url}) . "\" />\n";
$ret .= ($comm ? " \n" : " \n");
$ret .= " \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 .= " " . LJ::exml($u->{$type}) . " \n";
}
}
# blog activity
{
my $count = $u->number_of_posts;
$ret .= " \n";
$ret .= " \n";
$ret .= " \n";
$ret .= " $count \n";
$ret .= " \n";
$ret .= " \n";
}
# include a user's journal page and web site info
$ret .= " \n";
if ($u->{url}) {
$ret .= " {url});
$ret .= "\" dc:title=\"" . LJ::exml($u->{urlname}) . "\" />\n";
}
# user bio
if ($u->{'has_bio'} eq "Y") {
$u->{'bio'} = LJ::get_bio($u);
LJ::text_out(\$u->{'bio'});
LJ::CleanHTML::clean_userbio(\$u->{'bio'});
$ret .= " " . LJ::exml($u->{'bio'}) . " \n";
}
# user schools
if ($u->{'journaltype'} ne 'Y' &&
!$LJ::DISABLED{'schools'} &&
($u->{'opt_showschools'} eq '' || $u->{'opt_showschools'} eq 'Y')) {
my $schools = LJ::Schools::get_attended($u);
if ($u->{'journaltype'} ne 'C' && $schools && %$schools ) {
my @links;
foreach my $sid (sort { $schools->{$a}->{year_start} <=> $schools->{$b}->{year_start} } keys %$schools) {
my $link = "$LJ::SITEROOT/schools/" .
"?ctc=" . LJ::eurl($schools->{$sid}->{country}) .
"&sc=" . LJ::eurl($schools->{$sid}->{state}) .
"&cc=" . LJ::eurl($schools->{$sid}->{city}) .
"&sid=" . $sid ;
my $ename = LJ::ehtml($schools->{$sid}->{name});
$ret .= " {$sid}->{year_start}) {
$ret .= " ya:dateStart=\"$schools->{$sid}->{year_start}\"\n";
}
if (defined $schools->{$sid}->{year_end}) {
$ret .= " ya:dateFinish=\"$schools->{$sid}->{year_end}\"\n";
}
$ret .= " dc:title=\"$ename\"/>\n";
}
}
}
# icbm/location info
if ($u->{icbm}) {
my @loc = split(",", $u->{icbm});
$ret .= " \n";
}
# interests, please!
# arrayref of interests rows: [ intid, intname, intcount ]
my $intu = LJ::get_interests($u);
foreach my $int (@$intu) {
LJ::text_out(\$int->[1]); # 1==interest
$ret .= " [1]) . "\" " .
"rdf:resource=\"$LJ::SITEROOT/interests.bml?int=" . LJ::eurl($int->[1]) . "\" />\n";
}
# check if the user has a "FOAF-knows" group
my $groups = LJ::get_friend_group($u->{userid}, { name => 'FOAF-knows' });
my $mask = $groups ? 1 << $groups->{groupnum} : 0;
# now information on who you know, limited to a certain maximum number of users
my $friends = LJ::get_friends($u->{userid}, $mask);
my @ids = keys %$friends;
@ids = splice(@ids, 0, $LJ::MAX_FOAF_FRIENDS) if @ids > $LJ::MAX_FOAF_FRIENDS;
# now load
my %users;
LJ::load_userids_multiple([ map { $_, \$users{$_} } @ids ], [$u]);
# iterate to create data structure
foreach my $friendid (@ids) {
next if $friendid == $u->{userid};
my $fu = $users{$friendid};
next if $fu->{statusvis} =~ /[DXS]/ || $fu->{journaltype} ne 'P';
my $name = LJ::exml($fu->name_raw);
my $tagline = LJ::exml($fu->prop('journaltitle') || '');
my $upicurl = $fu->userpic ? $fu->userpic->url : '';
$ret .= $comm ? " \n" : " \n";
$ret .= " \n";
$ret .= " $fu->{'user'} \n";
$ret .= " $name \n";
$ret .= " $tagline \n";
$ret .= " $upicurl \n" if $upicurl;
$ret .= " \n";
$ret .= " \n";
$ret .= " \n";
$ret .= $comm ? " \n" : " \n";
}
# finish off the document
$ret .= $comm ? " \n" : " \n";
$ret .= "\n";
return $ret;
}
# YADIS capability discovery
sub create_view_yadis {
my ($journalinfo, $u, $opts) = @_;
my $person = ($u->{journaltype} eq 'P');
my $ret = "";
my $println = sub { $ret .= $_[0]."\n"; };
$println->('');
$println->('');
local $1;
$opts->{pathextra} =~ m!^(/.*)?$!;
my $viewchunk = $1;
my $view;
if ($viewchunk eq '') {
$view = "recent";
}
elsif ($viewchunk eq '/friends') {
$view = "friends";
}
else {
$view = undef;
}
if ($view eq 'recent') {
# Only people (not communities, etc) can be OpenID authenticated
if ($person && LJ::OpenID->server_enabled) {
$println->(' ');
$println->(' http://openid.net/signon/1.0 ');
$println->(' '.LJ::ehtml($LJ::OPENID_SERVER).' ');
$println->(' ');
}
}
elsif ($view eq 'friends') {
$println->(' ');
$println->(' http://openid.net/xmlns/groupmembership ');
$println->(' '.LJ::exml($LJ::SITEROOT).'/openid/groupmembership.bml ');
$println->(' '.LJ::exml($u->journal_base.'/friends').' ');
$println->(' ');
$println->(' ');
}
# Local site-specific content
# TODO: Give these hooks access to $view somehow?
LJ::run_hook("yadis_service_descriptors", \$ret);
$println->(' ');
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 $normalize_ns = sub {
my $str = shift;
$str =~ s/(<\w+)\s+xmlns="\Q$ns\E"/$1/og;
$str =~ s/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->{doc};
if ($u->should_block_robots) {
$xml->getDocumentElement->setAttribute( "xmlns:idx", "urn:atom-extension:indexing" );
$xml->getDocumentElement->setAttribute( "idx:index", "no" );
}
my $bot = LJ::run_hook("bot_director");
$xml->insertBefore( $xml->createComment( $bot ), $xml->documentElement())
if $bot;
$feed->id( "urn:lj:$LJ::DOMAIN:atom1:$u->{user}:userpics" );
$feed->title( "$u->{user}'s userpics" );
$feed->author( $author );
$feed->add_link( $make_link->( 'alternate', 'text/html', "$LJ::SITEROOT/allpics.bml?user=$u->{user}" ) );
$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 = LJ::get_userpic_info($u, {'load_comments' => 1, 'load_urls' => 1});
my %keywords = ();
while (my ($kw, $pic) = each %{$info->{kw}}) {
LJ::text_out(\$kw);
push @{$keywords{$pic->{picid}}}, LJ::ehtml($kw);
}
my %comments = ();
while (my ($pic, $comment) = each %{$info->{comment}}) {
LJ::text_out(\$comment);
$comments{$pic} = LJ::ehtml($comment);
}
my @pics;
push @pics, map { $info->{pic}->{$_} } sort { $a <=> $b }
grep { $info->{pic}->{$_}->{state} eq 'N' } keys %{$info->{pic}};
my $entry;
my %picdata;
# this is lame, but we have to do this iteration twice; we load the userpic data first, so that
# we can figure out what the most recently-uploaded userpic is. we need to put that into the feed
# before any of the values.
my $latest = 0;
foreach my $pic (@pics) {
LJ::load_userpics(\%picdata, [$u, $pic->{picid}] );
$latest = ($latest < $picdata{$pic->{picid}}->{picdate}) ? $picdata{$pic->{picid}}->{picdate} : $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->{doc};
$entry->id("urn:lj:$LJ::DOMAIN:atom1:$u->{user}:userpics:$pic->{picid}");
my $title = ($pic->{picid} == $u->{defaultpicid}) ? "default userpic" : "userpic";
$entry->title( $title );
$entry->updated( LJ::time_to_w3c($picdata{$pic->{picid}}->{picdate}, '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 $ekw = LJ::exml( $kw );
my $category = $entry_xml->createElement( 'category' );
$category->setAttribute( 'term', $ekw );
$category->setNamespace( $ns );
$entry_xml->getDocumentElement->appendChild( $category );
}
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 $normalize_ns->( $feed->as_xml() );
}
sub create_view_comments
{
my ($journalinfo, $u, $opts) = @_;
if (LJ::conf_test($LJ::DISABLED{latest_comments_rss}, $u)) {
$opts->{handler_return} = 404;
return 404;
}
unless ($u->get_cap('latest_comments_rss')) {
$opts->{handler_return} = 403;
return;
}
my $ret;
$ret .= "{'saycharset'}' ?>\n";
$ret .= LJ::run_hook("bot_director", "") . "\n";
$ret .= "\n";
# channel attributes
$ret .= "\n";
$ret .= " " . LJ::exml($journalinfo->{title}) . " \n";
$ret .= " $journalinfo->{link}\n";
$ret .= " Latest comments in " . LJ::exml($journalinfo->{title}) . " \n";
$ret .= " " . LJ::exml($journalinfo->{email}) . " \n" if $journalinfo->{email};
$ret .= " $journalinfo->{builddate} \n";
$ret .= " LiveJournal / $LJ::SITENAME \n";
$ret .= " " . $u->user . " \n";
$ret .= " " . $u->journaltype_readable . " \n";
# TODO: add 'language' field when user.lang has more useful information
### image block, returns info for their current userpic
if ($u->{'defaultpicid'}) {
my $pic = {};
LJ::load_userpics($pic, [ $u, $u->{'defaultpicid'} ]);
$pic = $pic->{$u->{'defaultpicid'}}; # flatten
$ret .= " \n";
$ret .= " $LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'} \n";
$ret .= " " . LJ::exml($journalinfo->{title}) . " \n";
$ret .= " $journalinfo->{link}\n";
$ret .= " $pic->{'width'} \n";
$ret .= " $pic->{'height'} \n";
$ret .= " \n\n";
}
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 .= "- \n";
$ret .= "
$thread_url \n";
$ret .= " " . LJ::time_to_http($r->{datepostunix}) . " \n";
$ret .= " " . LJ::exml($subject) . " \n" if $subject;
$ret .= " $thread_url\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 .= " " . LJ::exml($body) . " \n";
}
$ret .= " \n";
}
$ret .= " \n";
$ret .= " \n";
return $ret;
}
1;