this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #17 from websages/rss_fix

Fix: rss feed

authored by

Stephen Yeargin and committed by
GitHub
40d651d5 526890d8

+97 -19
+88 -11
htdocs/lib/tumble.pm
··· 6 6 7 7 use DBI; 8 8 use POSIX qw( strftime ); 9 + use Time::Local qw( timelocal timegm ); 9 10 use Cwd qw( abs_path getcwd ); 10 11 use File::Spec; 11 12 ··· 34 35 $self->{'arg'}->{'dtype'} ||= 'html'; 35 36 36 37 for ( $self->{'arg'}->{'dtype'} ) { 37 - /rss|xml/ && do { $self->header_props( -type => 'text/xml' ); }; 38 + /rss|xml/ && do { $self->header_props( -type => 'text/xml; charset=UTF-8' ); }; 38 39 /html/ && do { $self->header_props( -type => 'text/html; charset=UTF-8' ); }; 39 40 } 40 41 ··· 83 84 $data->{$item}->{'timestamp'} =~ 84 85 /(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):(\d{2})/ 85 86 ) { 87 + # Parse the timestamp: $1=year, $2=month, $3=day, $4=hour, $5=minute, $6=second 88 + my ($year, $month, $day, $hour, $minute, $second) = ($1, $2, $3, $4, $5, $6); 89 + 90 + # Convert to epoch time (month is 0-based in timelocal) 91 + # Note: timelocal interprets time as local time based on server timezone 92 + my $epoch = timelocal($second, $minute, $hour, $day, $month - 1, $year - 1900); 93 + 94 + # Format as RFC 822 date (RSS pubDate format) 95 + # Get localtime components for the timestamp 96 + my @lt = localtime($epoch); 97 + 98 + # Calculate timezone offset by comparing GMT and local time 99 + # timegm returns GMT epoch for given local time components 100 + my $gmt_epoch = timegm(@lt); 101 + my $local_epoch = timelocal(@lt); 102 + my $tz_offset_seconds = $local_epoch - $gmt_epoch; 103 + my $tz_offset_minutes = $tz_offset_seconds / 60; 104 + 105 + # Format timezone offset as +HHMM or -HHMM 106 + my $tz_sign = $tz_offset_minutes >= 0 ? '+' : '-'; 107 + my $tz_hours = abs(int($tz_offset_minutes / 60)); 108 + my $tz_mins = abs(int($tz_offset_minutes % 60)); 109 + my $tz_offset = sprintf("%s%02d%02d", $tz_sign, $tz_hours, $tz_mins); 110 + 86 111 $data->{$item}->{'timestamp'} = 87 - POSIX::strftime( 88 - "%a, %d %b %Y %T -0600", 0, $5, $4, $3, $2 - 1, $1 - 1900 89 - ); 112 + POSIX::strftime("%a, %d %b %Y %H:%M:%S $tz_offset", @lt); 90 113 91 114 if ( ( !$d ) || ( $3 ne $d ) ) { 92 115 $d = $3; ··· 153 176 $data->{$item}->{'url'} . 154 177 qq{" alt="image" />}; 155 178 }; 179 + 180 + /quote/ && do { 181 + # For quote items, build description text 182 + my $quote_text = $data->{$item}->{'quote'} || ''; 183 + my $author_text = $data->{$item}->{'author'} || ''; 184 + # Build the description text - will be escaped in wrap() function 185 + $content = '"' . $quote_text . '" --' . $author_text; 186 + }; 156 187 } 157 188 158 - $c .= $self->wrap( 189 + # For XML/RSS feeds, wrap HTML content in CDATA sections (for ircLink and image items) 190 + my $xml_content = $content; 191 + if ( $self->{'arg'}->{'dtype'} =~ /xml|rss/ && defined $content && $content ne '' && $data->{$item}->{'type'} ne 'quote' ) { 192 + # Wrap HTML content in CDATA for RSS descriptions (quote already has CDATA) 193 + $xml_content = '<![CDATA[' . $content . ']]>'; 194 + } 195 + 196 + my %template_vars = ( 159 197 wrapper => 'tumble_item_' . $data->{$item}->{'type'}, 160 198 author => $data->{$item}->{'user'}, 161 - baseurl => $CONFIG->{'baseurl'}, 162 - content => $content, 163 - 199 + baseurl => $CONFIG->{'baseurl'}, 164 200 %{$data->{$item}} 165 201 ); 202 + 203 + # Add content or description depending on item type 204 + if ( $data->{$item}->{'type'} eq 'quote' ) { 205 + # For quote items, pass description (will be escaped in wrap() function) 206 + $template_vars{'description'} = $content if defined $content; 207 + } elsif ( defined $xml_content ) { 208 + $template_vars{'content'} = $xml_content; 209 + } 210 + 211 + $c .= $self->wrap( %template_vars ); 166 212 } 167 213 168 - $c =~ s/\&/\&amp;/g if $c; 214 + # Escape XML special characters for non-CDATA sections 215 + if ( $self->{'arg'}->{'dtype'} =~ /xml|rss/ && $c ) { 216 + # Only escape if not already in CDATA sections 217 + # Escape & first, then other characters 218 + $c =~ s/&(?!lt;|gt;|amp;|quot;|apos;|#\d+;|#x[0-9a-f]+;)/&amp;/gi; 219 + # Don't escape < and > that are already in CDATA sections 220 + # But we need to escape them outside CDATA and in titles/links 221 + # Since we're wrapping descriptions in CDATA, we only need to escape in titles and links 222 + # This is handled by the XML escaping function below for non-CDATA content 223 + } elsif ( $c ) { 224 + # For HTML output, escape ampersands 225 + $c =~ s/\&/\&amp;/g; 226 + } 169 227 170 228 my ( $nav ); 171 229 ··· 346 404 } 347 405 } 348 406 407 + # Escape XML special characters for text fields in XML/RSS output 408 + my $is_xml = $self->{'arg'}->{'dtype'} =~ /xml|rss/; 409 + 349 410 map { 350 - chomp( $arg->{$_} ) if ref $arg->{$_}; 351 - $template->param( $_ => $arg->{$_} ); 411 + my $value = $arg->{$_}; 412 + chomp( $value ) if ref $value; 413 + 414 + # For XML output, escape text fields (but not content which should be CDATA) 415 + if ( $is_xml && !ref $value && $_ ne 'content' && $_ ne 'container' ) { 416 + # Escape XML special characters for titles, links, descriptions, etc. 417 + # Must escape & first, then < and > 418 + $value =~ s/&/&amp;/g; 419 + $value =~ s/</&lt;/g; 420 + $value =~ s/>/&gt;/g; 421 + # Escape quotes for attribute safety (though we're using in content, not attributes) 422 + # Also escape quotes in description text to be safe 423 + if ( $_ eq 'description' ) { 424 + $value =~ s/"/&quot;/g; 425 + } 426 + } 427 + 428 + $template->param( $_ => $value ); 352 429 } keys %{$arg}; 353 430 354 431 return $template->output();
+5 -4
htdocs/thtml/index.txml
··· 1 - <?xml version="1.0" encoding="iso-8859-1"?> 2 - <rss version="2.0"> 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> 3 3 <channel> 4 4 <title>tumblefish.</title> 5 5 <link>http://<!-- tmpl_var name="baseurl" --></link> 6 + <atom:link href="https://<!-- tmpl_var name="baseurl" -->/index.xml" rel="self" type="application/rss+xml" /> 6 7 <description>tumblefish.</description> 7 8 <language>en-us</language> 8 9 <generator>lsrfsh v2.00</generator> 9 10 10 - <managingEditor>scott@loserfish.org</managingEditor> 11 + <managingEditor>scott@loserfish.org (Scott)</managingEditor> 11 12 <copyright>http://creativecommons.org/licenses/by-sa/1.0</copyright> 12 - <webMaster>scott@loserfish.org</webMaster> 13 + <webMaster>scott@loserfish.org (Scott)</webMaster> 13 14 <ttl>15</ttl> 14 15 <!-- tmpl_var name="container" --> 15 16 </channel>
+1 -1
htdocs/thtml/tumble_item_image.txml
··· 1 1 <item> 2 2 <title><!-- tmpl_var name="title" --></title> 3 3 <link><!-- tmpl_var name="url" --></link> 4 - <guid>tumble-<!-- tmpl_var name="ircLinkID" --></guid> 4 + <guid isPermaLink="false">tumble-image-<!-- tmpl_var name="imageID" --></guid> 5 5 <description><!-- tmpl_var name="content" --></description> 6 6 <pubDate><!-- tmpl_var name="timestamp" --></pubDate> 7 7 </item>
+1 -1
htdocs/thtml/tumble_item_ircLink.txml
··· 1 1 <item> 2 2 <title><!-- tmpl_var name="title" --></title> 3 3 <link>http://<!-- tmpl_var name="baseurl" -->/irclink/?<!-- tmpl_var name="ircLinkID" --></link> 4 - <guid>tumble-<!-- tmpl_var name="ircLinkID" --></guid> 4 + <guid isPermaLink="false">tumble-<!-- tmpl_var name="ircLinkID" --></guid> 5 5 <description><!-- tmpl_var name="content" --></description> 6 6 <pubDate><!-- tmpl_var name="timestamp" --></pubDate> 7 7 </item>
+2 -2
htdocs/thtml/tumble_item_quote.txml
··· 1 1 <item> 2 2 <title><!-- tmpl_var name="quote" --></title> 3 3 <link>http://<!-- tmpl_var name="baseurl" -->/</link> 4 - <guid>tumble-<!-- tmpl_var name="ircLinkID" --></guid> 5 - <description>"<!-- tmpl_var name="quote" -->" --<!-- tmpl_var name="author" --></description> 4 + <guid isPermaLink="false">tumble-quote-<!-- tmpl_var name="quoteID" --></guid> 5 + <description><!-- tmpl_var name="description" --></description> 6 6 <pubDate><!-- tmpl_var name="timestamp" --></pubDate> 7 7 </item>