#!/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 },
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;
}
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.
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 ( $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 $entry_url = $entry_obj->url;
my $readmore = qq{(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
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,
{
preformatted => $logprops{$itemid}->{opt_preformatted},
cuturl => $u->{opt_synlevel} eq 'cut' ? $entry_url : "",
to_external_site => 1,
editor => $logprops{$itemid}->{editor},
}
);
# 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 );
}
if ( $u->journaltype eq 'C' && !$opts->{apilinks} ) {
$event =
"Posted by: "
. $posteru{ $it->{posterid} }->ljuser_display
. "
"
. $event;
}
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>!