Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/lib/txplib_misc.php - 6865 lines - 179104 bytes - Summary - Text - Print

Description: Collection of miscellaneous tools.

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