PageRenderTime 45ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/textpattern/publish/atom.php

http://textpattern.googlecode.com/
PHP | 434 lines | 255 code | 89 blank | 90 comment | 47 complexity | d068b6586701f1f2215b8b8602a38d3b MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1, GPL-2.0
  1. <?php
  2. /*
  3. * Textpattern Content Management System
  4. * http://textpattern.com
  5. *
  6. * Copyright (C) 2005 Dean Allen
  7. * Copyright (C) 2014 The Textpattern Development Team
  8. *
  9. * This file is part of Textpattern.
  10. *
  11. * Textpattern is free software; you can redistribute it and/or
  12. * modify it under the terms of the GNU General Public License
  13. * as published by the Free Software Foundation, version 2.
  14. *
  15. * Textpattern is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU General Public License
  21. * along with Textpattern. If not, see <http://www.gnu.org/licenses/>.
  22. */
  23. /**
  24. * Handles Atom feeds.
  25. *
  26. * @package XML
  27. */
  28. /**
  29. * @ignore
  30. */
  31. define("t_texthtml", ' type="text/html"');
  32. /**
  33. * @ignore
  34. */
  35. define("t_text", ' type="text"');
  36. /**
  37. * @ignore
  38. */
  39. define("t_html", ' type="html"');
  40. /**
  41. * @ignore
  42. */
  43. define("t_xhtml", ' type="xhtml"');
  44. /**
  45. * @ignore
  46. */
  47. define('t_appxhtml', ' type="xhtml"');
  48. /**
  49. * @ignore
  50. */
  51. define("r_relalt", ' rel="alternate"');
  52. /**
  53. * @ignore
  54. */
  55. define("r_relself", ' rel="self"');
  56. /**
  57. * Generates and outputs an Atom feed.
  58. *
  59. * This function can only be called once on a page. It outputs
  60. * an Atom feed based on the requested URL parameters. Accepts
  61. * HTTP GET parameters 'limit', 'area', 'section' and 'category'.
  62. */
  63. function atom()
  64. {
  65. global $thisarticle, $prefs;
  66. set_error_handler('feedErrorHandler');
  67. ob_clean();
  68. extract($prefs);
  69. $last = fetch('unix_timestamp(val)', 'txp_prefs', 'name', 'lastmod');
  70. extract(doSlash(gpsa(array(
  71. 'limit',
  72. 'area',
  73. ))));
  74. // Build filter criteria from a comma-separated list of sections and categories.
  75. $feed_filter_limit = get_pref('feed_filter_limit', 10);
  76. $section = gps('section');
  77. $category = gps('category');
  78. if (!is_scalar($section) || !is_scalar($category)) {
  79. txp_die('Not Found', 404);
  80. }
  81. $section = ($section ? array_slice(array_unique(do_list($section)), 0, $feed_filter_limit) : array());
  82. $category = ($category ? array_slice(array_unique(do_list($category)), 0, $feed_filter_limit) : array());
  83. $st = array();
  84. foreach ($section as $s) {
  85. $st[] = fetch_section_title($s);
  86. }
  87. $ct = array();
  88. foreach ($category as $c) {
  89. $ct[] = fetch_category_title($c);
  90. }
  91. $sitename .= ($section) ? ' - '.join(' - ', $st) : '';
  92. $sitename .= ($category) ? ' - '.join(' - ', $ct) : '';
  93. $pub = safe_row("RealName, email", "txp_users", "privs=1");
  94. // Feed header.
  95. $out[] = tag(htmlspecialchars($sitename), 'title', t_text);
  96. $out[] = tag(htmlspecialchars($site_slogan), 'subtitle', t_text);
  97. $out[] = '<link'.r_relself.' href="'.pagelinkurl(array(
  98. 'atom' => 1,
  99. 'area' => $area,
  100. 'section' => $section,
  101. 'category'=> $category,
  102. 'limit' => $limit
  103. )).'" />';
  104. $out[] = '<link'.r_relalt.t_texthtml.' href="'.hu.'" />';
  105. // Atom feeds with mail or domain name.
  106. $dn = explode('/', $siteurl);
  107. $mail_or_domain = ($use_mail_on_feeds_id) ? eE($blog_mail_uid) : $dn[0];
  108. $out[] = tag('tag:'.$mail_or_domain.','.$blog_time_uid.':'.$blog_uid.(($section) ? '/'.join(',', $section) : '').(($category)? '/'.join(',', $category) : ''), 'id');
  109. $out[] = tag('Textpattern', 'generator', ' uri="http://textpattern.com/" version="'.$version.'"');
  110. $out[] = tag(safe_strftime("w3cdtf", $last), 'updated');
  111. $auth[] = tag($pub['RealName'], 'name');
  112. $auth[] = ($include_email_atom) ? tag(eE($pub['email']), 'email') : '';
  113. $auth[] = tag(hu, 'uri');
  114. $out[] = tag(n.t.t.join(n.t.t, $auth).n, 'author');
  115. $out[] = callback_event('atom_head');
  116. // Feed items.
  117. $articles = array();
  118. $section = doSlash($section);
  119. $category = doSlash($category);
  120. if (!$area or $area == 'article') {
  121. $sfilter = (!empty($section)) ? "and Section in ('".join("','", $section)."')" : '';
  122. $cfilter = (!empty($category))? "and (Category1 in ('".join("','", $category)."') or Category2 in ('".join("','", $category)."'))" : '';
  123. $limit = ($limit) ? $limit : $rss_how_many;
  124. $limit = intval(min($limit, max(100, $rss_how_many)));
  125. $frs = safe_column("name", "txp_section", "in_rss != '1'");
  126. $query = array();
  127. foreach ($frs as $f) {
  128. $query[] = "and Section != '".doSlash($f)."'";
  129. }
  130. $query[] = $sfilter;
  131. $query[] = $cfilter;
  132. $expired = ($publish_expired_articles) ? '' : ' and (now() <= Expires or Expires = '.NULLDATETIME.') ';
  133. $rs = safe_rows_start(
  134. "*,
  135. ID as thisid,
  136. unix_timestamp(Posted) as uPosted,
  137. unix_timestamp(Expires) as uExpires,
  138. unix_timestamp(LastMod) as uLastMod",
  139. "textpattern",
  140. "Status=4 and Posted <= now() $expired".join(' ', $query).
  141. "order by Posted desc limit $limit"
  142. );
  143. if ($rs) {
  144. while ($a = nextRow($rs)) {
  145. extract($a);
  146. populateArticleData($a);
  147. $cb = callback_event('atom_entry');
  148. $e = array();
  149. $a['posted'] = $uPosted;
  150. if ($show_comment_count_in_feed) {
  151. $count = ($comments_count > 0) ? ' ['.$comments_count.']' : '';
  152. } else {
  153. $count = '';
  154. }
  155. $thisauthor = get_author_name($AuthorID);
  156. $e['thisauthor'] = tag(n.t.t.t.tag(htmlspecialchars($thisauthor), 'name').n.t.t, 'author');
  157. $e['issued'] = tag(safe_strftime('w3cdtf', $uPosted), 'published');
  158. $e['modified'] = tag(safe_strftime('w3cdtf', $uLastMod), 'updated');
  159. $escaped_title = htmlspecialchars($Title);
  160. $e['title'] = tag($escaped_title.$count, 'title', t_html);
  161. $permlink = permlinkurl($a);
  162. $e['link'] = '<link'.r_relalt.t_texthtml.' href="'.$permlink.'" />';
  163. $e['id'] = tag('tag:'.$mail_or_domain.','.$feed_time.':'.$blog_uid.'/'.$uid, 'id');
  164. $e['category1'] = (trim($Category1) ? '<category term="'.htmlspecialchars($Category1).'" />' : '');
  165. $e['category2'] = (trim($Category2) ? '<category term="'.htmlspecialchars($Category2).'" />' : '');
  166. $summary = trim(replace_relative_urls(parse($thisarticle['excerpt']), $permlink));
  167. $content = trim(replace_relative_urls(parse($thisarticle['body']), $permlink));
  168. if ($syndicate_body_or_excerpt) {
  169. // Short feed: use body as summary if there's no excerpt.
  170. if (!trim($summary)) {
  171. $summary = $content;
  172. }
  173. $content = '';
  174. }
  175. if (trim($content)) {
  176. $e['content'] = tag(n.escape_cdata($content).n, 'content', t_html);
  177. }
  178. if (trim($summary)) {
  179. $e['summary'] = tag(n.escape_cdata($summary).n, 'summary', t_html);
  180. }
  181. $articles[$ID] = tag(n.t.t.join(n.t.t, $e).n.$cb, 'entry');
  182. $etags[$ID] = strtoupper(dechex(crc32($articles[$ID])));
  183. $dates[$ID] = $uLastMod;
  184. }
  185. }
  186. } elseif ($area == 'link') {
  187. $cfilter = ($category) ? "category in ('".join("','", $category)."')" : '1';
  188. $limit = ($limit) ? $limit : $rss_how_many;
  189. $limit = intval(min($limit, max(100, $rss_how_many)));
  190. $rs = safe_rows_start("*", "txp_link", "$cfilter order by date desc, id desc limit $limit");
  191. if ($rs) {
  192. while ($a = nextRow($rs)) {
  193. extract($a);
  194. $e['title'] = tag(htmlspecialchars($linkname), 'title', t_html);
  195. $e['content'] = tag(n.htmlspecialchars($description).n, 'content', t_html);
  196. $url = (preg_replace("/^\/(.*)/", "https?://$siteurl/$1", $url));
  197. $url = preg_replace("/&((?U).*)=/", "&amp;\\1=", $url);
  198. $e['link'] = '<link'.r_relalt.t_texthtml.' href="'.$url.'" />';
  199. $e['issued'] = tag(safe_strftime('w3cdtf', strtotime($date)), 'published');
  200. $e['modified'] = tag(gmdate('Y-m-d\TH:i:s\Z',strtotime($date)), 'updated');
  201. $e['id'] = tag('tag:'.$mail_or_domain.','.safe_strftime('%Y-%m-%d', strtotime($date)).':'.$blog_uid.'/'.$id, 'id');
  202. $articles[$id] = tag(n.t.t.join(n.t.t, $e).n, 'entry');
  203. $etags[$id] = strtoupper(dechex(crc32($articles[$id])));
  204. $dates[$id] = $date;
  205. }
  206. }
  207. }
  208. if (!$articles) {
  209. if ($section) {
  210. if (safe_field('name', 'txp_section', "name in ('".join("','", $section)."')") == false) {
  211. txp_die(gTxt('404_not_found'), '404');
  212. }
  213. } elseif ($category) {
  214. switch ($area) {
  215. case 'link' :
  216. if (safe_field('id', 'txp_category', "name = '$category' and type = 'link'") == false) {
  217. txp_die(gTxt('404_not_found'), '404');
  218. }
  219. break;
  220. case 'article' :
  221. default :
  222. if (safe_field('id', 'txp_category', "name in ('".join("','", $category)."') and type = 'article'") == false) {
  223. txp_die(gTxt('404_not_found'), '404');
  224. }
  225. break;
  226. }
  227. }
  228. } else {
  229. // Turn on compression if we aren't using it already.
  230. if (extension_loaded('zlib') && ini_get("zlib.output_compression") == 0 &&
  231. ini_get('output_handler') != 'ob_gzhandler' && !headers_sent()
  232. )
  233. {
  234. // Make sure notices/warnings/errors don't fudge up the feed when compression is used.
  235. $buf = '';
  236. while ($b = @ob_get_clean()) {
  237. $buf .= $b;
  238. }
  239. @ob_start('ob_gzhandler');
  240. echo $buf;
  241. }
  242. handle_lastmod();
  243. $hims = serverset('HTTP_IF_MODIFIED_SINCE');
  244. $imsd = ($hims) ? strtotime($hims) : 0;
  245. if (is_callable('apache_request_headers')) {
  246. $headers = apache_request_headers();
  247. if (isset($headers["A-IM"])) {
  248. $canaim = strpos($headers["A-IM"], "feed");
  249. } else {
  250. $canaim = false;
  251. }
  252. } else {
  253. $canaim = false;
  254. }
  255. $hinm = stripslashes(serverset('HTTP_IF_NONE_MATCH'));
  256. $cutarticles = false;
  257. if ($canaim !== false) {
  258. foreach ($articles as $id => $thing) {
  259. if (strpos($hinm, $etags[$id])) {
  260. unset($articles[$id]);
  261. $cutarticles = true;
  262. $cut_etag = true;
  263. }
  264. if ($dates[$id] < $imsd) {
  265. unset($articles[$id]);
  266. $cutarticles = true;
  267. $cut_time = true;
  268. }
  269. }
  270. }
  271. if (isset($cut_etag) && isset($cut_time)) {
  272. header("Vary: If-None-Match, If-Modified-Since");
  273. } elseif (isset($cut_etag)) {
  274. header("Vary: If-None-Match");
  275. } elseif (isset($cut_time)) {
  276. header("Vary: If-Modified-Since");
  277. }
  278. $etag = @join("-", $etags);
  279. if (strstr($hinm, $etag)) {
  280. txp_status_header('304 Not Modified');
  281. exit(0);
  282. }
  283. if ($etag) {
  284. header('ETag: "'.$etag.'"');
  285. }
  286. if ($cutarticles) {
  287. // header("HTTP/1.1 226 IM Used");
  288. // This should be used as opposed to 200, but Apache doesn't like it.
  289. // http://intertwingly.net/blog/2004/09/11/Vary-ETag/ says that the status code should be 200.
  290. header("Cache-Control: no-store, im");
  291. header("IM: feed");
  292. }
  293. }
  294. $out = array_merge($out, $articles);
  295. header('Content-type: application/atom+xml; charset=utf-8');
  296. return chr(60).'?xml version="1.0" encoding="UTF-8"?'.chr(62).n.
  297. '<feed xml:lang="'.$language.'" xmlns="http://www.w3.org/2005/Atom">'.join(n, $out).'</feed>';
  298. }
  299. /**
  300. * Converts HTML entieties to UTF-8 characters.
  301. *
  302. * This is included only for backwards compatibility with older plugins.
  303. *
  304. * @param string $toUnicode
  305. * @return string
  306. * @deprecated in 4.0.4
  307. */
  308. function safe_hed($toUnicode)
  309. {
  310. if (version_compare(phpversion(), "5.0.0", ">=")) {
  311. $str = html_entity_decode($toUnicode, ENT_QUOTES, "UTF-8");
  312. } else {
  313. $trans_tbl = get_html_translation_table(HTML_ENTITIES);
  314. foreach ($trans_tbl as $k => $v) {
  315. $ttr[$v] = utf8_encode($k);
  316. }
  317. $str = strtr($toUnicode, $ttr);
  318. }
  319. return $str;
  320. }
  321. /**
  322. * Sanitises a string for use in a feed.
  323. *
  324. * Tries to resolve relative URLs and encode unescaped characters.
  325. *
  326. * This is included only for backwards compatibility with older plugins.
  327. *
  328. * @param string $toFeed
  329. * @param string $permalink
  330. * @return string
  331. * @deprecated in 4.0.4
  332. */
  333. function fixup_for_feed($toFeed, $permalink)
  334. {
  335. // Fix relative urls.
  336. $txt = str_replace('href="/','href="'.hu.'/',$toFeed);
  337. $txt = preg_replace("/href=\\\"#(.*)\"/","href=\"".$permalink."#\\1\"",$txt);
  338. // This was removed as entities shouldn't be stripped in Atom feeds when the content type is HTML.
  339. // Leaving it commented out as a reminder.
  340. //$txt = safe_hed($txt);
  341. // Encode and entify.
  342. $txt = preg_replace(array('/</','/>/',"/'/",'/"/'), array('&#60;','&#62;','&#039;','&#34;'), $txt);
  343. $txt = preg_replace("/&(?![#0-9]+;)/i",'&amp;', $txt);
  344. return $txt;
  345. }