Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/lib/txplib_publish.php - 1045 lines - 32883 bytes - Summary - Text - Print

Description: Tools for page routing and handling article data.

   1  <?php
   2  
   3  /*
   4   * Textpattern Content Management System
   5   * https://textpattern.com/
   6   *
   7   * Copyright (C) 2020 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 <https://www.gnu.org/licenses/>.
  22   */
  23  
  24  /**
  25   * Tools for page routing and handling article data.
  26   *
  27   * @since   4.5.0
  28   * @package Routing
  29   */
  30  
  31  /**
  32   * Build a query qualifier to remove non-frontpage articles from the result set.
  33   *
  34   * @return string An SQL qualifier for a query's 'WHERE' part
  35   */
  36  
  37  function filterFrontPage($field = 'Section', $column = 'on_frontpage')
  38  {
  39      static $filterFrontPage = array();
  40      global $txp_sections;
  41  
  42      is_array($column) or $column = do_list_unique($column);
  43      $key = $field.'.'.implode('.', $column);
  44  
  45      if (isset($filterFrontPage[$key])) {
  46          return $filterFrontPage[$key];
  47      }
  48  
  49      $filterFrontPage[$key] = false;
  50      $field = doSlash($field);
  51      $rs = array();
  52  
  53      foreach ($column as $col) {
  54          $rs += array_filter(array_column($txp_sections, $col, 'name'));
  55      }
  56  
  57      if ($rs) {
  58          $filterFrontPage[$key] = " AND $field IN(".join(',', quote_list(array_keys($rs))).")";
  59      }
  60  
  61      return $filterFrontPage[$key];
  62  }
  63  
  64  /**
  65   * Populates the current article data.
  66   *
  67   * Fills members of $thisarticle global from a database row.
  68   *
  69   * Keeps all article tag-related values in one place, in order to do easy
  70   * bugfixing and ease the addition of new article tags.
  71   *
  72   * @param array $rs An article as an associative array
  73   * @example
  74   * if ($rs = safe_rows_start("*,
  75   *     UNIX_TIMESTAMP(Posted) AS uPosted,
  76   *     UNIX_TIMESTAMP(Expires) AS uExpires,
  77   *     UNIX_TIMESTAMP(LastMod) AS uLastMod",
  78   *     'textpattern',
  79   *     "1 = 1"
  80   * ))
  81   * {
  82   *     global $thisarticle;
  83   *     while ($row = nextRow($rs))
  84   *     {
  85   *         populateArticleData($row);
  86   *         echo $thisarticle['title'];
  87   *     }
  88   * }
  89   */
  90  
  91  function populateArticleData($rs)
  92  {
  93      global $production_status, $thisarticle, $trace;
  94  
  95      foreach (article_column_map() as $key => $column) {
  96          $thisarticle[$key] = isset($rs[$column]) ? $rs[$column] : null;
  97      }
  98  
  99      if ($production_status === 'debug') {
 100          $trace->log("[Article: '{$thisarticle['thisid']}']");
 101      }
 102  }
 103  
 104  /**
 105   * Formats article info and populates the current article data.
 106   *
 107   * Fills members of $thisarticle global from a database row.
 108   *
 109   * Basically just converts an article's date values to UNIX timestamps.
 110   * Convenience for those who prefer doing conversion in application end instead
 111   * of in the SQL statement.
 112   *
 113   * @param array $rs An article as an associative array
 114   * @example
 115   * article_format_info(
 116   *     safe_row('*', 'textpattern', 'Status = 4 LIMIT 1')
 117   * )
 118   */
 119  
 120  function article_format_info($rs)
 121  {
 122      $rs['uPosted']  = isset($rs['Posted']) && ($unix_ts = strtotime($rs['Posted'])) !== false ? $unix_ts : null;
 123      $rs['uLastMod'] = isset($rs['LastMod']) && ($unix_ts = strtotime($rs['LastMod'])) !== false ? $unix_ts : null;
 124      $rs['uExpires'] = isset($rs['Expires']) && ($unix_ts = strtotime($rs['Expires'])) !== false ? $unix_ts : null;
 125      populateArticleData($rs);
 126  }
 127  
 128  /**
 129   * Maps 'textpattern' table's columns to article data values.
 130   *
 131   * This function returns an array of 'data-value' => 'column' pairs.
 132   *
 133   * @return array
 134   */
 135  
 136  function article_column_map()
 137  {
 138      static $column_map = array();
 139  
 140      if (empty($column_map)) {
 141          $column_map = array(
 142              'thisid'          => 'ID',
 143              'posted'          => 'uPosted', // Calculated value!
 144              'expires'         => 'uExpires', // Calculated value!
 145              'modified'        => 'uLastMod', // Calculated value!
 146              'annotate'        => 'Annotate',
 147              'comments_invite' => 'AnnotateInvite',
 148              'authorid'        => 'AuthorID',
 149              'title'           => 'Title',
 150              'url_title'       => 'url_title',
 151              'description'     => 'description',
 152              'category1'       => 'Category1',
 153              'category2'       => 'Category2',
 154              'section'         => 'Section',
 155              'keywords'        => 'Keywords',
 156              'article_image'   => 'Image',
 157              'comments_count'  => 'comments_count',
 158              'body'            => 'Body_html',
 159              'excerpt'         => 'Excerpt_html',
 160              'override_form'   => 'override_form',
 161              'status'          => 'Status',
 162          );
 163  
 164          foreach (getCustomFields() as $i => $name) {
 165              isset($column_map[$name]) or $column_map[$name] = 'custom_'.$i;
 166          }
 167      }
 168  
 169      return $column_map;
 170  }
 171  
 172  /**
 173   * Find an adjacent article relative to a provided threshold level.
 174   *
 175   * @param  scalar $threshold      The value to compare against
 176   * @param  string $s              Optional section restriction
 177   * @param  string $type           Lesser or greater neighbour? Either '<' (previous) or '>' (next)
 178   * @param  array  $atts           Attribute of article at threshold
 179   * @param  string $threshold_type 'cooked': Use $threshold as SQL clause; 'raw': Use $threshold as an escapable scalar
 180   * @return array|bool An array populated with article data, or 'false' in case of no matches
 181   */
 182  
 183  function getNeighbour($threshold, $s, $type, $atts = array(), $threshold_type = 'raw')
 184  {
 185      static $cache = array();
 186      static $types = array(
 187          '>' => array(
 188              'desc' => '>',
 189              'asc'  => '<',
 190          ),
 191          '<' => array(
 192              'desc' => '<',
 193              'asc'  => '>',
 194          ),
 195      );
 196  
 197      $key = md5($threshold.$s.$type.join(n, $atts));
 198  
 199      if (isset($cache[$key])) {
 200          return $cache[$key];
 201      }
 202  
 203      $thisid = isset($atts['thisid']) ? intval($atts['thisid']) : 0;
 204      $sortdir = isset($atts['sortdir']) ? strtolower($atts['sortdir']) : 'asc';
 205      $sortby = isset($atts['sortby']) ? $atts['sortby'] : 'Posted';
 206  
 207      // Invert $type for ascending sortdir.
 208      $type = ($type == '>') ? $types['>'][$sortdir] : $types['<'][$sortdir];
 209  
 210      // Escape threshold and treat it as a string unless explicitly told otherwise.
 211      if ($threshold_type != 'cooked') {
 212          $threshold = "'".doSlash($threshold)."'";
 213      }
 214  
 215      $where = isset($atts['*']) ? $atts['*'] : '1';
 216      $q = array(
 217          "SELECT *, UNIX_TIMESTAMP(Posted) AS uPosted, UNIX_TIMESTAMP(Expires) AS uExpires, UNIX_TIMESTAMP(LastMod) AS uLastMod FROM ".safe_pfx('textpattern')."
 218              WHERE ($sortby $type $threshold OR ".($thisid ? "$sortby = $threshold AND ID $type $thisid" : "0").")",
 219          "AND $where",
 220          "ORDER BY $sortby",
 221          ($type == '<') ? 'DESC' : 'ASC',
 222          ', ID '.($type == '<' ? 'DESC' : 'ASC'),
 223          "LIMIT 1",
 224      );
 225  
 226      $cache[$key] = getRow(join(' ', $q));
 227  
 228      return (is_array($cache[$key])) ? $cache[$key] : false;
 229  }
 230  
 231  /**
 232   * Find next and previous articles relative to a provided threshold level.
 233   *
 234   * @param  int    $id        The "pivot" article's id; use zero (0) to indicate $thisarticle
 235   * @param  scalar $threshold The value to compare against if $id != 0
 236   * @param  string $s         Optional section restriction if $id != 0
 237   * @return array An array populated with article data
 238   */
 239  
 240  function getNextPrev($id = 0, $threshold = null, $s = '')
 241  {
 242      $threshold_type = 'cooked';
 243      $atts = filterAtts() or $atts = filterAtts(array());
 244  
 245      if ($id !== 0) {
 246          // Pivot is specific article by ID: In lack of further information,
 247          // revert to default sort order 'Posted desc'.
 248          $atts += array(
 249              'sortby'  => 'Posted',
 250              'sortdir' => 'DESC',
 251              'thisid'  => $id,
 252          );
 253          $threshold_type = 'raw';
 254      } else {
 255          // Pivot is $thisarticle: Use article attributes to find its neighbours.
 256          assert_article();
 257          global $thisarticle;
 258          if (!is_array($thisarticle)) {
 259              return array();
 260          }
 261  
 262          $s = $thisarticle['section'];
 263          $atts += array(
 264              'thisid' => $thisarticle['thisid'],
 265              'sort'   => 'Posted DESC',
 266          );
 267          $atts['sort'] = trim($atts['sort']);
 268  
 269          if (empty($atts['sort'])) {
 270              $atts['sortby'] = !empty($atts['id']) ? "FIELD(ID, ".$atts['id'].")" : 'Posted';
 271              $atts['sortdir'] = !empty($atts['id']) ? 'ASC' : 'DESC';
 272          } elseif (preg_match('/^([$\w\x{0080}-\x{FFFF}]+|`[\x{0001}-\x{FFFF}]+`)(?i)(\s+asc|\s+desc)?$/u', $atts['sort'], $m)) {
 273              // The clause's first verb is a MySQL column identifier.
 274              $atts['sortby'] = trim($m[1], ' `');
 275              $atts['sortdir'] = (isset($m[2]) ? trim($m[2]) : 'ASC');
 276          } elseif (preg_match('/^((?>[^(),]|(\((?:[^()]|(?2))*\)))+)(\basc|\bdesc)?$/Ui', $atts['sort'], $m)) {
 277              // More complex unique clause.
 278              $atts['sortby'] = trim($m[1]);
 279              $atts['sortdir'] = (isset($m[3]) ? $m[3] : 'ASC');
 280          } else {
 281              $atts['sortby'] = 'Posted';
 282              $atts['sortdir'] = 'DESC';
 283          }
 284  
 285          // Attributes with special treatment.
 286          switch ($atts['sortby']) {
 287              case 'Posted':
 288                  $threshold = "FROM_UNIXTIME(".doSlash($thisarticle['posted']).")";
 289                  break;
 290              case 'Expires':
 291                  $threshold = "FROM_UNIXTIME(".doSlash($thisarticle['expires']).")";
 292                  break;
 293              case 'LastMod':
 294                  $threshold = "FROM_UNIXTIME(".doSlash($thisarticle['modified']).")";
 295                  break;
 296              default:
 297                  // Retrieve current threshold value per sort column from $thisarticle.
 298                  $threshold_type = 'raw';
 299                  $acm = array_flip(article_column_map());
 300  
 301                  if (isset($acm[$atts['sortby']])) {
 302                      $key = $acm[$atts['sortby']];
 303                      $threshold = $thisarticle[$key];
 304                  } else {
 305                      $threshold = safe_field($atts['sortby'], 'textpattern', 'ID='.$atts['thisid']);
 306                  }
 307          }
 308      }
 309  
 310      $out['next'] = getNeighbour($threshold, $s, '>', $atts, $threshold_type);
 311      $out['prev'] = getNeighbour($threshold, $s, '<', $atts, $threshold_type);
 312  
 313      return $out;
 314  }
 315  
 316  /**
 317   * Gets the site last modification date.
 318   *
 319   * @return  string
 320   * @package Pref
 321   */
 322  
 323  function lastMod()
 324  {
 325      $last = safe_field("UNIX_TIMESTAMP(val)", 'txp_prefs', "name = 'lastmod'");
 326  
 327      return gmdate("D, d M Y H:i:s \G\M\T", $last);
 328  }
 329  
 330  /**
 331   * Parse a string and replace any Textpattern tags with their actual value.
 332   *
 333   * @param   string    $thing     The raw string
 334   * @param   null|bool $condition Process true/false part
 335   * @return  string               The parsed string
 336   * @package TagParser
 337   */
 338  
 339  function parse($thing, $condition = true, $in_tag = true)
 340  {
 341      global $pretext, $production_status, $trace, $txp_parsed, $txp_else, $txp_atts, $txp_tag;
 342      static $short_tags = null;
 343  
 344      if ($in_tag) {
 345          empty($txp_atts['not']) or $condition = empty($condition);
 346      }
 347  
 348      $txp_tag = !empty($condition);
 349  
 350      if ($production_status === 'debug') {
 351          $trace->log('['.($condition ? 'true' : 'false').']');
 352      }
 353  
 354      if (!isset($short_tags)) {
 355          $short_tags = get_pref('enable_short_tags', false);
 356      }
 357  
 358      if (!$short_tags && false === strpos($thing, '<txp:') ||
 359          $short_tags && !preg_match('@<(?:'.TXP_PATTERN.'):@', $thing)) {
 360          return $condition ? ($thing === null ? '1' : $thing) : '';
 361      }
 362  
 363      $hash = sha1($thing);
 364  
 365      if (!isset($txp_parsed[$hash])) {
 366          txp_tokenize($thing, $hash);
 367      }
 368  
 369      $tag = $txp_parsed[$hash];
 370  
 371      if (empty($tag)) {
 372          return $condition ? $thing : '';
 373      }
 374  
 375      list($first, $last) = $txp_else[$hash];
 376  
 377      if ($condition) {
 378          $last = $first - 2;
 379          $first   = 1;
 380      } elseif ($first <= $last) {
 381          $first  += 2;
 382      } else {
 383          return '';
 384      }
 385  
 386      $isempty = false;
 387      $dotest = !empty($txp_atts['evaluate']) && $in_tag;
 388      $evaluate = !$dotest ? null :
 389          ($txp_atts['evaluate'] === true ? true : do_list($txp_atts['evaluate']));
 390  
 391      if (isset($txp_else[$hash]['test']) && (!$evaluate || $evaluate === true)) {
 392          $evaluate = $txp_else[$hash]['test'];
 393      }
 394  
 395      if ($evaluate) {
 396          $test = is_array($evaluate) ? array_fill_keys($evaluate, array()) : false;
 397          $isempty = $last >= $first || $test !== false;
 398      }
 399  
 400      if (empty($test)) {
 401          for ($out = $tag[$first - 1]; $first <= $last; $first++) {
 402              $txp_tag = $tag[$first];
 403              $nextag = processTags($txp_tag[1], $txp_tag[2], $txp_tag[3]);
 404              $out .= $nextag.$tag[++$first];
 405              $isempty = $isempty && trim($nextag) === '';
 406          }
 407      } else {
 408          if ($pre = !isset($test[0])) {
 409              $test[0] = array();
 410          }
 411  
 412          $out = array($first-1 => $tag[$first-1]);
 413  
 414          for ($n = $first; $n <= $last; $n++) {
 415              $txp_tag = $tag[$n];
 416              $out[$n] = null;
 417  
 418              if (isset($test[($n+1)/2])) {
 419                  $test[($n+1)/2][] = $n;
 420              } elseif (isset($test[$txp_tag[1]])) {
 421                  $test[$txp_tag[1]][] = $n;
 422              } else {
 423                  $test[0][] = $n;
 424              }
 425  
 426              $out[$n] = $tag[++$n];
 427          }
 428  
 429          foreach ($test as $k => $t) {
 430              if (!$k && $pre && $dotest && $isempty == empty($txp_atts['not'])) {
 431                  $out = false;    
 432                  break;
 433              }
 434  
 435              foreach ($t as $n) {
 436                  $txp_tag = $tag[$n];
 437                  $nextag = processTags($txp_tag[1], $txp_tag[2], $txp_tag[3]);
 438                  $out[$n] = $nextag;
 439                  $k and ($isempty = $isempty && trim($nextag) === '');
 440              }
 441          }
 442  
 443          if (is_array($out)) {
 444              $out = implode('', $out);
 445          }
 446      }
 447  
 448      if ($dotest && $isempty == empty($txp_atts['not'])) {
 449          $out = false;
 450          $condition = false;
 451      }
 452  
 453      $txp_tag = !empty($condition);
 454  
 455      return $out;
 456  }
 457  
 458  /**
 459   * Guesstimate whether a given function name may be a valid tag handler.
 460   *
 461   * @param   string $tag function name
 462   * @return  bool FALSE if the function name is not a valid tag handler
 463   * @package TagParser
 464   */
 465  
 466  function maybe_tag($tag)
 467  {
 468      static $tags = null;
 469  
 470      if ($tags === null) {
 471          global $plugins;
 472  
 473          if (empty($plugins)) {
 474              $tags = false;
 475          } else {
 476              $match = array();
 477  
 478              foreach ($plugins as $p) {
 479                  $pfx = strpos($p, '_') === false ? $p : strtok($p, '_').'_';
 480                  $match[$pfx] = preg_quote($pfx, '/');
 481              }
 482  
 483              $match = '/^('.implode('|', $match).')/i';
 484              $tags = get_defined_functions();
 485              $tags = array_filter($tags['user'], function ($f) use ($match) {
 486                  return preg_match($match, $f);
 487              });
 488              $tags = array_flip($tags);
 489          }
 490      }
 491  
 492      return isset($tags[$tag]);
 493  }
 494  
 495  /**
 496   * Parse a tag for attributes and hand over to the tag handler function.
 497   *
 498   * @param  string      $tag   The tag name
 499   * @param  string      $atts  The attribute string
 500   * @param  string|null $thing The tag's content in case of container tags
 501   * @return string Parsed tag result
 502   * @package TagParser
 503   */
 504  
 505  function processTags($tag, $atts = '', $thing = null)
 506  {
 507      global $pretext, $production_status, $txp_current_tag, $txp_atts, $txp_tag, $trace;
 508      static $registry = null, $maxpass, $globals;
 509  
 510      if (empty($tag)) {
 511          return;
 512      }
 513  
 514      if ($registry === null) {
 515          $maxpass = get_pref('secondpass', 1);
 516          $registry = Txp::get('\Textpattern\Tag\Registry');
 517          $globals = array_filter(
 518              $registry->getRegistered(true),
 519              function ($v) {
 520                  return !is_bool($v);
 521              }
 522          );
 523      }
 524  
 525      $old_tag = $txp_current_tag;
 526      $old_atts = $txp_atts;
 527      $dotrace = $production_status !== 'live' && is_array($txp_tag);
 528  
 529      if ($dotrace) {
 530          $txp_current_tag = $txp_tag[0].$txp_tag[3].$txp_tag[4];
 531          $tag_stop = $txp_tag[4];
 532          $trace->start($txp_tag[0]);
 533      }
 534  
 535      if ($atts) {
 536          $split = splat($atts);
 537      } else {
 538          $txp_atts = null;
 539          $split = array();
 540      }
 541  
 542      if (!isset($txp_atts['txp-process'])) {
 543          $out = $registry->process($tag, $split, $thing);
 544      } else {
 545          $process = empty($txp_atts['txp-process']) || is_numeric($txp_atts['txp-process']) ? (int) $txp_atts['txp-process'] : 1;
 546  
 547          if ($process <= $pretext['secondpass'] + 1) {
 548              unset($txp_atts['txp-process']);
 549              $out = $process > 0 ? $registry->process($tag, $split, $thing) : '';
 550          } else {
 551              $txp_atts['txp-process'] = $process;
 552              $out = '';
 553          }
 554      }
 555  
 556      if ($out === false) {
 557          if (maybe_tag($tag)) { // Deprecated in 4.6.0.
 558              trigger_error($tag.' '.gTxt('unregistered_tag'), E_USER_NOTICE);
 559              $out = $registry->register($tag)->process($tag, $split, $thing);
 560          } else {
 561              trigger_error($tag.' '.gTxt('unknown_tag'), E_USER_WARNING);
 562              $out = '';
 563          }
 564      }
 565  
 566      if (isset($txp_atts['txp-process']) && (int) $txp_atts['txp-process'] > $pretext['secondpass'] + 1) {
 567          $out = $pretext['secondpass'] < $maxpass ? $txp_current_tag : '';
 568      } else {
 569          if ($thing === null && !empty($txp_atts['not'])) {
 570              $out = $out ? '' : '1';
 571          }
 572  
 573          unset($txp_atts['txp-process'], $txp_atts['not'], $txp_atts['evaluate']);
 574  
 575          if ($txp_atts && $txp_tag !== false) {
 576              $pretext['_txp_atts'] = true;
 577  
 578              foreach ($txp_atts as $attr => &$val) {
 579                  if (isset($val) && isset($globals[$attr])) {
 580                      $out = $registry->processAttr($attr, $split, $out);
 581                  }
 582              }
 583  
 584              $pretext['_txp_atts'] = false;
 585          }
 586      }
 587  
 588      $txp_atts = $old_atts;
 589      $txp_current_tag = $old_tag;
 590  
 591      if ($dotrace) {
 592          $trace->stop($tag_stop);
 593      }
 594  
 595      return $out;
 596  }
 597  
 598  /**
 599   * Checks a named item's existence in a database table.
 600   *
 601   * The given database table is prefixed with 'txp_'. As such this function can
 602   * only be used with core database tables.
 603   *
 604   * @param   string $table The database table name
 605   * @param   string $val   The name to look for
 606   * @param   bool   $debug Dump the query
 607   * @return  bool|string The item's name, or FALSE when it doesn't exist
 608   * @package Filter
 609   * @example
 610   * if ($r = ckEx('section', 'about'))
 611   * {
 612   *     echo "Section '{$r}' exists.";
 613   * }
 614   */
 615  
 616  function ckEx($table, $val, $debug = false)
 617  {
 618      $table === 'textpattern' or $table = 'txp_'.$table;
 619  
 620      if (is_array($val)) {
 621          $fields = implode(',', array_keys($val));
 622          $where = join_qs(quote_list(array_filter($val)), ' AND ');
 623  
 624          return safe_row($fields, $table, $where." LIMIT 1", $debug);
 625      } else {
 626          $fields = 'name';
 627          $where = "name = '".doSlash($val)."'";
 628  
 629          return safe_field($fields, $table, $where." LIMIT 1", $debug);
 630      }
 631  }
 632  
 633  /**
 634   * Checks if the given category exists.
 635   *
 636   * @param   string $type  The category type, either 'article', 'file', 'link', 'image'
 637   * @param   string $val   The category name to look for
 638   * @param   bool   $debug Dump the query
 639   * @return  bool|array The category's data, or FALSE when it doesn't exist
 640   * @package Filter
 641   * @see     ckEx()
 642   * @example
 643   * if ($r = ckCat('article', 'development'))
 644   * {
 645   *     echo "Category {$r['name']} exists.";
 646   * }
 647   */
 648  
 649  function ckCat($type, $val, $debug = false)
 650  {
 651      return safe_row("name, title, description, type", 'txp_category', "name = '".doSlash($val)."' AND type = '".doSlash($type)."' LIMIT 1", $debug);
 652  }
 653  
 654  /**
 655   * Lookup an article by ID.
 656   *
 657   * This function takes an article's ID, and checks if it's been published. If it
 658   * has, returns the section and the ID as an array. FALSE otherwise.
 659   *
 660   * @param   int  $val   The article ID
 661   * @param   bool $debug Dump the query
 662   * @return  array|bool Array of ID and section on success, FALSE otherwise
 663   * @package Filter
 664   * @example
 665   * if ($r = ckExID(36))
 666   * {
 667   *     echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}.";
 668   * }
 669   */
 670  
 671  function ckExID($val, $debug = false)
 672  {
 673      return safe_row("ID, Section", 'textpattern', "ID = ".intval($val)." AND Status >= 4 LIMIT 1", $debug);
 674  }
 675  
 676  /**
 677   * Lookup an article by URL title.
 678   *
 679   * This function takes an article's URL title, and checks if the article has
 680   * been published. If it has, returns the section and the ID as an array.
 681   * FALSE otherwise.
 682   *
 683   * @param   string $val   The URL title
 684   * @param   bool   $debug Dump the query
 685   * @return  array|bool Array of ID and section on success, FALSE otherwise
 686   * @package Filter
 687   * @example
 688   * if ($r = ckExID('my-article-title'))
 689   * {
 690   *     echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}.";
 691   * }
 692   */
 693  
 694  function lookupByTitle($val, $debug = false)
 695  {
 696      return safe_row("ID, Section", 'textpattern', "url_title = '".doSlash($val)."' AND Status >= 4 LIMIT 1", $debug);
 697  }
 698  
 699  /**
 700   * Lookup a published article by URL title and section.
 701   *
 702   * This function takes an article's URL title, and checks if the article has
 703   * been published. If it has, returns the section and the ID as an array.
 704   * FALSE otherwise.
 705   *
 706   * @param   string $val     The URL title
 707   * @param   string $section The section name
 708   * @param   bool   $debug   Dump the query
 709   * @return  array|bool Array of ID and section on success, FALSE otherwise
 710   * @package Filter
 711   * @example
 712   * if ($r = ckExID('my-article-title', 'my-section'))
 713   * {
 714   *     echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}.";
 715   * }
 716   */
 717  
 718  function lookupByTitleSection($val, $section, $debug = false)
 719  {
 720      return safe_row("ID, Section", 'textpattern', "url_title = '".doSlash($val)."' AND Section = '".doSlash($section)."' AND Status >= 4 LIMIT 1", $debug);
 721  }
 722  
 723  /**
 724   * Lookup live article by ID and section.
 725   *
 726   * @param   int    $id      Article ID
 727   * @param   string $section Section name
 728   * @param   bool   $debug
 729   * @return  array|bool
 730   * @package Filter
 731   */
 732  
 733  function lookupByIDSection($id, $section, $debug = false)
 734  {
 735      return safe_row("ID, Section", 'textpattern', "ID = ".intval($id)." AND Section = '".doSlash($section)."' AND Status >= 4 LIMIT 1", $debug);
 736  }
 737  
 738  /**
 739   * Lookup live article by ID.
 740   *
 741   * @param   int  $id    Article ID
 742   * @param   bool $debug
 743   * @return  array|bool
 744   * @package Filter
 745   */
 746  
 747  function lookupByID($id, $debug = false)
 748  {
 749      return safe_row("ID, Section", 'textpattern', "ID = ".intval($id)." AND Status >= 4 LIMIT 1", $debug);
 750  }
 751  
 752  /**
 753   * Lookup live article by date and URL title.
 754   *
 755   * @param   string $when  date wildcard
 756   * @param   string $title URL title
 757   * @param   bool   $debug
 758   * @return  array|bool
 759   * @package Filter
 760   */
 761  
 762  function lookupByDateTitle($when, $title, $debug = false)
 763  {
 764      return safe_row("ID, Section", 'textpattern', "posted LIKE '".doSlash($when)."%' AND url_title LIKE '".doSlash($title)."' AND Status >= 4 LIMIT 1");
 765  }
 766  
 767  /**
 768   * Save and retrieve the individual article's attributes plus article list
 769   * attributes for next/prev tags.
 770   *
 771   * @param   array $atts
 772   * @param   bool $iscustom
 773   * @return  array/string
 774   * @since   4.5.0
 775   * @package TagParser
 776   */
 777  
 778  function filterAtts($atts = null, $iscustom = null)
 779  {
 780      global $pretext, $trace, $thisarticle;
 781      static $out = array();
 782  
 783      if ($atts === false) {
 784          return $out = array();
 785      } elseif (!is_array($atts)) {
 786          // TODO: deal w/ nested txp:article[_custom] tags. See https://github.com/textpattern/textpattern/issues/1009
 787          $trace->log('[filterAtts ignored]');
 788  
 789          return $out;
 790      } elseif (isset($atts['*'])) {
 791          return $out = $atts;
 792      }
 793  
 794      $exclude = isset($atts['exclude']) ? $atts['exclude'] : '';
 795      unset($atts['exclude']);
 796  
 797      if ($exclude && $exclude !== true) {
 798          $exclude = array_map('strtolower', do_list_unique($exclude));
 799          $excluded = array_filter($exclude, 'is_numeric');
 800          empty($excluded) or $exclude = array_diff($exclude, $excluded);
 801      } else {
 802          $exclude or $exclude = array();
 803          $excluded = array();
 804      }
 805  
 806      $exclude === true or $exclude = array_fill_keys($exclude, true);
 807  
 808      $customFields = getCustomFields();
 809      $customlAtts = array_null(array_flip($customFields));
 810  
 811      $extralAtts = array(
 812          'form'          => 'default',
 813          'allowoverride' => !$iscustom,
 814          'limit'         => 10,
 815          'offset'        => 0,
 816          'pageby'        => null,
 817          'pgonly'        => 0,
 818          'wraptag'       => '',
 819          'break'         => '',
 820          'breakby'       => '',
 821          'breakclass'    => '',
 822          'breakform'     => '',
 823          'label'         => '',
 824          'labeltag'      => '',
 825          'class'         => '',
 826          'searchall'     => !$iscustom && !empty($pretext['q']),
 827      );
 828  
 829      if ($iscustom) {
 830          $customlAtts = array(
 831              'category'  => '',
 832              'section'   => '',
 833              'author'    => '',
 834              'month'     => '',
 835              'expired'   => get_pref('publish_expired_articles'),
 836          ) + $customlAtts;
 837      } else {
 838          $extralAtts += array(
 839              'listform'     => '',
 840              'searchform'   => '',
 841              'searchsticky' => 0,
 842          );
 843      }
 844  
 845      if ($exclude && is_array($exclude)) {
 846          foreach ($exclude as $cField => $val) {
 847              if (array_key_exists($cField, $customlAtts) && !isset($atts[$cField])) {
 848                  $atts[$cField] = $val;
 849              }
 850          }
 851      }
 852  
 853      // Getting attributes.
 854      $theAtts = lAtts(array(
 855          'fields'        => null,
 856          'sort'          => '',
 857          'keywords'      => '',
 858          'time'          => null,
 859          'status'        => empty($atts['id']) ? STATUS_LIVE : true,
 860          'frontpage'     => !$iscustom,
 861          'match'         => 'Category',
 862          'depth'         => 0,
 863          'id'            => '',
 864          'excerpted'     => ''
 865      ) + $extralAtts + $customlAtts, $atts);
 866  
 867      // For the txp:article tag, some attributes are taken from globals;
 868      // override them, then stash all filter attributes.
 869      extract($pretext);
 870  
 871      if (!$iscustom) {
 872          $theAtts['category'] = ($c) ? $c : '';
 873          $theAtts['section'] = ($s && $s != 'default') ? $s : '';
 874          $theAtts['author'] = (!empty($author) ? $author : '');
 875          $theAtts['month'] = (!empty($month) ? $month : '');
 876          $theAtts['expired'] = get_pref('publish_expired_articles');
 877          $theAtts['frontpage'] = ($theAtts['frontpage'] && !$theAtts['section']);
 878      } else {
 879          $q = '';
 880      }
 881  
 882      extract($theAtts);
 883  
 884      // Treat sticky articles differently wrt search filtering, etc.
 885      $issticky = in_array(strtolower($status), array('sticky', STATUS_STICKY));
 886  
 887      if ($status === true) {
 888          $status = array(STATUS_LIVE, STATUS_STICKY);
 889      } else {
 890          $status = array($issticky ? STATUS_STICKY : STATUS_LIVE);
 891      }
 892  
 893      // Categories
 894      $operator = 'AND';
 895      $match = parse_qs($match);
 896  
 897      if (isset($match['category'])) {
 898          isset($match['category1']) or $match['category1'] = $match['category'];
 899          isset($match['category2']) or $match['category2'] = $match['category'];
 900          $operator = 'OR';
 901      }
 902  
 903      $categories = $category === true ? false : do_list_unique($category);
 904      $catquery = array();
 905  
 906      if ($categories && (!$depth || $categories = getTree($categories, 'article', '1', 'txp_category', $depth))) {
 907          $categories  = join("','", doSlash($categories));
 908      }
 909  
 910      for ($i = 1; $i <= 2; $i++) {
 911          $not = isset($exclude["category{$i}"]) ? '!' : '';
 912  
 913          if (isset($match['category'.$i])) {
 914              if ($match['category'.$i] === false) {
 915                  if ($categories) {
 916                      $catquery[] = "$not(Category{$i} IN ('$categories'))";
 917                  } elseif ($category === true || $not) {
 918                      $catquery[] = "$not(Category{$i} != '')";
 919                  }
 920              } elseif (($val = gps($match['category'.$i], false)) !== false) {
 921                  $catquery[] = "$not(Category{$i} IN (".implode(',', quote_list(is_array($val) ? $val : do_list($val)))."))";
 922              }
 923          } elseif ($not) {
 924              $catquery[] = "(Category{$i} = '')";
 925          }
 926      }
 927  
 928      $not = $iscustom && ($exclude === true || isset($exclude['category'])) ? '!' : '';
 929      $catquery = join(" $operator ", $catquery);
 930      $category  = !$catquery  ? '' : " AND $not($catquery)";
 931  
 932      // ID
 933      $not = $exclude === true || isset($exclude['id']) ? 'NOT' : '';
 934      $ids = $id ? ($id === true ? array(article_id()) : array_map('intval', do_list_unique($id, array(',', '-')))) : array();
 935      $id        = ((!$ids)        ? '' : " AND ID $not IN (".join(',', $ids).")")
 936          .(!$excluded   ? '' : " AND ID NOT IN (".join(',', $excluded).")");
 937      $getid = $ids && !$not;
 938  
 939      // Section
 940      // searchall=0 can be used to show search results for the current
 941      // section only.
 942      if ($q && $searchall && !$issticky) {
 943          $section = '';
 944      }
 945  
 946      $not = $iscustom && ($exclude === true || isset($exclude['section'])) ? 'NOT' : '';
 947      $section !== true or $section = processTags('section');
 948      $getid = $getid || $section && !$not;
 949      $section   = (!$section   ? '' : " AND Section $not IN ('".join("','", doSlash(do_list_unique($section)))."')").
 950          ($getid || $searchall? '' : filterFrontPage('Section', 'page'));
 951  
 952  
 953      // Author
 954      $not = $iscustom && ($exclude === true || isset($exclude['author'])) ? 'NOT' : '';
 955      $author !== true or $author = processTags('author', 'escape="" title=""');
 956      $author    = (!$author)    ? '' : " AND AuthorID $not IN ('".join("','", doSlash(do_list_unique($author)))."')";
 957  
 958      $frontpage = ($frontpage && (!$q || $issticky)) ? filterFrontPage() : '';
 959      $excerpted = (!$excerpted) ? '' : " AND Excerpt !=''";
 960  
 961      if ($time === null || $month || !$expired || $expired == '1') {
 962          $not = $iscustom && ($month || $time !== null) && ($exclude === true || isset($exclude['month']));
 963          $timeq = buildTimeSql($month, $time === null ? 'past' : $time);
 964          $timeq = ' AND '.($not ? "!($timeq)" : $timeq);
 965      } else {
 966          $timeq = '';
 967      }
 968  
 969      if ($expired && $expired != '1') {
 970          $timeq .= ' AND '.buildTimeSql($expired, $time === null && !strtotime($expired) ? 'any' : $time, 'Expires');
 971      } elseif (!$expired) {
 972          $timeq .= ' AND (Expires IS NULL OR '.now('expires').' <= Expires)';
 973      }
 974  
 975      if ($q && $searchsticky) {
 976          $statusq = " AND Status >= ".STATUS_LIVE;
 977      } else {
 978          $statusq = " AND Status IN (".implode(',', $status).")";
 979      }
 980  
 981      $custom = '';
 982  
 983      if ($customFields) {
 984          foreach ($customFields as $cField) {
 985              if (isset($atts[$cField])) {
 986                  $customPairs[$cField] = $atts[$cField];
 987              }
 988  
 989              if (isset($match[$cField])) {
 990                  if ($match[$cField] === false && isset($thisarticle[$cField])) {
 991                      $customPairs[$cField] = $thisarticle[$cField];
 992                  } elseif (($val = gps($match[$cField] === false ? $cField : $match[$cField], false)) !== false) {
 993                      $customPairs[$cField] = $val;
 994                  }
 995              }
 996          }
 997  
 998          if (!empty($customPairs)) {
 999              $custom = buildCustomSql($customFields, $customPairs, $exclude);
1000          }
1001      }
1002  
1003      // Allow keywords for no-custom articles. That tagging mode, you know.
1004      $keywords !== true or $keywords = processTags('keywords');
1005  
1006      if ($keywords) {
1007          $keyparts = array();
1008          $not = $exclude === true || in_array('keywords', $exclude) ? '!' : '';
1009          $keys = doSlash(do_list_unique($keywords));
1010  
1011          foreach ($keys as $key) {
1012              $keyparts[] = "FIND_IN_SET('".$key."', Keywords)";
1013          }
1014  
1015          !$keyparts or $keywords = " AND $not(".join(' or ', $keyparts).")";
1016      }
1017  
1018      $theAtts['status'] = implode(',', $status);
1019      $theAtts['id'] = implode(',', $ids);
1020      $theAtts['sort'] = sanitizeForSort($sort);
1021      $theAtts['*'] = '1'.$timeq.$id.$category.$section.$excerpted.$author.$statusq.$frontpage.$keywords.$custom;
1022  
1023      if (!$iscustom) {
1024          $out = array_diff_key($theAtts, $extralAtts);
1025          $trace->log('[filterAtts accepted]');
1026      }
1027  
1028      return $theAtts;
1029  }
1030  
1031  /**
1032   * Set a flag to postpone tag processing.
1033   *
1034   * @param   int $pass
1035   * @return  null
1036   * @since   4.7.0
1037   * @package TagParser
1038   */
1039  
1040  function postpone_process($pass = null)
1041  {
1042      global $pretext, $txp_atts;
1043  
1044      $txp_atts['txp-process'] = intval($pass === null ? $pretext['secondpass'] + 2 : $pass);
1045  }

title

Description

title

Description

title

Description

title

title

Body