Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/lib/txplib_misc.php - 5823 lines - 159434 bytes - Summary - Text - Print

Description: Collection of miscellaneous tools.

   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   * Collection of miscellaneous tools.
  26   *
  27   * @package Misc
  28   */
  29  
  30  /**
  31   * Strips NULL bytes.
  32   *
  33   * @param  string|array $in The input value
  34   * @return mixed
  35   */
  36  
  37  function deNull($in)
  38  {
  39      return is_array($in) ? doArray($in, 'deNull') : strtr($in, array("\0" => ''));
  40  }
  41  
  42  /**
  43   * Strips carriage returns and linefeeds.
  44   *
  45   * @param  string|array $in The input value
  46   * @return mixed
  47   */
  48  
  49  function deCRLF($in)
  50  {
  51      return is_array($in) ? doArray($in, 'deCRLF') : strtr($in, array(
  52          "\n" => '',
  53          "\r" => '',
  54      ));
  55  }
  56  
  57  /**
  58   * Applies a callback to a given string or an array.
  59   *
  60   * @param  string|array $in       An array or a string to run through the callback function
  61   * @param  callback     $function The callback function
  62   * @return mixed
  63   * @example
  64   * echo doArray(array('value1', 'value2'), 'intval');
  65   */
  66  
  67  function doArray($in, $function)
  68  {
  69      if (is_array($in)) {
  70          return array_map($function, $in);
  71      }
  72  
  73      if (is_array($function)) {
  74          return call_user_func($function, $in);
  75      }
  76  
  77      return $function($in);
  78  }
  79  
  80  /**
  81   * Un-quotes a quoted string or an array of values.
  82   *
  83   * @param  string|array $in The input value
  84   * @return mixed
  85   */
  86  
  87  function doStrip($in)
  88  {
  89      return is_array($in) ? doArray($in, 'doStrip') : doArray($in, 'stripslashes');
  90  }
  91  
  92  /**
  93   * Strips HTML and PHP tags from a string or an array.
  94   *
  95   * @param  string|array $in The input value
  96   * @return mixed
  97   * @example
  98   * echo doStripTags('<p>Hello world!</p>');
  99   */
 100  
 101  function doStripTags($in)
 102  {
 103      return is_array($in) ? doArray($in, 'doStripTags') : doArray($in, 'strip_tags');
 104  }
 105  
 106  /**
 107   * Converts entity escaped brackets back to characters.
 108   *
 109   * @param  string|array $in The input value
 110   * @return mixed
 111   */
 112  
 113  function doDeEnt($in)
 114  {
 115      return doArray($in, 'deEntBrackets');
 116  }
 117  
 118  /**
 119   * Converts entity escaped brackets back to characters.
 120   *
 121   * @param  string $in The input value
 122   * @return string
 123   */
 124  
 125  function deEntBrackets($in)
 126  {
 127      $array = array(
 128          '&#60;'  => '<',
 129          '&lt;'   => '<',
 130          '&#x3C;' => '<',
 131          '&#62;'  => '>',
 132          '&gt;'   => '>',
 133          '&#x3E;' => '>',
 134      );
 135  
 136      foreach ($array as $k => $v) {
 137          $in = preg_replace("/".preg_quote($k)."/i", $v, $in);
 138      }
 139  
 140      return $in;
 141  }
 142  
 143  /**
 144   * Escapes special characters for use in an SQL statement.
 145   *
 146   * Always use this function when dealing with user-defined values in SQL
 147   * statements. If this function is not used to escape user-defined data in a
 148   * statement, the query is vulnerable to SQL injection attacks.
 149   *
 150   * @param   string|array $in The input value
 151   * @return  mixed An array of escaped values or a string depending on $in
 152   * @package DB
 153   * @example
 154   * echo safe_field('column', 'table', "color = '" . doSlash(gps('color')) . "'");
 155   */
 156  
 157  function doSlash($in)
 158  {
 159      return doArray($in, 'safe_escape');
 160  }
 161  
 162  /**
 163   * Escape SQL LIKE pattern's wildcards for use in an SQL statement.
 164   *
 165   * @param   string|array $in The input value
 166   * @return  mixed An array of escaped values or a string depending on $in
 167   * @since   4.6.0
 168   * @package DB
 169   * @example
 170   * echo safe_field('column', 'table', "color LIKE '" . doLike(gps('color')) . "'");
 171   */
 172  
 173  function doLike($in)
 174  {
 175      return doArray($in, 'safe_escape_like');
 176  }
 177  
 178  /**
 179   * A shell for htmlspecialchars() with $flags defaulting to ENT_QUOTES.
 180   *
 181   * @param   string $string The string being converted
 182   * @param   int    $flags A bitmask of one or more flags. The default is ENT_QUOTES
 183   * @param   string $encoding Defines encoding used in conversion. The default is UTF-8
 184   * @param   bool   $double_encode When double_encode is turned off PHP will not encode existing HTML entities, the default is to convert everything
 185   * @return  string
 186   * @see     https://www.php.net/manual/en/function.htmlspecialchars.php
 187   * @since   4.5.0
 188   * @package Filter
 189   */
 190  
 191  function txpspecialchars($string, $flags = ENT_QUOTES, $encoding = 'UTF-8', $double_encode = true)
 192  {
 193      //    Ignore ENT_HTML5 and ENT_XHTML for now.
 194      //    ENT_HTML5 and ENT_XHTML are defined in PHP 5.4+ but we consistently encode single quotes as &#039; in any doctype.
 195      //    global $prefs;
 196      //    static $h5 = null;
 197      //
 198      //    if (defined(ENT_HTML5)) {
 199      //        if ($h5 === null) {
 200      //            $h5 = ($prefs['doctype'] == 'html5' && txpinterface == 'public');
 201      //        }
 202      //
 203      //        if ($h5) {
 204      //            $flags = ($flags | ENT_HTML5) & ~ENT_HTML401;
 205      //        }
 206      //    }
 207      //
 208      return htmlspecialchars($string, $flags, $encoding, $double_encode);
 209  }
 210  
 211  /**
 212   * Converts special characters to HTML entities.
 213   *
 214   * @param   array|string $in The input value
 215   * @return  mixed The array or string with HTML syntax characters escaped
 216   * @package Filter
 217   */
 218  
 219  function doSpecial($in)
 220  {
 221      return doArray($in, 'txpspecialchars');
 222  }
 223  
 224  /**
 225   * Converts the given value to NULL.
 226   *
 227   * @param   mixed $a The input value
 228   * @return  null
 229   * @package Filter
 230   * @access  private
 231   */
 232  
 233  function _null($a)
 234  {
 235      return null;
 236  }
 237  
 238  /**
 239   * Converts an array of values to NULL.
 240   *
 241   * @param   array $in The array
 242   * @return  array
 243   * @package Filter
 244   */
 245  
 246  function array_null($in)
 247  {
 248      return array_map('_null', $in);
 249  }
 250  
 251  /**
 252   * Escapes a page title. Converts &lt;, &gt;, ', " characters to HTML entities.
 253   *
 254   * @param   string $title The input string
 255   * @return  string The string escaped
 256   * @package Filter
 257   */
 258  
 259  function escape_title($title)
 260  {
 261      return strtr($title, array(
 262          '<' => '&#60;',
 263          '>' => '&#62;',
 264          "'" => '&#39;',
 265          '"' => '&#34;',
 266      ));
 267  }
 268  
 269  /**
 270   * Sanitises a string for use in a JavaScript string.
 271   *
 272   * Escapes \, \n, \r, " and ' characters. It removes 'PARAGRAPH SEPARATOR'
 273   * (U+2029) and 'LINE SEPARATOR' (U+2028). When you need to pass a string
 274   * from PHP to JavaScript, use this function to sanitise the value to avoid
 275   * XSS attempts.
 276   *
 277   * @param   string $js JavaScript input
 278   * @return  string Escaped JavaScript
 279   * @since   4.4.0
 280   * @package Filter
 281   */
 282  
 283  function escape_js($js)
 284  {
 285      $js = preg_replace('/[\x{2028}\x{2029}]/u', '', $js);
 286  
 287      return addcslashes($js, "\\\'\"\n\r");
 288  }
 289  
 290  /**
 291   * Escapes CDATA section for an XML document.
 292   *
 293   * @param   string $str The string
 294   * @return  string XML representation wrapped in CDATA tags
 295   * @package XML
 296   */
 297  
 298  function escape_cdata($str)
 299  {
 300      return '<![CDATA['.str_replace(']]>', ']]]><![CDATA[]>', $str).']]>';
 301  }
 302  
 303  /**
 304   * Returns a localisation string.
 305   *
 306   * @param   string $var    String name
 307   * @param   array  $atts   Replacement pairs
 308   * @param   string $escape Convert special characters to HTML entities. Either "html" or ""
 309   * @return  string A localisation string
 310   * @package L10n
 311   */
 312  
 313  function gTxt($var, $atts = array(), $escape = 'html')
 314  {
 315      global $event, $plugin, $txp_current_plugin;
 316      static $txpLang = null;
 317  
 318      if ($txpLang === null) {
 319          $txpLang = Txp::get('\Textpattern\L10n\Lang');
 320          $lang = txpinterface == 'admin' ? get_pref('language_ui', TEXTPATTERN_DEFAULT_LANG) : LANG;
 321          $loaded = $txpLang->load($lang, true);
 322  
 323          if (empty($loaded) || !in_array($event, $loaded)) {
 324              load_lang($lang, $event);
 325          }
 326      }
 327  
 328      // Hackish
 329      if (isset($txp_current_plugin) && isset($plugin['textpack'])) {
 330          $txpLang->loadTextpack($plugin['textpack']);
 331          unset($plugin['textpack']);
 332      }
 333  
 334      return $txpLang->txt($var, $atts, $escape);
 335  }
 336  
 337  /**
 338   * Returns given timestamp in a format of 01 Jan 2001 15:19:16.
 339   *
 340   * @param   int $timestamp The UNIX timestamp
 341   * @return  string A formatted date
 342   * @access  private
 343   * @see     safe_stftime()
 344   * @package DateTime
 345   * @example
 346   * echo gTime();
 347   */
 348  
 349  function gTime($timestamp = 0)
 350  {
 351      return safe_strftime('%d&#160;%b&#160;%Y %X', $timestamp);
 352  }
 353  
 354  /**
 355   * Creates a dumpfile from a backtrace and outputs given parameters.
 356   *
 357   * @package Debug
 358   */
 359  
 360  function dmp()
 361  {
 362      static $f = false;
 363  
 364      if (defined('txpdmpfile')) {
 365          global $prefs;
 366  
 367          if (!$f) {
 368              $f = fopen($prefs['tempdir'].'/'.txpdmpfile, 'a');
 369          }
 370  
 371          $stack = get_caller();
 372          fwrite($f, "\n[".$stack[0].t.safe_strftime('iso8601')."]\n");
 373      }
 374  
 375      $a = func_get_args();
 376  
 377      if (!$f) {
 378          echo "<pre dir=\"auto\">".n;
 379      }
 380  
 381      foreach ($a as $thing) {
 382          $out = is_scalar($thing) ? strval($thing) : var_export($thing, true);
 383  
 384          if ($f) {
 385              fwrite($f, $out.n);
 386          } else {
 387              echo txpspecialchars($out).n;
 388          }
 389      }
 390  
 391      if (!$f) {
 392          echo "</pre>".n;
 393      }
 394  }
 395  
 396  /**
 397   * Gets the given language's strings from the database.
 398   *
 399   * Fetches the given language from the database and returns the strings
 400   * as an array.
 401   *
 402   * If no $events is specified, only appropriate strings for the current context
 403   * are returned. If 'txpinterface' constant equals 'admin' all strings are
 404   * returned. Otherwise, only strings from events 'common' and 'public'.
 405   *
 406   * If $events is FALSE, returns all strings.
 407   *
 408   * @param   string            $lang   The language code
 409   * @param   array|string|bool $events An array of loaded events
 410   * @return  array
 411   * @package L10n
 412   * @example
 413   * print_r(
 414   *     load_lang('en-gb', false)
 415   * );
 416   */
 417  
 418  function load_lang($lang, $events = null)
 419  {
 420      global $production_status, $event, $textarray;
 421  
 422      isset($textarray) or $textarray = array();
 423      $textarray = array_merge($textarray, Txp::get('\Textpattern\L10n\Lang')->load($lang, $events));
 424  
 425      if (($production_status !== 'live' || $event === 'diag')
 426          && @$debug = parse_ini_file(txpath.DS.'mode.ini')
 427      ) {
 428          $textarray += (array)$debug;
 429          Txp::get('\Textpattern\L10n\Lang')->setPack($textarray);
 430      }
 431  
 432      return $textarray;
 433  }
 434  
 435  /**
 436   * Gets a list of user groups.
 437   *
 438   * @return  array
 439   * @package User
 440   * @example
 441   * print_r(
 442   *     get_groups()
 443   * );
 444   */
 445  
 446  function get_groups()
 447  {
 448      global $txp_groups;
 449  
 450      return doArray($txp_groups, 'gTxt');
 451  }
 452  
 453  /**
 454   * Checks if a user has privileges to the given resource.
 455   *
 456   * @param   string $res  The resource
 457   * @param   mixed  $user The user. If no user name is supplied, assume the current logged in user
 458   * @return  bool
 459   * @package User
 460   * @example
 461   * add_privs('my_privilege_resource', '1,2,3');
 462   * if (has_privs('my_privilege_resource', 'username'))
 463   * {
 464   *     echo "'username' has privileges to 'my_privilege_resource'.";
 465   * }
 466   */
 467  
 468  function has_privs($res = null, $user = '')
 469  {
 470      global $txp_user, $txp_permissions;
 471      static $privs;
 472  
 473      if (is_array($user)) {
 474          $level = isset($user['privs']) ? $user['privs'] : null;
 475          $user = isset($user['name']) ? $user['name'] : '';
 476      }
 477  
 478      $user = (string) $user;
 479  
 480      if ($user === '') {
 481          $user = (string) $txp_user;
 482      }
 483  
 484      if ($user !== '') {
 485          if (!isset($privs[$user])) {
 486              $privs[$user] = isset($level) ?
 487                  $level :
 488                  safe_field("privs", 'txp_users', "name = '".doSlash($user)."'");
 489          }
 490  
 491          if (!isset($res)) {
 492              return $privs[$user];
 493          } elseif (isset($txp_permissions[$res]) && $privs[$user] && $txp_permissions[$res]) {
 494              return in_list($privs[$user], $txp_permissions[$res]);
 495          }
 496      }
 497  
 498      return false;
 499  }
 500  
 501  /**
 502   * Adds dynamic privileges.
 503   *
 504   * @param   array $pluggable The array, see global $txp_options
 505   * @since   4.7.2
 506   * @package User
 507   */
 508  
 509  function plug_privs($pluggable = null, $user = null)
 510  {
 511      global $txp_options;
 512  
 513      isset($pluggable) or $pluggable = $txp_options;
 514      $level = isset($user['privs']) ? $user['privs'] : has_privs();
 515  
 516      foreach ((array)$pluggable as $pref => $pane) {
 517          if (is_array($pane)) {
 518              if (isset($pane[0])) {
 519                  if (!in_list($level, $pane[0])) {
 520                      return;
 521                  }
 522  
 523                  unset($pane[0]);
 524              }
 525          } else {
 526              $pane = array('prefs.'.$pref => $pane);
 527          }
 528  
 529          array_walk($pane, function (&$item) use ($level) {
 530              if ($item === true) {
 531                  $item = $level;
 532              }
 533          });
 534  
 535          if (get_pref($pref)) {
 536              add_privs($pane);
 537          } else {
 538              add_privs(array_fill_keys(array_keys($pane), null));
 539          }
 540      }
 541  }
 542  
 543  /**
 544   * Grants privileges to user-groups.
 545   *
 546   * Will not let you override existing privs.
 547   *
 548   * @param   mixed  $res  The resource
 549   * @param   string $perm List of user-groups, e.g. '1,2,3'
 550   * @package User
 551   * @example
 552   * add_privs('my_admin_side_panel_event', '1,2,3,4,5');
 553   */
 554  
 555  function add_privs($res, $perm = '1')
 556  {
 557      global $txp_permissions;
 558  
 559      if (!is_array($res)) {
 560          $res = array($res => $perm);
 561      }
 562  
 563      foreach ($res as $priv => $group) {
 564          if ($group === null) {
 565              unset($txp_permissions[$priv]);
 566          } else {
 567              $group .= (empty($txp_permissions[$priv]) ? '' : ','.$txp_permissions[$priv]);
 568              $group = join(',', do_list_unique($group));
 569              $txp_permissions[$priv] = $group;
 570          }
 571      }
 572  }
 573  
 574  /**
 575   * Require privileges from a user to the given resource.
 576   *
 577   * Terminates the script if user doesn't have required privileges.
 578   *
 579   * @param   string|null $res  The resource, or NULL
 580   * @param   string      $user The user. If no user name is supplied, assume the current logged in user
 581   * @package User
 582   * @example
 583   * require_privs('article.edit');
 584   */
 585  
 586  function require_privs($res = null, $user = '')
 587  {
 588      if ($res === null || !has_privs($res, $user)) {
 589          pagetop(gTxt('restricted_area'));
 590          echo graf(gTxt('restricted_area'), array('class' => 'restricted-area'));
 591          end_page();
 592          exit;
 593      }
 594  }
 595  
 596  /**
 597   * Gets a list of users having access to a resource.
 598   *
 599   * @param   string $res The resource, e.g. 'article.edit.published'
 600   * @return  array  A list of usernames
 601   * @since   4.5.0
 602   * @package User
 603   */
 604  
 605  function the_privileged($res, $real = false)
 606  {
 607      global $txp_permissions;
 608  
 609      $out = array();
 610  
 611      if (isset($txp_permissions[$res])) {
 612          foreach (safe_rows("name, RealName", 'txp_users', "FIND_IN_SET(privs, '".$txp_permissions[$res]."') ORDER BY ".($real ? "RealName" : "name")." ASC") as $user) {
 613              extract($user);
 614              $out[$name] = $real ? $RealName : $name;
 615          }
 616      }
 617  
 618      return $out;
 619  }
 620  
 621  /**
 622   * Gets the dimensions of an image for a HTML &lt;img&gt; tag.
 623   *
 624   * @param   string      $name The filename
 625   * @return  string|bool height="100" width="40", or FALSE on failure
 626   * @package Image
 627   * @example
 628   * if ($size = sizeImage('/path/to/image.png'))
 629   * {
 630   *     echo "&lt;img src='image.png' {$size} /&gt;";
 631   * }
 632   */
 633  
 634  function sizeImage($name)
 635  {
 636      $size = @getimagesize($name);
 637  
 638      return is_array($size) ? $size[3] : false;
 639  }
 640  
 641  /**
 642   * Gets an image as an array.
 643   *
 644   * @param   int $id image ID
 645   * @param   string $name image name
 646   * @return  array|bool An image data array, or FALSE on failure
 647   * @package Image
 648   * @example
 649   * if ($image = imageFetchInfo($id))
 650   * {
 651   *     print_r($image);
 652   * }
 653   */
 654  
 655  function imageFetchInfo($id = "", $name = "")
 656  {
 657      global $thisimage, $p;
 658      static $cache = array();
 659  
 660      if ($id) {
 661          if (isset($cache['i'][$id])) {
 662              return $cache['i'][$id];
 663          } else {
 664              $where = 'id = '.intval($id).' LIMIT 1';
 665          }
 666      } elseif ($name) {
 667          if (isset($cache['n'][$name])) {
 668              return $cache['n'][$name];
 669          } else {
 670              $where = "name = '".doSlash($name)."' LIMIT 1";
 671          }
 672      } elseif ($thisimage) {
 673          $id = (int) $thisimage['id'];
 674          return $cache['i'][$id] = $thisimage;
 675      } elseif ($p) {
 676          if (isset($cache['i'][$p])) {
 677              return $cache['i'][$p];
 678          } else {
 679              $where = 'id = '.intval($p).' LIMIT 1';
 680          }
 681      } else {
 682          assert_image();
 683          return false;
 684      }
 685  
 686      $rs = safe_row("*", 'txp_image', $where);
 687  
 688      if ($rs) {
 689          $id = (int) $rs['id'];
 690          return $cache['i'][$id] = image_format_info($rs);
 691      } else {
 692          trigger_error(gTxt('unknown_image'));
 693      }
 694  
 695      return false;
 696  }
 697  
 698  /**
 699   * Formats image info.
 700   *
 701   * Takes an image data array generated by imageFetchInfo() and formats the contents.
 702   *
 703   * @param   array $image The image
 704   * @return  array
 705   * @see     imageFetchInfo()
 706   * @access  private
 707   * @package Image
 708   */
 709  
 710  function image_format_info($image)
 711  {
 712      if (($unix_ts = @strtotime($image['date'])) > 0) {
 713          $image['date'] = $unix_ts;
 714      }
 715  
 716      return $image;
 717  }
 718  
 719  /**
 720   * Formats link info.
 721   *
 722   * @param   array $link The link to format
 723   * @return  array Formatted link data
 724   * @access  private
 725   * @package Link
 726   */
 727  
 728  function link_format_info($link)
 729  {
 730      if (($unix_ts = @strtotime($link['date'])) > 0) {
 731          $link['date'] = $unix_ts;
 732      }
 733  
 734      return $link;
 735  }
 736  
 737  /**
 738   * Gets a HTTP GET or POST parameter.
 739   *
 740   * Internally strips CRLF from GET parameters and removes NULL bytes.
 741   *
 742   * @param   string $thing The parameter to get
 743   * @return  string|array The value of $thing, or an empty string
 744   * @package Network
 745   * @example
 746   * if (gps('sky') == 'blue' && gps('roses') == 'red')
 747   * {
 748   *     echo 'Roses are red, sky is blue.';
 749   * }
 750   */
 751  
 752  function gps($thing, $default = '')
 753  {
 754      global $pretext;
 755  
 756      if (isset($_GET[$thing])) {
 757          $out = $_GET[$thing];
 758          $out = doArray($out, 'deCRLF');
 759      } elseif (isset($_POST[$thing])) {
 760          $out = $_POST[$thing];
 761      } elseif (is_numeric($thing) && isset($pretext[abs($thing)])) {
 762          $thing >= 0 or $thing += $pretext[0] + 1;
 763          $out = $pretext[$thing];
 764      } else {
 765          return $default;
 766      }
 767  
 768      $out = doArray($out, 'deNull');
 769  
 770      return $out;
 771  }
 772  
 773  /**
 774   * Gets an array of HTTP GET or POST parameters.
 775   *
 776   * @param   array $array The parameters to extract
 777   * @return  array
 778   * @package Network
 779   * @example
 780   * extract(gpsa(array('sky', 'roses'));
 781   * if ($sky == 'blue' && $roses == 'red')
 782   * {
 783   *     echo 'Roses are red, sky is blue.';
 784   * }
 785   */
 786  
 787  function gpsa($array)
 788  {
 789      if (is_array($array)) {
 790          $out = array();
 791  
 792          foreach ($array as $a) {
 793              $out[$a] = gps($a);
 794          }
 795  
 796          return $out;
 797      }
 798  
 799      return false;
 800  }
 801  
 802  /**
 803   * Gets a HTTP POST parameter.
 804   *
 805   * Internally removes NULL bytes.
 806   *
 807   * @param   string $thing The parameter to get
 808   * @return  string|array The value of $thing, or an empty string
 809   * @package Network
 810   * @example
 811   * if (ps('sky') == 'blue' && ps('roses') == 'red')
 812   * {
 813   *     echo 'Roses are red, sky is blue.';
 814   * }
 815   */
 816  
 817  function ps($thing)
 818  {
 819      $out = '';
 820  
 821      if (isset($_POST[$thing])) {
 822          $out = $_POST[$thing];
 823      }
 824  
 825      $out = doArray($out, 'deNull');
 826  
 827      return $out;
 828  }
 829  
 830  /**
 831   * Gets an array of HTTP POST parameters.
 832   *
 833   * @param   array $array The parameters to extract
 834   * @return  array
 835   * @package Network
 836   * @example
 837   * extract(psa(array('sky', 'roses'));
 838   * if ($sky == 'blue' && $roses == 'red')
 839   * {
 840   *     echo 'Roses are red, sky is blue.';
 841   * }
 842   */
 843  
 844  function psa($array)
 845  {
 846      foreach ($array as $a) {
 847          $out[$a] = ps($a);
 848      }
 849  
 850      return $out;
 851  }
 852  
 853  /**
 854   * Gets an array of HTTP POST parameters and strips HTML and PHP tags
 855   * from values.
 856   *
 857   * @param   array $array The parameters to extract
 858   * @return  array
 859   * @package Network
 860   */
 861  
 862  function psas($array)
 863  {
 864      foreach ($array as $a) {
 865          $out[$a] = doStripTags(ps($a));
 866      }
 867  
 868      return $out;
 869  }
 870  
 871  /**
 872   * Gets all received HTTP POST parameters.
 873   *
 874   * @return  array
 875   * @package Network
 876   */
 877  
 878  function stripPost()
 879  {
 880      if (isset($_POST)) {
 881          return $_POST;
 882      }
 883  
 884      return '';
 885  }
 886  
 887  /**
 888   * Gets a variable from $_SERVER global array.
 889   *
 890   * @param   mixed $thing The variable
 891   * @return  mixed The variable, or an empty string on error
 892   * @package System
 893   * @example
 894   * echo serverSet('HTTP_USER_AGENT');
 895   */
 896  
 897  function serverSet($thing)
 898  {
 899      return (isset($_SERVER[$thing])) ? $_SERVER[$thing] : '';
 900  }
 901  
 902  /**
 903   * Gets the client's IP address.
 904   *
 905   * Supports proxies and uses 'X_FORWARDED_FOR' HTTP header if deemed necessary.
 906   *
 907   * @return  string
 908   * @package Network
 909   * @example
 910   * if ($ip = remote_addr())
 911   * {
 912   *     echo "Your IP address is: {$ip}.";
 913   * }
 914   */
 915  
 916  function remote_addr()
 917  {
 918      $ip = serverSet('REMOTE_ADDR');
 919  
 920      if (($ip == '127.0.0.1' || $ip == '::1' || $ip == '::ffff:127.0.0.1' || $ip == serverSet('SERVER_ADDR')) && serverSet('HTTP_X_FORWARDED_FOR')) {
 921          $ips = explode(', ', serverSet('HTTP_X_FORWARDED_FOR'));
 922          $ip = $ips[0];
 923      }
 924  
 925      return $ip;
 926  }
 927  
 928  /**
 929   * Gets a variable from HTTP POST or a prefixed cookie.
 930   *
 931   * Fetches either a HTTP cookie of the given name prefixed with
 932   * 'txp_', or a HTTP POST parameter without a prefix.
 933   *
 934   * @param   string $thing The variable
 935   * @return  array|string The variable or an empty string
 936   * @package Network
 937   * @example
 938   * if ($cs = psc('myVariable'))
 939   * {
 940   *     echo "'txp_myVariable' cookie or 'myVariable' POST parameter contained: '{$cs}'.";
 941   * }
 942   */
 943  
 944  function pcs($thing)
 945  {
 946      if (isset($_COOKIE["txp_".$thing])) {
 947          return $_COOKIE["txp_".$thing];
 948      } elseif (isset($_POST[$thing])) {
 949          return $_POST[$thing];
 950      }
 951  
 952      return '';
 953  }
 954  
 955  /**
 956   * Gets a HTTP cookie.
 957   *
 958   * @param   string $thing The cookie
 959   * @return  string The cookie or an empty string
 960   * @package Network
 961   * @example
 962   * if ($cs = cs('myVariable'))
 963   * {
 964   *     echo "'myVariable' cookie contained: '{$cs}'.";
 965   * }
 966   */
 967  
 968  function cs($thing)
 969  {
 970      if (isset($_COOKIE[$thing])) {
 971          return $_COOKIE[$thing];
 972      }
 973  
 974      return '';
 975  }
 976  
 977  /**
 978   * Sets a HTTP cookie (polyfill).
 979   *
 980   * @param   string $name The cookie name
 981   * @param   string $value The cookie value
 982   * @param   array  $options The cookie options
 983   * @package Network
 984   */
 985  
 986  function set_cookie($name, $value = '', $options = array())
 987  {
 988      $options += array (
 989          'expires' => time() - 3600,
 990          'path' => '',
 991          'domain' => '',
 992          'secure' => false,
 993          'httponly' => false,
 994          'samesite' => 'Lax' // None || Lax  || Strict
 995      );
 996  
 997      if (version_compare(phpversion(), '7.3.0') >= 0) {
 998          return setcookie($name, $value, $options);
 999      }
1000  
1001      extract($options);
1002  
1003      return setcookie($name, $value, $expires, $path.'; samesite='.$samesite, $domain, $secure, $httponly);
1004  }
1005  
1006  /**
1007   * Converts a boolean to a localised "Yes" or "No" string.
1008   *
1009   * @param   bool $status The boolean. Ignores type and as such can also take a string or an integer
1010   * @return  string No if FALSE, Yes otherwise
1011   * @package L10n
1012   * @example
1013   * echo yes_no(3 * 3 === 2);
1014   */
1015  
1016  function yes_no($status)
1017  {
1018      return ($status) ? gTxt('yes') : gTxt('no');
1019  }
1020  
1021  /**
1022   * Gets UNIX timestamp with microseconds.
1023   *
1024   * @return  float
1025   * @package DateTime
1026   * @example
1027   * echo getmicrotime();
1028   */
1029  
1030  function getmicrotime()
1031  {
1032      list($usec, $sec) = explode(" ", microtime());
1033  
1034      return ((float) $usec + (float) $sec);
1035  }
1036  
1037  /**
1038   * Loads the given plugin or checks if it was loaded.
1039   *
1040   * @param  string $name  The plugin
1041   * @param  bool   $force If TRUE loads the plugin even if it's disabled
1042   * @return bool TRUE if the plugin is loaded
1043   * @example
1044   * if (load_plugin('abc_plugin'))
1045   * {
1046   *     echo "'abc_plugin' is active.";
1047   * }
1048   */
1049  
1050  function load_plugin($name, $force = false)
1051  {
1052      global $plugin, $plugins, $plugins_ver, $prefs, $txp_current_plugin, $textarray;
1053  
1054      if (is_array($plugins) && in_array($name, $plugins)) {
1055          return true;
1056      }
1057  
1058      if (!empty($prefs['plugin_cache_dir'])) {
1059          $dir = rtrim($prefs['plugin_cache_dir'], '/').'/';
1060  
1061          // In case it's a relative path.
1062          if (!is_dir($dir)) {
1063              $dir = rtrim(realpath(txpath.'/'.$dir), '/').'/';
1064          }
1065  
1066          if (is_file($dir.$name.'.php')) {
1067              $plugins[] = $name;
1068              $old_plugin = isset($plugin) ? $plugin : null;
1069              set_error_handler("pluginErrorHandler");
1070  
1071              if (isset($txp_current_plugin)) {
1072                  $txp_parent_plugin = $txp_current_plugin;
1073              }
1074  
1075              $txp_current_plugin = $name;
1076              include $dir.$name.'.php';
1077              $txp_current_plugin = isset($txp_parent_plugin) ? $txp_parent_plugin : null;
1078              $plugins_ver[$name] = isset($plugin['version']) ? $plugin['version'] : 0;
1079  
1080              if (isset($plugin['textpack'])) {
1081                  Txp::get('\Textpattern\L10n\Lang')->loadTextpack($plugin['textpack']);
1082              }
1083  
1084              restore_error_handler();
1085              $plugin = $old_plugin;
1086  
1087              return true;
1088          }
1089      }
1090  
1091      $version = safe_field("version", 'txp_plugin', ($force ? '' : "status = 1 AND ")."name = '".doSlash($name)."'");
1092  
1093      if ($version !== false) {
1094          $plugins[] = $name;
1095          $plugins_ver[$name] = $version;
1096          set_error_handler("pluginErrorHandler");
1097  
1098          if (isset($txp_current_plugin)) {
1099              $txp_parent_plugin = $txp_current_plugin;
1100          }
1101  
1102          $txp_current_plugin = $name;
1103          $dir = sanitizeForFile($name);
1104          $filename = PLUGINPATH.DS.$dir.DS.$dir.'.php';
1105  
1106          if (!is_file($filename)) {
1107              $code = safe_field("code", 'txp_plugin', "name = '".doSlash($name)."'");
1108              \Txp::get('\Textpattern\Plugin\Plugin')->updateFile($txp_current_plugin, $code);
1109          }
1110  
1111          $ok = @include_once($filename);
1112          $txp_current_plugin = isset($txp_parent_plugin) ? $txp_parent_plugin : null;
1113          restore_error_handler();
1114  
1115          return $ok;
1116      }
1117  
1118      return false;
1119  }
1120  
1121  /**
1122   * Loads a plugin.
1123   *
1124   * Identical to load_plugin() except upon failure it issues an E_USER_ERROR.
1125   *
1126   * @param  string $name The plugin
1127   * @return bool
1128   * @see    load_plugin()
1129   */
1130  
1131  function require_plugin($name)
1132  {
1133      if (!load_plugin($name)) {
1134          trigger_error(gTxt('plugin_include_error', array('{name}' => $name)), E_USER_ERROR);
1135  
1136          return false;
1137      }
1138  
1139      return true;
1140  }
1141  
1142  /**
1143   * Loads a plugin.
1144   *
1145   * Identical to load_plugin() except upon failure it issues an E_USER_WARNING.
1146   *
1147   * @param  string $name The plugin
1148   * @return bool
1149   * @see    load_plugin()
1150   */
1151  
1152  function include_plugin($name)
1153  {
1154      if (!load_plugin($name)) {
1155          trigger_error(gTxt('plugin_include_error', array('{name}' => $name)), E_USER_WARNING);
1156  
1157          return false;
1158      }
1159  
1160      return true;
1161  }
1162  
1163  /**
1164   * Error handler for plugins.
1165   *
1166   * @param   int    $errno
1167   * @param   string $errstr
1168   * @param   string $errfile
1169   * @param   int    $errline
1170   * @access  private
1171   * @package Debug
1172   */
1173  
1174  function pluginErrorHandler($errno, $errstr, $errfile, $errline)
1175  {
1176      global $production_status, $txp_current_plugin;
1177  
1178      $error = array();
1179  
1180      if ($production_status == 'testing') {
1181          $error = array(
1182              E_WARNING           => 'Warning',
1183              E_RECOVERABLE_ERROR => 'Catchable fatal error',
1184              E_USER_ERROR        => 'User_Error',
1185              E_USER_WARNING      => 'User_Warning',
1186          );
1187      } elseif ($production_status == 'debug') {
1188          $error = array(
1189              E_WARNING           => 'Warning',
1190              E_NOTICE            => 'Notice',
1191              E_RECOVERABLE_ERROR => 'Catchable fatal error',
1192              E_USER_ERROR        => 'User_Error',
1193              E_USER_WARNING      => 'User_Warning',
1194              E_USER_NOTICE       => 'User_Notice',
1195          );
1196  
1197          if (!isset($error[$errno])) {
1198              $error[$errno] = $errno;
1199          }
1200      }
1201  
1202      if (!isset($error[$errno]) || !error_reporting()) {
1203          return;
1204      }
1205  
1206      printf(
1207          '<pre dir="auto">'.gTxt('plugin_load_error').' <b>%s</b> -> <b>%s: %s on line %s</b></pre>',
1208          $txp_current_plugin,
1209          $error[$errno],
1210          $errstr,
1211          $errline
1212      );
1213  
1214      if ($production_status == 'debug') {
1215          print "\n<pre class=\"backtrace\" dir=\"ltr\"><code>".txpspecialchars(join("\n", get_caller(10)))."</code></pre>";
1216      }
1217  }
1218  
1219  /**
1220   * Error handler for page templates.
1221   *
1222   * @param   int    $errno
1223   * @param   string $errstr
1224   * @param   string $errfile
1225   * @param   int    $errline
1226   * @access  private
1227   * @package Debug
1228   */
1229  
1230  function tagErrorHandler($errno, $errstr, $errfile, $errline)
1231  {
1232      global $production_status, $txp_current_tag, $txp_current_form, $pretext, $trace;
1233  
1234      $error = array();
1235  
1236      if ($production_status == 'testing') {
1237          $error = array(
1238              E_WARNING           => 'Warning',
1239              E_RECOVERABLE_ERROR => 'Textpattern Catchable fatal error',
1240              E_USER_ERROR        => 'Textpattern Error',
1241              E_USER_WARNING      => 'Textpattern Warning',
1242          );
1243      } elseif ($production_status == 'debug') {
1244          $error = array(
1245              E_WARNING           => 'Warning',
1246              E_NOTICE            => 'Notice',
1247              E_RECOVERABLE_ERROR => 'Textpattern Catchable fatal error',
1248              E_USER_ERROR        => 'Textpattern Error',
1249              E_USER_WARNING      => 'Textpattern Warning',
1250              E_USER_NOTICE       => 'Textpattern Notice',
1251          );
1252  
1253          if (!isset($error[$errno])) {
1254              $error[$errno] = $errno;
1255          }
1256      }
1257  
1258      if (!isset($error[$errno]) || !error_reporting()) {
1259          return;
1260      }
1261  
1262      if (empty($pretext['page'])) {
1263          $page = gTxt('none');
1264      } else {
1265          $page = $pretext['page'];
1266      }
1267  
1268      if (!isset($txp_current_form)) {
1269          $txp_current_form = gTxt('none');
1270      }
1271  
1272      $locus = gTxt('while_parsing_page_form', array(
1273          '{page}' => $page,
1274          '{form}' => $txp_current_form,
1275      ));
1276  
1277      printf(
1278          "<pre dir=\"auto\">".gTxt('tag_error').' <b>%s</b> -> <b> %s: %s %s</b></pre>',
1279          txpspecialchars($txp_current_tag),
1280          $error[$errno],
1281          $errstr,
1282          $locus
1283      );
1284  
1285      if ($production_status == 'debug') {
1286          print "\n<pre class=\"backtrace\" dir=\"ltr\"><code>".txpspecialchars(join("\n", get_caller(10)))."</code></pre>";
1287  
1288          $trace->log(gTxt('tag_error').' '.$txp_current_tag.' -> '.$error[$errno].': '.$errstr.' '.$locus);
1289      }
1290  }
1291  
1292  /**
1293   * Error handler for XML feeds.
1294   *
1295   * @param   int    $errno
1296   * @param   string $errstr
1297   * @param   string $errfile
1298   * @param   int    $errline
1299   * @access  private
1300   * @package Debug
1301   */
1302  
1303  function feedErrorHandler($errno, $errstr, $errfile, $errline)
1304  {
1305      global $production_status;
1306  
1307      if ($production_status != 'debug') {
1308          return;
1309      }
1310  
1311      return tagErrorHandler($errno, $errstr, $errfile, $errline);
1312  }
1313  
1314  /**
1315   * Error handler for public-side.
1316   *
1317   * @param   int    $errno
1318   * @param   string $errstr
1319   * @param   string $errfile
1320   * @param   int    $errline
1321   * @access  private
1322   * @package Debug
1323   */
1324  
1325  function publicErrorHandler($errno, $errstr, $errfile, $errline)
1326  {
1327      global $production_status;
1328  
1329      $error = array();
1330  
1331      if ($production_status == 'testing') {
1332          $error = array(
1333              E_WARNING      => 'Warning',
1334              E_USER_ERROR   => 'Textpattern Error',
1335              E_USER_WARNING => 'Textpattern Warning',
1336          );
1337      } elseif ($production_status == 'debug') {
1338          $error = array(
1339              E_WARNING      => 'Warning',
1340              E_NOTICE       => 'Notice',
1341              E_USER_ERROR   => 'Textpattern Error',
1342              E_USER_WARNING => 'Textpattern Warning',
1343              E_USER_NOTICE  => 'Textpattern Notice',
1344          );
1345  
1346          if (!isset($error[$errno])) {
1347              $error[$errno] = $errno;
1348          }
1349      }
1350  
1351      if (!isset($error[$errno]) || !error_reporting()) {
1352          return;
1353      }
1354  
1355      printf(
1356          "<pre dir=\"auto\">".gTxt('general_error').' <b>%s: %s on line %s</b></pre>',
1357          $error[$errno],
1358          $errstr,
1359          $errline
1360      );
1361  
1362      if ($production_status == 'debug') {
1363          print "\n<pre class=\"backtrace\" dir=\"ltr\"><code>".txpspecialchars(join("\n", get_caller(10)))."</code></pre>";
1364      }
1365  }
1366  
1367  /**
1368   * Loads plugins.
1369   *
1370   * @param bool $type If TRUE loads admin-side plugins, otherwise public
1371   */
1372  
1373  function load_plugins($type = false, $pre = null)
1374  {
1375      global $prefs, $plugins, $plugins_ver, $app_mode, $trace;
1376      static $rs = null;
1377  
1378      if (!is_array($plugins)) {
1379          $plugins = array();
1380      }
1381  
1382      $trace->start('[Loading plugins]');
1383  
1384      if (!empty($prefs['plugin_cache_dir'])) {
1385          $dir = rtrim($prefs['plugin_cache_dir'], '/').'/';
1386  
1387          // In case it's a relative path.
1388          if (!is_dir($dir)) {
1389              $dir = rtrim(realpath(txpath.'/'.$dir), '/').'/';
1390          }
1391  
1392          $files = glob($dir.'*.php');
1393  
1394          if ($files) {
1395              natsort($files);
1396  
1397              foreach ($files as $f) {
1398                  $trace->start("[Loading plugin from cache dir: '$f']");
1399                  load_plugin(basename($f, '.php'));
1400                  $trace->stop();
1401              }
1402          }
1403      }
1404  
1405      if (!isset($rs)) {
1406          $admin = ($app_mode == 'async' ? '4,5' : '1,3,4,5');
1407          $where = 'status = 1 AND type IN ('.($type ? $admin : '0,1,5').')'.
1408              ($plugins ? ' AND name NOT IN ('.join(',', quote_list($plugins)).')' : '');
1409  
1410          $rs = safe_rows("name, version, load_order", 'txp_plugin', $where." ORDER BY load_order ASC, name ASC");
1411      }
1412  
1413      if ($rs) {
1414          $old_error_handler = set_error_handler("pluginErrorHandler");
1415          $pre = intval($pre);
1416  
1417          $writable = is_dir(PLUGINPATH) && is_writable(PLUGINPATH);
1418  
1419          foreach ($rs as $a) {
1420              if (!isset($plugins_ver[$a['name']]) && (!$pre || $a['load_order'] < $pre)) {
1421                  $plugins[] = $a['name'];
1422                  $plugins_ver[$a['name']] = $a['version'];
1423                  $GLOBALS['txp_current_plugin'] = $a['name'];
1424                  $trace->start("[Loading plugin: '{$a['name']}' version '{$a['version']}']");
1425  
1426                  $dir = $a['name'];
1427                  $filename = PLUGINPATH.DS.$dir.DS.$dir.'.php';
1428  
1429                  if ($writable && !is_file($filename)) {
1430                      $code = safe_field('code', 'txp_plugin', "name='".doSlash($a['name'])."'");
1431                      \Txp::get('\Textpattern\Plugin\Plugin')->updateFile($a['name'], $code);
1432                  }
1433  
1434                  $eval_ok = @include($filename);
1435                  $trace->stop();
1436  
1437                  if ($eval_ok === false) {
1438                      trigger_error(gTxt('plugin_include_error', array('{name}' => $a['name'])), E_USER_WARNING);
1439                  }
1440  
1441                  unset($GLOBALS['txp_current_plugin']);
1442              }
1443          }
1444  
1445          restore_error_handler();
1446      }
1447  
1448      $trace->stop();
1449  }
1450  
1451  /**
1452   * Attaches a handler to a callback event.
1453   *
1454   * @param   callback $func  The callback function
1455   * @param   string   $event The callback event
1456   * @param   string   $step  The callback step
1457   * @param   bool     $pre   Before or after. Works only with selected callback events
1458   * @package Callback
1459   * @example
1460   * register_callback('my_callback_function', 'article.updated');
1461   * function my_callback_function($event)
1462   * {
1463   *     return "'$event' fired.";
1464   * }
1465   */
1466  
1467  function register_callback($func, $event, $step = '', $pre = 0)
1468  {
1469      global $plugin_callback;
1470  
1471      $plugin_callback[] = array(
1472          'function' => $func,
1473          'event'    => $event,
1474          'step'     => $step,
1475          'pre'      => $pre,
1476      );
1477  }
1478  
1479  /**
1480   * Call an event's callback.
1481   *
1482   * Executes all callback handlers attached to the matched event and step.
1483   *
1484   * When called, any event handlers attached with register_callback() to the
1485   * matching event, step and pre will be called. The handlers, callback
1486   * functions, will be executed in the same order they were registered.
1487   *
1488   * Any extra arguments will be passed to the callback handlers in the same
1489   * argument position. This allows passing any type of data to the attached
1490   * handlers. Callback handlers will also receive the event and the step.
1491   *
1492   * Returns a combined value of all values returned by the callback handlers.
1493   *
1494   * @param   string         $event The callback event
1495   * @param   string         $step  Additional callback step
1496   * @param   bool|int|array $pre   Allows two callbacks, a prepending and an appending, with same event and step. Array allows return values chaining
1497   * @return  mixed  The value returned by the attached callback functions, or an empty string
1498   * @package Callback
1499   * @see     register_callback()
1500   * @example
1501   * register_callback('my_callback_function', 'my_custom_event');
1502   * function my_callback_function($event, $step, $extra)
1503   * {
1504   *     return "Passed '$extra' on '$event'.";
1505   * }
1506   * echo callback_event('my_custom_event', '', 0, 'myExtraValue');
1507   */
1508  
1509  function callback_event($event, $step = '', $pre = 0)
1510  {
1511      global $plugin_callback, $production_status, $trace;
1512  
1513      if (!is_array($plugin_callback)) {
1514          return '';
1515      }
1516  
1517      list($pre, $renew) = (array)$pre + array(0, null);
1518      $trace->start("[Callback_event: '$event', step='$step', pre='$pre']");
1519  
1520      // Any payload parameters?
1521      $argv = func_get_args();
1522      $argv = (count($argv) > 3) ? array_slice($argv, 3) : array();
1523  
1524      foreach ($plugin_callback as $c) {
1525          if ($c['event'] == $event && (empty($c['step']) || $c['step'] == $step) && $c['pre'] == $pre) {
1526              if (is_callable($c['function'])) {
1527                  if ($production_status !== 'live') {
1528                      $trace->start("\t[Call function: '".Txp::get('\Textpattern\Type\TypeCallable', $c['function'])->toString()."'".
1529                          (empty($argv) ? '' : ", argv='".serialize($argv)."'")."]");
1530                  }
1531  
1532                  $return_value = call_user_func_array($c['function'], array_merge(array(
1533                      $event,
1534                      $step
1535                  ), $argv));
1536  
1537                  if (isset($renew)) {
1538                      $argv[$renew] = $return_value;
1539                  }
1540  
1541                  if (isset($out) && !isset($renew)) {
1542                      if (is_array($return_value) && is_array($out)) {
1543                          $out = array_merge($out, $return_value);
1544                      } elseif (is_bool($return_value) && is_bool($out)) {
1545                          $out = $return_value && $out;
1546                      } else {
1547                          $out .= $return_value;
1548                      }
1549                  } else {
1550                      $out = $return_value;
1551                  }
1552  
1553                  if ($production_status !== 'live') {
1554                      $trace->stop();
1555                  }
1556              } elseif ($production_status === 'debug') {
1557                  trigger_error(gTxt('unknown_callback_function', array('{function}' => Txp::get('\Textpattern\Type\TypeCallable', $c['function'])->toString())), E_USER_WARNING);
1558              }
1559          }
1560      }
1561  
1562      $trace->stop();
1563  
1564      if (isset($out)) {
1565          return $out;
1566      }
1567  
1568      return '';
1569  }
1570  
1571  /**
1572   * Call an event's callback with two optional byref parameters.
1573   *
1574   * @param   string $event   The callback event
1575   * @param   string $step    Optional callback step
1576   * @param   bool   $pre     Allows two callbacks, a prepending and an appending, with same event and step
1577   * @param   mixed  $data    Optional arguments for event handlers
1578   * @param   mixed  $options Optional arguments for event handlers
1579   * @return  array Collection of return values from event handlers
1580   * @since   4.5.0
1581   * @package Callback
1582   */
1583  
1584  function callback_event_ref($event, $step = '', $pre = 0, &$data = null, &$options = null)
1585  {
1586      global $plugin_callback, $production_status;
1587  
1588      if (!is_array($plugin_callback)) {
1589          return array();
1590      }
1591  
1592      $return_value = array();
1593  
1594      foreach ($plugin_callback as $c) {
1595          if ($c['event'] == $event and (empty($c['step']) or $c['step'] == $step) and $c['pre'] == $pre) {
1596              if (is_callable($c['function'])) {
1597                  // Cannot call event handler via call_user_func() as this would
1598                  // dereference all arguments. Side effect: callback handler
1599                  // *must* be ordinary function, *must not* be class method in
1600                  // PHP <5.4. See https://bugs.php.net/bug.php?id=47160.
1601                  $return_value[] = $c['function']($event, $step, $data, $options);
1602              } elseif ($production_status == 'debug') {
1603                  trigger_error(gTxt('unknown_callback_function', array('{function}' => Txp::get('\Textpattern\Type\TypeCallable', $c['function'])->toString())), E_USER_WARNING);
1604              }
1605          }
1606      }
1607  
1608      return $return_value;
1609  }
1610  
1611  /**
1612   * Checks if a callback event has active handlers.
1613   *
1614   * @param   string $event The callback event
1615   * @param   string $step  The callback step
1616   * @param   bool   $pre   The position
1617   * @return  bool TRUE if the event is active, FALSE otherwise
1618   * @since   4.6.0
1619   * @package Callback
1620   * @example
1621   * if (has_handler('article_saved'))
1622   * {
1623   *     echo "There are active handlers for 'article_saved' event.";
1624   * }
1625   */
1626  
1627  function has_handler($event, $step = '', $pre = 0)
1628  {
1629      return (bool) callback_handlers($event, $step, $pre, false);
1630  }
1631  
1632  /**
1633   * Lists handlers attached to an event.
1634   *
1635   * @param   string $event The callback event
1636   * @param   string $step  The callback step
1637   * @param   bool   $pre   The position
1638   * @param   bool   $as_string Return callables in string representation
1639   * @return  array|bool An array of handlers, or FALSE
1640   * @since   4.6.0
1641   * @package Callback
1642   * @example
1643   * if ($handlers = callback_handlers('article_saved'))
1644   * {
1645   *     print_r($handlers);
1646   * }
1647   */
1648  
1649  function callback_handlers($event, $step = '', $pre = 0, $as_string = true)
1650  {
1651      global $plugin_callback;
1652  
1653      $out = array();
1654  
1655      foreach ((array) $plugin_callback as $c) {
1656          if ($c['event'] == $event && (!$c['step'] || $c['step'] == $step) && $c['pre'] == $pre) {
1657              if ($as_string) {
1658                  $out[] = Txp::get('\Textpattern\Type\TypeCallable', $c['function'])->toString();
1659              } else {
1660                  $out[] = $c['function'];
1661              }
1662          }
1663      }
1664  
1665      if ($out) {
1666          return $out;
1667      }
1668  
1669      return false;
1670  }
1671  
1672  /**
1673   * Merge the second array into the first array.
1674   *
1675   * @param   array $pairs The first array
1676   * @param   array $atts  The second array
1677   * @param   bool  $warn  If TRUE triggers errors if second array contains values that are not in the first
1678   * @return  array The two arrays merged
1679   * @package TagParser
1680   */
1681  
1682  function lAtts($pairs, $atts, $warn = true)
1683  {
1684      global $pretext, $production_status, $txp_atts;
1685      static $globals = null, $global_atts, $partial;
1686  
1687      if ($globals === null) {
1688          $global_atts = Txp::get('\Textpattern\Tag\Registry')->getRegistered(true);
1689          $globals = array_filter($global_atts);
1690      }
1691  
1692      if (isset($atts['yield']) && !isset($pairs['yield'])) {
1693          isset($partial) or $partial = Txp::get('\Textpattern\Tag\Registry')->getTag('yield');
1694  
1695          foreach (parse_qs($atts['yield']) as $name => $alias) {
1696              $value = call_user_func($partial, array('name' => $alias === false ? $name : $alias));
1697  
1698              if (isset($value)) {
1699                  $atts[$name] = $value;
1700              }
1701          }
1702  
1703          unset($atts['yield']);
1704      }
1705  
1706      if (empty($pretext['_txp_atts'])) {
1707          foreach ($atts as $name => $value) {
1708              if (array_key_exists($name, $pairs)) {
1709                  if ($pairs[$name] !== null) {
1710                      unset($txp_atts[$name]);
1711                  }
1712  
1713                  $pairs[$name] = $value;
1714              } elseif ($warn && $production_status !== 'live' && !array_key_exists($name, $global_atts)) {
1715                  trigger_error(gTxt('unknown_attribute', array('{att}' => $name)));
1716              }
1717          }
1718      } else { // don't import unset globals
1719          foreach ($atts as $name => $value) {
1720              if (array_key_exists($name, $pairs) && (!isset($globals[$name]) || isset($txp_atts[$name]))) {
1721                  $pairs[$name] = $value;
1722                  unset($txp_atts[$name]);
1723              }
1724          }
1725      }
1726  
1727      return $pairs ? $pairs : false;
1728  }
1729  
1730  /**
1731   * Sanitises a string for use in an article's URL title.
1732   *
1733   * @param   string $text  The title or an URL
1734   * @param   bool   $force Force sanitisation
1735   * @return  string|null
1736   * @package URL
1737   */
1738  
1739  function stripSpace($text, $force = false)
1740  {
1741      if ($force || get_pref('attach_titles_to_permalinks')) {
1742          $text = trim(sanitizeForUrl($text, '/[^\p{L}\p{N}\-_\s\/\\\\\x{1F300}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{2600}-\x{27BF}]/u'), '-');
1743  
1744          if (get_pref('permlink_format')) {
1745              return (function_exists('mb_strtolower') ? mb_strtolower($text, 'UTF-8') : strtolower($text));
1746          } else {
1747              return str_replace('-', '', $text);
1748          }
1749      }
1750  }
1751  
1752  /**
1753   * Sanitises a string for use in a URL.
1754   *
1755   * Be aware that you still have to urlencode the string when appropriate.
1756   * This function just makes the string look prettier and excludes some
1757   * unwanted characters, but leaves UTF-8 letters and digits intact.
1758   *
1759   * @param  string $text  The string
1760   * @param  string $strip The regex of the characters to strip
1761   * @return string
1762   * @package URL
1763   */
1764  
1765  function sanitizeForUrl($text, $strip = '/[^\p{L}\p{N}\-_\s\/\\\\]/u')
1766  {
1767      $out = callback_event('sanitize_for_url', '', 0, $text);
1768  
1769      if ($out !== '') {
1770          return $out;
1771      }
1772  
1773      // Remove named entities and tags.
1774      $text = preg_replace("/(^|&\S+;)|(<[^>]*>)/U", "", dumbDown($text));
1775      // Remove all characters except letter, number, dash, space and backslash
1776      $text = preg_replace($strip, '', $text);
1777      // Collapse spaces, minuses, (back-)slashes.
1778      $text = trim(preg_replace('/[\s\-\/\\\\]+/', '-', $text), '-');
1779  
1780      return $text;
1781  }
1782  
1783  /**
1784   * Sanitises a string for use in a filename.
1785   *
1786   * @param   string $text The string
1787   * @return  string
1788   * @package File
1789   */
1790  
1791  function sanitizeForFile($text)
1792  {
1793      $out = callback_event('sanitize_for_file', '', 0, $text);
1794  
1795      if ($out !== '') {
1796          return $out;
1797      }
1798  
1799      // Remove control characters and " * \ : < > ? / |
1800      $text = preg_replace('/[\x00-\x1f\x22\x2a\x2f\x3a\x3c\x3e\x3f\x5c\x7c\x7f]+/', '', $text);
1801      // Remove duplicate dots and any leading or trailing dots/spaces.
1802      $text = preg_replace('/[.]{2,}/', '.', trim($text, '. '));
1803  
1804      return $text;
1805  }
1806  
1807  /**
1808   * Sanitises a string for use in a page template's name.
1809   *
1810   * @param   string $text The string
1811   * @return  string
1812   * @package Filter
1813   * @access  private
1814   */
1815  
1816  function sanitizeForPage($text)
1817  {
1818      $out = callback_event('sanitize_for_page', '', 0, $text);
1819  
1820      if ($out !== '') {
1821          return $out;
1822      }
1823  
1824      return trim(preg_replace('/[<>&"\']/', '', $text));
1825  }
1826  
1827  /**
1828   * Sanitizes a string for use in a ORDER BY clause.
1829   *
1830   * @param   string $text The string
1831   * @return  string
1832   * @package Filter
1833   * @access  private
1834   */
1835  
1836  function sanitizeForSort($text)
1837  {
1838      return trim(strtr($text, array('#' => ' ', '--' => ' ')));
1839  }
1840  
1841  /**
1842   * Transliterates a string to ASCII.
1843   *
1844   * Used to generate RFC 3986 compliant and pretty ASCII-only URLs.
1845   *
1846   * @param   string $str  The string to convert
1847   * @param   string $lang The language which translation table is used
1848   * @see     sanitizeForUrl()
1849   * @package L10n
1850   */
1851  
1852  function dumbDown($str, $lang = null)
1853  {
1854      static $array;
1855  
1856      if ($lang === null) {
1857          $lang = get_pref('language_ui', LANG);
1858      }
1859  
1860      if (empty($array[$lang])) {
1861          $array[$lang] = array( // Nasty, huh?
1862              '&#192;' => 'A', '&Agrave;' => 'A', '&#193;' => 'A', '&Aacute;' => 'A', '&#194;' => 'A', '&Acirc;' => 'A',
1863              '&#195;' => 'A', '&Atilde;' => 'A', '&#196;' => 'Ae', '&Auml;' => 'A', '&#197;' => 'A', '&Aring;' => 'A',
1864              '&#198;' => 'Ae', '&AElig;' => 'AE',
1865              '&#256;' => 'A', '&#260;' => 'A', '&#258;' => 'A',
1866              '&#199;' => 'C', '&Ccedil;' => 'C', '&#262;' => 'C', '&#268;' => 'C', '&#264;' => 'C', '&#266;' => 'C',
1867              '&#270;' => 'D', '&#272;' => 'D', '&#208;' => 'D', '&ETH;' => 'D',
1868              '&#200;' => 'E', '&Egrave;' => 'E', '&#201;' => 'E', '&Eacute;' => 'E', '&#202;' => 'E', '&Ecirc;' => 'E', '&#203;' => 'E', '&Euml;' => 'E',
1869              '&#274;' => 'E', '&#280;' => 'E', '&#282;' => 'E', '&#276;' => 'E', '&#278;' => 'E',
1870              '&#284;' => 'G', '&#286;' => 'G', '&#288;' => 'G', '&#290;' => 'G',
1871              '&#292;' => 'H', '&#294;' => 'H',
1872              '&#204;' => 'I', '&Igrave;' => 'I', '&#205;' => 'I', '&Iacute;' => 'I', '&#206;' => 'I', '&Icirc;' => 'I', '&#207;' => 'I', '&Iuml;' => 'I',
1873              '&#298;' => 'I', '&#296;' => 'I', '&#300;' => 'I', '&#302;' => 'I', '&#304;' => 'I',
1874              '&#306;' => 'IJ',
1875              '&#308;' => 'J',
1876              '&#310;' => 'K',
1877              '&#321;' => 'K', '&#317;' => 'K', '&#313;' => 'K', '&#315;' => 'K', '&#319;' => 'K',
1878              '&#209;' => 'N', '&Ntilde;' => 'N', '&#323;' => 'N', '&#327;' => 'N', '&#325;' => 'N', '&#330;' => 'N',
1879              '&#210;' => 'O', '&Ograve;' => 'O', '&#211;' => 'O', '&Oacute;' => 'O', '&#212;' => 'O', '&Ocirc;' => 'O', '&#213;' => 'O', '&Otilde;' => 'O',
1880              '&#214;' => 'Oe', '&Ouml;' => 'Oe',
1881              '&#216;' => 'O', '&Oslash;' => 'O', '&#332;' => 'O', '&#336;' => 'O', '&#334;' => 'O',
1882              '&#338;' => 'OE',
1883              '&#340;' => 'R', '&#344;' => 'R', '&#342;' => 'R',
1884              '&#346;' => 'S', '&#352;' => 'S', '&#350;' => 'S', '&#348;' => 'S', '&#536;' => 'S',
1885              '&#356;' => 'T', '&#354;' => 'T', '&#358;' => 'T', '&#538;' => 'T',
1886              '&#217;' => 'U', '&Ugrave;' => 'U', '&#218;' => 'U', '&Uacute;' => 'U', '&#219;' => 'U', '&Ucirc;' => 'U',
1887              '&#220;' => 'Ue', '&#362;' => 'U', '&Uuml;' => 'Ue',
1888              '&#366;' => 'U', '&#368;' => 'U', '&#364;' => 'U', '&#360;' => 'U', '&#370;' => 'U',
1889              '&#372;' => 'W',
1890              '&#221;' => 'Y', '&Yacute;' => 'Y', '&#374;' => 'Y', '&#376;' => 'Y',
1891              '&#377;' => 'Z', '&#381;' => 'Z', '&#379;' => 'Z',
1892              '&#222;' => 'T', '&THORN;' => 'T',
1893              '&#224;' => 'a', '&#225;' => 'a', '&#226;' => 'a', '&#227;' => 'a', '&#228;' => 'ae',
1894              '&auml;' => 'ae',
1895              '&#229;' => 'a', '&#257;' => 'a', '&#261;' => 'a', '&#259;' => 'a', '&aring;' => 'a',
1896              '&#230;' => 'ae',
1897              '&#231;' => 'c', '&#263;' => 'c', '&#269;' => 'c', '&#265;' => 'c', '&#267;' => 'c',
1898              '&#271;' => 'd', '&#273;' => 'd', '&#240;' => 'd',
1899              '&#232;' => 'e', '&#233;' => 'e', '&#234;' => 'e', '&#235;' => 'e', '&#275;' => 'e',
1900              '&#281;' => 'e', '&#283;' => 'e', '&#277;' => 'e', '&#279;' => 'e',
1901              '&#402;' => 'f',
1902              '&#285;' => 'g', '&#287;' => 'g', '&#289;' => 'g', '&#291;' => 'g',
1903              '&#293;' => 'h', '&#295;' => 'h',
1904              '&#236;' => 'i', '&#237;' => 'i', '&#238;' => 'i', '&#239;' => 'i', '&#299;' => 'i',
1905              '&#297;' => 'i', '&#301;' => 'i', '&#303;' => 'i', '&#305;' => 'i',
1906              '&#307;' => 'ij',
1907              '&#309;' => 'j',
1908              '&#311;' => 'k', '&#312;' => 'k',
1909              '&#322;' => 'l', '&#318;' => 'l', '&#314;' => 'l', '&#316;' => 'l', '&#320;' => 'l',
1910              '&#241;' => 'n', '&#324;' => 'n', '&#328;' => 'n', '&#326;' => 'n', '&#329;' => 'n',
1911              '&#331;' => 'n',
1912              '&#242;' => 'o', '&#243;' => 'o', '&#244;' => 'o', '&#245;' => 'o', '&#246;' => 'oe',
1913              '&ouml;' => 'oe',
1914              '&#248;' => 'o', '&#333;' => 'o', '&#337;' => 'o', '&#335;' => 'o',
1915              '&#339;' => 'oe',
1916              '&#341;' => 'r', '&#345;' => 'r', '&#343;' => 'r',
1917              '&#353;' => 's',
1918              '&#249;' => 'u', '&#250;' => 'u', '&#251;' => 'u', '&#252;' => 'ue', '&#363;' => 'u',
1919              '&uuml;' => 'ue',
1920              '&#367;' => 'u', '&#369;' => 'u', '&#365;' => 'u', '&#361;' => 'u', '&#371;' => 'u',
1921              '&#373;' => 'w',
1922              '&#253;' => 'y', '&#255;' => 'y', '&#375;' => 'y',
1923              '&#382;' => 'z', '&#380;' => 'z', '&#378;' => 'z',
1924              '&#254;' => 't',
1925              '&#223;' => 'ss',
1926              '&#383;' => 'ss',
1927              '&agrave;' => 'a', '&aacute;' => 'a', '&acirc;' => 'a', '&atilde;' => 'a', '&auml;' => 'ae',
1928              '&aring;' => 'a', '&aelig;' => 'ae', '&ccedil;' => 'c', '&eth;' => 'd',
1929              '&egrave;' => 'e', '&eacute;' => 'e', '&ecirc;' => 'e', '&euml;' => 'e',
1930              '&igrave;' => 'i', '&iacute;' => 'i', '&icirc;' => 'i', '&iuml;' => 'i',
1931              '&ntilde;' => 'n',
1932              '&ograve;' => 'o', '&oacute;' => 'o', '&ocirc;' => 'o', '&otilde;' => 'o', '&ouml;' => 'oe',
1933              '&oslash;' => 'o',
1934              '&ugrave;' => 'u', '&uacute;' => 'u', '&ucirc;' => 'u', '&uuml;' => 'ue',
1935              '&yacute;' => 'y', '&yuml;' => 'y',
1936              '&thorn;' => 't',
1937              '&szlig;' => 'ss',
1938          );
1939  
1940          if (is_file(txpath.'/lib/i18n-ascii.txt')) {
1941              $i18n = parse_ini_file(txpath.'/lib/i18n-ascii.txt', true);
1942  
1943              // Load the global map.
1944              if (isset($i18n['default']) && is_array($i18n['default'])) {
1945                  $array[$lang] = array_merge($array[$lang], $i18n['default']);
1946  
1947                  // Base language overrides: 'de-AT' applies the 'de' section.
1948                  if (preg_match('/([a-zA-Z]+)-.+/', $lang, $m)) {
1949                      if (isset($i18n[$m[1]]) && is_array($i18n[$m[1]])) {
1950                          $array[$lang] = array_merge($array[$lang], $i18n[$m[1]]);
1951                      }
1952                  }
1953  
1954                  // Regional language overrides: 'de-AT' applies the 'de-AT' section.
1955                  if (isset($i18n[$lang]) && is_array($i18n[$lang])) {
1956                      $array[$lang] = array_merge($array[$lang], $i18n[$lang]);
1957                  }
1958              }
1959              // Load an old file (no sections) just in case.
1960              else {
1961                  $array[$lang] = array_merge($array[$lang], $i18n);
1962              }
1963          }
1964      }
1965  
1966      return strtr($str, $array[$lang]);
1967  }
1968  
1969  /**
1970   * Cleans a URL.
1971   *
1972   * @param   string $url The URL
1973   * @return  string
1974   * @access  private
1975   * @package URL
1976   */
1977  
1978  function clean_url($url)
1979  {
1980      return preg_replace("/\"|'|(?:\s.*$)/", '', $url);
1981  }
1982  
1983  /**
1984   * Replace the last space with a &#160; non-breaking space.
1985   *
1986   * @param   string $str The string
1987   * @return  string
1988   */
1989  
1990  function noWidow($str)
1991  {
1992      if (REGEXP_UTF8 == 1) {
1993          return preg_replace('@[ ]+([[:punct:]]?[\p{L}\p{N}\p{Pc}]+[[:punct:]]?)$@u', '&#160;$1', rtrim($str));
1994      }
1995  
1996      return preg_replace('@[ ]+([[:punct:]]?\w+[[:punct:]]?)$@', '&#160;$1', rtrim($str));
1997  }
1998  
1999  /**
2000   * Checks if an IP is on a spam blocklist.
2001   *
2002   * @param   string       $ip     The IP address
2003   * @param   string|array $checks The checked lists. Defaults to 'spam_blacklists' preferences string
2004   * @return  string|bool The lists the IP is on or FALSE
2005   * @package Comment
2006   * @example
2007   * if (is_blacklisted('192.0.2.1'))
2008   * {
2009   *     echo "'192.0.2.1' is on the blocklist.";
2010   * }
2011   */
2012  
2013  function is_blacklisted($ip, $checks = '')
2014  {
2015      if (!$checks) {
2016          $checks = do_list_unique(get_pref('spam_blacklists'));
2017      }
2018  
2019      $rip = join('.', array_reverse(explode('.', $ip)));
2020  
2021      foreach ((array) $checks as $a) {
2022          $parts = explode(':', $a, 2);
2023          $rbl   = $parts[0];
2024  
2025          if (isset($parts[1])) {
2026              foreach (explode(':', $parts[1]) as $code) {
2027                  $codes[] = strpos($code, '.') ? $code : '127.0.0.'.$code;
2028              }
2029          }
2030  
2031          $hosts = $rbl ? @gethostbynamel($rip.'.'.trim($rbl, '. ').'.') : false;
2032  
2033          if ($hosts and (!isset($codes) or array_intersect($hosts, $codes))) {
2034              $listed[] = $rbl;
2035          }
2036      }
2037  
2038      return (!empty($listed)) ? join(', ', $listed) : false;
2039  }
2040  
2041  /**
2042   * Checks if the user is authenticated on the public-side.
2043   *
2044   * @param   string $user The checked username. If not provided, any user is accepted
2045   * @return  array|bool An array containing details about the user; name, RealName, email, privs. FALSE when the user hasn't authenticated.
2046   * @package User
2047   * @example
2048   * if ($user = is_logged_in())
2049   * {
2050   *     echo "Logged in as {$user['RealName']}";
2051   * }
2052   */
2053  
2054  function is_logged_in($user = '')
2055  {
2056      static $users = array();
2057  
2058      $name = substr(cs('txp_login_public'), 10);
2059  
2060      if (!strlen($name) || strlen($user) && $user !== $name) {
2061          return false;
2062      }
2063  
2064      if (!isset($users[$name])) {
2065          $users[$name] = safe_row("nonce, name, RealName, email, privs", 'txp_users', "name = '".doSlash($name)."'");
2066      }
2067  
2068      $rs = $users[$name];
2069  
2070      if ($rs && substr(md5($rs['nonce']), -10) === substr(cs('txp_login_public'), 0, 10)) {
2071          unset($rs['nonce']);
2072  
2073          return $rs;
2074      } else {
2075          return false;
2076      }
2077  }
2078  
2079  /**
2080   * Updates the path to the site.
2081   *
2082   * @param   string $here The path
2083   * @access  private
2084   * @package Pref
2085   */
2086  
2087  function updateSitePath($here)
2088  {
2089      set_pref('path_to_site', $here, 'publish', PREF_HIDDEN);
2090  }
2091  
2092  /**
2093   * Converts Textpattern tag's attribute list to an array.
2094   *
2095   * @param   string $text The attribute list, e.g. foobar="1" barfoo="0"
2096   * @return  array Array of attributes
2097   * @access  private
2098   * @package TagParser
2099   */
2100  
2101  function splat($text)
2102  {
2103      static $stack = array(), $parse = array(), $global_atts = array(), $globals = null;
2104      global $production_status, $trace, $txp_atts;
2105  
2106      if ($globals === null) {
2107          $globals = array_filter(Txp::get('\Textpattern\Tag\Registry')->getRegistered(true));
2108      }
2109  
2110      $sha = sha1($text);
2111  
2112      if (!isset($stack[$sha])) {
2113          $stack[$sha] = $parse[$sha] = array();
2114  
2115          if (preg_match_all('@([\w\-]+)(?:\s*=\s*(?:"((?:[^"]|"")*)"|\'((?:[^\']|\'\')*)\'|([^\s\'"/>]+)))?@s', $text, $match, PREG_SET_ORDER)) {
2116              foreach ($match as $m) {
2117                  $name = strtolower($m[1]);
2118  
2119                  switch (count($m)) {
2120                      case 2:
2121                          $val = true;
2122                          break;
2123                      case 3:
2124                          $val = str_replace('""', '"', $m[2]);
2125                          break;
2126                      case 4:
2127                          $val = str_replace("''", "'", $m[3]);
2128  
2129                          if (strpos($m[3], ':') !== false) {
2130                              $parse[$sha][] = $name;
2131                          }
2132  
2133                          break;
2134                      case 5:
2135                          $val = $m[4];
2136                          trigger_error(gTxt('attribute_values_must_be_quoted'), E_USER_WARNING);
2137                          break;
2138                  }
2139  
2140                  $stack[$sha][$name] = $val;
2141              }
2142          }
2143  
2144          $global_atts[$sha] = array_intersect_key($stack[$sha], $globals) or $global_atts[$sha] = null;
2145      }
2146  
2147      $txp_atts = $global_atts[$sha];
2148  
2149      if (empty($parse[$sha])) {
2150          return $stack[$sha];
2151      }
2152  
2153      $atts = $stack[$sha];
2154  
2155      if ($production_status !== 'live') {
2156          foreach ($parse[$sha] as $p) {
2157              $trace->start("[attribute '".$p."']");
2158              $atts[$p] = parse($atts[$p], true, false);
2159              isset($txp_atts[$p]) and $txp_atts[$p] = $atts[$p];
2160              $trace->stop('[/attribute]');
2161          }
2162      } else {
2163          foreach ($parse[$sha] as $p) {
2164              $atts[$p] = parse($atts[$p], true, false);
2165              isset($txp_atts[$p]) and $txp_atts[$p] = $atts[$p];
2166          }
2167      }
2168  
2169      return $atts;
2170  }
2171  
2172  /**
2173   * Replaces CR and LF with spaces, and drops NULL bytes.
2174   *
2175   * Used for sanitising email headers.
2176   *
2177   * @param      string $str The string
2178   * @return     string
2179   * @package    Mail
2180   * @deprecated in 4.6.0
2181   * @see        \Textpattern\Mail\Encode::escapeHeader()
2182   */
2183  
2184  function strip_rn($str)
2185  {
2186      return Txp::get('\Textpattern\Mail\Encode')->escapeHeader($str);
2187  }
2188  
2189  /**
2190   * Validates a string as an email address.
2191   *
2192   * <code>
2193   * if (is_valid_email('john.doe@example.com'))
2194   * {
2195   *     echo "'john.doe@example.com' validates.";
2196   * }
2197   * </code>
2198   *
2199   * @param      string $address The email address
2200   * @return     bool
2201   * @package    Mail
2202   * @deprecated in 4.6.0
2203   * @see        filter_var()
2204   */
2205  
2206  function is_valid_email($address)
2207  {
2208      return (bool) filter_var($address, FILTER_VALIDATE_EMAIL);
2209  }
2210  
2211  /**
2212   * Sends an email message as the currently logged in user.
2213   *
2214   * <code>
2215   * if (txpMail('john.doe@example.com', 'Subject', 'Some message'))
2216   * {
2217   *     echo "Email sent to 'john.doe@example.com'.";
2218   * }
2219   * </code>
2220   *
2221   * @param   string $to_address The receiver
2222   * @param   string $subject    The subject
2223   * @param   string $body       The message
2224   * @param   string $reply_to The reply to address
2225   * @return  bool   Returns FALSE when sending failed
2226   * @see     \Textpattern\Mail\Compose
2227   * @package Mail
2228   */
2229  
2230  function txpMail($to_address, $subject, $body, $reply_to = null)
2231  {
2232      global $txp_user;
2233  
2234      // Send the email as the currently logged in user.
2235      if ($txp_user) {
2236          $sender = safe_row(
2237              "RealName, email",
2238              'txp_users',
2239              "name = '".doSlash($txp_user)."'"
2240          );
2241  
2242          if ($sender && is_valid_email(get_pref('publisher_email'))) {
2243              $sender['email'] = get_pref('publisher_email');
2244          }
2245      }
2246      // If not logged in, the receiver is the sender.
2247      else {
2248          $sender = safe_row(
2249              "RealName, email",
2250              'txp_users',
2251              "email = '".doSlash($to_address)."'"
2252          );
2253      }
2254  
2255      if ($sender) {
2256          extract($sender);
2257  
2258          try {
2259              $message = Txp::get('\Textpattern\Mail\Compose')
2260                  ->from($email, $RealName)
2261                  ->to($to_address)
2262                  ->subject($subject)
2263                  ->body($body);
2264  
2265              if ($reply_to) {
2266                  $message->replyTo($reply_to);
2267              }
2268  
2269              $message->send();
2270          } catch (\Textpattern\Mail\Exception $e) {
2271              return false;
2272          }
2273  
2274          return true;
2275      }
2276  
2277      return false;
2278  }
2279  
2280  /**
2281   * Encodes a string for use in an email header.
2282   *
2283   * @param      string $string The string
2284   * @param      string $type   The type of header, either "text" or "phrase"
2285   * @return     string
2286   * @package    Mail
2287   * @deprecated in 4.6.0
2288   * @see        \Textpattern\Mail\Encode::header()
2289   */
2290  
2291  function encode_mailheader($string, $type)
2292  {
2293      try {
2294          return Txp::get('\Textpattern\Mail\Encode')->header($string, $type);
2295      } catch (\Textpattern\Mail\Exception $e) {
2296          trigger_error($e->getMessage(), E_USER_WARNING);
2297      }
2298  }
2299  
2300  /**
2301   * Converts an email address into unicode entities.
2302   *
2303   * @param      string $txt The email address
2304   * @return     string Encoded email address
2305   * @package    Mail
2306   * @deprecated in 4.6.0
2307   * @see        \Textpattern\Mail\Encode::entityObfuscateAddress()
2308   */
2309  
2310  function eE($txt)
2311  {
2312      return Txp::get('\Textpattern\Mail\Encode')->entityObfuscateAddress($txt);
2313  }
2314  
2315  /**
2316   * Strips PHP tags from a string.
2317   *
2318   * @param  string $in The input
2319   * @return string
2320   */
2321  
2322  function stripPHP($in)
2323  {
2324      return preg_replace("/".chr(60)."\?(?:php)?|\?".chr(62)."/i", '', $in);
2325  }
2326  
2327  /**
2328   * Creates a form template.
2329   *
2330   * On a successful run, will trigger a 'form.create > done' callback event.
2331   *
2332   * @param   string $name The name
2333   * @param   string $type The type
2334   * @param   string $Form The template
2335   * @return  bool FALSE on error
2336   * @since   4.6.0
2337   * @package Template
2338   */
2339  
2340  function create_form($name, $type, $Form)
2341  {
2342      $types = get_form_types();
2343  
2344      if (form_exists($name) || !is_valid_form($name) || !in_array($type, array_keys($types))) {
2345          return false;
2346      }
2347  
2348      if (
2349          safe_insert(
2350              'txp_form',
2351              "name = '".doSlash($name)."',
2352              type = '".doSlash($type)."',
2353              Form = '".doSlash($Form)."'"
2354          ) === false
2355      ) {
2356          return false;
2357      }
2358  
2359      callback_event('form.create', 'done', 0, compact('name', 'type', 'Form'));
2360  
2361      return true;
2362  }
2363  
2364  /**
2365   * Checks if a form template exists.
2366   *
2367   * @param   string $name The form
2368   * @return  bool TRUE if the form exists
2369   * @since   4.6.0
2370   * @package Template
2371   */
2372  
2373  function form_exists($name)
2374  {
2375      return (bool) safe_row("name", 'txp_form', "name = '".doSlash($name)."'");
2376  }
2377  
2378  /**
2379   * Validates a string as a form template name.
2380   *
2381   * @param   string $name The form name
2382   * @return  bool TRUE if the string validates
2383   * @since   4.6.0
2384   * @package Template
2385   */
2386  
2387  function is_valid_form($name)
2388  {
2389      if (function_exists('mb_strlen')) {
2390          $length = mb_strlen($name, '8bit');
2391      } else {
2392          $length = strlen($name);
2393      }
2394  
2395      return $name && !preg_match('/^\s|[<>&"\']|\s$/u', $name) && $length <= 64;
2396  }
2397  
2398  /**
2399   * Gets a "since days ago" date format from a given UNIX timestamp.
2400   *
2401   * @param   int $stamp UNIX timestamp
2402   * @return  string "n days ago"
2403   * @package DateTime
2404   */
2405  
2406  function since($stamp)
2407  {
2408      $diff = (time() - $stamp);
2409  
2410      if ($diff <= 3600) {
2411          $mins = round($diff / 60);
2412          $since = ($mins <= 1) ? ($mins == 1) ? '1 '.gTxt('minute') : gTxt('a_few_seconds') : "$mins ".gTxt('minutes');
2413      } elseif (($diff <= 86400) && ($diff > 3600)) {
2414          $hours = round($diff / 3600);
2415          $since = ($hours <= 1) ? '1 '.gTxt('hour') : "$hours ".gTxt('hours');
2416      } elseif ($diff >= 86400) {
2417          $days = round($diff / 86400);
2418          $since = ($days <= 1) ? "1 ".gTxt('day') : "$days ".gTxt('days');
2419      }
2420  
2421      return gTxt('ago', array('{since}' => $since));
2422  }
2423  
2424  /**
2425   * Calculates a timezone offset.
2426   *
2427   * Calculates the offset between the server local time and the
2428   * user's selected timezone at a given point in time.
2429   *
2430   * @param   int $timestamp The timestamp. Defaults to time()
2431   * @return  int The offset in seconds
2432   * @package DateTime
2433   */
2434  
2435  function tz_offset($timestamp = null)
2436  {
2437      global $gmtoffset, $timezone_key;
2438      static $dtz = array(), $timezone_server = null;
2439  
2440      if ($timezone_server === null) {
2441          $timezone_server = date_default_timezone_get();
2442      }
2443  
2444      if ($timezone_server === $timezone_key) {
2445          return 0;
2446      }
2447  
2448      if ($timestamp === null) {
2449          $timestamp = time();
2450      }
2451  
2452      try {
2453          if (!isset($dtz[$timezone_server])) {
2454              $dtz[$timezone_server] = new \DateTimeZone($timezone_server);
2455          }
2456  
2457          $transition = $dtz[$timezone_server]->getTransitions($timestamp, $timestamp);
2458          $serveroffset = $transition[0]['offset'];
2459      } catch (\Exception $e) {
2460          extract(getdate($timestamp));
2461          $serveroffset = gmmktime($hours, $minutes, 0, $mon, $mday, $year) - mktime($hours, $minutes, 0, $mon, $mday, $year);
2462      }
2463  
2464      try {
2465          if (!isset($dtz[$timezone_key])) {
2466              $dtz[$timezone_key] = new \DateTimeZone($timezone_key);
2467          }
2468  
2469          $transition = $dtz[$timezone_key]->getTransitions($timestamp, $timestamp);
2470          $siteoffset = $transition[0]['offset'];
2471      } catch (\Exception $e) {
2472          $siteoffset = $gmtoffset;
2473      }
2474  
2475      return $siteoffset - $serveroffset;
2476  }
2477  
2478  /**
2479   * Formats a time.
2480   *
2481   * Respects the locale and local timezone, and makes sure the
2482   * output string is encoded in UTF-8.
2483   *
2484   * @param   string $format          The date format
2485   * @param   int    $time            UNIX timestamp. Defaults to time()
2486   * @param   bool   $gmt             Return GMT time
2487   * @param   string $override_locale Override the locale
2488   * @return  string Formatted date
2489   * @package DateTime
2490   * @example
2491   * echo safe_strftime('w3cdtf');
2492   */
2493  
2494  function safe_strftime($format, $time = '', $gmt = false, $override_locale = '')
2495  {
2496      static $charsets = array(), $txpLocale = null;
2497  
2498      if (!$time) {
2499          $time = time();
2500      }
2501  
2502      if ($txpLocale === null) {
2503          $txpLocale = Txp::get('\Textpattern\L10n\Locale');
2504      }
2505  
2506      // We could add some other formats here.
2507      if ($format == 'iso8601' || $format == 'w3cdtf') {
2508          $format = '%Y-%m-%dT%H:%M:%SZ';
2509          $gmt = true;
2510      } elseif ($format == 'rfc822') {
2511          $format = '%a, %d %b %Y %H:%M:%S GMT';
2512          $gmt = true;
2513          $override_locale = 'C';
2514      }
2515  
2516      if ($override_locale) {
2517          $oldLocale = $txpLocale->getLocale(LC_TIME);
2518  
2519          if ($oldLocale != $override_locale) {
2520              $txpLocale->setLocale(LC_TIME, $override_locale);
2521          } else {
2522              $oldLocale = null;
2523          }
2524      }
2525  
2526      if ($format == 'since') {
2527          $str = since($time);
2528      } elseif ($gmt) {
2529          $str = gmstrftime($format, $time);
2530      } else {
2531          $tztime = $time + tz_offset($time);
2532          $format = str_replace('%s', $tztime, $format);
2533          $str = strftime($format, $tztime);
2534      }
2535  
2536      if (!isset($charsets[$override_locale])) {
2537          $charsets[$override_locale] = $txpLocale->getCharset(LC_TIME, IS_WIN ? 'Windows-1252' : 'ISO-8859-1');
2538      }
2539  
2540      $charset = $charsets[$override_locale];
2541  
2542      if ($charset != 'UTF-8' && $format != 'since') {
2543          $new = '';
2544          if (is_callable('iconv')) {
2545              $new = @iconv($charset, 'UTF-8', $str);
2546          }
2547  
2548          if ($new) {
2549              $str = $new;
2550          } elseif (is_callable('utf8_encode')) {
2551              $str = utf8_encode($str);
2552          }
2553      }
2554  
2555      // Revert to the old locale.
2556      if (isset($oldLocale)) {
2557          $txpLocale->setLocale(LC_TIME, $oldLocale);
2558      }
2559  
2560      return $str;
2561  }
2562  
2563  /**
2564   * Converts a time string from the Textpattern timezone to GMT.
2565   *
2566   * @param   string $time_str The time string
2567   * @return  int UNIX timestamp
2568   * @package DateTime
2569   */
2570  
2571  function safe_strtotime($time_str)
2572  {
2573      $ts = strtotime($time_str);
2574  
2575      // tz_offset calculations are expensive
2576      $tz_offset = tz_offset($ts);
2577  
2578      return strtotime($time_str, time() + $tz_offset) - $tz_offset;
2579  }
2580  
2581  /**
2582   * Generic error handler.
2583   *
2584   * @param   int    $errno
2585   * @param   string $errstr
2586   * @param   string $errfile
2587   * @param   int    $errline
2588   * @access  private
2589   * @package Debug
2590   */
2591  
2592  function myErrorHandler($errno, $errstr, $errfile, $errline)
2593  {
2594      if (!error_reporting()) {
2595          return;
2596      }
2597  
2598      echo '<pre dir="auto">'.n.n."$errno: $errstr in $errfile at line $errline\n";
2599  
2600      if (is_callable('debug_backtrace')) {
2601          echo "Backtrace:\n";
2602          $trace = debug_backtrace();
2603  
2604          foreach ($trace as $ent) {
2605              if (isset($ent['file'])) {
2606                  echo $ent['file'].':';
2607              }
2608  
2609              if (isset($ent['function'])) {
2610                  echo $ent['function'].'(';
2611  
2612                  if (isset($ent['args'])) {
2613                      $args = '';
2614  
2615                      foreach ($ent['args'] as $arg) {
2616                          $args .= $arg.',';
2617                      }
2618  
2619                      echo rtrim($args, ',');
2620                  }
2621  
2622                  echo ') ';
2623              }
2624  
2625              if (isset($ent['line'])) {
2626                  echo 'at line '.$ent['line'].' ';
2627              }
2628  
2629              if (isset($ent['file'])) {
2630                  echo 'in '.$ent['file'];
2631              }
2632  
2633              echo "\n";
2634          }
2635      }
2636  
2637      echo "</pre>";
2638  }
2639  
2640  /**
2641   * Renders a download link.
2642   *
2643   * @param   int    $id       The file ID
2644   * @param   string $label    The label
2645   * @param   string $filename The filename
2646   * @return  string HTML
2647   * @package File
2648   */
2649  
2650  function make_download_link($id, $label = '', $filename = '')
2651  {
2652      if ((string) $label === '') {
2653          $label = gTxt('download');
2654      }
2655  
2656      $url = filedownloadurl($id, $filename);
2657  
2658      // Do not use the array() form of passing $atts to href().
2659      // Doing so breaks download links on the admin side due to
2660      // double-encoding of the ampersands.
2661      return href($label, $url, ' title = "'.gTxt('download').'"');
2662  }
2663  
2664  /**
2665   * Sets error reporting level.
2666   *
2667   * @param   string $level The level. Either "debug", "live" or "testing"
2668   * @package Debug
2669   */
2670  
2671  function set_error_level($level)
2672  {
2673      if ($level == 'debug') {
2674          error_reporting(E_ALL | E_STRICT);
2675      } elseif ($level == 'live') {
2676          // Don't show errors on screen.
2677          $suppress = E_NOTICE | E_USER_NOTICE | E_WARNING | E_STRICT | (defined('E_DEPRECATED') ? E_DEPRECATED : 0);
2678          error_reporting(E_ALL ^ $suppress);
2679          @ini_set("display_errors", "1");
2680      } else {
2681          // Default is 'testing': display everything except notices.
2682          error_reporting((E_ALL | E_STRICT) ^ (E_NOTICE | E_USER_NOTICE));
2683      }
2684  }
2685  
2686  /**
2687   * Translates upload error code to a localised error message.
2688   *
2689   * @param   int $err_code The error code
2690   * @return  string The $err_code as a message
2691   * @package File
2692   */
2693  
2694  function upload_get_errormsg($err_code)
2695  {
2696      $msg = '';
2697  
2698      switch ($err_code) {
2699          // Value: 0; There is no error, the file uploaded with success.
2700          case UPLOAD_ERR_OK:
2701              $msg = '';
2702              break;
2703          // Value: 1; The uploaded file exceeds the upload_max_filesize directive in php.ini.
2704          case UPLOAD_ERR_INI_SIZE:
2705              $msg = gTxt('upload_err_ini_size');
2706              break;
2707          // Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
2708          case UPLOAD_ERR_FORM_SIZE:
2709              $msg = gTxt('upload_err_form_size');
2710              break;
2711          // Value: 3; The uploaded file was only partially uploaded.
2712          case UPLOAD_ERR_PARTIAL:
2713              $msg = gTxt('upload_err_partial');
2714              break;
2715          // Value: 4; No file was uploaded.
2716          case UPLOAD_ERR_NO_FILE:
2717              $msg = gTxt('upload_err_no_file');
2718              break;
2719          // Value: 6; Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
2720          case UPLOAD_ERR_NO_TMP_DIR:
2721              $msg = gTxt('upload_err_tmp_dir');
2722              break;
2723          // Value: 7; Failed to write file to disk. Introduced in PHP 5.1.0.
2724          case UPLOAD_ERR_CANT_WRITE:
2725              $msg = gTxt('upload_err_cant_write');
2726              break;
2727          // Value: 8; File upload stopped by extension. Introduced in PHP 5.2.0.
2728          case UPLOAD_ERR_EXTENSION:
2729              $msg = gTxt('upload_err_extension');
2730              break;
2731      }
2732  
2733      return $msg;
2734  }
2735  
2736  /**
2737   * Formats a file size.
2738   *
2739   * @param   int    $bytes    Size in bytes
2740   * @param   int    $decimals Number of decimals
2741   * @param   string $format   The format the size is represented
2742   * @return  string Formatted file size
2743   * @package File
2744   * @example
2745   * echo format_filesize(168642);
2746   */
2747  
2748  function format_filesize($bytes, $decimals = 2, $format = '')
2749  {
2750      $units = array('b', 'k', 'm', 'g', 't', 'p', 'e', 'z', 'y');
2751  
2752      if (in_array($format, $units)) {
2753          $pow = array_search($format, $units);
2754      } else {
2755          $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
2756          $pow = min($pow, count($units) - 1);
2757      }
2758  
2759      $bytes /= pow(1024, $pow);
2760  
2761      $separators = localeconv();
2762      $sep_dec = isset($separators['decimal_point']) ? $separators['decimal_point'] : '.';
2763      $sep_thous = isset($separators['thousands_sep']) ? $separators['thousands_sep'] : ',';
2764  
2765      return number_format($bytes, $decimals, $sep_dec, $sep_thous).sp.gTxt('units_'.$units[$pow]);
2766  }
2767  
2768  /**
2769   * Gets a file download as an array.
2770   *
2771   * @param   string $where SQL where clause
2772   * @return  array|bool An array of files, or FALSE on failure
2773   * @package File
2774   * @example
2775   * if ($file = fileDownloadFetchInfo('id = 1'))
2776   * {
2777   *     print_r($file);
2778   * }
2779   */
2780  
2781  function fileDownloadFetchInfo($where)
2782  {
2783      $rs = safe_row("*", 'txp_file', $where);
2784  
2785      if ($rs) {
2786          return file_download_format_info($rs);
2787      }
2788  
2789      return false;
2790  }
2791  
2792  /**
2793   * Formats file download info.
2794   *
2795   * Takes a data array generated by fileDownloadFetchInfo()
2796   * and formats the contents.
2797   *
2798   * @param   array $file The file info to format
2799   * @return  array Formatted file info
2800   * @access  private
2801   * @package File
2802   */
2803  
2804  function file_download_format_info($file)
2805  {
2806      if (($unix_ts = @strtotime($file['created'])) > 0) {
2807          $file['created'] = $unix_ts;
2808      }
2809  
2810      if (($unix_ts = @strtotime($file['modified'])) > 0) {
2811          $file['modified'] = $unix_ts;
2812      }
2813  
2814      return $file;
2815  }
2816  
2817  /**
2818   * Formats file download's modification and creation timestamps.
2819   *
2820   * Used by file_download tags.
2821   *
2822   * @param   array $params
2823   * @return  string
2824   * @access  private
2825   * @package File
2826   */
2827  
2828  function fileDownloadFormatTime($params)
2829  {
2830      extract(lAtts(array(
2831          'ftime'  => '',
2832          'format' => '',
2833      ), $params));
2834  
2835      if (!empty($ftime)) {
2836          if ($format) {
2837              return safe_strftime($format, $ftime);
2838          }
2839  
2840          return safe_strftime(get_pref('archive_dateformat'), $ftime);
2841      }
2842  
2843      return '';
2844  }
2845  
2846  /**
2847   * file_get_contents wrapper.
2848   *
2849   */
2850  
2851  function txp_get_contents($file)
2852  {
2853      return is_readable($file) ? file_get_contents($file) : null;
2854  }
2855  
2856  /**
2857   * Returns the contents of the found files as an array.
2858   *
2859   */
2860  
2861  function get_files_content($dir, $ext)
2862  {
2863      $result = array();
2864      foreach ((array)@scandir($dir) as $file) {
2865          if (preg_match('/^(.+)\.'.$ext.'$/', $file, $match)) {
2866              $result[$match[1]] = file_get_contents("$dir/$file");
2867          }
2868      }
2869  
2870      return $result;
2871  }
2872  
2873  /**
2874   * Checks if a function is disabled.
2875   *
2876   * @param   string $function The function name
2877   * @return  bool TRUE if the function is disabled
2878   * @package System
2879   * @example
2880   * if (is_disabled('mail'))
2881   * {
2882   *     echo "'mail' function is disabled.";
2883   * }
2884   */
2885  
2886  function is_disabled($function)
2887  {
2888      static $disabled;
2889  
2890      if (!isset($disabled)) {
2891          $disabled = do_list(ini_get('disable_functions'));
2892      }
2893  
2894      return in_array($function, $disabled);
2895  }
2896  
2897  /**
2898   * Joins two strings to form a single filesystem path.
2899   *
2900   * @param   string $base The base directory
2901   * @param   string $path The second path, a relative filename
2902   * @return  string A path to a file
2903   * @package File
2904   */
2905  
2906  function build_file_path($base, $path)
2907  {
2908      $base = rtrim($base, '/\\');
2909      $path = ltrim($path, '/\\');
2910  
2911      return $base.DS.$path;
2912  }
2913  
2914  /**
2915   * Gets a user's real name.
2916   *
2917   * @param   string $name The username
2918   * @return  string A real name, or username if empty
2919   * @package User
2920   */
2921  
2922  function get_author_name($name)
2923  {
2924      static $authors = array();
2925  
2926      if (isset($authors[$name])) {
2927          return $authors[$name];
2928      }
2929  
2930      $realname = fetch('RealName', 'txp_users', 'name', $name);
2931      $authors[$name] = $realname;
2932  
2933      return ($realname) ? $realname : $name;
2934  }
2935  
2936  /**
2937   * Gets a user's email address.
2938   *
2939   * @param   string $name The username
2940   * @return  string
2941   * @package User
2942   */
2943  
2944  function get_author_email($name)
2945  {
2946      static $authors = array();
2947  
2948      if (isset($authors[$name])) {
2949          return $authors[$name];
2950      }
2951  
2952      $email = fetch('email', 'txp_users', 'name', $name);
2953      $authors[$name] = $email;
2954  
2955      return $email;
2956  }
2957  
2958  /**
2959   * Checks if a database table contains items just from one user.
2960   *
2961   * @param   string $table The database table
2962   * @param   string $col   The column
2963   * @return  bool
2964   * @package User
2965   * @example
2966   * if (has_single_author('textpattern', 'AuthorID'))
2967   * {
2968   *     echo "'textpattern' table has only content from one author.";
2969   * }
2970   */
2971  
2972  function has_single_author($table, $col = 'author')
2973  {
2974      static $cache = array();
2975  
2976      if (!isset($cache[$table][$col])) {
2977          $cache[$table][$col] = (safe_field("COUNT(name)", 'txp_users', "1 = 1") <= 1) &&
2978              (safe_field("COUNT(DISTINCT(".doSlash($col)."))", doSlash($table), "1 = 1") <= 1);
2979      }
2980  
2981      return $cache[$table][$col];
2982  }
2983  
2984  /**
2985   * Parse a string and store the result.
2986   *
2987   * @param   string        $thing        The raw string
2988   * @param   null|string   $hash         The string SHA1 hash
2989   * @param   bool|callable $transform    The function applied to txp tags
2990   * @package TagParser
2991   */
2992  
2993  function txp_tokenize($thing, $hash = null, $transform = null)
2994  {
2995      global $txp_parsed, $txp_else;
2996      static $short_tags = null;
2997  
2998      isset($short_tags) or $short_tags = get_pref('enable_short_tags', false);
2999  
3000      $f = '@(</?(?:'.TXP_PATTERN.'):\w+(?:\[-?\d+\])?(?:\s+[\w\-]+(?:\s*=\s*(?:"(?:[^"]|"")*"|\'(?:[^\']|\'\')*\'|[^\s\'"/>]+))?)*\s*/?\>)@s';
3001      $t = '@^</?('.TXP_PATTERN.'):(\w+)(?:\[(-?\d+)\])?(.*)\>$@s';
3002  
3003      $parsed = preg_split($f, $thing, -1, PREG_SPLIT_DELIM_CAPTURE);
3004      $last = count($parsed);
3005  
3006      if (isset($transform) && (is_bool($transform) || is_callable($transform))) {
3007          $transform !== true or $transform = 'txpspecialchars';
3008  
3009          for ($i = 1; $i < $last; $i+=2) {
3010              $parsed[$i] = $transform === false ? null : call_user_func($transform, $parsed[$i]);
3011          }
3012      }
3013  
3014      if ($hash === false) {
3015          return $parsed;
3016      } elseif (!is_string($hash)) {
3017          $hash = sha1($thing);
3018      }
3019  
3020      $inside  = array($parsed[0]);
3021      $tags    = array($inside);
3022      $tag     = array();
3023      $outside = array();
3024      $order = array(array());
3025      $else    = array(-1);
3026      $count   = array(-1);
3027      $level   = 0;
3028  
3029      for ($i = 1; $i < $last || $level > 0; $i++) {
3030          $chunk = $i < $last ? $parsed[$i] : '</txp:'.$tag[$level-1][2].'>';
3031          preg_match($t, $chunk, $tag[$level]);
3032          $count[$level] += 2;
3033  
3034          if ($tag[$level][2] === 'else') {
3035              $else[$level] = $count[$level];
3036          } elseif ($tag[$level][1] === 'txp:') {
3037              // Handle <txp::shortcode />.
3038              $tag[$level][4] .= ' form="'.$tag[$level][2].'"';
3039              $tag[$level][2] = 'output_form';
3040          } elseif ($short_tags && $tag[$level][1] !== 'txp') {
3041              // Handle <short::tags />.
3042              $tag[$level][2] = rtrim($tag[$level][1], ':').'_'.$tag[$level][2];
3043          }
3044  
3045          if ($chunk[strlen($chunk) - 2] === '/') {
3046              // Self closed tag.
3047              if ($chunk[1] === '/') {
3048                  trigger_error(gTxt('ambiguous_tag_format', array('{chunk}' => $chunk)), E_USER_WARNING);
3049              }
3050  
3051              $tags[$level][] = array($chunk, $tag[$level][2], trim(rtrim($tag[$level][4], '/')), null, null);
3052              $inside[$level] .= $chunk;
3053              empty($tag[$level][3]) or $order[$level][count($tags[$level])/2] = $tag[$level][3];
3054          } elseif ($chunk[1] !== '/') {
3055              // Opening tag.
3056              $inside[$level] .= $chunk;
3057              empty($tag[$level][3]) or $order[$level][(count($tags[$level])+1)/2] = $tag[$level][3];
3058              $level++;
3059              $outside[$level] = $chunk;
3060              $inside[$level] = '';
3061              $else[$level] = $count[$level] = -1;
3062              $tags[$level] = array();
3063              $order[$level] = array();
3064          } else {
3065              // Closing tag.
3066              if ($level < 1) {
3067                  trigger_error(gTxt('missing_open_tag', array('{chunk}' => $chunk)), E_USER_WARNING);
3068                  $tags[$level][] = array($chunk, null, '', null, null);
3069                  $inside[$level] .= $chunk;
3070              } else {
3071                  if ($i >= $last) {
3072                      trigger_error(gTxt('missing_close_tag', array('{chunk}' => $outside[$level])), E_USER_WARNING);
3073                  } elseif ($tag[$level-1][2] != $tag[$level][2]) {
3074                      trigger_error(gTxt('mismatch_open_close_tag', array(
3075                          '{from}' => $outside[$level],
3076                          '{to}'   => $chunk,
3077                      )), E_USER_WARNING);
3078                  }
3079  
3080                  $sha = sha1($inside[$level]);
3081                  txp_fill_parsed($sha, $tags[$level], $order[$level], $count[$level], $else[$level]);
3082      
3083                  $level--;
3084                  $tags[$level][] = array($outside[$level+1], $tag[$level][2], trim($tag[$level][4]), $inside[$level+1], $chunk);
3085                  $inside[$level] .= $inside[$level+1].$chunk;
3086              }
3087          }
3088  
3089          $chunk = ++$i < $last ? $parsed[$i] : '';
3090          $tags[$level][] = $chunk;
3091          $inside[$level] .= $chunk;
3092      }
3093  
3094      txp_fill_parsed($hash, $tags[0], $order[0], $count[0] + 2, $else[0]);
3095  }
3096  
3097  /** Auxiliary **/
3098  
3099  function txp_fill_parsed($sha, $tags, $order, $count, $else) {
3100      global $txp_parsed, $txp_else;
3101  
3102      $txp_parsed[$sha] = $count > 2 ? $tags : false;
3103      $txp_else[$sha] = array($else > 0 ? $else : $count, $count - 2);
3104  
3105      if (!empty($order)) {
3106          $pre = array_filter($order, function ($v) {return $v > 0;});
3107          $post = array_filter($order, function ($v) {return $v < 0;});
3108  
3109          if  ($pre) {
3110              asort($pre);
3111          }
3112  
3113          if  ($post) {
3114              asort($post);
3115          }
3116  
3117          $txp_else[$sha]['test'] = $post ? array_merge(array_keys($pre), array(0), array_keys($post)) : ($pre ? array_keys($pre) : null);
3118          //rtrim(trim(implode(',', array_keys($pre)).',0,'.implode(',', array_keys($post)), ','), '0');
3119      }
3120  }
3121  
3122  
3123  /**
3124   * Extracts a statement from a if/else condition.
3125   *
3126   * @param   string  $thing     Statement in Textpattern tag markup presentation
3127   * @param   bool    $condition TRUE to return if statement, FALSE to else
3128   * @return  string             Either if or else statement
3129   * @since   4.8.2
3130   * @see     parse
3131   * @package TagParser
3132   * @example
3133   * echo getIfElse('true &lt;txp:else /&gt; false', 1 === 1);
3134   */
3135  
3136  function getIfElse($thing, $condition = true)
3137  {
3138      global $txp_parsed, $txp_else;
3139  
3140      if (!$thing || strpos($thing, ':else') === false) {
3141          return $condition ? $thing : null;
3142      }
3143  
3144      $hash = sha1($thing);
3145  
3146      if (!isset($txp_parsed[$hash])) {
3147          txp_tokenize($thing, $hash);
3148      }
3149  
3150      $tag = $txp_parsed[$hash];
3151      list($first, $last) = $txp_else[$hash];
3152  
3153      if ($condition) {
3154          $last = $first - 2;
3155          $first   = 1;
3156      } elseif ($first <= $last) {
3157          $first  += 2;
3158      } else {
3159          return null;
3160      }
3161  
3162      for ($out = $tag[$first - 1]; $first <= $last; $first++) {
3163          $out .= $tag[$first][0].$tag[$first][3].$tag[$first][4].$tag[++$first];
3164      }
3165  
3166      return $out;
3167  }
3168  
3169  /**
3170   * Extracts a statement from a if/else condition to parse.
3171   *
3172   * @param   string  $thing     Statement in Textpattern tag markup presentation
3173   * @param   bool    $condition TRUE to return if statement, FALSE to else
3174   * @return  string             Either if or else statement
3175   * @deprecated in 4.6.0
3176   * @see     parse
3177   * @package TagParser
3178   * @example
3179   * echo parse(EvalElse('true &lt;txp:else /&gt; false', 1 === 1));
3180   */
3181  
3182  function EvalElse($thing, $condition)
3183  {
3184      global $txp_atts;
3185  
3186      if (!empty($txp_atts['not'])) {
3187          $condition = empty($condition);
3188          unset($txp_atts['not']);
3189      }
3190  
3191      if (empty($condition)) {
3192          $txp_atts = null;
3193      }
3194  
3195      return (string)getIfElse($thing, $condition);
3196  }
3197  
3198  /**
3199   * Gets a form template's contents.
3200   *
3201   * The form template's reading method can be modified by registering a handler
3202   * to a 'form.fetch' callback event. Any value returned by the callback function
3203   * will be used as the form template markup.
3204   *
3205   * @param   array|string $name The form
3206   * @return  string
3207   * @package TagParser
3208   */
3209  
3210  function fetch_form($name, $theme = null)
3211  {
3212      global $skin;
3213      static $forms = array();
3214  
3215      isset($theme) or $theme = $skin;
3216      isset($forms[$theme]) or $forms[$theme] = array();
3217      $fetch = is_array($name);
3218  
3219      if ($fetch || !isset($forms[$theme][$name])) {
3220          $names = $fetch ? array_diff($name, array_keys($forms[$theme])) : array($name);
3221  
3222          if (has_handler('form.fetch')) {
3223              foreach ($names as $name) {
3224                  $forms[$theme][$name] = callback_event('form.fetch', '', false, compact('name', 'skin', 'theme'));
3225              }
3226          } elseif ($fetch) {
3227              $nameset = implode(',', quote_list($names));
3228  
3229              if ($nameset and $rs = safe_rows_start('name, Form', 'txp_form', "name IN (".$nameset.") AND skin = '".doSlash($theme)."'")) {
3230                  while ($row = nextRow($rs)) {
3231                      $forms[$theme][$row['name']] = $row['Form'];
3232                  }
3233              }
3234          } else {
3235              $forms[$theme][$name] = safe_field('Form', 'txp_form', "name ='".doSlash($name)."' AND skin = '".doSlash($theme)."'");
3236          }
3237  
3238          foreach ($names as $form) {
3239              if (empty($forms[$theme][$form])) {
3240                  trigger_error(gTxt('form_not_found').' '.$theme.'.'.$form);
3241                  $forms[$theme][$form] = false;
3242              }
3243          }
3244      }
3245  
3246      if (!$fetch) {
3247          return $forms[$theme][$name];
3248      }
3249  }
3250  
3251  /**
3252   * Parses a form template.
3253   *
3254   * @param   string $name The form
3255   * @return  string The parsed contents
3256   * @package TagParser
3257   */
3258  
3259  function parse_form($name, $theme = null)
3260  {
3261      global $production_status, $skin, $txp_current_form, $trace;
3262      static $stack = array(), $depth = null;
3263  
3264      if ($depth === null) {
3265          $depth = get_pref('form_circular_depth', 15);
3266      }
3267  
3268      isset($theme) or $theme = $skin;
3269      $name = (string) $name;
3270      $f = fetch_form($name, $theme);
3271  
3272      if ($f === false) {
3273          return false;
3274      }
3275  
3276      if (!isset($stack[$name])) {
3277          $stack[$name] = 1;
3278      } elseif ($stack[$name] >= $depth) {
3279          trigger_error(gTxt('form_circular_reference', array('{name}' => $name)));
3280  
3281          return '';
3282      } else {
3283          $stack[$name]++;
3284      }
3285  
3286      $old_form = $txp_current_form;
3287      $txp_current_form = $name;
3288  
3289      if ($production_status === 'debug') {
3290          $trace->log("[Form: '$theme.$name']");
3291          $trace->log("[Nesting forms: '".join("' / '", array_keys(array_filter($stack)))."'".($stack[$name] > 1 ? '('.$stack[$name].')' : '')."]");
3292      }
3293  
3294      $out = parse($f);
3295  
3296      $txp_current_form = $old_form;
3297      $stack[$name]--;
3298  
3299      return $out;
3300  }
3301  
3302  /**
3303   * Gets a page template's contents.
3304   *
3305   * The page template's reading method can be modified by registering a handler
3306   * to a 'page.fetch' callback event. Any value returned by the callback function
3307   * will be used as the template markup.
3308   *
3309   * @param   string      $name The template
3310   * @param   string      $theme The public theme
3311   * @return  string|bool The page template, or FALSE on error
3312   * @package TagParser
3313   * @since   4.6.0
3314   * @example
3315   * echo fetch_page('default');
3316   */
3317  
3318  function fetch_page($name, $theme)
3319  {
3320      global $pretext, $trace;
3321  
3322      if (empty($theme)) {
3323          if (empty($pretext['skin'])) {
3324              $pretext = safe_row("skin, page, css", "txp_section", "name='default'") + $pretext;
3325          }
3326  
3327          $theme = $pretext['skin'];
3328      }
3329  
3330      if (has_handler('page.fetch')) {
3331          $page = callback_event('page.fetch', '', false, compact('name', 'theme'));
3332      } else {
3333          $page = safe_field('user_html', 'txp_page', "name = '".doSlash($name)."' AND skin = '".doSlash($theme)."'");
3334      }
3335  
3336      if ($page === false) {
3337          return false;
3338      }
3339  
3340      $trace->log("[Page: '$theme.$name']");
3341  
3342      return $page;
3343  }
3344  
3345  /**
3346   * Parses a page template.
3347   *
3348   * @param   string      $name  The template to parse
3349   * @param   string      $theme The public theme
3350   * @param   string      $page  Default content to parse
3351   * @return  string|bool The parsed page template, or FALSE on error
3352   * @since   4.6.0
3353   * @package TagParser
3354   * @example
3355   * echo parse_page('default');
3356   */
3357  
3358  function parse_page($name, $theme, $page = '')
3359  {
3360      global $pretext, $trace;
3361  
3362      if (!$page) {
3363          $page = fetch_page($name, $theme);
3364      }
3365  
3366      if ($page !== false) {
3367          while ($pretext['secondpass'] <= get_pref('secondpass', 1) && preg_match('@<(?:'.TXP_PATTERN.'):@', $page)) {
3368              $page = parse($page);
3369              // the function so nice, he ran it twice
3370              $pretext['secondpass']++;
3371              $trace->log('[ ~~~ secondpass ('.$pretext['secondpass'].') ~~~ ]');
3372          }
3373      }
3374  
3375      return $page;
3376  }
3377  
3378  /**
3379   * Gets a HTML select field containing all categories, or sub-categories.
3380   *
3381   * @param   string $name Return specified parent category's sub-categories
3382   * @param   string $cat  The selected category option
3383   * @param   string $id   The HTML ID
3384   * @return  string|bool HTML select field or FALSE on error
3385   * @package Form
3386   */
3387  
3388  function event_category_popup($name, $cat = '', $id = '', $atts = array())
3389  {
3390      $rs = getTree('root', $name);
3391  
3392      if ($rs) {
3393          return treeSelectInput('category', $rs, $cat, $id, 0, $atts);
3394      }
3395  
3396      return false;
3397  }
3398  
3399  /**
3400   * Gets a category's title.
3401   *
3402   * @param  string $name The category
3403   * @param  string $type Category's type. Either "article", "file", "image" or "link"
3404   * @return string|bool The title or FALSE on error
3405   */
3406  
3407  function fetch_category_title($name, $type = 'article')
3408  {
3409      static $cattitles = array();
3410      global $thiscategory;
3411  
3412      if (isset($cattitles[$type][$name])) {
3413          return $cattitles[$type][$name];
3414      }
3415  
3416      if (!empty($thiscategory['title']) && $thiscategory['name'] == $name && $thiscategory['type'] == $type) {
3417          $cattitles[$type][$name] = $thiscategory['title'];
3418  
3419          return $thiscategory['title'];
3420      }
3421  
3422      $f = safe_field("title", 'txp_category', "name = '".doSlash($name)."' AND type = '".doSlash($type)."'");
3423      $cattitles[$type][$name] = $f;
3424  
3425      return $f;
3426  }
3427  
3428  /**
3429   * Gets a section's title.
3430   *
3431   * @param  string $name The section
3432   * @return string|bool The title or FALSE on error
3433   */
3434  
3435  function fetch_section_title($name)
3436  {
3437      static $sectitles = array();
3438      global $thissection, $txp_sections;
3439  
3440      // Try cache.
3441      if (isset($sectitles[$name])) {
3442          return $sectitles[$name];
3443      }
3444  
3445      if (!empty($thissection) && $thissection['name'] == $name) {
3446          return $thissection['title'];
3447      } elseif ($name == 'default' or empty($name)) {
3448          return '';
3449      } elseif (isset($txp_sections[$name])) {
3450          return $sectitles[$name] = $txp_sections[$name]['title'];
3451      }
3452  
3453      $f = safe_field("title", 'txp_section', "name = '".doSlash($name)."'");
3454  
3455      return $sectitles[$name] = $f;
3456  }
3457  
3458  /**
3459   * Updates an article's comment count.
3460   *
3461   * @param   int $id The article
3462   * @return  bool
3463   * @package Comment
3464   */
3465  
3466  function update_comments_count($id)
3467  {
3468      $id = assert_int($id);
3469      $thecount = safe_field("COUNT(*)", 'txp_discuss', "parentid = '".$id."' AND visible = ".VISIBLE);
3470      $thecount = assert_int($thecount);
3471      $updated = safe_update('textpattern', "comments_count = ".$thecount, "ID = '".$id."'");
3472  
3473      return ($updated) ? true : false;
3474  }
3475  
3476  /**
3477   * Recalculates and updates comment counts.
3478   *
3479   * @param   array $parentids List of articles to update
3480   * @package Comment
3481   */
3482  
3483  function clean_comment_counts($parentids)
3484  {
3485      $parentids = array_map('assert_int', $parentids);
3486      $parentids = array_filter($parentids);
3487  
3488      if ($parentids) {
3489          $rs = safe_rows_start("parentid, COUNT(*) AS thecount", 'txp_discuss', "parentid IN (".implode(',', $parentids).") AND visible = ".VISIBLE." GROUP BY parentid");
3490  
3491          if (!$rs) {
3492              return;
3493          }
3494  
3495          $updated = array();
3496  
3497          while ($a = nextRow($rs)) {
3498              safe_update('textpattern', "comments_count = ".$a['thecount'], "ID = ".$a['parentid']);
3499              $updated[] = $a['parentid'];
3500          }
3501  
3502          // We still need to update all those, that have zero comments left.
3503          $leftover = array_diff($parentids, $updated);
3504  
3505          if ($leftover) {
3506              safe_update('textpattern', "comments_count = 0", "ID IN (".implode(',', $leftover).")");
3507          }
3508      }
3509  }
3510  
3511  /**
3512   * Parses and formats comment message using Textile.
3513   *
3514   * @param   string $msg The comment message
3515   * @return  string HTML markup
3516   * @package Comment
3517   */
3518  
3519  function markup_comment($msg)
3520  {
3521      $textile = new \Textpattern\Textile\RestrictedParser();
3522  
3523      return $textile->parse($msg);
3524  }
3525  
3526  /**
3527   * Updates site's last modification date.
3528   *
3529   * When this action is performed, it will trigger a
3530   * 'site.update > {event}' callback event and pass
3531   * any record set that triggered the update, along
3532   * with the exact time the update was triggered.
3533   *
3534   * @param   $trigger Textpattern event or step that triggered the update
3535   * @param   $rs      Record set data at the time of update
3536   * @package Pref
3537   * @example
3538   * update_lastmod();
3539   */
3540  
3541  function update_lastmod($trigger = '', $rs = array())
3542  {
3543      $whenStamp = time();
3544      $whenDate = strftime('%Y-%m-%d %H:%M:%S', $whenStamp);
3545  
3546      safe_upsert('txp_prefs', "val = '$whenDate'", "name = 'lastmod'");
3547      callback_event('site.update', $trigger, 0, $rs, compact('whenStamp', 'whenDate'));
3548  }
3549  
3550  /**
3551   * Gets the site's last modification date.
3552   *
3553   * @param   int $unix_ts UNIX timestamp
3554   * @return  int UNIX timestamp
3555   * @package Pref
3556   */
3557  
3558  function get_lastmod($unix_ts = null)
3559  {
3560      if ($unix_ts === null) {
3561          $unix_ts = @strtotime(get_pref('lastmod'));
3562      }
3563  
3564      // Check for future articles that are now visible.
3565      if (txpinterface === 'public' && $max_article = safe_field("UNIX_TIMESTAMP(Posted)", 'textpattern', "Posted <= ".now('posted')." AND Status >= 4 ORDER BY Posted DESC LIMIT 1")) {
3566          $unix_ts = max($unix_ts, $max_article);
3567      }
3568  
3569      return $unix_ts;
3570  }
3571  
3572  /**
3573   * Sets headers.
3574   *
3575   * @param   array $headers    'name' => 'value'
3576   * @param   bool  $rewrite    If TRUE, rewrites existing headers
3577   */
3578  
3579  function set_headers($headers = array('Content-Type' => 'text/html; charset=utf-8'), $rewrite = false)
3580  {
3581      if (headers_sent()) {
3582          return;
3583      }
3584  
3585      $rewrite = (int)$rewrite;
3586      $out = $headers_low = array();
3587  
3588      if (($rewrite != 1 || in_array(true, $headers, true)) && $headers_list = headers_list()) {
3589          foreach ($headers_list as $header) {
3590              list($name, $value) = explode(':', $header, 2) + array(null, null);
3591              $headers_low[strtolower(trim($name))] = $value;
3592          }
3593      }
3594  
3595      foreach ($headers as $name => $header) {
3596          $name_low = strtolower(trim($name));
3597  
3598          if ((string)$header === '') {
3599              !$rewrite or header_remove($name && $name != 1 ? $name : null);
3600          } elseif ($header === true) {
3601              if ($name == 1) {
3602                  $out = array_merge($out, $headers_low);
3603              } elseif (isset($headers_low[$name_low])) {
3604                  $out[$name_low] = $headers_low[$name_low];
3605              }
3606          } elseif ($name == 1) {
3607              txp_status_header($header);
3608          } elseif ($rewrite == 1 || !isset($headers_low[$name_low])) {
3609              header($name ? $name.': '.$header : $header);
3610          } elseif ($rewrite) {
3611              $header = implode(', ', do_list_unique($headers_low[$name_low].','.$header));
3612              header($name ? $name.': '.$header : $header);
3613          }
3614      }
3615  
3616      return $out ? $out : null;
3617  }
3618  
3619  /**
3620   * Sends and handles a lastmod header.
3621   *
3622   * @param   int|null $unix_ts The last modification date as a UNIX timestamp
3623   * @param   bool     $exit    If TRUE, terminates the script
3624   * @return  array|null Array of sent HTTP status and the lastmod header, or NULL
3625   * @package Pref
3626   */
3627  
3628  function handle_lastmod($unix_ts = null, $exit = true)
3629  {
3630      // Disable caching when not in production
3631      if (get_pref('production_status') != 'live') {
3632          header('Cache-Control: no-cache, no-store, max-age=0');
3633      } elseif (get_pref('send_lastmod')) {
3634          $unix_ts = get_lastmod($unix_ts);
3635  
3636          // Make sure lastmod isn't in the future.
3637          $unix_ts = min($unix_ts, time());
3638  
3639          $last = safe_strftime('rfc822', $unix_ts, 1);
3640          header("Last-Modified: $last");
3641  
3642          $etag = base_convert($unix_ts, 10, 32);
3643          header('ETag: "' . $etag . '"');
3644  
3645          // Get timestamp from request caching headers
3646          if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
3647              $hims = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
3648              $imsd = ($hims) ? strtotime($hims) : 0;
3649          } elseif (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
3650              $hinm = trim(trim($_SERVER['HTTP_IF_NONE_MATCH']), '"');
3651              $hinm_apache_gzip_workaround = explode('-gzip', $hinm);
3652              $hinm_apache_gzip_workaround = $hinm_apache_gzip_workaround[0];
3653              $inmd = ($hinm) ? base_convert($hinm_apache_gzip_workaround, 32, 10) : 0;
3654          }
3655  
3656          // Check request timestamps against the current timestamp
3657          if ((isset($imsd) && $imsd >= $unix_ts) ||
3658              (isset($inmd) && $inmd >= $unix_ts)) {
3659              log_hit('304');
3660  
3661              header('Content-Length: 0');
3662  
3663              txp_status_header('304 Not Modified');
3664  
3665              if ($exit) {
3666                  exit();
3667              }
3668  
3669              return array('304', $last);
3670          }
3671  
3672          return array('200', $last);
3673      }
3674  }
3675  
3676  /**
3677   * Gets preferences as an array.
3678   *
3679   * Returns preference values from the database as an array. Shouldn't be used to
3680   * retrieve selected preferences, see get_pref() instead.
3681   *
3682   * By default only the global preferences are returned.
3683   * If the optional user name parameter is supplied, the private preferences
3684   * for that user are returned.
3685   *
3686   * @param   string $user User name.
3687   * @return  array
3688   * @package Pref
3689   * @access  private
3690   * @see     get_pref()
3691   */
3692  
3693  function get_prefs($user = '')
3694  {
3695      $out = array();
3696      $user = implode(',', (array) quote_list($user));
3697  
3698      $r = safe_rows_start("name, val", 'txp_prefs', "user_name IN (".$user.") ORDER BY FIELD(user_name, ".$user.")");
3699  
3700      if ($r) {
3701          while ($a = nextRow($r)) {
3702              $out[$a['name']] = $a['val'];
3703          }
3704      }
3705  
3706      return $out;
3707  }
3708  
3709  /**
3710   * Creates or updates a preference.
3711   *
3712   * @param   string $name       The name
3713   * @param   string $val        The value
3714   * @param   string $event      The section the preference appears in
3715   * @param   int    $type       Either PREF_CORE, PREF_PLUGIN, PREF_HIDDEN
3716   * @param   string $html       The HTML control type the field uses. Can take a custom function name
3717   * @param   int    $position   Used to sort the field on the Preferences panel
3718   * @param   bool   $is_private If PREF_PRIVATE, is created as a user pref
3719   * @return  bool FALSE on error
3720   * @package Pref
3721   * @example
3722   * if (set_pref('myPref', 'value'))
3723   * {
3724   *     echo "'myPref' created or updated.";
3725   * }
3726   */
3727  
3728  function set_pref($name, $val, $event = 'publish', $type = PREF_CORE, $html = 'text_input', $position = 0, $is_private = PREF_GLOBAL)
3729  {
3730      global $prefs;
3731      $prefs[$name] = $val;
3732      $user_name = null;
3733  
3734      if ($is_private == PREF_PRIVATE) {
3735          $user_name = PREF_PRIVATE;
3736      }
3737  
3738      if (pref_exists($name, $user_name)) {
3739          return update_pref($name, $val, null, null, null, null, $user_name);
3740      }
3741  
3742      return create_pref($name, $val, $event, $type, $html, $position, $user_name);
3743  }
3744  
3745  /**
3746   * Gets a preference string.
3747   *
3748   * Prefers global system-wide preferences over a user's private preferences.
3749   *
3750   * @param   string $thing   The named variable
3751   * @param   mixed  $default Used as a replacement if named pref isn't found
3752   * @param   bool   $from_db If TRUE checks database opposed $prefs variable in memory
3753   * @return  string Preference value or $default
3754   * @package Pref
3755   * @example
3756   * if (get_pref('enable_xmlrpc_server'))
3757   * {
3758   *     echo "XML-RPC server is enabled.";
3759   * }
3760   */
3761  
3762  function get_pref($thing, $default = '', $from_db = false)
3763  {
3764      global $prefs, $txp_user;
3765  
3766      if ($from_db) {
3767          $name = doSlash($thing);
3768          $user_name = doSlash($txp_user);
3769  
3770          $field = safe_field(
3771              "val",
3772              'txp_prefs',
3773              "name = '$name' AND (user_name = '' OR user_name = '$user_name') ORDER BY user_name LIMIT 1"
3774          );
3775  
3776          if ($field !== false) {
3777              $prefs[$thing] = $field;
3778          }
3779      }
3780  
3781      if (isset($prefs[$thing])) {
3782          return $prefs[$thing];
3783      }
3784  
3785      return $default;
3786  }
3787  
3788  /**
3789   * Removes a preference string.
3790   *
3791   * Removes preference strings based on the given arguments. Use NULL to omit an argument.
3792   *
3793   * @param   string|null      $name      The preference string name
3794   * @param   string|null      $event     The preference event
3795   * @param   string|null|bool $user_name The owner. If PREF_PRIVATE, the current user
3796   * @return  bool TRUE on success
3797   * @since   4.6.0
3798   * @package Pref
3799   * @example
3800   * if (remove_pref(null, 'myEvent'))
3801   * {
3802   *     echo "Removed all preferences from 'myEvent'.";
3803   * }
3804   */
3805  
3806  function remove_pref($name = null, $event = null, $user_name = null)
3807  {
3808      global $txp_user;
3809  
3810      $sql = array();
3811  
3812      if ($user_name === PREF_PRIVATE) {
3813          if (!$txp_user) {
3814              return false;
3815          }
3816  
3817          $user_name = $txp_user;
3818      }
3819  
3820      if ($user_name !== null) {
3821          $sql[] = "user_name = '".doSlash((string) $user_name)."'";
3822      }
3823  
3824      if ($event !== null) {
3825          $sql[] = "event = '".doSlash($event)."'";
3826      }
3827  
3828      if ($name !== null) {
3829          $sql[] = "name = '".doSlash($name)."'";
3830      }
3831  
3832      if ($sql) {
3833          return safe_delete('txp_prefs', join(" AND ", $sql));
3834      }
3835  
3836      return false;
3837  }
3838  
3839  /**
3840   * Checks if a preference string exists.
3841   *
3842   * Searches for matching preference strings based on the given arguments.
3843   *
3844   * The $user_name argument can be used to limit the search to a specific user,
3845   * or to global and private strings. If NULL, matches are searched from both
3846   * private and global strings.
3847   *
3848   * @param   string           $name      The preference string name
3849   * @param   string|null|bool $user_name Either the username, NULL, PREF_PRIVATE or PREF_GLOBAL
3850   * @return  bool TRUE if the string exists, or FALSE on error
3851   * @since   4.6.0
3852   * @package Pref
3853   * @example
3854   * if (pref_exists('myPref'))
3855   * {
3856   *     echo "'myPref' exists.";
3857   * }
3858   */
3859  
3860  function pref_exists($name, $user_name = null)
3861  {
3862      global $txp_user;
3863  
3864      $sql = array();
3865      $sql[] = "name = '".doSlash($name)."'";
3866  
3867      if ($user_name === PREF_PRIVATE) {
3868          if (!$txp_user) {
3869              return false;
3870          }
3871  
3872          $user_name = $txp_user;
3873      }
3874  
3875      if ($user_name !== null) {
3876          $sql[] = "user_name = '".doSlash((string) $user_name)."'";
3877      }
3878  
3879      if (safe_row("name", 'txp_prefs', join(" AND ", $sql))) {
3880          return true;
3881      }
3882  
3883      return false;
3884  }
3885  
3886  /**
3887   * Creates a preference string.
3888   *
3889   * When a string is created, will trigger a 'preference.create > done' callback event.
3890   *
3891   * @param   string      $name       The name
3892   * @param   string      $val        The value
3893   * @param   string      $event      The section the preference appears in
3894   * @param   int         $type       Either PREF_CORE, PREF_PLUGIN, PREF_HIDDEN
3895   * @param   string      $html       The HTML control type the field uses. Can take a custom function name
3896   * @param   int         $position   Used to sort the field on the Preferences panel
3897   * @param   string|bool $user_name  The user name, PREF_GLOBAL or PREF_PRIVATE
3898   * @return  bool TRUE if the string exists, FALSE on error
3899   * @since   4.6.0
3900   * @package Pref
3901   * @example
3902   * if (create_pref('myPref', 'value', 'site', PREF_PLUGIN, 'text_input', 25))
3903   * {
3904   *     echo "'myPref' created.";
3905   * }
3906   */
3907  
3908  function create_pref($name, $val, $event = 'publish', $type = PREF_CORE, $html = 'text_input', $position = 0, $user_name = PREF_GLOBAL)
3909  {
3910      global $txp_user;
3911  
3912      if ($user_name === PREF_PRIVATE) {
3913          if (!$txp_user) {
3914              return false;
3915          }
3916  
3917          $user_name = $txp_user;
3918      }
3919  
3920      if (pref_exists($name, $user_name)) {
3921          return true;
3922      }
3923  
3924      $val = is_scalar($val) ? (string)$val : json_encode($val, TEXTPATTERN_JSON);
3925  
3926      if (
3927          safe_insert(
3928              'txp_prefs',
3929              "name = '".doSlash($name)."',
3930              val = '".doSlash($val)."',
3931              event = '".doSlash($event)."',
3932              html = '".doSlash($html)."',
3933              type = ".intval($type).",
3934              position = ".intval($position).",
3935              user_name = '".doSlash((string) $user_name)."'"
3936          ) === false
3937      ) {
3938          return false;
3939      }
3940  
3941      callback_event('preference.create', 'done', 0, compact('name', 'val', 'event', 'type', 'html', 'position', 'user_name'));
3942  
3943      return true;
3944  }
3945  
3946  /**
3947   * Updates a preference string.
3948   *
3949   * Updates a preference string's properties. The $name and $user_name
3950   * arguments are used for selecting the updated string, and rest of the
3951   * arguments take the new values. Use NULL to omit an argument.
3952   *
3953   * When a string is updated, will trigger a 'preference.update > done' callback event.
3954   *
3955   * @param   string           $name       The update preference string's name
3956   * @param   string|null      $val        The value
3957   * @param   string|null      $event      The section the preference appears in
3958   * @param   int|null         $type       Either PREF_CORE, PREF_PLUGIN, PREF_HIDDEN
3959   * @param   string|null      $html       The HTML control type the field uses. Can take a custom function name
3960   * @param   int|null         $position   Used to sort the field on the Preferences panel
3961   * @param   string|bool|null $user_name  The updated string's owner, PREF_GLOBAL or PREF_PRIVATE
3962   * @return  bool             FALSE on error
3963   * @since   4.6.0
3964   * @package Pref
3965   * @example
3966   * if (update_pref('myPref', 'New value.'))
3967   * {
3968   *     echo "Updated 'myPref' value.";
3969   * }
3970   */
3971  
3972  function update_pref($name, $val = null, $event = null, $type = null, $html = null, $position = null, $user_name = PREF_GLOBAL)
3973  {
3974      global $txp_user;
3975  
3976      $where = $set = array();
3977      $where[] = "name = '".doSlash($name)."'";
3978  
3979      if ($user_name === PREF_PRIVATE) {
3980          if (!$txp_user) {
3981              return false;
3982          }
3983  
3984          $user_name = $txp_user;
3985      }
3986  
3987      if ($user_name !== null) {
3988          $where[] = "user_name = '".doSlash((string) $user_name)."'";
3989      }
3990  
3991      if (isset($val)) {
3992          $val = is_scalar($val) ? (string)$val : json_encode($val, TEXTPATTERN_JSON);
3993      }
3994  
3995      foreach (array('val', 'event', 'type', 'html', 'position') as $field) {
3996          if ($$field !== null) {
3997              $set[] = $field." = '".doSlash($$field)."'";
3998          }
3999      }
4000  
4001      if ($set && safe_update('txp_prefs', join(', ', $set), join(" AND ", $where))) {
4002          callback_event('preference.update', 'done', 0, compact('name', 'val', 'event', 'type', 'html', 'position', 'user_name'));
4003  
4004          return true;
4005      }
4006  
4007      return false;
4008  }
4009  
4010  /**
4011   * Renames a preference string.
4012   *
4013   * When a string is renamed, will trigger a 'preference.rename > done' callback event.
4014   *
4015   * @param   string $newname   The new name
4016   * @param   string $name      The current name
4017   * @param   string $user_name Either the username, PREF_GLOBAL or PREF_PRIVATE
4018   * @return  bool FALSE on error
4019   * @since   4.6.0
4020   * @package Pref
4021   * @example
4022   * if (rename_pref('mynewPref', 'myPref'))
4023   * {
4024   *     echo "Renamed 'myPref' to 'mynewPref'.";
4025   * }
4026   */
4027  
4028  function rename_pref($newname, $name, $user_name = null)
4029  {
4030      global $txp_user;
4031  
4032      $where = array();
4033      $where[] = "name = '".doSlash($name)."'";
4034  
4035      if ($user_name === PREF_PRIVATE) {
4036          if (!$txp_user) {
4037              return false;
4038          }
4039  
4040          $user_name = $txp_user;
4041      }
4042  
4043      if ($user_name !== null) {
4044          $where[] = "user_name = '".doSlash((string) $user_name)."'";
4045      }
4046  
4047      if (safe_update('txp_prefs', "name = '".doSlash($newname)."'", join(" AND ", $where))) {
4048          callback_event('preference.rename', 'done', 0, compact('newname', 'name', 'user_name'));
4049  
4050          return true;
4051      }
4052  
4053      return false;
4054  }
4055  
4056  /**
4057   * Gets a list of custom fields.
4058   *
4059   * @return  array
4060   * @package CustomField
4061   */
4062  
4063  function getCustomFields()
4064  {
4065      global $prefs;
4066      static $out = null;
4067  
4068      // Have cache?
4069      if (!is_array($out)) {
4070          $cfs = preg_grep('/^custom_\d+_set/', array_keys($prefs));
4071          $out = array();
4072  
4073          foreach ($cfs as $name) {
4074              preg_match('/(\d+)/', $name, $match);
4075  
4076              if ($prefs[$name] !== '') {
4077                  $out[$match[1]] = strtolower($prefs[$name]);
4078              }
4079          }
4080      }
4081  
4082      return $out;
4083  }
4084  
4085  /**
4086   * Build a query qualifier to filter non-matching custom fields from the
4087   * result set.
4088   *
4089   * @param   array $custom An array of 'custom_field_name' => field_number tuples
4090   * @param   array $pairs  Filter criteria: An array of 'name' => value tuples
4091   * @return  bool|string An SQL qualifier for a query's 'WHERE' part
4092   * @package CustomField
4093   */
4094  
4095  function buildCustomSql($custom, $pairs, $exclude = array())
4096  {
4097      if ($pairs) {
4098          foreach ($pairs as $k => $val) {
4099              $no = array_search($k, $custom);
4100  
4101              if ($no !== false) {
4102                  $not = ($exclude === true || isset($exclude[$k])) ? 'NOT ' : '';
4103  
4104                  if ($val === true) {
4105                      $out[] = "({$not}custom_{$no} != '')";
4106                  } else {
4107                      $val = doSlash($val);
4108                      $parts = array();
4109  
4110                      foreach ((array)$val as $v) {
4111                          list($from, $to) = explode('%%', $v, 2) + array(null, null);
4112  
4113                          if (!isset($to)) {
4114                              $parts[] = "{$not}custom_{$no} LIKE '$from'";
4115                          } elseif ($from !== '') {
4116                              $parts[] = $to === '' ? "{$not}custom_{$no} >= '$from'" :  "{$not}custom_{$no} BETWEEN '$from' AND '$to'";
4117                          } elseif ($to !== '') {
4118                              $parts[] = "{$not}custom_{$no} <= '$to'";
4119                          }
4120                      }
4121  
4122                      if ($parts) {
4123                          $out[] = '('.join(' OR ', $parts).')';
4124                      }
4125                  }
4126              }
4127          }
4128      }
4129  
4130      return !empty($out) ? ' AND '.join(' AND ', $out).' ' : false;
4131  }
4132  
4133  /**
4134   * Build a query qualifier to filter time fields from the
4135   * result set.
4136   *
4137   * @param   string $month A starting time point
4138   * @param   string $time  A time offset
4139   * @param   string $field The field to filter
4140   * @return  string An SQL qualifier for a query's 'WHERE' part
4141   */
4142  
4143  function buildTimeSql($month, $time, $field = 'Posted')
4144  {
4145      $safe_field = '`'.doSlash($field).'`';
4146      $timeq = '1';
4147  
4148      if ($month === 'past' || $month === 'any' || $month === 'future') {
4149          if ($month === 'past') {
4150              $timeq = "$safe_field <= ".now($field);
4151          } elseif ($month === 'future') {
4152              $timeq = "$safe_field > ".now($field);
4153          }
4154      } elseif ($time === 'past' || $time === 'any' || $time === 'future') {
4155          if ($time === 'past') {
4156              $timeq = "$safe_field <= ".now($field);
4157          } elseif ($time === 'future') {
4158              $timeq = "$safe_field > ".now($field);
4159          }
4160  
4161          $timeq .= ($month ? " AND $safe_field LIKE '".doSlash($month)."%'" : '');
4162      } elseif (strpos($time, '%') !== false) {
4163          $start = $month ? strtotime($month) : time() or $start = time();
4164          $timeq = "$safe_field LIKE '".doSlash(strftime($time, $start))."%'";
4165      } else {
4166          $start = $month ? safe_strtotime($month) : false;
4167  
4168          if ($start === false) {
4169              $from = $month ? "'".doSlash($month)."'" : now($field);
4170              $start = time();
4171          } else {
4172              $from = "FROM_UNIXTIME($start)";
4173          }
4174  
4175          if ($time === 'since') {
4176              $timeq = "$safe_field > $from";
4177          } elseif ($time === 'until') {
4178              $timeq = "$safe_field <= $from";
4179          } else {
4180              $stop = strtotime($time, $start) or $stop = time();
4181  
4182              if ($start > $stop) {
4183                  list($start, $stop) = array($stop, $start);
4184              }
4185  
4186              $timeq = ($start == $stop ?
4187                  "$safe_field = FROM_UNIXTIME($start)" :
4188                  "$safe_field BETWEEN FROM_UNIXTIME($start) AND FROM_UNIXTIME($stop)"
4189              );
4190          }
4191      }
4192  
4193      return $timeq;
4194  }
4195  
4196  /**
4197   * Sends a HTTP status header.
4198   *
4199   * @param   string $status The HTTP status code
4200   * @package Network
4201   * @example
4202   * txp_status_header('403 Forbidden');
4203   */
4204  
4205  function txp_status_header($status = '200 OK')
4206  {
4207      if (IS_FASTCGI) {
4208          header("Status: $status");
4209      } elseif (serverSet('SERVER_PROTOCOL') == 'HTTP/1.0') {
4210          header("HTTP/1.0 $status");
4211      } else {
4212          header("HTTP/1.1 $status");
4213      }
4214  }
4215  
4216  /**
4217   * Terminates normal page rendition and outputs an error page.
4218   *
4219   * @param   string|array $msg    The error message
4220   * @param   string       $status HTTP status code
4221   * @param   string       $url    Redirects to the specified URL. Can be used with $status of 301, 302 and 307
4222   * @package Tag
4223   */
4224  
4225  function txp_die($msg, $status = '503', $url = '')
4226  {
4227      global $connected, $txp_error_message, $txp_error_status, $txp_error_code, $pretext, $production_status, $trace;
4228  
4229      // Make it possible to call this function as a tag, e.g. in an article
4230      // <txp:txp_die status="410" />.
4231      if (is_array($msg)) {
4232          extract(lAtts(array(
4233              'msg'    => '',
4234              'status' => '503',
4235              'url'    => '',
4236          ), $msg));
4237      }
4238  
4239      // Intentionally incomplete - just the ones we're likely to use.
4240      $codes = array(
4241          '200' => 'OK',
4242          '301' => 'Moved Permanently',
4243          '302' => 'Found',
4244          '303' => 'See Other',
4245          '304' => 'Not Modified',
4246          '307' => 'Temporary Redirect',
4247          '308' => 'Permanent Redirect',
4248          '401' => 'Unauthorized',
4249          '403' => 'Forbidden',
4250          '404' => 'Not Found',
4251          '410' => 'Gone',
4252          '414' => 'Request-URI Too Long',
4253          '451' => 'Unavailable For Legal Reasons',
4254          '500' => 'Internal Server Error',
4255          '501' => 'Not Implemented',
4256          '503' => 'Service Unavailable'
4257      );
4258  
4259      if ($status) {
4260          if (isset($codes[strval($status)])) {
4261              $status = strval($status).' '.$codes[$status];
4262          }
4263  
4264          txp_status_header($status);
4265      }
4266  
4267      $code = (int) $status;
4268  
4269      callback_event('txp_die', $code, 0, $url);
4270  
4271      // Redirect with status.
4272      if ($url && in_array($code, array(301, 302, 303, 307, 308))) {
4273          ob_end_clean();
4274          header("Location: $url", true, $code);
4275          die('<html><head><meta http-equiv="refresh" content="0;URL='.txpspecialchars($url).'"></head><body><p>Document has <a href="'.txpspecialchars($url).'">moved here</a>.</p></body></html>');
4276      }
4277  
4278      $out = false;
4279      $skin = empty($pretext['skin']) ? null : $pretext['skin'];
4280  
4281      if ($connected && @txpinterface == 'public') {
4282          $out = fetch_page("error_{$code}", $skin) or $out = fetch_page('error_default', $skin);
4283      }
4284  
4285      if ($out === false) {
4286          $out = <<<eod
4287  <!DOCTYPE html>
4288  <html lang="en">
4289  <head>
4290     <meta charset="utf-8">
4291     <meta name="robots" content="noindex">
4292     <title>Textpattern Error: <txp:error_status /></title>
4293  </head>
4294  <body>
4295      <p><txp:error_message /></p>
4296  </body>
4297  </html>
4298  eod;
4299      }
4300  
4301      header("Content-Type: text/html; charset=utf-8");
4302      $debug = $production_status === 'live' ?
4303          '' :
4304          $trace->summary().($production_status === 'debug' ? $trace->result() : '');
4305  
4306      if (is_callable('parse')) {
4307          $txp_error_message = $msg;
4308          $txp_error_status = $status;
4309          $txp_error_code = $code;
4310          set_error_handler("tagErrorHandler");
4311          die(parse($out).$debug);
4312      } else {
4313          $out = preg_replace(
4314              array('@<txp:error_status[^>]*/>@', '@<txp:error_message[^>]*/>@'),
4315              array($status, $msg),
4316              $out
4317          );
4318  
4319          die($out.$debug);
4320      }
4321  }
4322  
4323  /**
4324   * Get field => alias array.
4325   *
4326   * @param   string $match
4327   * @return  array()
4328   * @since   4.8.0
4329   * @package TagParser
4330   */
4331  
4332  function parse_qs($match, $sep='=')
4333  {
4334      $pairs = array();
4335  
4336      foreach (do_list_unique($match) as $chunk) {
4337          $name = strtok($chunk, $sep);
4338          $alias = strtok($sep);
4339          $pairs[strtolower($name)] = $alias;
4340      };
4341  
4342      return $pairs;
4343  }
4344  
4345  /**
4346   * Gets a URL-encoded and HTML entity-escaped query string for a URL.
4347   *
4348   * Builds a HTTP query string from an associative array.
4349   *
4350   * @param   array $q The parameters for the query
4351   * @return  string The query, including starting "?".
4352   * @package URL
4353   * @example
4354   * echo join_qs(array('param1' => 'value1', 'param2' => 'value2'));
4355   */
4356  
4357  function join_qs($q, $sep = '&amp;')
4358  {
4359      $qs = array();
4360      $sql = $sep !== '&amp;';
4361  
4362      foreach ($q as $k => $v) {
4363          if (is_array($v)) {
4364              $v = join(',', $v);
4365          }
4366  
4367          if ($k && (string) $v !== '') {
4368              $qs[$k] = $sql ? "$k = $v" : urlencode($k).'='.urlencode($v);
4369          }
4370      }
4371  
4372      if (!isset($sep)) {
4373          return $qs;
4374      }
4375  
4376      $str = join($sep, $qs);
4377  
4378      return  $str ? ($sql ? '' : '?').$str : '';
4379  }
4380  
4381  /**
4382   * Builds a HTML attribute list from an array.
4383   *
4384   * Takes an array of raw HTML attributes, and returns a properly
4385   * sanitised HTML attribute string for use in a HTML tag.
4386   *
4387   * Internally handles HTML boolean attributes, array lists and query strings.
4388   * If an attributes value is set as a boolean, the attribute is considered
4389   * as one too. If a value is NULL, it's omitted and the attribute is added
4390   * without a value. An array value is converted to a space-separated list,
4391   * or for 'href' and 'src' to a URL encoded query string.
4392   *
4393   * @param   array|string  $atts  HTML attributes
4394   * @param   int           $flags TEXTPATTERN_STRIP_EMPTY_STRING
4395   * @return  string HTML attribute list
4396   * @since   4.6.0
4397   * @package HTML
4398   * @example
4399   * echo join_atts(array('class' => 'myClass', 'disabled' => true));
4400   */
4401  
4402  function join_atts($atts, $flags = TEXTPATTERN_STRIP_EMPTY_STRING, $glue = ' ')
4403  {
4404      if (!is_array($atts)) {
4405          return $atts ? ' '.trim($atts) : '';
4406      }
4407  
4408      $list = '';
4409      $txp = $flags & TEXTPATTERN_STRIP_TXP;
4410  
4411      foreach ($atts as $name => $value) {
4412          if (($flags & TEXTPATTERN_STRIP_EMPTY && !$value) || ($value === false) || ($txp && $value === null)) {
4413              continue;
4414          } elseif ($value === null || $txp && $value === true) {
4415              $list .= ' '.$name;
4416              continue;
4417          } elseif (is_array($value)) {
4418              if ($name == 'href' || $name == 'src') {
4419                  $value = join_qs($value);
4420              } else {
4421                  $value = txpspecialchars(join($glue, $value));
4422              }
4423          } elseif ($name != 'href' && $name != 'src') {
4424              $value = txpspecialchars($value === true ? $name : $value);
4425          } else {
4426              $value = txpspecialchars(str_replace('&amp;', '&', $value));
4427          }
4428  
4429          if (!($flags & TEXTPATTERN_STRIP_EMPTY_STRING && $value === '')) {
4430              $list .= ' '.$name.'="'.$value.'"';
4431          }
4432      }
4433  
4434      return $list;
4435  }
4436  
4437  /**
4438   * Builds a page URL from an array of parameters.
4439   *
4440   * The $inherit can be used to add parameters to an existing url, e.g:
4441   * pagelinkurl(array('pg' => 2), $pretext).
4442   *
4443   * Cannot be used to link to an article. See permlinkurl() and permlinkurl_id() instead.
4444   *
4445   * @param   array $parts   The parts used to construct the URL
4446   * @param   array $inherit Can be used to add parameters to an existing url
4447   * @return  string
4448   * @see     permlinkurl()
4449   * @see     permlinkurl_id()
4450   * @package URL
4451   */
4452  
4453  function pagelinkurl($parts, $inherit = array(), $url_mode = null)
4454  {
4455      global $permlink_mode, $prefs, $txp_context, $txp_sections;
4456  
4457      // Link to an article.
4458      if (!empty($parts['id'])) {
4459          return permlinkurl_id($parts['id']);
4460      }
4461  
4462      $keys = $parts;
4463      empty($inherit) or $keys += $inherit;
4464      empty($txp_context) or $keys += $txp_context;
4465      unset($keys['id']);
4466  
4467      if (isset($prefs['custom_url_func'])
4468          && is_callable($prefs['custom_url_func'])
4469          && ($url = call_user_func($prefs['custom_url_func'], $keys, PAGELINKURL)) !== false) {
4470          return $url;
4471      }
4472  
4473      if (isset($keys['s'])) {
4474          if (!isset($url_mode) && isset($txp_sections[$keys['s']])) {
4475              $url_mode = $txp_sections[$keys['s']]['permlink_mode'];
4476          }
4477  
4478          if ($keys['s'] == 'default') {
4479              unset($keys['s']);
4480          }
4481      }
4482  
4483      if (empty($url_mode)) {
4484          $url_mode = $permlink_mode;
4485      }
4486  
4487      // 'article' context is implicit, no need to add it to the page URL.
4488      if (isset($keys['context']) && $keys['context'] == 'article') {
4489          unset($keys['context']);
4490      }
4491  
4492      $numkeys = array();
4493  
4494      foreach ($keys as $key => $v) {
4495          if (is_numeric($key)) {
4496              $numkeys[$key] = urlencode($v).'/';
4497              unset($keys[$key]);
4498          }
4499      }
4500  
4501      if ($url_mode == 'messy') {
4502          if (!empty($keys['context'])) {
4503              $keys['context'] = gTxt($keys['context'].'_context');
4504          }
4505  
4506          return hu.'index.php'.join_qs($keys);
4507      } else {
4508          // All clean URL modes use the same schemes for list pages.
4509          $url = hu;
4510  
4511          if (!empty($keys['rss'])) {
4512              $url = hu.'rss/';
4513              unset($keys['rss']);
4514          } elseif (!empty($keys['atom'])) {
4515              $url = hu.'atom/';
4516              unset($keys['atom']);
4517          } elseif (!empty($keys['s'])) {
4518              if (!empty($keys['context'])) {
4519                  $keys['context'] = gTxt($keys['context'].'_context');
4520              }
4521              $url = hu.urlencode($keys['s']).'/';
4522              unset($keys['s']);
4523              if (!empty($keys['c']) && ($url_mode == 'section_category_title' || $url_mode == 'breadcrumb_title')) {
4524                  $catpath = $url_mode == 'breadcrumb_title' ?
4525                      array_column(getRootPath($keys['c'], empty($keys['context']) ? 'article' : $keys['context']), 'name') :
4526                      array($keys['c']);
4527                  $url .= implode('/', array_map('urlencode', array_reverse($catpath))).'/';
4528                  unset($keys['c']);
4529              }
4530          } elseif (!empty($keys['month']) && $url_mode == 'year_month_day_title') {
4531              if (!empty($keys['context'])) {
4532                  $keys['context'] = gTxt($keys['context'].'_context');
4533              }
4534              $url = hu.implode('/', explode('-', urlencode($keys['month']))).'/';
4535              unset($keys['month']);
4536          } elseif (!empty($keys['author'])) {
4537              $ct = empty($keys['context']) ? '' : strtolower(urlencode(gTxt($keys['context'].'_context'))).'/';
4538              $url = hu.strtolower(urlencode(gTxt('author'))).'/'.$ct.urlencode($keys['author']).'/';
4539              unset($keys['author'], $keys['context']);
4540          } elseif (!empty($keys['c'])) {
4541              $ct = empty($keys['context']) ? '' : strtolower(urlencode(gTxt($keys['context'].'_context'))).'/';
4542              $url = hu.strtolower(urlencode(gTxt('category'))).'/'.$ct;
4543              $catpath = $url_mode == 'breadcrumb_title' ?
4544                  array_column(getRootPath($keys['c'], empty($keys['context']) ? 'article' : $keys['context']), 'name') :
4545                  array($keys['c']);
4546              $url .= implode('/', array_map('urlencode', array_reverse($catpath))).'/';
4547              unset($keys['c'], $keys['context']);
4548          }
4549  
4550          return (empty($prefs['no_trailing_slash']) ? $url : rtrim($url, '/')).join_qs($keys);
4551      }
4552  }
4553  
4554  /**
4555   * Gets a URL for the given article.
4556   *
4557   * If you need to generate a list of article URLs from already fetched table
4558   * rows, consider using permlinkurl() over this due to performance benefits.
4559   *
4560   * @param   int $id The article ID
4561   * @return  string The URL
4562   * @see     permlinkurl()
4563   * @package URL
4564   * @example
4565   * echo permlinkurl_id(12);
4566   */
4567  
4568  function permlinkurl_id($id)
4569  {
4570      global $permlinks, $thisarticle;
4571  
4572      $id = (int) $id;
4573  
4574      if (isset($permlinks[$id])) {
4575          return permlinkurl(array('id' => $id));
4576      }
4577  
4578      if (isset($thisarticle['thisid']) && $thisarticle['thisid'] == $id) {
4579          return permlinkurl($thisarticle);
4580      }
4581  
4582      $rs = safe_row(
4583          "ID AS thisid, Section, Title, url_title, Category1, Category2, UNIX_TIMESTAMP(Posted) AS posted, UNIX_TIMESTAMP(Expires) AS expires",
4584          'textpattern',
4585          "ID = $id"
4586      );
4587  
4588      return permlinkurl($rs);
4589  }
4590  
4591  /**
4592   * Generates an article URL from the given data array.
4593   *
4594   * @param   array $article_array An array consisting of keys 'thisid', 'section', 'title', 'url_title', 'posted', 'expires'
4595   * @return  string The URL
4596   * @package URL
4597   * @see     permlinkurl_id()
4598   * @example
4599   * echo permlinkurl_id(array(
4600   *     'thisid'    => 12,
4601   *     'section'   => 'blog',
4602   *     'url_title' => 'my-title',
4603   *     'posted'    => 1345414041,
4604   *     'expires'   => 1345444077
4605   * ));
4606   */
4607  
4608  function permlinkurl($article_array, $hu = hu)
4609  {
4610      global $permlink_mode, $prefs, $permlinks, $production_status, $txp_sections;
4611      static $internals = array('id', 's', 'context', 'pg', 'p'), $now = null;
4612  
4613      if (isset($prefs['custom_url_func'])
4614          and is_callable($prefs['custom_url_func'])
4615          and ($url = call_user_func($prefs['custom_url_func'], $article_array, PERMLINKURL)) !== false) {
4616          return $url;
4617      }
4618  
4619      extract(lAtts(array(
4620          'thisid'    => null,
4621          'id'        => null,
4622          'title'     => null,
4623          'url_title' => null,
4624          'section'   => null,
4625          'category1' => null,
4626          'category2' => null,
4627          'posted'    => null,
4628          'uposted'   => null,
4629          'expires'   => null,
4630          'uexpires'  => null,
4631      ), array_change_key_case($article_array, CASE_LOWER), false));
4632  
4633      if (empty($thisid)) {
4634          $thisid = $id;
4635      }
4636  
4637      $thisid = (int) $thisid;
4638      $keys = get_context(null);
4639  
4640      foreach ($internals as $key) {
4641          unset($keys[$key]);
4642      }
4643  
4644      if (isset($permlinks[$thisid])) {
4645          return $hu.($permlinks[$thisid] === true ?
4646              'index.php'.join_qs(array('id' => $thisid) + $keys) :
4647              $permlinks[$thisid].join_qs($keys)
4648          );
4649      }
4650  
4651      if (!isset($now)) {
4652          $now = strftime('%F %T');
4653      }
4654  
4655      if (empty($prefs['publish_expired_articles']) &&
4656          !empty($expires) &&
4657          $production_status != 'live' &&
4658          txpinterface == 'public' &&
4659          (is_numeric($expires) ? $expires < time()
4660              : (isset($uexpires) ? $uexpires < time()
4661              : $expires < $now)
4662          )
4663      ) {
4664          trigger_error(gTxt('permlink_to_expired_article', array('{id}' => $thisid)), E_USER_NOTICE);
4665      }
4666  
4667      if (empty($section)) {
4668          $url_mode = 'messy';
4669      } elseif (isset($txp_sections[$section])) {
4670          $url_mode = empty($txp_sections[$section]['permlink_mode']) ? $permlink_mode : $txp_sections[$section]['permlink_mode'];
4671      } else {
4672          $url_mode = $permlink_mode;
4673      }
4674  
4675      if (empty($url_title) && !in_array($url_mode, array('section_id_title', 'id_title'))) {
4676          $url_mode = 'messy';
4677      }
4678  
4679      $section = urlencode($section);
4680      $url_title = urlencode($url_title);
4681  
4682      switch ($url_mode) {
4683          case 'section_id_title':
4684              if ($url_title && $prefs['attach_titles_to_permalinks']) {
4685                  $out = "$section/$thisid/$url_title";
4686              } else {
4687                  $out = "$section/$thisid";
4688              }
4689              break;
4690          case 'year_month_day_title':
4691              list($y, $m, $d) = explode("-", date("Y-m-d", isset($uposted) ? $uposted : $posted));
4692              $out =  "$y/$m/$d/$url_title";
4693              break;
4694          case 'id_title':
4695              if ($url_title && $prefs['attach_titles_to_permalinks']) {
4696                  $out = "$thisid/$url_title";
4697              } else {
4698                  $out = "$thisid";
4699              }
4700              break;
4701          case 'section_title':
4702              $out = "$section/$url_title";
4703              break;
4704          case 'section_category_title':
4705              $out = $section.'/'.
4706                  (empty($category1) ? '' : urlencode($category1).'/').
4707                  (empty($category2) ? '' : urlencode($category2).'/').$url_title;
4708              break;
4709          case 'breadcrumb_title':
4710              $out = $section.'/';
4711              if (empty($category1)) {
4712                  if (!empty($category2)) {
4713                      $path = array_reverse(array_column(getRootPath($category2), 'name'));
4714                      $out .= implode('/', array_map('urlencode', $path)).'/';
4715                  }
4716              } elseif (empty($category2)) {
4717                  $path = array_reverse(array_column(getRootPath($category1), 'name'));
4718                  $out .= implode('/', array_map('urlencode', $path)).'/';
4719              } else {
4720                  $c2_path = array_reverse(array_column(getRootPath($category2), 'name'));
4721                  if (in_array($category1, $c2_path)) {
4722                      $out .= implode('/', array_map('urlencode', $c2_path)).'/';
4723                  } else {
4724                      $c1_path = array_reverse(array_column(getRootPath($category1), 'name'));
4725                      if (in_array($category2, $c1_path)) {
4726                          $out .= implode('/', array_map('urlencode', $c1_path)).'/';
4727                      } else {
4728                          $c0_path = array_intersect($c1_path, $c2_path);
4729                          $out .= ($c0_path ? implode('/', array_map('urlencode', $c0_path)).'/' : '').
4730                              urlencode($category1).'/'.urlencode($category2).'/';
4731                      }
4732                  }
4733              }
4734              $out .= $url_title;
4735              break;
4736          case 'title_only':
4737              $out = $url_title;
4738              break;
4739          case 'messy':
4740              $out = "index.php";
4741              $keys['id'] = $thisid;
4742              break;
4743      }
4744  
4745      $permlinks[$thisid] = $url_mode == 'messy' ? true : $out;
4746  
4747      return $hu.$out.join_qs($keys);
4748  }
4749  
4750  /**
4751   * Gets a file download URL.
4752   *
4753   * @param   int    $id       The ID
4754   * @param   string $filename The filename
4755   * @return  string
4756   * @package File
4757   */
4758  
4759  function filedownloadurl($id, $filename = '')
4760  {
4761      global $permlink_mode;
4762  
4763      if ($permlink_mode == 'messy') {
4764          return hu.'index.php'.join_qs(array(
4765              's'  => 'file_download',
4766              'id' => (int) $id,
4767          ));
4768      }
4769  
4770      if ($filename) {
4771          $filename = '/'.urlencode($filename);
4772  
4773          // FIXME: work around yet another mod_deflate problem (double compression)
4774          // https://blogs.msdn.microsoft.com/wndp/2006/08/21/content-encoding-content-type/
4775          if (preg_match('/gz$/i', $filename)) {
4776              $filename .= a;
4777          }
4778      }
4779  
4780      return hu.'file_download/'.intval($id).$filename;
4781  }
4782  
4783  /**
4784   * Gets an image's absolute URL.
4785   *
4786   * @param   int    $id        The image
4787   * @param   string $ext       The file extension
4788   * @param   bool   $thumbnail If TRUE returns a URL to the thumbnail
4789   * @return  string
4790   * @package Image
4791   */
4792  
4793  function imagesrcurl($id, $ext, $thumbnail = false)
4794  {
4795      global $img_dir;
4796      $thumbnail = $thumbnail ? 't' : '';
4797  
4798      return ihu.$img_dir.'/'.$id.$thumbnail.$ext;
4799  }
4800  
4801  /**
4802   * Checks if a value exists in a list.
4803   *
4804   * @param  string $val   The searched value
4805   * @param  string $list  The value list
4806   * @param  string $delim The list boundary
4807   * @return bool Returns TRUE if $val is found, FALSE otherwise
4808   * @example
4809   * if (in_list('red', 'blue, green, red, yellow'))
4810   * {
4811   *     echo "'red' found from the list.";
4812   * }
4813   */
4814  
4815  function in_list($val, $list, $delim = ',')
4816  {
4817      return in_array((string) $val, do_list($list, $delim), true);
4818  }
4819  
4820  /**
4821   * Split a string by string.
4822   *
4823   * Trims the created values of whitespace.
4824   *
4825   * @param  string $list  The string
4826   * @param  string $delim The boundary
4827   * @return array
4828   * @example
4829   * print_r(
4830   *     do_list('value1, value2, value3')
4831   * );
4832   */
4833  
4834  function do_list($list, $delim = ',')
4835  {
4836      if (is_array($delim)) {
4837          list($delim, $range) = $delim + array(null, null);
4838      }
4839  
4840      $array = explode($delim, $list);
4841  
4842      if (isset($range) && strpos($list, $range) !== false) {
4843          $pattern = '/^\s*(\w|[-+]?\d+)\s*'.preg_quote($range, '/').'\s*(\w|[-+]?\d+)\s*$/';
4844          $out = array();
4845  
4846          foreach ($array as $item) {
4847              if (!preg_match($pattern, $item, $match)) {
4848                  $out[] = trim($item);
4849              } else {
4850                  list($m, $start, $end) = $match;
4851                  foreach(range($start, $end) as $v) {
4852                      $out[] = $v;
4853                  }
4854              }
4855          }
4856      }
4857  
4858      return isset($out) ? $out : array_map('trim', $array);
4859  }
4860  
4861  /**
4862   * Split a string by string, returning only unique results.
4863   *
4864   * Trims unique values of whitespace. Flags permit exclusion of empty strings.
4865   *
4866   * @param  string $list  The string
4867   * @param  string $delim The boundary
4868   * @param  int    $flags TEXTPATTERN_STRIP_NONE | TEXTPATTERN_STRIP_EMPTY | TEXTPATTERN_STRIP_EMPTY_STRING
4869   * @return array
4870   * @example
4871   * print_r(
4872   *     do_list_unique('value1, value2, value3')
4873   * );
4874   */
4875  
4876  function do_list_unique($list, $delim = ',', $flags = TEXTPATTERN_STRIP_EMPTY_STRING)
4877  {
4878      $out = array_unique(do_list($list, $delim));
4879  
4880      if ($flags & TEXTPATTERN_STRIP_EMPTY) {
4881          $out = array_filter($out);
4882      }
4883  
4884      if ($flags & TEXTPATTERN_STRIP_EMPTY_STRING) {
4885          $out = array_filter($out, function ($v) {
4886              return ($v=='') ? false : true;
4887          });
4888      }
4889  
4890      return $out;
4891  }
4892  
4893  /**
4894   * Wraps a string in single quotes.
4895   *
4896   * @param  string $val The input string
4897   * @return string
4898   */
4899  
4900  function doQuote($val)
4901  {
4902      return "'$val'";
4903  }
4904  
4905  /**
4906   * Escapes special characters for use in an SQL statement and wraps the value
4907   * in quote.
4908   *
4909   * Useful for creating an array/string of values for use in an SQL statement.
4910   *
4911   * @param   string|array $in The input value
4912   * @param   string|null  $separator The separator
4913   * @return  mixed
4914   * @package DB
4915   * @example
4916   * if ($r = safe_row('name', 'myTable', 'type in(' . quote_list(array('value1', 'value2'), ',') . ')')
4917   * {
4918   *     echo "Found '{$r['name']}'.";
4919   * }
4920   */
4921  
4922  function quote_list($in, $separator = null)
4923  {
4924      $out = doArray(doSlash($in), 'doQuote');
4925  
4926      return isset($separator) ? implode($separator, $out) : $out;
4927  }
4928  
4929  /**
4930   * Adds a line to the tag trace.
4931   *
4932   * @param   string $msg             The message
4933   * @param   int    $tracelevel_diff Change trace level
4934   * @deprecated in 4.6.0
4935   * @package Debug
4936   */
4937  
4938  function trace_add($msg, $level = 0, $dummy = null)
4939  {
4940      global $trace;
4941  
4942      if ((int) $level > 0) {
4943          $trace->start($msg);
4944      } elseif ((int) $level < 0) {
4945          $trace->stop();
4946      } else {
4947          $trace->log($msg);
4948      }
4949  
4950      // TODO: Uncomment this to trigger deprecated warning in a version (or two).
4951      // Due to the radical changes under the hood, plugin authors will probably
4952      // support dual 4.5/4.6 plugins for the short term. Deprecating this
4953      // immediately causes unnecessary pain for developers.
4954  //    trigger_error(gTxt('deprecated_function_with', array('{name}' => __FUNCTION__, '{with}' => 'class Trace')), E_USER_NOTICE);
4955  }
4956  
4957  /**
4958   * Push current article on the end of data stack.
4959   *
4960   * Populates $stack_article global with the current $thisarticle.
4961   */
4962  
4963  function article_push()
4964  {
4965      global $thisarticle, $stack_article;
4966      $stack_article[] = $thisarticle;
4967  }
4968  
4969  /**
4970   * Advance to the next article in the current data stack.
4971   *
4972   * Populates $thisarticle global with the last article from the
4973   * stack stored in $stack_article.
4974   */
4975  
4976  function article_pop()
4977  {
4978      global $thisarticle, $stack_article;
4979      $thisarticle = array_pop($stack_article);
4980  }
4981  
4982  /**
4983   * Gets a path relative to the site's root directory.
4984   *
4985   * @param   string $path The filename to parse
4986   * @param   string $pfx  The root directory
4987   * @return  string The absolute $path converted to relative
4988   * @package File
4989   */
4990  
4991  function relative_path($path, $pfx = null)
4992  {
4993      if ($pfx === null) {
4994          $pfx = dirname(txpath);
4995      }
4996  
4997      return preg_replace('@^/'.preg_quote(ltrim($pfx, '/'), '@').'/?@', '', $path);
4998  }
4999  
5000  /**
5001   * Gets a backtrace.
5002   *
5003   * @param   int $num   The limit
5004   * @param   int $start The offset
5005   * @return  array A backtrace
5006   * @package Debug
5007   */
5008  
5009  function get_caller($num = 1, $start = 2)
5010  {
5011      $out = array();
5012  
5013      if (!is_callable('debug_backtrace')) {
5014          return $out;
5015      }
5016  
5017      $bt = debug_backtrace();
5018  
5019      for ($i = $start; $i < $num+$start; $i++) {
5020          if (!empty($bt[$i])) {
5021              $t = '';
5022  
5023              if (!empty($bt[$i]['file'])) {
5024                  $t .= relative_path($bt[$i]['file']);
5025              }
5026  
5027              if (!empty($bt[$i]['line'])) {
5028                  $t .= ':'.$bt[$i]['line'];
5029              }
5030  
5031              if ($t) {
5032                  $t .= ' ';
5033              }
5034  
5035              if (!empty($bt[$i]['class'])) {
5036                  $t .= $bt[$i]['class'];
5037              }
5038  
5039              if (!empty($bt[$i]['type'])) {
5040                  $t .= $bt[$i]['type'];
5041              }
5042  
5043              if (!empty($bt[$i]['function'])) {
5044                  $t .= $bt[$i]['function'];
5045                  $t .= '()';
5046              }
5047  
5048              $out[] = $t;
5049          }
5050      }
5051  
5052      return $out;
5053  }
5054  
5055  /**
5056   * Sets a locale.
5057   *
5058   * The function name is misleading but remains for legacy reasons.
5059   *
5060   * @param      string $lang
5061   * @return     string Current locale
5062   * @package    L10n
5063   * @deprecated in 4.6.0
5064   * @see        \Textpattern\L10n\Locale::setLocale()
5065   */
5066  
5067  function getlocale($lang)
5068  {
5069      global $locale;
5070  
5071      Txp::get('\Textpattern\L10n\Locale')->setLocale(LC_TIME, array($lang, $locale));
5072  
5073      return Txp::get('\Textpattern\L10n\Locale')->getLocale(LC_TIME);
5074  }
5075  
5076  /**
5077   * Fetch meta description from the given (or automatic) context.
5078   *
5079   * Category context may be refined by specifying the content type as well
5080   * after a dot. e.g. category.image to check image context category.
5081   *
5082   * @param string $type Flavour of meta content to fetch (section, category, article)
5083   */
5084  
5085  function getMetaDescription($type = null)
5086  {
5087      global $thisarticle, $thiscategory, $thissection, $c, $s, $context, $txp_sections;
5088  
5089      $content = '';
5090  
5091      if ($type === null) {
5092          if ($thiscategory) {
5093              $content = $thiscategory['description'];
5094          } elseif ($thissection) {
5095              $content = $thissection['description'];
5096          } elseif ($thisarticle) {
5097              $content = $thisarticle['description'];
5098          } elseif ($c) {
5099              $content = safe_field("description", 'txp_category', "name = '".doSlash($c)."' AND type = '".doSlash($context)."'");
5100          } elseif ($s) {
5101              $content = isset($txp_sections[$s]) ? $txp_sections[$s]['description'] : '';
5102          }
5103      } else {
5104          if (strpos($type, 'category') === 0) {
5105              // Category context.
5106              if ($thiscategory) {
5107                  $content = $thiscategory['description'];
5108              } else {
5109                  $thisContext = $context;
5110                  $catParts = do_list($type, '.');
5111  
5112                  if (isset($catParts[1])) {
5113                      $thisContext = $catParts[1];
5114                  }
5115  
5116                  $clause = " AND type = '".$thisContext."'";
5117                  $content = safe_field("description", 'txp_category', "name = '".doSlash($c)."'".$clause);
5118              }
5119          } elseif ($type === 'section') {
5120              $theSection = ($thissection) ? $thissection['name'] : $s;
5121              $content = isset($txp_sections[$theSection]) ? $txp_sections[$theSection]['description'] : '';
5122          } elseif ($type === 'article') {
5123              assert_article();
5124              $content = ($thisarticle? $thisarticle['description'] : '');
5125          }
5126      }
5127  
5128      return $content;
5129  }
5130  
5131  /**
5132   * Get some URL data.
5133   * @param mixed $context The data to retrieve
5134   * @param array $internals Data restrictions
5135   * @return array The retrieved data
5136   */
5137  
5138  function get_context($context = true, $internals = array('id', 's', 'c', 'context', 'q', 'm', 'pg', 'p', 'month', 'author', 'f'))
5139  {
5140      global $pretext, $txp_context;
5141  
5142      if (!isset($context)) {
5143          return empty($txp_context) ? array() : $txp_context;
5144      } elseif (empty($context)) {
5145          return array();
5146      } elseif (!is_array($context)) {
5147          $context = array_fill_keys($context === true ? $internals : do_list_unique($context), null);
5148      }
5149  
5150      $out = array();
5151  
5152      foreach ($context as $q => $v) {
5153          if (isset($pretext[$q]) && in_array($q, $internals)) {
5154              $out[$q] = $q === 'author' ? $pretext['realname'] : $pretext[$q];
5155          } elseif (isset($v)) {
5156              $out[$q] = $v;
5157          } else {
5158              $out[$q] = gps($q, null);
5159          }
5160      }
5161  
5162      return $out;
5163  }
5164  
5165  /**
5166   * Assert article context error.
5167   */
5168  
5169  function assert_article()
5170  {
5171      global $thisarticle;
5172  
5173      if (empty($thisarticle)) {
5174          trigger_error(gTxt('error_article_context'));
5175  
5176          return false;
5177      }
5178  
5179      return true;
5180  }
5181  
5182  /**
5183   * Assert comment context error.
5184   */
5185  
5186  function assert_comment()
5187  {
5188      global $thiscomment;
5189  
5190      if (empty($thiscomment)) {
5191          trigger_error(gTxt('error_comment_context'));
5192      }
5193  }
5194  
5195  /**
5196   * Assert file context error.
5197   */
5198  
5199  function assert_file()
5200  {
5201      global $thisfile;
5202  
5203      if (empty($thisfile)) {
5204          trigger_error(gTxt('error_file_context'));
5205      }
5206  }
5207  
5208  /**
5209   * Assert image context error.
5210   */
5211  
5212  function assert_image()
5213  {
5214      global $thisimage;
5215  
5216      if (empty($thisimage)) {
5217          trigger_error(gTxt('error_image_context'));
5218      }
5219  }
5220  
5221  /**
5222   * Assert link context error.
5223   */
5224  
5225  function assert_link()
5226  {
5227      global $thislink;
5228  
5229      if (empty($thislink)) {
5230          trigger_error(gTxt('error_link_context'));
5231      }
5232  }
5233  
5234  /**
5235   * Assert section context error.
5236   */
5237  
5238  function assert_section()
5239  {
5240      global $thissection;
5241  
5242      if (empty($thissection)) {
5243          trigger_error(gTxt('error_section_context'));
5244      }
5245  }
5246  
5247  /**
5248   * Assert category context error.
5249   */
5250  
5251  function assert_category()
5252  {
5253      global $thiscategory;
5254  
5255      if (empty($thiscategory)) {
5256          trigger_error(gTxt('error_category_context'));
5257      }
5258  }
5259  
5260  /**
5261   * Validate a variable as an integer.
5262   *
5263   * @param  mixed $myvar The variable
5264   * @return int|bool The variable or FALSE on error
5265   */
5266  
5267  function assert_int($myvar)
5268  {
5269      if (is_numeric($myvar) && $myvar == intval($myvar)) {
5270          return (int) $myvar;
5271      }
5272  
5273      trigger_error(gTxt('assert_int_value', array('{name}' => (string) $myvar)), E_USER_ERROR);
5274  
5275      return false;
5276  }
5277  
5278  /**
5279   * Validate a variable as a string.
5280   *
5281   * @param  mixed $myvar The variable
5282   * @return string|bool The variable or FALSE on error
5283   */
5284  
5285  function assert_string($myvar)
5286  {
5287      if (is_string($myvar)) {
5288          return $myvar;
5289      }
5290  
5291      trigger_error(gTxt('assert_string_value', array('{name}' => (string) $myvar)), E_USER_ERROR);
5292  
5293      return false;
5294  }
5295  
5296  /**
5297   * Validate a variable as an array.
5298   *
5299   * @param  mixed $myvar The variable
5300   * @return array|bool The variable or FALSE on error
5301   */
5302  
5303  function assert_array($myvar)
5304  {
5305      if (is_array($myvar)) {
5306          return $myvar;
5307      }
5308  
5309      trigger_error(gTxt('assert_array_value', array('{name}' => (string) $myvar)), E_USER_ERROR);
5310  
5311      return false;
5312  }
5313  
5314  /**
5315   * Converts relative links in HTML markup to absolute.
5316   *
5317   * @param   string $html      The HTML to check
5318   * @param   string $permalink Optional URL part appended to the links
5319   * @return  string HTML
5320   * @package URL
5321   */
5322  
5323  function replace_relative_urls($html, $permalink = '')
5324  {
5325      global $siteurl;
5326  
5327      // URLs like "/foo/bar" - relative to the domain.
5328      if (serverSet('HTTP_HOST')) {
5329          $html = preg_replace('@(<a[^>]+href=")/(?!/)@', '$1'.PROTOCOL.serverSet('HTTP_HOST').'/', $html);
5330          $html = preg_replace('@(<img[^>]+src=")/(?!/)@', '$1'.PROTOCOL.serverSet('HTTP_HOST').'/', $html);
5331      }
5332  
5333      // "foo/bar" - relative to the textpattern root,
5334      // leave "http:", "mailto:" et al. as absolute URLs.
5335      $html = preg_replace('@(<a[^>]+href=")(?!\w+:|//)@', '$1'.PROTOCOL.$siteurl.'/$2', $html);
5336      $html = preg_replace('@(<img[^>]+src=")(?!\w+:|//)@', '$1'.PROTOCOL.$siteurl.'/$2', $html);
5337  
5338      if ($permalink) {
5339          $html = preg_replace("/href=\\\"#(.*)\"/", "href=\"".$permalink."#\\1\"", $html);
5340      }
5341  
5342      return ($html);
5343  }
5344  
5345  /**
5346   * Used for clean URL test.
5347   *
5348   * @param  array $pretext
5349   * @access private
5350   */
5351  
5352  function show_clean_test($pretext)
5353  {
5354      ob_clean();
5355      if (is_array($pretext) && isset($pretext['req'])) {
5356          echo md5($pretext['req']).n;
5357      }
5358  
5359      if (serverSet('SERVER_ADDR') === serverSet('REMOTE_ADDR')) {
5360          var_export($pretext);
5361      }
5362  }
5363  
5364  /**
5365   * Calculates paging.
5366   *
5367   * Takes a total number of items, a per page limit and the current page number,
5368   * and in return returns the page number, an offset and a number of pages.
5369   *
5370   * @param  int $total The number of items in total
5371   * @param  int $limit The number of items per page
5372   * @param  int $page  The page number
5373   * @return array Array of page, offset and number of pages.
5374   * @example
5375   * list($page, $offset, $num_pages) = pager(150, 10, 1);
5376   * echo "Page {$page} of {$num_pages}. Offset is {$offset}.";
5377   */
5378  
5379  function pager($total, $limit, $page)
5380  {
5381      $total = (int) $total;
5382      $limit = (int) $limit;
5383      $page = (int) $page;
5384  
5385      $num_pages = ceil($total / $limit);
5386  
5387      $page = min(max($page, 1), $num_pages);
5388  
5389      $offset = max(($page - 1) * $limit, 0);
5390  
5391      return array($page, $offset, $num_pages);
5392  }
5393  
5394  /**
5395   * Word-wrap a string using a zero width space.
5396   *
5397   * @param  string $text  The input string
5398   * @param  int    $width Target line length
5399   * @param  string $break Is not used
5400   * @return string
5401   */
5402  
5403  function soft_wrap($text, $width, $break = '&#8203;')
5404  {
5405      $wbr = chr(226).chr(128).chr(139);
5406      $words = explode(' ', $text);
5407  
5408      foreach ($words as $wordnr => $word) {
5409          $word = preg_replace('|([,./\\>?!:;@-]+)(?=.)|', '$1 ', $word);
5410          $parts = explode(' ', $word);
5411  
5412          foreach ($parts as $partnr => $part) {
5413              $len = strlen(utf8_decode($part));
5414  
5415              if (!$len) {
5416                  continue;
5417              }
5418  
5419              $parts[$partnr] = preg_replace('/(.{'.ceil($len/ceil($len/$width)).'})(?=.)/u', '$1'.$wbr, $part);
5420          }
5421  
5422          $words[$wordnr] = join($wbr, $parts);
5423      }
5424  
5425      return join(' ', $words);
5426  }
5427  
5428  /**
5429   * Removes prefix from a string.
5430   *
5431   * @param  string $str The string
5432   * @param  string $pfx The prefix
5433   * @return string
5434   */
5435  
5436  function strip_prefix($str, $pfx)
5437  {
5438      return preg_replace('/^'.preg_quote($pfx, '/').'/', '', $str);
5439  }
5440  
5441  /**
5442   * Sends an XML envelope.
5443   *
5444   * Wraps an array of name => value tuples into an XML envelope, supports one
5445   * level of nested arrays at most.
5446   *
5447   * @param   array $response
5448   * @return  string XML envelope
5449   * @package XML
5450   */
5451  
5452  function send_xml_response($response = array())
5453  {
5454      static $headers_sent = false;
5455  
5456      if (!$headers_sent) {
5457          ob_clean();
5458          header('Content-Type: text/xml; charset=utf-8');
5459          $out[] = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>';
5460          $headers_sent = true;
5461      }
5462  
5463      $default_response = array('http-status' => '200 OK');
5464  
5465      // Backfill default response properties.
5466      $response = $response + $default_response;
5467  
5468      txp_status_header($response['http-status']);
5469      $out[] = '<textpattern>';
5470  
5471      foreach ($response as $element => $value) {
5472          if (is_array($value)) {
5473              $out[] = t."<$element>".n;
5474  
5475              foreach ($value as $e => $v) {
5476                  // Character escaping in values;
5477                  // @see https://www.w3.org/TR/REC-xml/#sec-references.
5478                  $v = str_replace(array("\t", "\n", "\r"), array("&#x9;", "&#xA;", "&#xD;"), htmlentities($v, ENT_QUOTES, 'UTF-8'));
5479                  $out[] = t.t."<$e value='$v' />".n;
5480              }
5481  
5482              $out[] = t."</$element>".n;
5483          } else {
5484              $value = str_replace(array("\t", "\n", "\r"), array("&#x9;", "&#xA;", "&#xD;"), htmlentities($value, ENT_QUOTES, 'UTF-8'));
5485              $out[] = t."<$element value='$value' />".n;
5486          }
5487      }
5488  
5489      $out[] = '</textpattern>';
5490      echo join(n, $out);
5491  }
5492  
5493  /**
5494   * Sends a text/javascript response.
5495   *
5496   * @param   string $out The JavaScript
5497   * @since   4.4.0
5498   * @package Ajax
5499   */
5500  
5501  function send_script_response($out = '')
5502  {
5503      static $headers_sent = false;
5504  
5505      if (!$headers_sent) {
5506          ob_clean();
5507          header('Content-Type: text/javascript; charset=utf-8');
5508          txp_status_header('200 OK');
5509          $headers_sent = true;
5510      }
5511  
5512      echo ";\n".$out.";\n";
5513  }
5514  
5515  /**
5516   * Sends an application/json response.
5517   *
5518   * If the provided $out is not a string, its encoded as JSON. Any string is
5519   * treated as it were valid JSON.
5520   *
5521   * @param   mixed $out The JSON
5522   * @since   4.6.0
5523   * @package Ajax
5524   */
5525  
5526  function send_json_response($out = '')
5527  {
5528      static $headers_sent = false;
5529  
5530      if (!$headers_sent) {
5531          ob_clean();
5532          header('Content-Type: application/json; charset=utf-8');
5533          txp_status_header('200 OK');
5534          $headers_sent = true;
5535      }
5536  
5537      if (!is_string($out)) {
5538          $out = json_encode($out, TEXTPATTERN_JSON);
5539      }
5540  
5541      echo $out;
5542  }
5543  
5544  /**
5545   * Performs regular housekeeping.
5546   *
5547   * @access private
5548   */
5549  
5550  function janitor()
5551  {
5552      global $prefs, $auto_dst, $timezone_key, $is_dst;
5553  
5554      // Update DST setting.
5555      if ($auto_dst && $timezone_key) {
5556          $is_dst = Txp::get('\Textpattern\Date\Timezone')->isDst(null, $timezone_key);
5557  
5558          if ($is_dst != $prefs['is_dst']) {
5559              $prefs['is_dst'] = $is_dst;
5560              set_pref('is_dst', $is_dst, 'publish', PREF_HIDDEN);
5561          }
5562      }
5563  }
5564  
5565  /**
5566   * Protection from those who'd bomb the site by GET.
5567   *
5568   * Origin of the infamous 'Nice try' message and an even more useful '503'
5569   * HTTP status.
5570   */
5571  
5572  function bombShelter()
5573  {
5574      global $prefs;
5575      $in = serverSet('REQUEST_URI');
5576  
5577      if (!empty($prefs['max_url_len']) and strlen($in) > $prefs['max_url_len']) {
5578          txp_status_header('503 Service Unavailable');
5579          exit('Nice try.');
5580      }
5581  }
5582  
5583  /**
5584   * Test whether the client accepts a certain response format.
5585   *
5586   * Discards formats with a quality factor below 0.1
5587   *
5588   * @param   string  $format One of 'html', 'txt', 'js', 'css', 'json', 'xml', 'rdf', 'atom', 'rss'
5589   * @return  boolean $format TRUE if accepted
5590   * @since   4.5.0
5591   * @package Network
5592   */
5593  
5594  function http_accept_format($format)
5595  {
5596      static $formats = array(
5597          'html' => array('text/html', 'application/xhtml+xml', '*/*'),
5598          'txt'  => array('text/plain', '*/*'),
5599          'js'   => array('application/javascript', 'application/x-javascript', 'text/javascript', 'application/ecmascript', 'application/x-ecmascript', '*/*'),
5600          'css'  => array('text/css', '*/*'),
5601          'json' => array('application/json', 'application/x-json', '*/*'),
5602          'xml'  => array('text/xml', 'application/xml', 'application/x-xml', '*/*'),
5603          'rdf'  => array('application/rdf+xml', '*/*'),
5604          'atom' => array('application/atom+xml', '*/*'),
5605          'rss'  => array('application/rss+xml', '*/*'),
5606      );
5607      static $accepts = array();
5608      static $q = array();
5609  
5610      if (empty($accepts)) {
5611          // Build cache of accepted formats.
5612          $accepts = preg_split('/\s*,\s*/', serverSet('HTTP_ACCEPT'), null, PREG_SPLIT_NO_EMPTY);
5613  
5614          foreach ($accepts as $i => &$a) {
5615              // Sniff out quality factors if present.
5616              if (preg_match('/(.*)\s*;\s*q=([.0-9]*)/', $a, $m)) {
5617                  $a = $m[1];
5618                  $q[$a] = floatval($m[2]);
5619              } else {
5620                  $q[$a] = 1.0;
5621              }
5622  
5623              // Discard formats with quality factors below an arbitrary threshold
5624              // as jQuery adds a wildcard '*/*; q=0.01' to the 'Accepts' header
5625              // for XHR requests.
5626              if ($q[$a] < 0.1) {
5627                  unset($q[$a]);
5628                  unset($accepts[$i]);
5629              }
5630          }
5631      }
5632  
5633      return isset($formats[$format]) && count(array_intersect($formats[$format], $accepts)) > 0;
5634  }
5635  
5636  /**
5637   * Return a list of status codes and their associated names.
5638   *
5639   * The list can be extended with a 'status.types > types' callback event.
5640   * Callback functions get passed three arguments: '$event', '$step' and
5641   * '$status_list'. The third parameter contains a reference to an array of
5642   * 'status_code => label' pairs.
5643   *
5644   * @param   bool  Return the list with L10n labels (for UI purposes) or raw values (for comparisons)
5645   * @param   array List of status keys (numbers) to exclude
5646   * @return  array A status array
5647   * @since   4.6.0
5648   */
5649  
5650  function status_list($labels = true, $exclude = array())
5651  {
5652      $status_list = array(
5653          STATUS_DRAFT   => 'draft',
5654          STATUS_HIDDEN  => 'hidden',
5655          STATUS_PENDING => 'pending',
5656          STATUS_LIVE    => 'live',
5657          STATUS_STICKY  => 'sticky',
5658      );
5659  
5660      if (!is_array($exclude)) {
5661          $exclude = array();
5662      }
5663  
5664      foreach ($exclude as $remove) {
5665          unset($status_list[(int) $remove]);
5666      }
5667  
5668      callback_event_ref('status.types', 'types', 0, $status_list);
5669  
5670      if ($labels) {
5671          $status_list = array_map('gTxt', $status_list);
5672      }
5673  
5674      return $status_list;
5675  }
5676  
5677  /**
5678   * Translates article status names into numerical status codes.
5679   *
5680   * @param  string $name    Status name
5681   * @param  int    $default Status code to return if $name is not a defined status name
5682   * @return int Matching numerical status
5683   */
5684  
5685  function getStatusNum($name, $default = STATUS_LIVE)
5686  {
5687      $statuses = status_list(false);
5688      $status = strtolower($name);
5689      $num = array_search($status, $statuses);
5690  
5691      if ($num === false) {
5692          $num = $default;
5693      }
5694  
5695      return (int) $num;
5696  }
5697  
5698  /**
5699   * Gets the maximum allowed file upload size.
5700   *
5701   * Computes the maximum acceptable file size to the application if the
5702   * user-selected value is larger than the maximum allowed by the current PHP
5703   * configuration.
5704   *
5705   * @param  int $user_max Desired upload size supplied by the administrator
5706   * @return int Actual value; the lower of user-supplied value or system-defined value
5707   */
5708  
5709  function real_max_upload_size($user_max, $php = true)
5710  {
5711      // The minimum of the candidates, is the real max. possible size
5712      $candidates = $php ? array($user_max,
5713          ini_get('post_max_size'),
5714          ini_get('upload_max_filesize')
5715      ) : array($user_max);
5716      $real_max = null;
5717  
5718      foreach ($candidates as $item) {
5719          $val = floatval($item);
5720          $modifier = strtolower(substr(trim($item), -1));
5721  
5722          switch ($modifier) {
5723              // The 'G' modifier is available since PHP 5.1.0
5724              case 'g':
5725                  $val *= 1024;
5726                  // no break
5727              case 'm':
5728                  $val *= 1024;
5729                  // no break
5730              case 'k':
5731                  $val *= 1024;
5732          }
5733  
5734          if ($val >= 1) {
5735              if (is_null($real_max) || $val < $real_max) {
5736                  $real_max = floor($val);
5737              }
5738          }
5739      }
5740  
5741      // 2^53 - 1 is max safe JavaScript integer, let 8192Tb
5742      return number_format(min($real_max, pow(2, 53) - 1), 0, '.', '');
5743  }
5744  
5745  // -------------------------------------------------------------
5746  
5747  function txp_match($atts, $what)
5748  {
5749      static $dlmPool = array('/', '@', '#', '~', '`', '|', '!', '%');
5750  
5751      extract($atts + array(
5752          'value'     => null,
5753          'match'     => 'exact',
5754          'separator' => '',
5755      ));
5756  
5757  
5758      if ($value !== null) {
5759          switch ($match) {
5760              case '':
5761              case 'exact':
5762                  $cond = (is_array($what) ? implode('', $what) == $value : $what == $value);
5763                  break;
5764              case 'any':
5765                  $values = do_list_unique($value);
5766                  $cond = false;
5767                  $cf_contents = $separator && !is_array($what) ? do_list_unique($what, $separator) : $what;
5768  
5769                  foreach ($values as $term) {
5770                      if (is_array($cf_contents) ? in_array($term, $cf_contents) : strpos($cf_contents, $term) !== false) {
5771                          $cond = true;
5772                          break;
5773                      }
5774                  }
5775                  break;
5776              case 'all':
5777                  $values = do_list_unique($value);
5778                  $cond = true;
5779                  $cf_contents = $separator && !is_array($what) ? do_list_unique($what, $separator) : $what;
5780  
5781                  foreach ($values as $term) {
5782                      if (is_array($cf_contents) ? !in_array($term, $cf_contents) : strpos($cf_contents, $term) === false) {
5783                          $cond = false;
5784                          break;
5785                      }
5786                  }
5787                  break;
5788              case 'pattern':
5789                  // Cannot guarantee that a fixed delimiter won't break preg_match
5790                  // (and preg_quote doesn't help) so dynamically assign the delimiter
5791                  // based on the first entry in $dlmPool that is NOT in the value
5792                  // attribute. This minimises (does not eliminate) the possibility
5793                  // of a TXP-initiated preg_match error, while still preserving
5794                  // errors outside TXP's control (e.g. mangled user-submitted
5795                  // PCRE pattern).
5796                  if ($separator === true) {
5797                      $dlm = $value;
5798                  } elseif ($separator && in_array($separator, $dlmPool)) {
5799                      $dlm = strpos($value, $separator) === 0 ? $value : $separator.$value.$separator;
5800                  } else {
5801                      $dlm = array_diff($dlmPool, preg_split('//', $value));
5802                      $dlm = reset($dlm);
5803                      $dlm = $dlm.$value.$dlm;
5804                  }
5805  
5806                  $cond = preg_match($dlm, is_array($what) ? implode('', $what) : $what);
5807                  break;
5808              default:
5809                  trigger_error(gTxt('invalid_attribute_value', array('{name}' => 'match')), E_USER_NOTICE);
5810                  $cond = false;
5811          }
5812      } else {
5813          $cond = ($what !== null);
5814      }
5815  
5816      return !empty($cond);
5817  }
5818  
5819  /*** Polyfills ***/
5820  
5821  if (!function_exists('array_column')) {
5822      include txpath.'/lib/array_column.php';
5823  }

title

Description

title

Description

title

Description

title

title

Body