Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/publish/atom.php - 420 lines - 13537 bytes - Summary - Text - Print

Description: Handles Atom feeds.

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

title

Description

title

Description

title

Description

title

title

Body