5 no warnings
'uninitialized';
11 use XML
::Atom
::Person
;
15 'rss' => { handler
=> \
&create_view_rss
, need_items
=> 1 },
16 'atom' => { handler
=> \
&create_view_atom
, need_items
=> 1 },
17 'foaf' => { handler
=> \
&create_view_foaf
, },
18 'yadis' => { handler
=> \
&create_view_yadis
, },
19 'userpics' => { handler
=> \
&create_view_userpics
, },
20 'comments' => { handler
=> \
&create_view_comments
, },
21 'rss_friends' => { handler
=> \
&create_view_rss
, need_items
=> 1, paid_only
=> 1 },
22 'atom_friends' => { handler
=> \
&create_view_atom
, need_items
=> 1, paid_only
=> 1 },
26 my ($u, $remote, $opts) = @_;
28 $opts->{pathextra
} =~ s!^/(\w+)!!;
30 my $viewfunc = $feedtypes{$feedtype};
32 # /ya/rss -> special rss for yandex
33 if ($feedtype eq 'ya') {
34 $opts->{pathextra
} =~ s!^/(\w+)!!; # cut off '/rss' part
36 $viewfunc = $feedtypes{$feedtype} if $feedtype eq 'rss';
37 $opts->{include_statistics
} = 1;
40 my $remote_ip = LJ
::get_remote_ip
();
42 foreach my $block ( @LJ::YANDEX_RSS_IP_BLOCKS
) {
43 my $net = Net
::Netmask
->new($block);
45 next unless $net->match($remote_ip);
51 $opts->{'handler_return'} = 403;
56 unless ( $viewfunc ) {
57 $opts->{'handler_return'} = 404;
61 LJ
::Request
->notes('codepath' => "feed.$feedtype") if LJ
::Request
->is_inited;
63 my $dbr = LJ
::get_db_reader
();
65 my $user = $u->{'user'};
67 LJ
::load_user_props
($u, qw
/journaltitle journalsubtitle opt_synlevel/);
69 LJ
::text_out
(\
$u->{$_})
70 foreach qw
/name url urlname/;
72 # opt_synlevel will default to 'full'
73 $u->{'opt_synlevel'} = 'full'
74 unless $u->{'opt_synlevel'} =~ /^(?:full|summary|title)$/;
76 # some data used throughout the channel
79 link => LJ
::journal_base
($u) . "/",
80 title
=> $u->{journaltitle
} || $u->{name
} || $u->{user
},
81 subtitle
=> $u->{journalsubtitle
} || $u->{name
},
82 builddate
=> LJ
::TimeUtil
->time_to_http(time()),
85 # if we do not want items for this view, just call out
86 $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
88 LJ
::run_hooks
('make_feed', $feedtype, $u, { remote
=> $remote });
90 return $viewfunc->{handler
}->($journalinfo, $u, $opts)
91 unless $viewfunc->{need_items
};
93 # for syndicated accounts, redirect to the syndication URL
94 # However, we only want to do this if the data we're returning
95 # is similar. (Not FOAF, for example)
96 if ( $u->{'journaltype'} eq 'Y' ) {
97 my $synurl = $dbr->selectrow_array("SELECT synurl FROM syndicated WHERE userid=$u->{'userid'}");
98 return 'No syndication URL available.' unless $synurl;
100 $opts->{'redir'} = $synurl;
104 my %FORM = LJ
::Request
->args;
107 my (@itemids, @objs);
109 # for consistency, we call ditemids "itemid" in user-facing settings
110 my $ditemid = $FORM{itemid
} + 0;
113 my $entry = LJ
::Entry
->new($u, ditemid
=> $ditemid);
115 if ( ! $entry || ! $entry->valid || ! $entry->visible_to($remote) ) {
116 $opts->{'handler_return'} = 404;
120 if (LJ
::Entry
::Repost
->substitute_content($entry,
121 { 'original_post_obj' => \
$entry,} )) {
123 if ( ! $entry || ! $entry->valid || ! $entry->visible_to($remote) ) {
124 $opts->{'handler_return'} = 404;
131 elsif ($viewfunc->{'paid_only'} && $u->get_cap('paid')) {
132 @objs = map { $_->{'entry'} } @
{ LJ
::Journal
::FriendsFeed
->get_items(
133 'remoteid' => $remote ?
$remote->userid : 0,
134 'userid' => $u->{'userid'},
138 # Warning: array @itemids is not filling here, so entries will be outputted without tags.
140 $journalinfo->{title
} .= ' ' . LJ
::Lang
::ml
('feeds.title.friends');
141 $journalinfo->{link} .= 'friends/';
144 LJ
::get_recent_items
({
146 'userid' => $u->{'userid'},
148 'order' => 'logtime',
149 'tagids' => $opts->{tagids
},
150 'tagmode' => $opts->{tagmode
},
151 'itemids' => \
@itemids,
152 'friendsview' => 1, # this returns rlogtimes
153 'dateformat' => 'S2', # S2 format time format is easier
154 'entry_objects' => \
@objs,
160 $opts->{'contenttype'} = 'text/xml; charset=' . $opts->{'saycharset'};
162 # set last-modified header, then let apache figure out
163 # whether we actually need to send the feed.
166 for my $obj (@objs) {
167 # revtime of the item.
168 my $revtime = $obj->prop('revtime');
169 $lastmod = $revtime if $revtime > $lastmod;
172 # use the logtime of the item.
173 my $itime = $LJ::EndOfTime
- $obj->{rlogtime
};
174 $lastmod = $itime if $itime > $lastmod;
178 LJ
::Request
->set_last_modified($lastmod) if $lastmod;
180 # use this $lastmod as the feed's last-modified time
181 # we would've liked to use something like
182 # LJ::get_timeupdate_multi instead, but that only changes
183 # with new updates and doesn't change on edits.
184 $journalinfo->{'modtime'} = $lastmod;
186 # regarding $r->set_etag:
187 # http://perl.apache.org/docs/general/correct_headers/correct_headers.html#Entity_Tags
188 # It is strongly recommended that you do not use this method unless you
189 # know what you are doing. set_etag() is expecting to be used in
190 # conjunction with a static request for a file on disk that has been
191 # stat()ed in the course of the current request. It is inappropriate and
192 # "dangerous" to use it for dynamic content.
193 if ((my $status = LJ
::Request
->meets_conditions) != LJ
::Request
::OK
()) {
194 $opts->{handler_return
} = $status;
198 $journalinfo->{email
} = $u->email_for_feeds if $u && $u->email_for_feeds;
200 # load tags now that we have no chance of jumping out early
201 my $logtags = LJ
::Tags
::get_logtags
($u, \
@itemids);
206 foreach my $entry_obj (@objs) {
207 next ENTRY
if $entry_obj->poster->{'statusvis'} eq 'S';
208 next ENTRY
if $entry_obj && $entry_obj->is_suspended_for($remote);
210 next ENTRY
if $entry_obj->original_post;
212 my $ditemid = $entry_obj->{ditemid
};
213 if ( $LJ::UNICODE
&& $entry_obj->prop('unknown8bit') ) {
216 \
$entry_obj->{'subject'},
217 \
$entry_obj->{'event'},
222 # see if we have a subject and clean it
223 my $subject = $entry_obj->{'subject'};
226 $subject =~ s/[\r\n]/ /g;
227 LJ
::CleanHTML
::clean_subject_all
(\
$subject);
230 # an HTML link to the entry. used if we truncate or summarize
231 my $readmore = "<b>(<a href=\"$journalinfo->{link}$ditemid.html\">Read more ...</a>)</b>";
233 # empty string so we don't waste time cleaning an entry that won't be used
234 my $event = $u->{'opt_synlevel'} eq 'title' ?
'' : $entry_obj->event_raw;
236 # clean the event, if non-empty
240 # users without 'full_rss' get their logtext bodies truncated
241 # do this now so that the html cleaner will hopefully fix html we break
242 unless (LJ
::get_cap
($u, 'full_rss')) {
243 my $trunc = LJ
::text_trim
($event, 0, 80);
244 $event = "$trunc $readmore" if $trunc ne $event;
247 LJ
::CleanHTML
::clean_event
(\
$event,
250 'preformatted' => $entry_obj->prop('opt_preformatted'),
251 'journalid' => $u->userid,
252 'posterid' => $entry_obj->{'posterid'},
253 'entry_url' => $entry_obj->url,
258 # do this after clean so we don't have to about know whether or not
259 # the event is preformatted
260 if ( $u->{'opt_synlevel'} eq 'summary' ) {
261 # assume the first paragraph is terminated by two <br> or a </p>
262 # valid XML tags should be handled, even though it makes an uglier regex
265 (?
=<) ## followed by "<" (zero-width positive look-ahead assertion)
266 ## and then either </p> or 2 BRs,
267 ## where BR is one of: <br></br>, <br> or <br/>
268 ( (?
:<br\s
*/?\>(?:</br\s
*>)?\s
*){2} | (?
:</p\s
*>) ) !six
)
270 # everything before the matched tag + the tag itself
271 # + a link to read more
272 $event = $1 . $2 . $readmore;
276 while ( $event =~ /<lj-poll-(\d+)>/g ) {
279 my $name = LJ
::Poll
->new($pollid)->name;
281 LJ
::Poll
->clean_poll(\
$name);
287 $event =~ s!<lj-poll-$pollid>!<div><a href="$LJ::SITEROOT/poll/?id=$pollid">View Poll: $name</a></div>!g;
290 my %args = LJ
::Request
->args;
291 LJ
::EmbedModule
->expand_entry($u, \
$event, expand_full
=> 1)
292 if %args && $args{'unfold_embed'};
295 if $event =~ m!<lj-phonepost journalid=[\'\"]\d+[\'\"] dpid=[\'\"](\d+)[\'\"]( /)?>!;
300 if ( $entry_obj->prop('current_mood') ) {
301 $mood = $entry_obj->prop('current_mood');
303 elsif ( $entry_obj->prop('current_moodid') ) {
304 $mood = LJ
::mood_name
($entry_obj->prop('current_moodid') + 0);
307 my $alldateparts = $entry_obj->{'eventtime'};
308 $alldateparts =~ s/[-:]/ /g;
310 my $createtime = $LJ::EndOfTime
- $entry_obj->{rlogtime
};
313 itemid
=> $entry_obj->jitemid,
317 createtime
=> $createtime,
318 eventtime
=> $alldateparts,
319 modtime
=> $entry_obj->prop('revtime') || $createtime,
320 comments
=> $entry_obj->comments_shown,
321 music
=> $entry_obj->prop('current_music'),
324 tags
=> [ values %{$logtags->{$entry_obj->jitemid} || {}} ],
325 security
=> $entry_obj->security,
326 posterid
=> $entry_obj->poster->id,
327 replycount
=> $entry_obj->prop('replycount'),
328 posteruser
=> $entry_obj->poster->user,
332 # fix up the build date to use entry-time
333 $journalinfo->{'builddate'} = LJ
::TimeUtil
->time_to_http($LJ::EndOfTime
- $objs[0]->{'rlogtime'}),
335 return $viewfunc->{handler
}->($journalinfo, $u, $opts, \
@cleanitems, \
@objs);
338 # the creator for the RSS XML syndication view
339 sub create_view_rss
{
340 my ($journalinfo, $u, $opts, $cleanitems, $objs) = @_;
343 # For Yandex ( http://blogs.yandex.ru/faq.xml?id=542563 )
344 # if 'copyright' tag contains 'noindex', this rss will not be indexed.
345 my $copyright = $u->should_block_robots ?
'NOINDEX' : '';
348 $ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
349 $ret .= LJ
::run_hook
("bot_director", "<!-- ", " -->") . "\n";
350 $ret .= "<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/' " .
351 "xmlns:media='http://search.yahoo.com/mrss/' " .
352 "xmlns:atom10='http://www.w3.org/2005/Atom'>\n";
355 $ret .= "<channel>\n";
356 $ret .= " <title>" . LJ
::exml
($journalinfo->{title
}) . "</title>\n";
357 $ret .= " <link>$journalinfo->{link}</link>\n";
358 $ret .= " <description>" . LJ
::exml
("$journalinfo->{title} - $LJ::SITENAME") . "</description>\n";
359 $ret .= " <managingEditor>" . LJ
::exml
($journalinfo->{email
}) . "</managingEditor>\n" if $journalinfo->{email
};
360 $ret .= " <lastBuildDate>$journalinfo->{builddate}</lastBuildDate>\n";
361 $ret .= " <generator>LiveJournal / $LJ::SITENAME</generator>\n";
362 $ret .= " <lj:journal>" . $u->user . "</lj:journal>\n";
363 $ret .= " <lj:journalid>" . $u->userid . "</lj:journalid>\n";
364 $ret .= " <lj:journaltype>" . $u->journaltype_readable . "</lj:journaltype>\n";
365 $ret .= " <copyright>" . $copyright . "</copyright>\n" if $copyright;
366 # TODO: add 'language' field when user.lang has more useful information
368 unless ($LJ::DISABLED
{'hubbub_discovery'}) {
369 foreach my $hub (@LJ::HUBBUB_HUBS
) {
370 $ret .= " <atom10:link rel='hub' href='" . LJ
::exml
($hub) . "' />\n";
374 ### image block, returns info for their current userpic
375 if ($u->{'defaultpicid'}) {
377 LJ
::load_userpics
($pic, [ $u, $u->{'defaultpicid'} ]);
378 $pic = $pic->{$u->{'defaultpicid'}}; # flatten
380 $ret .= " <image>\n";
381 $ret .= " <url>$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}</url>\n";
382 $ret .= " <title>" . LJ
::exml
($journalinfo->{title
}) . "</title>\n";
383 $ret .= " <link>$journalinfo->{link}</link>\n";
384 $ret .= " <width>$pic->{'width'}</width>\n";
385 $ret .= " <height>$pic->{'height'}</height>\n";
386 $ret .= " </image>\n\n";
389 # output individual item blocks
390 foreach my $it (@
$cleanitems) {
391 my $entry = $it->{entry
};
392 my $itemid = $it->{itemid
};
393 my $ditemid = $it->{ditemid
};
394 my $url = $entry->url;
397 $ret .= " <guid isPermaLink='true'>$url</guid>\n";
398 $ret .= " <pubDate>" . LJ
::TimeUtil
->time_to_http($it->{createtime
}) . "</pubDate>\n";
399 $ret .= " <title>" . LJ
::exml
($it->{subject
}) . "</title>\n" if $it->{subject
};
400 $ret .= " <author>" . LJ
::exml
($journalinfo->{email
}) . "</author>" if $journalinfo->{email
};
401 $ret .= " <link>$url</link>\n";
402 # omit the description tag if we're only syndicating titles
403 # note: the $event was also emptied earlier, in make_feed
404 unless ($u->{'opt_synlevel'} eq 'title') {
405 $ret .= " <description>" . LJ
::exml
($it->{event
}) . "</description>\n";
407 if ($it->{comments
}) {
408 $ret .= " <comments>" . $entry->url . "</comments>\n";
410 $ret .= " <category>$_</category>\n" foreach map { LJ
::exml
($_) } @
{$it->{tags
} || []};
411 # support 'podcasting' enclosures
412 $ret .= LJ
::run_hook
( "pp_rss_enclosure",
413 { userid
=> $u->{userid
}, ppid
=> $it->{ppid
} }) if $it->{ppid
};
414 # TODO: add author field with posterid's email address, respect communities
415 $ret .= " <lj:music>" . LJ
::exml
($it->{music
}) . "</lj:music>\n" if $it->{music
};
416 $ret .= " <media:title type=\"plain\">" . LJ
::exml
($it->{music
}) . "</media:title>\n" if $it->{music
};
417 $ret .= " <lj:mood>" . LJ
::exml
($it->{mood
}) . "</lj:mood>\n" if $it->{mood
};
418 $ret .= " <lj:security>" . LJ
::exml
($it->{security
}) . "</lj:security>\n" if $it->{security
};
419 unless ($u->{'userid'} == $it->{'posterid'}) {
420 $ret .= " <lj:poster>" . LJ
::exml
($it->{'posteruser'}) . "</lj:poster>\n";
421 $ret .= " <lj:posterid>" . $it->{'posterid'} . "</lj:posterid>\n";
423 $ret .= " <lj:reply-count>$it->{replycount}</lj:reply-count>\n";
425 if ($opts->{include_statistics
}) {
427 my $now = DateTime
->now(time_zone
=> 'Europe/Moscow');
428 my $yesterday = $now->clone->subtract(days
=> 1);
431 my $data_v = LJ
::PersonalStats
::DB
->fetch('post_stats', {
432 type
=> 0, # post views only in journal
433 date
=> $now->strftime("%Y-%m"),
434 journal_id
=> $u->userid,
437 # hits, today and yesterday
438 my $data_t = LJ
::PersonalStats
::DB
->fetch('post_stats', {
439 type
=> 0, # post views only in journal
440 date
=> $now->strftime("%Y-%m-%d"),
441 journal_id
=> $u->userid,
444 my $data_y = LJ
::PersonalStats
::DB
->fetch('post_stats', {
445 type
=> 0, # post views only in journal
446 date
=> $yesterday->strftime("%Y-%m-%d"),
447 journal_id
=> $u->userid,
451 # sum last 24 hours: 00 to current hour and current hour + 1 to 23 in yesterday
455 foreach my $el (@
$data_t) {
456 $today_hits[ $el->{time_id
} ] = $el->{hits
};
459 for (my $i = 0; $i <= $now->hour; $i++) {
460 $sum += $today_hits[$i];
463 if ($now->hour < 23) {
465 foreach my $el (@
$data_y) {
466 $yesterday_hits[ $el->{time_id
} ] = $el->{hits
};
469 for (my $i = $now->hour + 1; $i <= 23; $i++) {
470 $sum += $yesterday_hits[$i];
474 $ret .= "<pageviews>$sum</pageviews>\n";
477 foreach my $el (@
$data_v) {
478 $day_visitors[ $el->{time_id
} ] = $el->{visitors
};
480 my $today_visitors = $day_visitors[$now->day] || 0;
482 $ret .= "<visitors>$today_visitors</visitors>\n";
488 $ret .= "</channel>\n";
495 # the creator for the Atom view
497 # single_entry - only output an <entry>..</entry> block. off by default
498 # apilinks - output AtomAPI links for posting a new entry or
499 # getting/editing/deleting an existing one. off by default
500 # TODO: define and use an 'lj:' namespace
502 # TODO: Remove lines marked with 'COMPAT' - they are only present
503 # to allow backwards compatibility with atom parsers that are pre 0.6-draft.
504 # We create tags valid for 1.1-draft, but we want to be nice during
505 # atom's (and atom users) continuing transition. 1.0 parsers, according
506 # to spec, should NOT barf on unknown tags.
507 # * Where we can't be compatible, we use Atom 1.0. *
508 # http://www.ietf.org/internet-drafts/draft-ietf-atompub-format-11.txt
512 my ( $j, $u, $opts, $cleanitems, $entrylist ) = @_;
513 my ( $feed, $xml, $ns );
515 $ns = "http://www.w3.org/2005/Atom";
517 # Strip namespace from child tags. Set default namespace, let
518 # child tags inherit from it. So ghetto that we even have to do this
519 # and LibXML can't on its own.
520 my $normalize_ns = sub {
522 $str =~ s/(<\w+)\s+xmlns="\Q$ns\E"/$1/og;
523 $str =~ s/<feed\b/<feed xmlns="$ns" xmlns:lj="$LJ::SITEROOT"/;
524 $str =~ s/<entry>/<entry xmlns="$ns" xmlns:lj="$LJ::SITEROOT">/ if $opts->{'single_entry'};
528 # AtomAPI interface path
529 my $api = $opts->{'apilinks'} ?
"$LJ::SITEROOT/interface/atom" :
530 $u->journal_base . "/data/atom";
532 my $make_link = sub {
533 my ( $rel, $type, $href, $title ) = @_;
534 my $link = XML
::Atom
::Link
->new( Version
=> 1 );
536 $link->type($type) if $type;
538 $link->title( $title ) if $title;
542 my $author = XML
::Atom
::Person
->new( Version
=> 1 );
543 my $journalu = $j->{u
};
544 $author->email( $journalu->email_for_feeds ) if $journalu && $journalu->email_for_feeds;
545 $author->name( $u->{'name'} );
548 unless ($opts->{'single_entry'}) {
549 $feed = XML
::Atom
::Feed
->new( Version
=> 1 );
553 die "Error: XML-LibXML is required"; ## sudo yum install perl-XML-LibXML
556 if ($u->should_block_robots) {
557 $xml->getDocumentElement->setAttribute( "xmlns:idx", "urn:atom-extension:indexing" );
558 $xml->getDocumentElement->setAttribute( "idx:index", "no" );
561 $xml->insertBefore( $xml->createComment( LJ
::run_hook
("bot_director") ), $xml->documentElement());
564 $feed->id( "urn:lj:$LJ::DOMAIN:atom1:$u->{user}" );
565 $feed->title( $j->{'title'} || $u->{user
} );
566 if ( $j->{'subtitle'} ) {
567 $feed->subtitle( $j->{'subtitle'} );
570 $feed->author( $author );
571 $feed->add_link( $make_link->( 'alternate', 'text/html', $j->{'link'} ) );
576 ?
( 'application/x.atom+xml', "$api/feed" )
577 : ( 'text/xml', $api )
580 $feed->updated( LJ
::TimeUtil
->time_to_w3c($j->{'modtime'}, 'Z') );
582 my $ljinfo = $xml->createElement( 'lj:journal' );
583 $ljinfo->setAttribute( 'userid', $u->userid );
584 $ljinfo->setAttribute( 'username', LJ
::exml
($u->user) );
585 $ljinfo->setAttribute( 'type', LJ
::exml
($u->journaltype_readable) );
586 $xml->getDocumentElement->appendChild( $ljinfo );
588 # link to the AtomAPI version of this feed
592 'application/x.atom+xml',
593 ( $opts->{'apilinks'} ?
"$api/feed" : $api ),
601 'application/x.atom+xml',
605 ) if $opts->{'apilinks'};
607 unless ($LJ::DISABLED
{'hubbub_discovery'}) {
608 foreach my $hub (@LJ::HUBBUB_HUBS
) {
609 $feed->add_link($make_link->('hub', undef, $hub));
614 my $posteru = LJ
::load_userids
( map { $_->{posterid
} } @
$cleanitems);
616 # output individual item blocks
617 foreach my $it ( @
$cleanitems ) {
618 my $obj = $it->{entry
};
619 my $itemid = $it->{itemid
};
620 my $ditemid = $it->{ditemid
};
621 my $poster = $posteru->{$it->{posterid
}};
623 my $entry = XML
::Atom
::Entry
->new( Version
=> 1 );
624 my $entry_xml = $entry->{doc
};
626 $entry->id("urn:lj:$LJ::DOMAIN:atom1:$u->{user}:$ditemid");
628 # author isn't required if it is in the main <feed>
629 # only add author if we are in a single entry view, or
630 # the journal entry isn't owned by the journal owner. (communities)
631 if ( $opts->{'single_entry'} || $journalu->email_raw ne $poster->email_raw ) {
632 my $author = XML
::Atom
::Person
->new( Version
=> 1 );
633 $author->email( $poster->email_visible ) if $poster->email_visible;
634 $author->name( $poster->{name
} );
635 $entry->author( $author );
637 # and the lj-specific stuff
638 my $postauthor = $entry_xml->createElement( 'lj:poster' );
639 $postauthor->setAttribute( 'user', LJ
::exml
($poster->user));
640 $postauthor->setAttribute( 'userid', $poster->userid);
641 $entry_xml->getDocumentElement->appendChild( $postauthor );
645 $make_link->( 'alternate', 'text/html', $obj->url ) #"$j->{'link'}$ditemid.html" )
648 $make_link->( 'self', 'text/xml', "$api/?itemid=$ditemid" )
653 'service.edit', 'application/x.atom+xml',
654 "$api/edit/$itemid", 'Edit this post'
656 ) if $opts->{'apilinks'};
658 # NOTE: Atom 0.3 allowed for "issued", where we put the time the
659 # user says it was. There's no equivalent in later versions of
660 # Atom, though. And Atom 0.3 is deprecated. Oh well.
662 my ($year, $mon, $mday, $hour, $min, $sec) = split(/ /, $it->{eventtime
});
663 my $event_date = sprintf("%04d-%02d-%02dT%02d:%02d:%02d",
664 $year, $mon, $mday, $hour, $min, $sec);
667 # title can't be blank and can't be absent, so we have to fake some subject
668 $entry->title( $it->{'subject'} ||
669 "$journalu->{user} \@ $event_date"
673 $entry->published( LJ
::TimeUtil
->time_to_w3c($it->{createtime
}, "Z") );
674 $entry->updated( LJ
::TimeUtil
->time_to_w3c($it->{modtime
}, "Z") );
676 # XML::Atom 0.13 doesn't support categories. Maybe later?
677 foreach my $tag ( @
{$it->{tags
} || []} ) {
678 $tag = LJ
::exml
( $tag );
679 my $category = $entry_xml->createElement( 'category' );
680 $category->setAttribute( 'term', $tag );
681 $category->setNamespace( $ns );
682 $entry_xml->getDocumentElement->appendChild( $category );
685 if ($it->{'music'}) {
686 my $music = $entry_xml->createElement( 'lj:music' );
687 $music->appendTextNode( $it->{'music'} );
688 $entry_xml->getDocumentElement->appendChild( $music );
691 # if syndicating the complete entry
692 # -print a content tag
693 # elsif syndicating summaries
694 # -print a summary tag
695 # else (code omitted), we're syndicating title only
696 # -print neither (the title has already been printed)
697 # note: the $event was also emptied earlier, in make_feed
699 # a lack of a content element is allowed, as long
700 # as we maintain a proper 'alternate' link (above)
701 my $make_content = sub {
702 my $content = $entry_xml->createElement( $_[0] );
703 $content->setAttribute( 'type', 'html' );
704 $content->setNamespace( $ns );
705 $content->appendTextNode( $it->{'event'} );
706 $entry_xml->getDocumentElement->appendChild( $content );
708 if ($u->{'opt_synlevel'} eq 'full') {
709 # Do this manually for now, until XML::Atom supports new
710 # content type classifications.
711 $make_content->('content');
712 } elsif ($u->{'opt_synlevel'} eq 'summary') {
713 $make_content->('summary');
716 if ( $opts->{'single_entry'} ) {
717 return $normalize_ns->( $entry->as_xml() );
720 $feed->add_entry( $entry );
724 return $normalize_ns->( $feed->as_xml() );
727 # create a FOAF page for a user
728 sub create_view_foaf
{
729 my ($journalinfo, $u, $opts) = @_;
730 my $comm = ($u->{journaltype
} eq 'C');
734 # return nothing if we're not a user
735 unless ($u->{journaltype
} eq 'P' || $comm) {
736 $opts->{handler_return
} = 404;
740 # set our content type
741 $opts->{contenttype
} = 'application/rdf+xml; charset=' . $opts->{saycharset
};
743 # setup userprops we will need
744 LJ
::load_user_props
($u, qw{
745 aolim icq yahoo jabber msn icbm url urlname external_foaf_url country city journaltitle
748 # create bare foaf document, for now
749 $ret = "<?xml version='1.0'?>\n";
750 $ret .= LJ
::run_hook
("bot_director", "<!-- ", " -->");
751 $ret .= "<rdf:RDF\n";
752 $ret .= " xml:lang=\"en\"\n";
753 $ret .= " xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n";
754 $ret .= " xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\"\n";
755 $ret .= " xmlns:foaf=\"http://xmlns.com/foaf/0.1/\"\n";
756 $ret .= " xmlns:ya=\"http://blogs.yandex.ru/schema/foaf/\"\n";
757 $ret .= " xmlns:lj=\"http://www.livejournal.org/rss/lj/1.0/\"\n";
758 $ret .= " xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"\n";
759 $ret .= " xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
761 # precompute some values
763 if ($u->is_validated) {
764 my $remote = LJ
::get_remote
();
765 my $email_visible = $u->email_visible($remote);
766 $digest = Digest
::SHA1
::sha1_hex
("mailto:$email_visible") if $email_visible;
770 $ret .= ($comm ?
" <foaf:Group>\n" : " <foaf:Person>\n");
771 $ret .= " <foaf:nick>$u->{user}</foaf:nick>\n";
772 $ret .= " <foaf:name>". LJ
::exml
($u->{name
}) ."</foaf:name>\n";
773 $ret .= " <lj:journaltitle>". LJ
::exml
($u->{journaltitle
}) ."</lj:journaltitle>\n" if $u->{journaltitle
};
774 $ret .= " <lj:journalsubtitle>". LJ
::exml
($u->{journalsubtitle
}) ."</lj:journalsubtitle>\n" if $u->{journalsubtitle
};
775 $ret .= " <foaf:openid rdf:resource=\"" . $u->journal_base . "/\" />\n" unless $comm;
778 if ($u->{'country'}) {
779 my $ecountry = LJ
::eurl
($u->{'country'});
780 $ret .= " <ya:country dc:title=\"$ecountry\" rdf:resource=\"$LJ::SITEROOT/directory.bml?opt_sort=ut&s_loc=1&loc_cn=$ecountry\"/>\n";
782 my $estate = ''; # FIXME: add state. Yandex didn't need it.
783 my $ecity = LJ
::eurl
($u->{'city'});
784 $ret .= " <ya:city dc:title=\"$ecity\" rdf:resource=\"$LJ::SITEROOT/directory.bml?opt_sort=ut&s_loc=1&loc_cn=$ecountry&loc_st=$estate&loc_ci=$ecity\"/>\n";
788 if ($u->{bdate
} && $u->{bdate
} ne "0000-00-00" && !$comm && $u->can_show_full_bday) {
789 $ret .= " <foaf:dateOfBirth>".$u->bday_string."</foaf:dateOfBirth>\n";
791 $ret .= " <foaf:mbox_sha1sum>$digest</foaf:mbox_sha1sum>\n" if $digest;
794 if (my $picid = $u->{'defaultpicid'}) {
795 $ret .= " <foaf:img rdf:resource=\"$LJ::USERPIC_ROOT/$picid/$u->{userid}\" />\n";
798 $ret .= " <foaf:page>\n";
799 $ret .= " <foaf:Document rdf:about=\"" . $u->profile_url . "\">\n";
800 $ret .= " <dc:title>$LJ::SITENAME Profile</dc:title>\n";
801 $ret .= " <dc:description>Full $LJ::SITENAME profile, including information such as interests and bio.</dc:description>\n";
802 $ret .= " </foaf:Document>\n";
803 $ret .= " </foaf:page>\n";
805 # we want to bail out if they have an external foaf file, because
806 # we want them to be able to provide their own information.
807 if ($u->{external_foaf_url
}) {
808 $ret .= " <rdfs:seeAlso rdf:resource=\"" . LJ
::eurl
($u->{external_foaf_url
}) . "\" />\n";
809 $ret .= ($comm ?
" </foaf:Group>\n" : " </foaf:Person>\n");
810 $ret .= "</rdf:RDF>\n";
814 # contact type information
816 aolim
=> 'aimChatID',
818 yahoo
=> 'yahooChatID',
820 jabber
=> 'jabberID',
822 if ($u->{allow_contactshow
} eq 'Y') {
823 foreach my $type (keys %types) {
824 next unless defined $u->{$type};
825 $ret .= " <foaf:$types{$type}>" . LJ
::exml
($u->{$type}) . "</foaf:$types{$type}>\n";
832 my $dbcr = LJ
::get_cluster_reader
($u);
833 my $num_comments_received = $u->num_comments_received( dbh
=> $dbcr ) || 0;
834 my $num_comments_posted = $u->num_comments_posted( dbh
=> $dbcr ) || 0;
836 my $count = $u->number_of_posts;
837 $ret .= " <ya:blogActivity>\n";
838 $ret .= " <ya:Posts>\n";
839 $ret .= " <ya:feed rdf:resource=\"" . LJ
::journal_base
($u) ."/data/rss\" dc:type=\"application/rss+xml\" />\n";
840 $ret .= " <ya:posted>$count</ya:posted>\n";
841 $ret .= " </ya:Posts>\n";
842 $ret .= " <ya:Comments>\n";
843 ##### we are don't have rss feed for user's comments
844 #### $ret .= " <ya:feed rdf:resource=\"recent comments rss\" dc:type=\"application/rss+xml\"/>\n";
846 $ret .= " <ya:posted>$num_comments_posted</ya:posted>\n";
847 $ret .= " <ya:received>$num_comments_received</ya:received>\n";
848 $ret .= " </ya:Comments>\n";
849 $ret .= " </ya:blogActivity>\n";
852 # include a user's journal page and web site info
853 my $time_create = ($u->timecreate) ? LJ
::TimeUtil
->time_to_w3c($u->timecreate) : '';
854 my $time_update = ($u->timeupdate) ? LJ
::TimeUtil
->time_to_w3c($u->timeupdate) : '';
855 $ret .= " <foaf:weblog rdf:resource='" . LJ
::journal_base
($u) . "/'";
856 $ret .= " lj:dateCreated='$time_create'" if $time_create;
857 $ret .= " lj:dateLastUpdated='$time_update'" if $time_update;
860 $ret .= " <foaf:homepage rdf:resource=\"" . LJ
::eurl
($u->{url
});
861 $ret .= "\" dc:title=\"" . LJ
::exml
($u->{urlname
}) . "\" />\n";
865 if ($u->{'has_bio'} eq "Y") {
866 $u->{'bio'} = LJ
::get_bio
($u);
867 LJ
::text_out
(\
$u->{'bio'});
868 LJ
::CleanHTML
::clean_userbio
(\
$u->{'bio'});
869 $ret .= " <ya:bio>" . LJ
::exml
($u->{'bio'}) . "</ya:bio>\n";
873 if ($u->{'journaltype'} ne 'Y' &&
874 !$LJ::DISABLED
{'schools'} &&
875 ($u->{'opt_showschools'} eq '' || $u->{'opt_showschools'} eq 'Y')) {
877 my $schools = LJ
::Schools
::get_attended
($u);
878 if ($u->{'journaltype'} ne 'C' && $schools && %$schools ) {
880 foreach my $sid (sort { $schools->{$a}->{year_start
} <=> $schools->{$b}->{year_start
} } keys %$schools) {
881 my $link = "$LJ::SITEROOT/schools/" .
882 "?ctc=" . LJ
::eurl
($schools->{$sid}->{country
}) .
883 "&sc=" . LJ
::eurl
($schools->{$sid}->{state}) .
884 "&cc=" . LJ
::eurl
($schools->{$sid}->{city
}) .
886 my $ename = LJ
::ehtml
($schools->{$sid}->{name
});
887 $ret .= " <ya:school\n";
888 $ret .= " rdf:resource=\"" . LJ
::exml
($link) . "\"\n";
889 if (defined $schools->{$sid}->{year_start
}) {
890 $ret .= " ya:dateStart=\"$schools->{$sid}->{year_start}\"\n";
892 if (defined $schools->{$sid}->{year_end
}) {
893 $ret .= " ya:dateFinish=\"$schools->{$sid}->{year_end}\"\n";
896 $ret .= " dc:title=\"$ename\"/>\n";
903 my @loc = split(",", $u->{icbm
});
904 $ret .= " <foaf:based_near><geo:Point geo:lat='" . $loc[0] . "'" .
905 " geo:long='" . $loc[1] . "' /></foaf:based_near>\n";
909 # arrayref of interests rows: [ intid, intname, intcount ]
910 my $intu = LJ
::get_interests
($u);
911 foreach my $int (@
$intu) {
912 LJ
::text_out
(\
$int->[1]); # 1==interest
913 $ret .= " <foaf:interest dc:title=\"". LJ
::exml
($int->[1]) . "\" " .
914 "rdf:resource=\"$LJ::SITEROOT/interests.bml?int=" . LJ
::eurl
($int->[1]) . "\" />\n";
917 # check if the user has a "FOAF-knows" group
918 my $groups = LJ
::get_friend_group
($u->{userid
}, { name
=> 'FOAF-knows' });
919 my $mask = $groups ?
1 << $groups->{groupnum
} : 0;
921 # now information on who you know, limited to a certain maximum number of users
922 my $friends = LJ
::get_friends
($u->{userid
}, $mask);
923 my @ids = keys %$friends;
924 @ids = splice(@ids, 0, $LJ::MAX_FOAF_FRIENDS
) if @ids > $LJ::MAX_FOAF_FRIENDS
;
928 LJ
::load_userids_multiple
([ map { $_, \
$users{$_} } @ids ], [$u]);
930 # iterate to create data structure
931 foreach my $friendid (@ids) {
932 next if $friendid == $u->{userid
};
933 my $fu = $users{$friendid};
934 next if $fu->{statusvis
} =~ /[DXS]/ || $fu->{journaltype
} ne 'P';
936 my $name = LJ
::exml
($fu->name_raw);
937 my $tagline = LJ
::exml
($fu->prop('journaltitle') || '');
938 my $upicurl = $fu->userpic ?
$fu->userpic->url : '';
940 $ret .= $comm ?
" <foaf:member>\n" : " <foaf:knows>\n";
941 $ret .= " <foaf:Person>\n";
942 $ret .= " <foaf:nick>$fu->{'user'}</foaf:nick>\n";
943 $ret .= " <foaf:member_name>$name</foaf:member_name>\n";
944 $ret .= " <foaf:tagLine>$tagline</foaf:tagLine>\n";
945 $ret .= " <foaf:image>$upicurl</foaf:image>\n" if $upicurl;
946 $ret .= " <rdfs:seeAlso rdf:resource=\"" . LJ
::journal_base
($fu) ."/data/foaf\" />\n";
947 $ret .= " <foaf:weblog rdf:resource=\"" . LJ
::journal_base
($fu) . "/\"/>\n";
948 $ret .= " </foaf:Person>\n";
949 $ret .= $comm ?
" </foaf:member>\n" : " </foaf:knows>\n";
952 # finish off the document
953 $ret .= $comm ?
" </foaf:Group>\n" : " </foaf:Person>\n";
954 $ret .= "</rdf:RDF>\n";
959 # YADIS capability discovery
960 sub create_view_yadis
{
961 my ($journalinfo, $u, $opts) = @_;
962 my $person = ($u->{journaltype
} eq 'P');
966 my $println = sub { $ret .= $_[0]."\n"; };
968 $println->('<?xml version="1.0" encoding="UTF-8"?>');
969 $println->('<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"><XRD>');
972 $opts->{pathextra
} =~ m!^(/.*)?$!;
976 if ($viewchunk eq '') {
979 elsif ($viewchunk eq '/friends') {
986 if ($view eq 'recent') {
987 # Only people (not communities, etc) can be OpenID authenticated
988 if ($person && LJ
::OpenID
->server_enabled) {
989 $println->(' <Service priority="0">');
990 $println->(' <Type>http://specs.openid.net/auth/2.0/signon</Type>');
991 $println->(' <URI>'.LJ
::ehtml
($LJ::OPENID_SERVER
).'</URI>');
992 $println->(' <LocalID>'.LJ
::ehtml
($u->journal_base) . '/' .'</LocalID>');
993 $println->(' </Service>');
996 elsif ($view eq 'friends') {
997 $println->(' <Service xmlns:gm="http://openid.net/xmlns/groupmembership/xrds">');
998 $println->(' <Type>http://openid.net/xmlns/groupmembership</Type>');
999 $println->(' <URI>'.LJ
::exml
($LJ::SITEROOT
).'/openid/groupmembership.bml</URI>');
1000 $println->(' <LocalID>'.LJ
::exml
($u->journal_base.'/friends').'</LocalID>');
1001 $println->(' <gm:CanEnumerate /><gm:CanQuery />');
1002 $println->(' </Service>');
1005 # Local site-specific content
1006 # TODO: Give these hooks access to $view somehow?
1007 LJ
::run_hook
("yadis_service_descriptors", \
$ret);
1009 $println->('</XRD></xrds:XRDS>');
1013 # create a userpic page for a user
1014 sub create_view_userpics
{
1015 my ($journalinfo, $u, $opts) = @_;
1016 my ( $feed, $xml, $ns );
1018 $ns = "http://www.w3.org/2005/Atom";
1020 my $normalize_ns = sub {
1022 $str =~ s/(<\w+)\s+xmlns="\Q$ns\E"/$1/og;
1023 $str =~ s/<feed\b/<feed xmlns="$ns"/;
1027 my $make_link = sub {
1028 my ( $rel, $type, $href, $title ) = @_;
1029 my $link = XML
::Atom
::Link
->new( Version
=> 1 );
1033 $link->title( $title ) if $title;
1037 my $author = XML
::Atom
::Person
->new( Version
=> 1 );
1038 $author->name( $u->{name
} );
1040 $feed = XML
::Atom
::Feed
->new( Version
=> 1 );
1041 $xml = $feed->{doc
};
1043 if ($u->should_block_robots) {
1044 $xml->getDocumentElement->setAttribute( "xmlns:idx", "urn:atom-extension:indexing" );
1045 $xml->getDocumentElement->setAttribute( "idx:index", "no" );
1048 my $bot = LJ
::run_hook
("bot_director");
1049 $xml->insertBefore( $xml->createComment( $bot ), $xml->documentElement())
1052 $feed->id( "urn:lj:$LJ::DOMAIN:atom1:$u->{user}:userpics" );
1053 $feed->title( "$u->{user}'s userpics" );
1055 $feed->author( $author );
1056 $feed->add_link( $make_link->( 'alternate', 'text/html', "$LJ::SITEROOT/allpics.bml?user=$u->{user}" ) );
1057 $feed->add_link( $make_link->( 'self', 'text/xml', $u->journal_base() . "/data/userpics" ) );
1059 # now start building all the userpic data
1060 # start up by loading all of our userpic information and creating that part of the feed
1061 my $info = LJ
::get_userpic_info
($u, {'load_comments' => 1, 'load_urls' => 1});
1064 while (my ($kw, $pic) = each %{$info->{kw
}}) {
1066 push @
{$keywords{$pic->{picid
}}}, LJ
::ehtml
($kw);
1070 while (my ($pic, $comment) = each %{$info->{comment
}}) {
1071 LJ
::text_out
(\
$comment);
1072 $comments{$pic} = LJ
::ehtml
($comment);
1076 push @pics, map { $info->{pic
}->{$_} } sort { $a <=> $b }
1077 grep { $info->{pic
}->{$_}->{state} eq 'N' } keys %{$info->{pic
}};
1082 # this is lame, but we have to do this iteration twice; we load the userpic data first, so that
1083 # we can figure out what the most recently-uploaded userpic is. we need to put that into the feed
1084 # before any of the <entry> values.
1087 foreach my $pic (@pics) {
1088 LJ
::load_userpics
(\
%picdata, [$u, $pic->{picid
}] );
1089 $latest = ($latest < $picdata{$pic->{picid
}}->{picdate
}) ?
$picdata{$pic->{picid
}}->{picdate
} : $latest;
1092 $feed->updated( LJ
::TimeUtil
->time_to_w3c($latest, 'Z') );
1094 foreach my $pic (@pics) {
1095 my $entry = XML
::Atom
::Entry
->new( Version
=> 1 );
1096 my $entry_xml = $entry->{doc
};
1098 $entry->id("urn:lj:$LJ::DOMAIN:atom1:$u->{user}:userpics:$pic->{picid}");
1100 my $title = ($pic->{picid
} == $u->{defaultpicid
}) ?
"default userpic" : "userpic";
1101 $entry->title( $title );
1103 $entry->updated( LJ
::TimeUtil
->time_to_w3c($picdata{$pic->{picid
}}->{picdate
}, 'Z') );
1106 $content = $entry_xml->createElement( "content" );
1107 $content->setAttribute( 'src', "$LJ::USERPIC_ROOT/$pic->{picid}/$u->{userid}" );
1108 $content->setNamespace( $ns );
1109 $entry_xml->getDocumentElement->appendChild( $content );
1111 foreach my $kw (@
{$keywords{$pic->{picid
}}}) {
1112 my $ekw = LJ
::exml
( $kw );
1113 my $category = $entry_xml->createElement( 'category' );
1114 $category->setAttribute( 'term', $ekw );
1115 $category->setNamespace( $ns );
1116 $entry_xml->getDocumentElement->appendChild( $category );
1119 if($comments{$pic->{picid
}}) {
1120 my $content = $entry_xml->createElement( "summary" );
1121 $content->setNamespace( $ns );
1122 $content->appendTextNode( $comments{$pic->{picid
}} );
1123 $entry_xml->getDocumentElement->appendChild( $content );
1126 $feed->add_entry( $entry );
1129 return $normalize_ns->( $feed->as_xml() );
1133 sub create_view_comments
1135 my ($journalinfo, $u, $opts) = @_;
1137 if (LJ
::conf_test
($LJ::DISABLED
{latest_comments_rss
}, $u)) {
1138 $opts->{handler_return
} = 404;
1142 unless ($u->get_cap('latest_comments_rss')) {
1143 $opts->{handler_return
} = 403;
1148 $ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
1149 $ret .= LJ
::run_hook
("bot_director", "<!-- ", " -->") . "\n";
1150 $ret .= "<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/'>\n";
1152 # channel attributes
1153 $ret .= "<channel>\n";
1154 $ret .= " <title>" . LJ
::exml
($journalinfo->{title
}) . "</title>\n";
1155 $ret .= " <link>$journalinfo->{link}</link>\n";
1156 $ret .= " <description>Latest comments in " . LJ
::exml
($journalinfo->{title
}) . "</description>\n";
1157 $ret .= " <managingEditor>" . LJ
::exml
($journalinfo->{email
}) . "</managingEditor>\n" if $journalinfo->{email
};
1158 $ret .= " <lastBuildDate>$journalinfo->{builddate}</lastBuildDate>\n";
1159 $ret .= " <generator>LiveJournal / $LJ::SITENAME</generator>\n";
1160 $ret .= " <lj:journal>" . $u->user . "</lj:journal>\n";
1161 $ret .= " <lj:journaltype>" . $u->journaltype_readable . "</lj:journaltype>\n";
1162 # TODO: add 'language' field when user.lang has more useful information
1164 ### image block, returns info for their current userpic
1165 if ($u->{'defaultpicid'}) {
1167 LJ
::load_userpics
($pic, [ $u, $u->{'defaultpicid'} ]);
1168 $pic = $pic->{$u->{'defaultpicid'}}; # flatten
1170 $ret .= " <image>\n";
1171 $ret .= " <url>$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}</url>\n";
1172 $ret .= " <title>" . LJ
::exml
($journalinfo->{title
}) . "</title>\n";
1173 $ret .= " <link>$journalinfo->{link}</link>\n";
1174 $ret .= " <width>$pic->{'width'}</width>\n";
1175 $ret .= " <height>$pic->{'height'}</height>\n";
1176 $ret .= " </image>\n\n";
1179 my @comments = $u->get_recent_talkitems(25);
1180 foreach my $r (@comments)
1182 my $c = LJ
::Comment
->new($u, jtalkid
=> $r->{jtalkid
});
1183 my $thread_url = $c->thread_url;
1184 my $subject = $c->subject_raw;
1185 LJ
::CleanHTML
::clean_subject_all
(\
$subject);
1188 $ret .= " <guid isPermaLink='true'>$thread_url</guid>\n";
1189 $ret .= " <pubDate>" . LJ
::TimeUtil
->time_to_http($r->{datepostunix
}) . "</pubDate>\n";
1190 $ret .= " <title>" . LJ
::exml
($subject) . "</title>\n" if $subject;
1191 $ret .= " <link>$thread_url</link>\n";
1192 # omit the description tag if we're only syndicating titles
1193 unless ($u->{'opt_synlevel'} eq 'title') {
1194 my $body = $c->body_raw;
1195 LJ
::CleanHTML
::clean_subject_all
(\
$body);
1196 $ret .= " <description>" . LJ
::exml
($body) . "</description>\n";
1198 $ret .= "</item>\n";
1201 $ret .= "</channel>\n";
1208 sub generate_hubbub_jobs
{
1210 my $joblist = shift;
1212 return if $LJ::DISABLED
{'hubbub'};
1214 foreach my $hub (@LJ::HUBBUB_HUBS
) {
1215 my $make_hubbub_job = sub {
1218 my $topic_url = $u->journal_base . "/data/$type";
1219 return TheSchwartz
::Job
->new(
1220 funcname
=> 'TheSchwartz::Worker::PubSubHubbubPublish',
1223 topic_url
=> $topic_url,
1229 push @
$joblist, $make_hubbub_job->("rss");
1230 push @
$joblist, $make_hubbub_job->("atom");