Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/lib/txplib_admin.php - 2052 lines - 56941 bytes - Summary - Text - Print

Description: Collection of password handling functions.

   1  <?php
   2  
   3  /*
   4   * Textpattern Content Management System
   5   * https://textpattern.com/
   6   *
   7   * Copyright (C) 2020 The Textpattern Development Team
   8   *
   9   * This file is part of Textpattern.
  10   *
  11   * Textpattern is free software; you can redistribute it and/or
  12   * modify it under the terms of the GNU General Public License
  13   * as published by the Free Software Foundation, version 2.
  14   *
  15   * Textpattern is distributed in the hope that it will be useful,
  16   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18   * GNU General Public License for more details.
  19   *
  20   * You should have received a copy of the GNU General Public License
  21   * along with Textpattern. If not, see <https://www.gnu.org/licenses/>.
  22   */
  23  
  24  /**
  25   * Collection of password handling functions.
  26   *
  27   * @package User
  28   */
  29  
  30  /**
  31   * Emails a new user with account details and requests they set a password.
  32   *
  33   * @param  string $name     The login name
  34   * @return bool FALSE on error.
  35   */
  36  
  37  function send_account_activation($name)
  38  {
  39      global $sitename;
  40  
  41      require_privs('admin.edit');
  42  
  43      $rs = safe_row("user_id, email, nonce, RealName, pass", 'txp_users', "name = '".doSlash($name)."'");
  44  
  45      if ($rs) {
  46          extract($rs);
  47  
  48          $expiryTimestamp = time() + (60 * 60 * ACTIVATION_EXPIRY_HOURS);
  49  
  50          $activation_code = generate_user_token($user_id, 'account_activation', $expiryTimestamp, $pass, $nonce);
  51  
  52          $expiryYear = safe_strftime('%Y', $expiryTimestamp);
  53          $expiryMonth = safe_strftime('%B', $expiryTimestamp);
  54          $expiryDay = safe_strftime('%Oe', $expiryTimestamp);
  55          $expiryTime = safe_strftime('%H:%M %Z', $expiryTimestamp);
  56  
  57          $authorLang = safe_field('val', 'txp_prefs', "name='language_ui' AND user_name = '".doSlash($name)."'");
  58          $authorLang = ($authorLang) ? $authorLang : TEXTPATTERN_DEFAULT_LANG;
  59  
  60          $txpLang = Txp::get('\Textpattern\L10n\Lang');
  61          $txpLang->swapStrings($authorLang, 'admin, common');
  62  
  63          $message = gTxt('salutation', array('{name}' => $RealName)).
  64              n.n.gTxt('you_have_been_registered').' '.$sitename.
  65  
  66              n.n.gTxt('your_login_is').' '.$name.
  67              n.n.gTxt('account_activation_confirmation').
  68              n.ahu.'index.php?lang='.$authorLang.'&activate='.$activation_code.
  69              n.n.gTxt('link_expires', array(
  70                  '{year}'  => $expiryYear,
  71                  '{month}' => $expiryMonth,
  72                  '{day}'   => $expiryDay,
  73                  '{time}'  => $expiryTime,
  74              ));
  75  
  76          $subject = gTxt('account_activation');
  77  
  78          $txpLang->swapStrings(null);
  79  
  80          if (txpMail($email, "[$sitename] ".$subject, $message)) {
  81              return gTxt('login_sent_to', array('{email}' => $email));
  82          } else {
  83              return array(gTxt('could_not_mail'), E_ERROR);
  84          }
  85      }
  86  }
  87  
  88  /**
  89   * Sends a password reset link to a user's email address.
  90   *
  91   * This function will return a success message even when the specified user
  92   * doesn't exist. Though an error message could be thrown when a user isn't
  93   * found, security best practice prevents leaking existing account names.
  94   *
  95   * @param  string $name The login name
  96   * @return string A localized message string
  97   * @see    send_new_password()
  98   * @see    reset_author_pass()
  99   * @example
 100   * echo send_reset_confirmation_request('username');
 101   */
 102  
 103  function send_reset_confirmation_request($name)
 104  {
 105      global $sitename;
 106  
 107      $expiryTimestamp = time() + (60 * RESET_EXPIRY_MINUTES);
 108      $safeName = doSlash($name);
 109  
 110      $rs = safe_query(
 111          "SELECT
 112              txp_users.user_id, txp_users.email,
 113              txp_users.nonce, txp_users.pass,
 114              txp_token.expires
 115          FROM ".safe_pfx('txp_users')." txp_users
 116          LEFT JOIN ".safe_pfx('txp_token')." txp_token
 117          ON txp_users.user_id = txp_token.reference_id
 118          AND txp_token.type = 'password_reset'
 119          WHERE txp_users.name = '$safeName'"
 120      );
 121  
 122      $row = nextRow($rs);
 123  
 124      if ($row) {
 125          extract($row);
 126  
 127          // Rate limit the reset requests.
 128          if ($expires) {
 129              $originalExpiry = strtotime($expires);
 130  
 131              if (($expiryTimestamp - $originalExpiry) < (60 * RESET_RATE_LIMIT_MINUTES)) {
 132                  return gTxt('password_reset_confirmation_request_sent');
 133              }
 134          }
 135  
 136          $confirm = generate_user_token($user_id, 'password_reset', $expiryTimestamp, $pass, $nonce);
 137  
 138          $expiryYear = safe_strftime('%Y', $expiryTimestamp);
 139          $expiryMonth = safe_strftime('%B', $expiryTimestamp);
 140          $expiryDay = safe_strftime('%Oe', $expiryTimestamp);
 141          $expiryTime = safe_strftime('%H:%M %Z', $expiryTimestamp);
 142  
 143          $authorLang = safe_field('val', 'txp_prefs', "name='language_ui' AND user_name = '$safeName'");
 144          $authorLang = ($authorLang) ? $authorLang : TEXTPATTERN_DEFAULT_LANG;
 145  
 146          $txpLang = Txp::get('\Textpattern\L10n\Lang');
 147          $txpLang->swapStrings($authorLang, 'admin, common');
 148  
 149          $message = gTxt('salutation', array('{name}' => $name)).
 150              n.n.gTxt('password_reset_confirmation').
 151              n.ahu.'index.php?lang='.$authorLang.'&confirm='.$confirm.
 152              n.n.gTxt('link_expires', array(
 153                  '{year}'  => $expiryYear,
 154                  '{month}' => $expiryMonth,
 155                  '{day}'   => $expiryDay,
 156                  '{time}'  => $expiryTime,
 157              ));
 158  
 159          $subject = gTxt('password_reset_confirmation_request');
 160          $txpLang->swapStrings(null);
 161  
 162          if (txpMail($email, "[$sitename] ".$subject, $message)) {
 163              return gTxt('password_reset_confirmation_request_sent');
 164          } else {
 165              return array(gTxt('could_not_mail'), E_ERROR);
 166          }
 167      } else {
 168          // Send generic 'request_sent' message so that (non-)existence of
 169          // account names are not leaked. Since this is a short circuit, there's
 170          // a possibility of a timing attack revealing the existence of an
 171          // account, which we could defend against to some degree.
 172          return gTxt('password_reset_confirmation_request_sent');
 173      }
 174  }
 175  
 176  /**
 177   * Emails a new user with login details.
 178   *
 179   * This function can be only executed when the currently authenticated user
 180   * trying to send the email was granted 'admin.edit' privileges.
 181   *
 182   * Should NEVER be used as sending plaintext passwords is wrong.
 183   * Will be removed in future, in lieu of sending reset request tokens.
 184   *
 185   * @param      string $RealName The real name
 186   * @param      string $name     The login name
 187   * @param      string $email    The email address
 188   * @param      string $password The password
 189   * @return     bool FALSE on error.
 190   * @deprecated in 4.6.0
 191   * @see        send_new_password(), send_reset_confirmation_request
 192   * @example
 193   * if (send_password('John Doe', 'login', 'example@example.tld', 'password'))
 194   * {
 195   *     echo "Login details sent.";
 196   * }
 197   */
 198  
 199  function send_password($RealName, $name, $email, $password)
 200  {
 201      global $sitename;
 202  
 203      require_privs('admin.edit');
 204  
 205      $message = gTxt('salutation', array('{name}' => $RealName)).
 206  
 207          n.n.gTxt('you_have_been_registered').' '.$sitename.
 208  
 209          n.n.gTxt('your_login_is').' '.$name.
 210          n.gTxt('your_password_is').' '.$password.
 211  
 212          n.n.gTxt('log_in_at').' '.ahu.'index.php';
 213  
 214      return txpMail($email, "[$sitename] ".gTxt('your_login_info'), $message);
 215  }
 216  
 217  /**
 218   * Sends a new password to an existing user.
 219   *
 220   * If the $name is FALSE, the password is sent to the currently
 221   * authenticated user.
 222   *
 223   * Should NEVER be used as sending plaintext passwords is wrong.
 224   * Will be removed in future, in lieu of sending reset request tokens.
 225   *
 226   * @param      string $password The new password
 227   * @param      string $email    The email address
 228   * @param      string $name     The login name
 229   * @return     bool FALSE on error.
 230   * @deprecated in 4.6.0
 231   * @see        send_reset_confirmation_request
 232   * @see        reset_author_pass()
 233   * @example
 234   * $pass = generate_password();
 235   * if (send_new_password($pass, 'example@example.tld', 'user'))
 236   * {
 237   *     echo "Password was sent to 'user'.";
 238   * }
 239   */
 240  
 241  function send_new_password($password, $email, $name)
 242  {
 243      global $txp_user, $sitename;
 244  
 245      if (empty($name)) {
 246          $name = $txp_user;
 247      }
 248  
 249      $message = gTxt('salutation', array('{name}' => $name)).
 250  
 251          n.n.gTxt('your_password_is').' '.$password.
 252  
 253          n.n.gTxt('log_in_at').' '.ahu.'index.php';
 254  
 255      return txpMail($email, "[$sitename] ".gTxt('your_new_password'), $message);
 256  }
 257  
 258  /**
 259   * Generates a password.
 260   *
 261   * Generates a random password of given length using the symbols set in
 262   * PASSWORD_SYMBOLS constant.
 263   *
 264   * Should NEVER be used as it is not cryptographically secure.
 265   * Will be removed in future, in lieu of sending reset request tokens.
 266   *
 267   * @param      int $length The length of the password
 268   * @return     string Random plain-text password
 269   * @deprecated in 4.6.0
 270   * @see        \Textpattern\Password\Generate
 271   * @see        \Textpattern\Password\Random
 272   * @example
 273   * echo generate_password(128);
 274   */
 275  
 276  function generate_password($length = 10)
 277  {
 278      static $chars;
 279  
 280      if (!$chars) {
 281          $chars = str_split(PASSWORD_SYMBOLS);
 282      }
 283  
 284      $pool = false;
 285      $pass = '';
 286  
 287      for ($i = 0; $i < $length; $i++) {
 288          if (!$pool) {
 289              $pool = $chars;
 290          }
 291  
 292          $index = mt_rand(0, count($pool) - 1);
 293          $pass .= $pool[$index];
 294          unset($pool[$index]);
 295          $pool = array_values($pool);
 296      }
 297  
 298      return $pass;
 299  }
 300  
 301  /**
 302   * Resets the given user's password and emails it.
 303   *
 304   * The old password is replaced with a new random-generated one.
 305   *
 306   * Should NEVER be used as sending plaintext passwords is wrong.
 307   * Will be removed in future, in lieu of sending reset request tokens.
 308   *
 309   * @param  string $name The login name
 310   * @return string A localized message string
 311   * @deprecated in 4.6.0
 312   * @see    PASSWORD_LENGTH
 313   * @see    generate_password()
 314   * @example
 315   * echo reset_author_pass('username');
 316   */
 317  
 318  function reset_author_pass($name)
 319  {
 320      $email = safe_field("email", 'txp_users', "name = '".doSlash($name)."'");
 321  
 322      $new_pass = Txp::get('\Textpattern\Password\Random')->generate(PASSWORD_LENGTH);
 323  
 324      $rs = change_user_password($name, $new_pass);
 325  
 326      if ($rs) {
 327          if (send_new_password($new_pass, $email, $name)) {
 328              return gTxt('password_sent_to').' '.$email;
 329          } else {
 330              return gTxt('could_not_mail').' '.$email;
 331          }
 332      } else {
 333          return gTxt('could_not_update_author').' '.txpspecialchars($name);
 334      }
 335  }
 336  
 337  /**
 338   * Loads client-side localisation scripts.
 339   *
 340   * Passes localisation strings from the database to JavaScript.
 341   *
 342   * Only works on the admin-side pages.
 343   *
 344   * @param   string|array $var   Scalar or array of string keys
 345   * @param   array        $atts  Array or array of arrays of variable substitution pairs
 346   * @param   array        $route Optional events/steps upon which to add the strings
 347   * @since   4.5.0
 348   * @package L10n
 349   * @example
 350   * gTxtScript(array('string1', 'string2', 'string3'));
 351   */
 352  
 353  function gTxtScript($var, $atts = array(), $route = array())
 354  {
 355      global $textarray_script, $event, $step;
 356  
 357      $targetEvent = empty($route[0]) ? null : (array)$route[0];
 358      $targetStep = empty($route[1]) ? null : (array)$route[1];
 359  
 360      if (($targetEvent === null || in_array($event, $targetEvent)) && ($targetStep === null || in_array($step, $targetStep))) {
 361          if (!is_array($textarray_script)) {
 362              $textarray_script = array();
 363          }
 364  
 365          $data = is_array($var) ? array_map('gTxt', $var, $atts) : (array) gTxt($var, $atts);
 366          $textarray_script = $textarray_script + array_combine((array) $var, $data);
 367      }
 368  }
 369  
 370  /**
 371   * Handle refreshing the passed AJAX content to the UI.
 372   *
 373   * @param  array $partials Partials array
 374   * @param  array $rs       Record set of the edited content
 375   */
 376  
 377  function updatePartials($partials, $rs, $types)
 378  {
 379      if (!is_array($types)) {
 380          $types = array($types);
 381      }
 382  
 383      foreach ($partials as $k => $p) {
 384          if (in_array($p['mode'], $types)) {
 385              $cb = $p['cb'];
 386              $partials[$k]['html'] = (is_array($cb) ? call_user_func($cb, $rs, $k) : $cb($rs, $k));
 387          }
 388      }
 389  
 390      return $partials;
 391  }
 392  
 393  /**
 394   * Handle refreshing the passed AJAX content to the UI.
 395   *
 396   * @param  array $partials Partials array
 397   * @return array           Response to send back to the browser
 398   */
 399  
 400  function updateVolatilePartials($partials)
 401  {
 402      $response = array();
 403  
 404      // Update the volatile partials.
 405      foreach ($partials as $k => $p) {
 406          // Volatile partials need a target DOM selector.
 407          if (empty($p['selector']) && $p['mode'] != PARTIAL_STATIC) {
 408              trigger_error(gTxt('empty_partial_selector', array('{name}' => $k)), E_USER_ERROR);
 409          } else {
 410              // Build response script.
 411              list($selector, $fragment) = (array)$p['selector'] + array(null, null);
 412  
 413              if ($p['mode'] == PARTIAL_VOLATILE) {
 414                  // Volatile partials replace *all* of the existing HTML
 415                  // fragment for their selector with the new one.
 416                  $selector = do_list($selector);
 417                  $fragment = isset($fragment) ? do_list($fragment) + $selector : $selector;
 418                  $response[] = 'var $html = $("<div>'.escape_js($p['html']).'</div>")';
 419  
 420                  foreach ($selector as $i => $sel) {
 421                      $response[] = '$("'.$sel.'").replaceWith($html.find("'.$fragment[$i].'"))';
 422                  }
 423              } elseif ($p['mode'] == PARTIAL_VOLATILE_VALUE) {
 424                  // Volatile partial values replace the *value* of elements
 425                  // matching their selector.
 426                  $response[] = '$("'.$selector.'").val("'.escape_js($p['html']).'")';
 427              }
 428          }
 429      }
 430  
 431      return $response;
 432  }
 433  
 434  /**
 435   * Lists image types that can be safely uploaded.
 436   *
 437   * Returns different results based on the logged in user's privileges.
 438   *
 439   * @param   int         $type If set, validates the given value
 440   * @return  mixed
 441   * @package Image
 442   * @since   4.6.0
 443   * @example
 444   * list($width, $height, $extension) = getimagesize('image');
 445   * if ($type = get_safe_image_types($extension))
 446   * {
 447   *     echo "Valid image of {$type}.";
 448   * }
 449   */
 450  
 451  function get_safe_image_types($type = null)
 452  {
 453      if (!has_privs('image.create.trusted')) {
 454          $extensions = array(0, '.gif', '.jpg', '.png');
 455      } else {
 456          $extensions = array(0, '.gif', '.jpg', '.png', '.swf', 0, 0, 0, 0, 0, 0, 0, 0, '.swf');
 457      }
 458  
 459      if (func_num_args() > 0) {
 460          return !empty($extensions[$type]) ? $extensions[$type] : false;
 461      }
 462  
 463      return $extensions;
 464  }
 465  
 466  /**
 467   * Checks if GD supports the given image type.
 468   *
 469   * @param   string $image_type Either '.gif', '.jpg', '.png'
 470   * @return  bool TRUE if the type is supported
 471   * @package Image
 472   */
 473  
 474  function check_gd($image_type)
 475  {
 476      if (!function_exists('gd_info')) {
 477          return false;
 478      }
 479  
 480      $gd_info = gd_info();
 481  
 482      switch ($image_type) {
 483          case '.gif':
 484              return ($gd_info['GIF Create Support'] == true);
 485              break;
 486          case '.jpg':
 487              return ($gd_info['JPEG Support'] == true);
 488              break;
 489          case '.png':
 490              return ($gd_info['PNG Support'] == true);
 491              break;
 492      }
 493  
 494      return false;
 495  }
 496  
 497  /**
 498   * Uploads an image.
 499   *
 500   * Can be used to upload a new image or replace an existing one.
 501   * If $id is specified, the image will be replaced. If $uploaded is set FALSE,
 502   * $file can take a local file instead of HTTP file upload variable.
 503   *
 504   * All uploaded files will included on the Images panel.
 505   *
 506   * @param   array        $file     HTTP file upload variables
 507   * @param   array        $meta     Image meta data, allowed keys 'caption', 'alt', 'category'
 508   * @param   int          $id       Existing image's ID
 509   * @param   bool         $uploaded If FALSE, $file takes a filename instead of upload vars
 510   * @return  array|string An array of array(message, id) on success, localized error string on error
 511   * @package Image
 512   * @example
 513   * print_r(image_data(
 514   *     $_FILES['myfile'],
 515   *     array(
 516   *         'caption' => '',
 517   *         'alt' => '',
 518   *         'category' => '',
 519   *     )
 520   * ));
 521   */
 522  
 523  function image_data($file, $meta = array(), $id = 0, $uploaded = true)
 524  {
 525      global $txp_user, $event;
 526  
 527      $name = $file['name'];
 528      $error = $file['error'];
 529      $file = $file['tmp_name'];
 530  
 531      if ($uploaded) {
 532          if ($error !== UPLOAD_ERR_OK) {
 533              return upload_get_errormsg($error);
 534          }
 535  
 536          $file = get_uploaded_file($file);
 537      }
 538  
 539      if (empty($file)) {
 540          return upload_get_errormsg(UPLOAD_ERR_NO_FILE);
 541      }
 542  
 543      if (get_pref('file_max_upload_size') < filesize($file)) {
 544          unlink($file);
 545  
 546          return upload_get_errormsg(UPLOAD_ERR_FORM_SIZE);
 547      }
 548  
 549      list($w, $h, $extension) = getimagesize($file);
 550      $ext = get_safe_image_types($extension);
 551  
 552      if (!$ext) {
 553          return gTxt('only_graphic_files_allowed');
 554      }
 555  
 556      $name = substr($name, 0, strrpos($name, '.')).$ext;
 557      $safename = doSlash($name);
 558      $meta = lAtts(array(
 559          'category' => '',
 560          'caption'  => '',
 561          'alt'      => '',
 562      ), (array) $meta, false);
 563  
 564      extract(doSlash($meta));
 565  
 566      $q = "
 567          name = '$safename',
 568          ext = '$ext',
 569          w = $w,
 570          h = $h,
 571          alt = '$alt',
 572          caption = '$caption',
 573          category = '$category',
 574          date = NOW(),
 575          author = '".doSlash($txp_user)."'
 576      ";
 577  
 578      if (empty($id)) {
 579          $rs = safe_insert('txp_image', $q);
 580  
 581          if ($rs) {
 582              $id = $GLOBALS['ID'] = $rs;
 583          } else {
 584              return gTxt('image_save_error');
 585          }
 586      } else {
 587          $id = assert_int($id);
 588      }
 589  
 590      $newpath = IMPATH.$id.$ext;
 591  
 592      if (shift_uploaded_file($file, $newpath) == false) {
 593          if (!empty($rs)) {
 594              safe_delete('txp_image', "id = '$id'");
 595              unset($GLOBALS['ID']);
 596          }
 597  
 598          return gTxt('directory_permissions', array('{path}' => $newpath));
 599      } elseif (empty($rs)) {
 600          $rs = safe_update('txp_image', $q, "id = $id");
 601  
 602          if (!$rs) {
 603              return gTxt('image_save_error');
 604          }
 605      }
 606  
 607      @chmod($newpath, 0644);
 608  
 609      // GD is supported
 610      if (check_gd($ext)) {
 611          // Auto-generate a thumbnail using the last settings
 612          if (get_pref('thumb_w') > 0 || get_pref('thumb_h') > 0) {
 613              $t = new txp_thumb($id);
 614              $t->crop = (bool) get_pref('thumb_crop');
 615              $t->hint = '0';
 616              $t->width = (int) get_pref('thumb_w');
 617              $t->height = (int) get_pref('thumb_h');
 618              $t->write();
 619          }
 620      }
 621  
 622      $message = gTxt('image_uploaded', array('{name}' => $name));
 623      update_lastmod('image_uploaded', compact('id', 'name', 'ext', 'w', 'h', 'alt', 'caption', 'category', 'txp_user'));
 624  
 625      // call post-upload plugins with new image's $id
 626      callback_event('image_uploaded', $event, false, $id);
 627  
 628      return array($message, $id);
 629  }
 630  
 631  /**
 632   * Error handler for admin-side pages.
 633   *
 634   * @param   int    $errno
 635   * @param   string $errstr
 636   * @param   string $errfile
 637   * @param   int    $errline
 638   * @access  private
 639   * @package Debug
 640   */
 641  
 642  function adminErrorHandler($errno, $errstr, $errfile, $errline)
 643  {
 644      global $production_status, $theme, $event, $step;
 645  
 646      $error = array();
 647  
 648      if ($production_status == 'testing') {
 649          $error = array(
 650              E_WARNING           => 'Warning',
 651              E_RECOVERABLE_ERROR => 'Catchable fatal error',
 652              E_USER_ERROR        => 'User_Error',
 653              E_USER_WARNING      => 'User_Warning',
 654          );
 655      } elseif ($production_status == 'debug') {
 656          $error = array(
 657              E_WARNING           => 'Warning',
 658              E_NOTICE            => 'Notice',
 659              E_RECOVERABLE_ERROR => 'Catchable fatal error',
 660              E_USER_ERROR        => 'User_Error',
 661              E_USER_WARNING      => 'User_Warning',
 662              E_USER_NOTICE       => 'User_Notice',
 663          );
 664  
 665          if (!isset($error[$errno])) {
 666              $error[$errno] = $errno;
 667          }
 668      }
 669  
 670      if (!isset($error[$errno]) || !error_reporting()) {
 671          return;
 672      }
 673  
 674      // When even a minimum environment is missing.
 675      if (!isset($production_status)) {
 676          echo '<pre dir="auto">'.gTxt('internal_error').' "'.$errstr.'"'.n."in $errfile at line $errline".'</pre>';
 677  
 678          return;
 679      }
 680  
 681      $backtrace = '';
 682  
 683      if (has_privs('debug.verbose')) {
 684          $msg = $error[$errno].' "'.$errstr.'"';
 685      } else {
 686          $msg = gTxt('internal_error');
 687      }
 688  
 689      if ($production_status == 'debug' && has_privs('debug.backtrace')) {
 690          $msg .= n."in $errfile at line $errline";
 691          $backtrace = join(n, get_caller(10, 1));
 692      }
 693  
 694      if ($errno == E_ERROR || $errno == E_USER_ERROR) {
 695          $httpstatus = 500;
 696      } else {
 697          $httpstatus = 200;
 698      }
 699  
 700      $out = "$msg.\n$backtrace";
 701  
 702      if (http_accept_format('html')) {
 703          if ($backtrace) {
 704              echo "<pre dir=\"auto\">$msg.</pre>".
 705                  n.'<pre class="backtrace" dir="ltr"><code>'.
 706                  txpspecialchars($backtrace).'</code></pre>';
 707          } elseif (is_object($theme)) {
 708              echo $theme->announce(array($out, E_ERROR), true);
 709          } else {
 710              echo "<pre dir=\"auto\">$out</pre>";
 711          }
 712      } elseif (http_accept_format('js')) {
 713          if (is_object($theme)) {
 714              send_script_response($theme->announce_async(array($out, E_ERROR), true));
 715          } else {
 716              send_script_response('/* '.$out.'*/');
 717          }
 718      } elseif (http_accept_format('xml')) {
 719          send_xml_response(array(
 720              'http-status'    => $httpstatus,
 721              'internal_error' => "$out",
 722          ));
 723      } else {
 724          txp_die($msg, 500);
 725      }
 726  }
 727  
 728  /**
 729   * Error handler for update scripts.
 730   *
 731   * @param   int    $errno
 732   * @param   string $errstr
 733   * @param   string $errfile
 734   * @param   int    $errline
 735   * @access  private
 736   * @package Debug
 737   */
 738  
 739  function updateErrorHandler($errno, $errstr, $errfile, $errline)
 740  {
 741      global $production_status;
 742  
 743      $old = $production_status;
 744      $production_status = 'debug';
 745  
 746      adminErrorHandler($errno, $errstr, $errfile, $errline);
 747  
 748      $production_status = $old;
 749  
 750      throw new Exception('update failed');
 751  }
 752  
 753  /**
 754   * Registers an admin-side extension page.
 755   *
 756   * For now this just does the same as register_callback().
 757   *
 758   * @param   callback $func  The callback function
 759   * @param   string   $event The callback event
 760   * @param   string   $step  The callback step
 761   * @param   bool     $top   The top or the bottom of the page
 762   * @access  private
 763   * @see     register_callback()
 764   * @package Callback
 765   */
 766  
 767  function register_page_extension($func, $event, $step = '', $top = 0)
 768  {
 769      register_callback($func, $event, $step, $top);
 770  }
 771  
 772  /**
 773   * Registers a new admin-side panel and adds a navigation link to the menu.
 774   *
 775   * @param   string $area  The menu the panel appears in, e.g. "home", "content", "presentation", "admin", "extensions"
 776   * @param   string $panel The panel's event
 777   * @param   string $title The menu item's label
 778   * @package Callback
 779   * @example
 780   * add_privs('abc_admin_event', '1,2');
 781   * register_tab('extensions', 'abc_admin_event', 'My Panel');
 782   * register_callback('abc_admin_function', 'abc_admin_event');
 783   */
 784  
 785  function register_tab($area, $panel, $title)
 786  {
 787      global $plugin_areas, $event;
 788  
 789      if ($event !== 'plugin') {
 790          $plugin_areas[$area][$title] = $panel;
 791      }
 792  }
 793  
 794  /**
 795   * Call an event's pluggable UI function.
 796   *
 797   * @param   string $event   The event
 798   * @param   string $element The element selector
 799   * @param   string $default The default interface markup
 800   * @return  mixed  Returned value from a callback handler, or $default if no custom UI was provided
 801   * @package Callback
 802   */
 803  
 804  function pluggable_ui($event, $element, $default = '')
 805  {
 806      $argv = func_get_args();
 807      $argv = array_merge(array(
 808          $event,
 809          $element,
 810         (string) $default === '' ? 0 : array(0, 0)
 811      ), array_slice($argv, 2));
 812      // Custom user interface, anyone?
 813      // Signature for called functions:
 814      // string my_called_func(string $event, string $step, string $default_markup[, mixed $context_data...])
 815      $ui = call_user_func_array('callback_event', $argv);
 816  
 817      // Either plugins provided a user interface, or we render our own.
 818      return ($ui === '') ? $default : $ui;
 819  }
 820  
 821  /**
 822   * Gets a list of form types.
 823   *
 824   * The list form types can be extended with a 'form.types > types'
 825   * callback event. Callback functions get passed three arguments: '$event',
 826   * '$step' and '$types'. The third parameter contains a reference to an
 827   * array of 'type => label' pairs.
 828   *
 829   * @return  array An array of form types
 830   * @since   4.6.0
 831   * @package Template
 832   */
 833  
 834  function get_form_types()
 835  {
 836      static $types = null;
 837  
 838      if ($types === null) {
 839          foreach (Txp::get('Textpattern\Skin\Form')->getTypes() as $type) {
 840              $types[$type] = gTxt($type);
 841          }
 842  
 843          callback_event_ref('form.types', 'types', 0, $types);
 844      }
 845  
 846      return $types;
 847  }
 848  
 849  /**
 850   * Gets a list of essential form templates.
 851   *
 852   * These forms can not be deleted or renamed. The array keys hold
 853   * the form names, the array values their group.
 854   *
 855   * The list forms can be extended with a 'form.essential > forms'
 856   * callback event. Callback functions get passed three arguments: '$event',
 857   * '$step' and '$essential'. The third parameter contains a reference to an
 858   * array of forms.
 859   *
 860   * @return  array An array of form names
 861   * @since   4.6.0
 862   * @package Template
 863   */
 864  
 865  function get_essential_forms()
 866  {
 867      static $essential = null;
 868  
 869      if ($essential === null) {
 870          $essential = array(
 871              'comments'         => 'comment',
 872              'comments_display' => 'comment',
 873              'comment_form'     => 'comment',
 874              'default'          => 'article',
 875              'plainlinks'       => 'link',
 876              'files'            => 'file',
 877          );
 878  
 879          callback_event_ref('form.essential', 'forms', 0, $essential);
 880      }
 881  
 882      return $essential;
 883  }
 884  
 885  /**
 886   * Renders a HTML &lt;select&gt; list of supported permanent link URL formats.
 887   *
 888   * @param  string $name HTML name and id of the list
 889   * @param  string $val  Initial (or current) selected item
 890   * @return string HTML
 891   */
 892  
 893  function permlinkmodes($name, $val, $blank = false)
 894  {
 895      $vals = array(
 896          'messy'                     => gTxt('messy'),
 897          'id_title'                  => gTxt('id_title'),
 898          'section_id_title'          => gTxt('section_id_title'),
 899          'section_category_title'    => gTxt('section_category_title'),
 900          'year_month_day_title'      => gTxt('year_month_day_title'),
 901          'breadcrumb_title'          => gTxt('breadcrumb_title'),
 902          'section_title'             => gTxt('section_title'),
 903          'title_only'                => gTxt('title_only')
 904      );
 905  
 906      return selectInput($name, $vals, $val, $blank, '', $name);
 907  }
 908  
 909  /**
 910   * Gets the name of the default publishing section.
 911   *
 912   * @return string The section
 913   */
 914  
 915  function getDefaultSection()
 916  {
 917      global $txp_sections;
 918  
 919      $name = get_pref('default_section');
 920  
 921      if (!isset($txp_sections[$name])) {
 922          foreach ($txp_sections as $name => $section) {
 923              if ($name != 'default') {
 924                  break;
 925              }
 926          }
 927  
 928          set_pref('default_section', $name, 'section', PREF_HIDDEN);
 929      }
 930  
 931      return $name;
 932  }
 933  
 934  /**
 935   * Updates a list's per page number.
 936   *
 937   * Gets the per page number from a "qty" HTTP POST/GET parameter and
 938   * creates a user-specific preference value "$name_list_pageby".
 939   *
 940   * @param string|null $name The name of the list
 941   * @deprecated in 4.7.0
 942   */
 943  
 944  function event_change_pageby($name = null)
 945  {
 946      global $event;
 947  
 948      Txp::get('\Textpattern\Admin\Paginator', $event, $name)->change();
 949  }
 950  
 951  /**
 952   * Generic multi-edit form's edit handler shared across panels.
 953   *
 954   * Receives an action from a multi-edit form and runs it in the given
 955   * database table.
 956   *
 957   * @param  string $table  The database table
 958   * @param  string $id_key The database column selected items match to. Column should be integer type
 959   * @return string Comma-separated list of affected items
 960   * @see    multi_edit()
 961   */
 962  
 963  function event_multi_edit($table, $id_key)
 964  {
 965      $method = ps('edit_method');
 966      $selected = ps('selected');
 967  
 968      if ($selected) {
 969          if ($method == 'delete') {
 970              foreach ($selected as $id) {
 971                  $id = assert_int($id);
 972  
 973                  if (safe_delete($table, "$id_key = '$id'")) {
 974                      $ids[] = $id;
 975                  }
 976              }
 977  
 978              return join(', ', $ids);
 979          }
 980      }
 981  
 982      return '';
 983  }
 984  
 985  /**
 986   * Verifies temporary directory.
 987   *
 988   * Verifies that the temporary directory is writeable.
 989   *
 990   * @param   string $dir The directory to check
 991   * @return  bool|null NULL on error, TRUE on success
 992   * @package Debug
 993   */
 994  
 995  function find_temp_dir()
 996  {
 997      global $path_to_site, $img_dir;
 998  
 999      if (IS_WIN) {
1000          $guess = array(
1001              txpath.DS.'tmp',
1002              getenv('TMP'),
1003              getenv('TEMP'),
1004              getenv('SystemRoot').DS.'Temp',
1005              'C:'.DS.'Temp',
1006              $path_to_site.DS.$img_dir,
1007          );
1008  
1009          foreach ($guess as $k => $v) {
1010              if (empty($v)) {
1011                  unset($guess[$k]);
1012              }
1013          }
1014      } else {
1015          $guess = array(
1016              txpath.DS.'tmp',
1017              '',
1018              DS.'tmp',
1019              $path_to_site.DS.$img_dir,
1020          );
1021      }
1022  
1023      foreach ($guess as $dir) {
1024          $tf = @tempnam($dir, 'txp_');
1025  
1026          if ($tf) {
1027              $tf = realpath($tf);
1028          }
1029  
1030          if ($tf and file_exists($tf)) {
1031              unlink($tf);
1032  
1033              return dirname($tf);
1034          }
1035      }
1036  
1037      return false;
1038  }
1039  
1040  /**
1041   * Moves an uploaded file and returns its new location.
1042   *
1043   * @param   string $f    The filename of the uploaded file
1044   * @param   string $dest The destination of the moved file. If omitted, the file is moved to the temp directory
1045   * @return  string|bool The new path or FALSE on error
1046   * @package File
1047   */
1048  
1049  function get_uploaded_file($f, $dest = '')
1050  {
1051      global $tempdir;
1052  
1053      if (!is_uploaded_file($f)) {
1054          return false;
1055      }
1056  
1057      if ($dest) {
1058          $newfile = $dest;
1059      } else {
1060          $newfile = tempnam($tempdir, 'txp_');
1061          if (!$newfile) {
1062              return false;
1063          }
1064      }
1065  
1066      // $newfile is created by tempnam(), but move_uploaded_file will overwrite it.
1067      if (move_uploaded_file($f, $newfile)) {
1068          return $newfile;
1069      }
1070  }
1071  
1072  /**
1073   * Gets an array of files in the Files directory that weren't uploaded
1074   * from Textpattern.
1075   *
1076   * Used for importing existing files on the server to Textpattern's files panel.
1077   *
1078   * @param   string $path    The directory to scan
1079   * @param   int    $options glob() options
1080   * @return  array An array of file paths
1081   * @package File
1082   */
1083  
1084  function get_filenames($path = null, $options = GLOB_NOSORT)
1085  {
1086      global $file_base_path;
1087  
1088      $files = array();
1089      $file_path = isset($path) ? $path : $file_base_path;
1090      $is_file = ($options & GLOB_ONLYDIR) ? 'is_dir' : 'is_file';
1091  
1092      if (!is_dir($file_path) || !is_readable($file_path)) {
1093          return array();
1094      }
1095  
1096      $cwd = getcwd();
1097  
1098      if (chdir($file_path)) {
1099          $directory = glob('*', $options);
1100  
1101          if ($directory) {
1102              foreach ($directory as $filename) {
1103                  if ($is_file($filename) && is_readable($filename)) {
1104                      $files[$filename] = $filename;
1105                  }
1106              }
1107  
1108              unset($directory);
1109          }
1110  
1111          if ($cwd) {
1112              chdir($cwd);
1113          }
1114      }
1115  
1116      if (!$files || isset($path)) {
1117          return $files;
1118      }
1119  
1120      $rs = safe_rows_start("filename", 'txp_file', "1 = 1");
1121  
1122      if ($rs && numRows($rs)) {
1123          while ($a = nextRow($rs)) {
1124              unset($files[$a['filename']]);
1125          }
1126      }
1127  
1128      return $files;
1129  }
1130  
1131  /**
1132   * Moves a file.
1133   *
1134   * @param   string $f    The file to move
1135   * @param   string $dest The destination
1136   * @return  bool TRUE on success, or FALSE on error
1137   * @package File
1138   */
1139  
1140  function shift_uploaded_file($f, $dest)
1141  {
1142      if (@rename($f, $dest)) {
1143          return true;
1144      }
1145  
1146      if (@copy($f, $dest)) {
1147          unlink($f);
1148  
1149          return true;
1150      }
1151  
1152      return false;
1153  }
1154  
1155  /**
1156   * Assigns assets to a different user.
1157   *
1158   * Changes the owner of user's assets. It will move articles, files, images
1159   * and links from one user to another.
1160   *
1161   * Should be run when a user's permissions are taken away, a username is
1162   * renamed or the user is removed from the site.
1163   *
1164   * Affected database tables can be extended with a 'user.assign_assets > columns'
1165   * callback event. Callback functions get passed three arguments: '$event',
1166   * '$step' and '$columns'. The third parameter contains a reference to an
1167   * array of 'table => column' pairs.
1168   *
1169   * On a successful run, will trigger a 'user.assign_assets > done' callback event.
1170   *
1171   * @param   string|array $owner     List of current owners
1172   * @param   string       $new_owner The new owner
1173   * @return  bool FALSE on error
1174   * @since   4.6.0
1175   * @package User
1176   * @example
1177   * if (assign_user_assets(array('user1', 'user2'), 'new_owner'))
1178   * {
1179   *     echo "Assigned assets by 'user1' and 'user2' to 'new_owner'.";
1180   * }
1181   */
1182  
1183  function assign_user_assets($owner, $new_owner)
1184  {
1185      static $columns = null;
1186  
1187      if (!$owner || !user_exists($new_owner)) {
1188          return false;
1189      }
1190  
1191      if ($columns === null) {
1192          $columns = array(
1193              'textpattern' => 'AuthorID',
1194              'txp_file'    => 'author',
1195              'txp_image'   => 'author',
1196              'txp_link'    => 'author',
1197          );
1198  
1199          callback_event_ref('user.assign_assets', 'columns', 0, $columns);
1200      }
1201  
1202      $names = join(',', quote_list((array) $owner));
1203      $assign = doSlash($new_owner);
1204  
1205      foreach ($columns as $table => $column) {
1206          if (safe_update($table, "$column = '$assign'", "$column IN ($names)") === false) {
1207              return false;
1208          }
1209      }
1210  
1211      callback_event('user.assign_assets', 'done', 0, compact('owner', 'new_owner', 'columns'));
1212  
1213      return true;
1214  }
1215  
1216  /**
1217   * Validates a string as a username.
1218   *
1219   * @param   string $name The username
1220   * @return  bool TRUE if the string valid
1221   * @since   4.6.0
1222   * @package User
1223   * @example
1224   * if (is_valid_username('john'))
1225   * {
1226   *     echo "'john' is a valid username.";
1227   * }
1228   */
1229  
1230  function is_valid_username($name)
1231  {
1232      if (function_exists('mb_strlen')) {
1233          $length = mb_strlen($name, '8bit');
1234      } else {
1235          $length = strlen($name);
1236      }
1237  
1238      return $name && !preg_match('/^\s|[,\'"<>]|\s$/u', $name) && $length <= 64;
1239  }
1240  
1241  /**
1242   * Creates a user account.
1243   *
1244   * On a successful run, will trigger a 'user.create > done' callback event.
1245   *
1246   * @param   string $name     The login name
1247   * @param   string $email    The email address
1248   * @param   string $password The password
1249   * @param   string $realname The real name
1250   * @param   int    $group    The user group
1251   * @return  bool FALSE on error
1252   * @since   4.6.0
1253   * @package User
1254   * @example
1255   * if (create_user('john', 'john.doe@example.com', 'DancingWalrus', 'John Doe', 1))
1256   * {
1257   *     echo "User 'john' created.";
1258   * }
1259   */
1260  
1261  function create_user($name, $email, $password, $realname = '', $group = 0)
1262  {
1263      $levels = get_groups();
1264  
1265      if (!$password || !is_valid_username($name) || !is_valid_email($email) || user_exists($name) || !isset($levels[$group])) {
1266          return false;
1267      }
1268  
1269      $nonce = md5(uniqid(mt_rand(), true));
1270      $hash = Txp::get('\Textpattern\Password\Hash')->hash($password);
1271  
1272      if (
1273          safe_insert(
1274              'txp_users',
1275              "name = '".doSlash($name)."',
1276              email = '".doSlash($email)."',
1277              pass = '".doSlash($hash)."',
1278              nonce = '".doSlash($nonce)."',
1279              privs = ".intval($group).",
1280              RealName = '".doSlash($realname)."'"
1281          ) === false
1282      ) {
1283          return false;
1284      }
1285  
1286      callback_event('user.create', 'done', 0, compact('name', 'email', 'password', 'realname', 'group', 'nonce', 'hash'));
1287  
1288      return true;
1289  }
1290  
1291  /**
1292   * Updates a user.
1293   *
1294   * Updates a user account's properties. The $user argument is used for
1295   * selecting the updated user, and rest of the arguments new values.
1296   * Use NULL to omit an argument.
1297   *
1298   * On a successful run, will trigger a 'user.update > done' callback event.
1299   *
1300   * @param   string      $user     The updated user
1301   * @param   string|null $email    The email address
1302   * @param   string|null $realname The real name
1303   * @param   array|null  $meta     Additional meta fields
1304   * @return  bool FALSE on error
1305   * @since   4.6.0
1306   * @package User
1307   * @example
1308   * if (update_user('login', null, 'John Doe'))
1309   * {
1310   *     echo "Updated user's real name.";
1311   * }
1312   */
1313  
1314  function update_user($user, $email = null, $realname = null, $meta = array())
1315  {
1316      if (($email !== null && !is_valid_email($email)) || !user_exists($user)) {
1317          return false;
1318      }
1319  
1320      $meta = (array) $meta;
1321      $meta['RealName'] = $realname;
1322      $meta['email'] = $email;
1323      $set = array();
1324  
1325      foreach ($meta as $name => $value) {
1326          if ($value !== null) {
1327              $set[] = $name." = '".doSlash($value)."'";
1328          }
1329      }
1330  
1331      if (
1332          safe_update(
1333              'txp_users',
1334              join(',', $set),
1335              "name = '".doSlash($user)."'"
1336          ) === false
1337      ) {
1338          return false;
1339      }
1340  
1341      callback_event('user.update', 'done', 0, compact('user', 'email', 'realname', 'meta'));
1342  
1343      return true;
1344  }
1345  
1346  /**
1347   * Changes a user's password.
1348   *
1349   * On a successful run, will trigger a 'user.password_change > done' callback event.
1350   *
1351   * @param   string $user     The updated user
1352   * @param   string $password The new password
1353   * @return  bool FALSE on error
1354   * @since   4.6.0
1355   * @package User
1356   * @example
1357   * if (change_user_password('login', 'WalrusWasDancing'))
1358   * {
1359   *     echo "Password changed.";
1360   * }
1361   */
1362  
1363  function change_user_password($user, $password)
1364  {
1365      if (!$user || !$password) {
1366          return false;
1367      }
1368  
1369      $hash = Txp::get('\Textpattern\Password\Hash')->hash($password);
1370  
1371      if (
1372          safe_update(
1373              'txp_users',
1374              "pass = '".doSlash($hash)."'",
1375              "name = '".doSlash($user)."'"
1376          ) === false
1377      ) {
1378          return false;
1379      }
1380  
1381      callback_event('user.password_change', 'done', 0, compact('user', 'password', 'hash'));
1382  
1383      return true;
1384  }
1385  
1386  /**
1387   * Removes a user.
1388   *
1389   * The user's assets are assigned to the given new owner.
1390   *
1391   * On a successful run, will trigger a 'user.remove > done' callback event.
1392   *
1393   * @param   string|array $user      List of removed users
1394   * @param   string       $new_owner Assign assets to
1395   * @return  bool FALSE on error
1396   * @since   4.6.0
1397   * @package User
1398   * @example
1399   * if (remove_user('user', 'new_owner'))
1400   * {
1401   *     echo "Removed 'user' and assigned assets to 'new_owner'.";
1402   * }
1403   */
1404  
1405  function remove_user($user, $new_owner)
1406  {
1407      if (!$user || !$new_owner) {
1408          return false;
1409      }
1410  
1411      $names = join(',', quote_list((array) $user));
1412  
1413      if (assign_user_assets($user, $new_owner) === false) {
1414          return false;
1415      }
1416  
1417      if (safe_delete('txp_prefs', "user_name IN ($names)") === false) {
1418          return false;
1419      }
1420  
1421      if (safe_delete('txp_users', "name IN ($names)") === false) {
1422          return false;
1423      }
1424  
1425      callback_event('user.remove', 'done', 0, compact('user', 'new_owner'));
1426  
1427      return true;
1428  }
1429  
1430  /**
1431   * Renames a user.
1432   *
1433   * On a successful run, will trigger a 'user.rename > done' callback event.
1434   *
1435   * @param   string $user    Updated user
1436   * @param   string $newname The new name
1437   * @return  bool FALSE on error
1438   * @since   4.6.0
1439   * @package User
1440   * @example
1441   * if (rename_user('login', 'newname'))
1442   * {
1443   *     echo "'login' renamed to 'newname'.";
1444   * }
1445   */
1446  
1447  function rename_user($user, $newname)
1448  {
1449      if (!is_scalar($user) || !is_valid_username($newname)) {
1450          return false;
1451      }
1452  
1453      if (assign_user_assets($user, $newname) === false) {
1454          return false;
1455      }
1456  
1457      if (
1458          safe_update(
1459              'txp_users',
1460              "name = '".doSlash($newname)."'",
1461              "name = '".doSlash($user)."'"
1462          ) === false
1463      ) {
1464          return false;
1465      }
1466  
1467      callback_event('user.rename', 'done', 0, compact('user', 'newname'));
1468  
1469      return true;
1470  }
1471  
1472  /**
1473   * Checks if a user exists.
1474   *
1475   * @param   string $user The user
1476   * @return  bool TRUE if the user exists
1477   * @since   4.6.0
1478   * @package User
1479   * @example
1480   * if (user_exists('john'))
1481   * {
1482   *     echo "'john' exists.";
1483   * }
1484   */
1485  
1486  function user_exists($user)
1487  {
1488      return (bool) safe_row("name", 'txp_users', "name = '".doSlash($user)."'");
1489  }
1490  
1491  /**
1492   * Changes a user's group.
1493   *
1494   * On a successful run, will trigger a 'user.change_group > done' callback event.
1495   *
1496   * @param   string|array $user  Updated users
1497   * @param   int          $group The new group
1498   * @return  bool FALSE on error
1499   * @since   4.6.0
1500   * @package User
1501   * @example
1502   * if (change_user_group('john', 1))
1503   * {
1504   *     echo "'john' is now publisher.";
1505   * }
1506   */
1507  
1508  function change_user_group($user, $group)
1509  {
1510      $levels = get_groups();
1511  
1512      if (!$user || !isset($levels[$group])) {
1513          return false;
1514      }
1515  
1516      $names = join(',', quote_list((array) $user));
1517  
1518      if (
1519          safe_update(
1520              'txp_users',
1521              "privs = ".intval($group),
1522              "name IN ($names)"
1523          ) === false
1524      ) {
1525          return false;
1526      }
1527  
1528      callback_event('user.change_group', 'done', 0, compact('user', 'group'));
1529  
1530      return true;
1531  }
1532  
1533  /**
1534   * Validates the given user credentials.
1535   *
1536   * Validates a given login and a password combination. If the combination is
1537   * correct, the user's login name is returned, FALSE otherwise.
1538   *
1539   * If $log is TRUE, also checks that the user has permissions to access the
1540   * admin side interface. On success, updates the user's last access timestamp.
1541   *
1542   * @param   string $user     The login
1543   * @param   string $password The password
1544   * @param   bool   $log      If TRUE, requires privilege level greater than 'none'
1545   * @return  string|bool The user's login name or FALSE on error
1546   * @package User
1547   */
1548  
1549  function txp_validate($user, $password, $log = true)
1550  {
1551      $safe_user = doSlash($user);
1552      $name = false;
1553  
1554      $r = safe_row("name, pass, privs", 'txp_users', "name = '$safe_user'");
1555  
1556      if (!$r) {
1557          return false;
1558      }
1559  
1560      // Check post-4.3-style passwords.
1561      if ($pass = Txp::get('\Textpattern\Password\Hash')->verify($password, $r['pass'])) {
1562          if (!$log || $r['privs'] > 0) {
1563              $name = $r['name'];
1564          }
1565  
1566          if ($pass === true) {
1567              safe_update('txp_users', "pass = '".doSlash(Txp::get('\Textpattern\Password\Hash')->hash($password))."'", "name = '$safe_user'");
1568          }
1569      } else {
1570          // No good password: check 4.3-style passwords.
1571          $pass = '*'.sha1(sha1($password, true));
1572  
1573          $name = safe_field("name", 'txp_users',
1574              "name = '$safe_user' AND privs > 0 AND (pass = UPPER('$pass') OR pass = LOWER('$pass'))");
1575  
1576          // Old password is good: migrate password to phpass.
1577          if ($name !== false) {
1578              safe_update('txp_users', "pass = '".doSlash(Txp::get('\Textpattern\Password\Hash')->hash($password))."'", "name = '$safe_user'");
1579          }
1580      }
1581  
1582      if ($name !== false && $log) {
1583          // Update the last access time.
1584          safe_update('txp_users', "last_access = NOW()", "name = '$safe_user'");
1585      }
1586  
1587      return $name;
1588  }
1589  
1590  /**
1591   * Calculates a password hash.
1592   *
1593   * @param   string $password The password
1594   * @return  string A hash
1595   * @see     PASSWORD_COMPLEXITY
1596   * @see     PASSWORD_PORTABILITY
1597   * @package User
1598   */
1599  
1600  function txp_hash_password($password)
1601  {
1602      static $phpass = null;
1603  
1604      if (!$phpass) {
1605          include_once txpath.'/lib/PasswordHash.php';
1606          $phpass = new PasswordHash(PASSWORD_COMPLEXITY, PASSWORD_PORTABILITY);
1607      }
1608  
1609      return $phpass->HashPassword($password);
1610  }
1611  
1612  /**
1613   * Create a secure token hash in the database from the passed information.
1614   *
1615   * @param  int    $ref             Reference to the user's account (user_id)
1616   * @param  string $type            Flavour of token to create
1617   * @param  int    $expiryTimestamp UNIX timestamp of when the token will expire
1618   * @param  string $pass            Password, used as part of the token generation
1619   * @param  string $nonce           Random nonce associated with the user's account
1620   * @return string                  Secure token suitable for emailing as part of a link
1621   * @since  4.6.1
1622   */
1623  
1624  function generate_user_token($ref, $type, $expiryTimestamp, $pass, $nonce)
1625  {
1626      $ref = assert_int($ref);
1627      $expiry = strftime('%Y-%m-%d %H:%M:%S', $expiryTimestamp);
1628  
1629      // The selector becomes an indirect reference to the user row id,
1630      // and thus does not leak information when publicly displayed.
1631      $selector = Txp::get('\Textpattern\Password\Random')->generate(12);
1632  
1633      // Use a hash of the nonce, selector and password.
1634      // This ensures that requests expire automatically when:
1635      //  a) The person logs in, or
1636      //  b) They successfully set/change their password
1637      // Using the selector in the hash just injects randomness, otherwise two requests
1638      // back-to-back would generate the same code.
1639      // Old requests for the same user id are purged when password is set.
1640      $token = bin2hex(pack('H*', substr(hash(HASHING_ALGORITHM, $nonce.$selector.$pass), 0, SALT_LENGTH)));
1641      $user_token = $token.$selector;
1642  
1643      // Remove any previous activation tokens and insert the new one.
1644      $safe_type = doSlash($type);
1645      safe_delete("txp_token", "reference_id = '$ref' AND type = '$safe_type'");
1646      safe_insert("txp_token",
1647              "reference_id = '$ref',
1648              type = '$safe_type',
1649              selector = '".doSlash($selector)."',
1650              token = '".doSlash($token)."',
1651              expires = '".doSlash($expiry)."'
1652          ");
1653  
1654      return $user_token;
1655  }
1656  
1657  /**
1658   * Display a modal client message in response to an AJAX request and
1659   * halt execution.
1660   *
1661   * @param   string|array $thing The $thing[0] is the message's text; $thing[1] is the message's type (one of E_ERROR or E_WARNING, anything else meaning "success"; not used)
1662   * @since   4.5.0
1663   * @package Ajax
1664   */
1665  
1666  function modal_halt($thing)
1667  {
1668      global $app_mode, $theme;
1669  
1670      if ($app_mode == 'async') {
1671          send_script_response($theme->announce_async($thing, true));
1672          die();
1673      }
1674  }
1675  
1676  /**
1677   * Sends an activity message to the client.
1678   *
1679   * @param   string|array $message The message
1680   * @param   int          $type    The type, either 0, E_ERROR, E_WARNING
1681   * @param   int          $flags   Flags, consisting of TEXTPATTERN_ANNOUNCE_ADAPTIVE | TEXTPATTERN_ANNOUNCE_ASYNC | TEXTPATTERN_ANNOUNCE_MODAL | TEXTPATTERN_ANNOUNCE_REGULAR
1682   * @package Announce
1683   * @since   4.6.0
1684   * @example
1685   * echo announce('My message', E_WARNING);
1686   */
1687  
1688  function announce($message, $type = 0, $flags = TEXTPATTERN_ANNOUNCE_ADAPTIVE)
1689  {
1690      global $app_mode, $theme;
1691  
1692      if (!is_array($message)) {
1693          $message = array($message, $type);
1694      }
1695  
1696      if ($flags & TEXTPATTERN_ANNOUNCE_ASYNC || ($flags & TEXTPATTERN_ANNOUNCE_ADAPTIVE && $app_mode === 'async')) {
1697          return $theme->announce_async($message);
1698      }
1699  
1700      if ($flags & TEXTPATTERN_ANNOUNCE_MODAL) {
1701          return $theme->announce_async($message, true);
1702      }
1703  
1704      return $theme->announce($message);
1705  }
1706  
1707  /**
1708   * Loads date definitions from a localisation file.
1709   *
1710   * @param      string $lang The language
1711   * @package    L10n
1712   * @deprecated in 4.6.0
1713   */
1714  
1715  function load_lang_dates($lang)
1716  {
1717      $filename = is_file(txpath.'/lang/'.$lang.'_dates.txt') ?
1718          txpath.'/lang/'.$lang.'_dates.txt' :
1719          txpath.'/lang/en-gb_dates.txt';
1720      $file = @file(txpath.'/lang/'.$lang.'_dates.txt', 'r');
1721  
1722      if (is_array($file)) {
1723          foreach ($file as $line) {
1724              if ($line[0] == '#' || strlen($line) < 2) {
1725                  continue;
1726              }
1727  
1728              list($name, $val) = explode('=>', $line, 2);
1729              $out[trim($name)] = trim($val);
1730          }
1731  
1732          return $out;
1733      }
1734  
1735      return false;
1736  }
1737  
1738  /**
1739   * Gets language strings for the given event.
1740   *
1741   * If no $lang is specified, the strings are loaded from the currently
1742   * active language.
1743   *
1744   * @param   string $event The event to get, e.g. "common", "admin", "public"
1745   * @param   string $lang  The language code
1746   * @return  array|string Array of string on success, or an empty string when no strings were found
1747   * @package L10n
1748   * @see     load_lang()
1749   * @example
1750   * print_r(
1751   *     load_lang_event('common')
1752   * );
1753   */
1754  
1755  function load_lang_event($event, $lang = LANG)
1756  {
1757      $installed = (false !== safe_field("name", 'txp_lang', "lang = '".doSlash($lang)."' LIMIT 1"));
1758  
1759      $lang_code = ($installed) ? $lang : TEXTPATTERN_DEFAULT_LANG;
1760  
1761      $rs = safe_rows_start("name, data", 'txp_lang', "lang = '".doSlash($lang_code)."' AND event = '".doSlash($event)."'");
1762  
1763      $out = array();
1764  
1765      if ($rs && !empty($rs)) {
1766          while ($a = nextRow($rs)) {
1767              $out[$a['name']] = $a['data'];
1768          }
1769      }
1770  
1771      return ($out) ? $out : '';
1772  }
1773  
1774  /**
1775   * Installs localisation strings from a Textpack.
1776   *
1777   * @param      string $textpack      The Textpack to install
1778   * @param      bool   $add_new_langs If TRUE, installs strings for any included language
1779   * @return     int Number of installed strings
1780   * @package    L10n
1781   * @deprecated in 4.7.0
1782   */
1783  
1784  function install_textpack($textpack, $add_new_langs = false)
1785  {
1786      return Txp::get('\Textpattern\L10n\Lang')->installTextpack($textpack, $add_new_langs);
1787  }
1788  
1789  /**
1790   * Generate a ciphered token.
1791   *
1792   * The token is reproducible, unique among sites and users, expires later.
1793   *
1794   * @return  string The token
1795   * @see     bouncer()
1796   * @package CSRF
1797   */
1798  
1799  function form_token()
1800  {
1801      static $token = null;
1802      global $txp_user;
1803  
1804      // Generate a ciphered token from the current user's nonce (thus valid for
1805      // login time plus 30 days) and a pinch of salt from the blog UID.
1806      if ($token === null && $txp_user) {
1807          $nonce = safe_field("nonce", 'txp_users', "name = '".doSlash($txp_user)."'");
1808          $token = md5($nonce.get_pref('blog_uid'));
1809      }
1810  
1811      return $token;
1812  }
1813  
1814  /**
1815   * Validates admin steps and protects against CSRF attempts using tokens.
1816   *
1817   * Takes an admin step and validates it against an array of valid steps.
1818   * The valid steps array indicates the step's token based session riding
1819   * protection needs.
1820   *
1821   * If the step requires CSRF token protection, and the request doesn't come with
1822   * a valid token, the request is terminated, defeating any CSRF attempts.
1823   *
1824   * If the $step isn't in valid steps, it returns FALSE, but the request
1825   * isn't terminated. If the $step is valid and passes CSRF validation,
1826   * returns TRUE.
1827   *
1828   * @param   string $step  Requested admin step
1829   * @param   array  $steps An array of valid steps with flag indicating CSRF needs, e.g. array('savething' => true, 'listthings' => false)
1830   * @return  bool If the $step is valid, proceeds and returns TRUE. Dies on CSRF attempt.
1831   * @see     form_token()
1832   * @package CSRF
1833   * @example
1834   * global $step;
1835   * if (bouncer($step, array(
1836   *     'browse'     => false,
1837   *     'edit'       => false,
1838   *     'save'       => true,
1839   *     'multi_edit' => true,
1840   * )))
1841   * {
1842   *     echo "The '{$step}' is valid.";
1843   * }
1844   */
1845  
1846  function bouncer($step, $steps)
1847  {
1848      global $event;
1849  
1850      if (empty($step)) {
1851          return true;
1852      }
1853  
1854      // Validate step.
1855      if (!array_key_exists($step, $steps)) {
1856          return false;
1857      }
1858  
1859      // Does this step require a token?
1860      if (!$steps[$step]) {
1861          return true;
1862      }
1863  
1864      // Validate token.
1865      if (gps('_txp_token') === form_token()) {
1866          return true;
1867      }
1868  
1869      die(gTxt('get_off_my_lawn', array(
1870          '{event}' => $event,
1871          '{step}'  => $step,
1872      )));
1873  }
1874  
1875  /**
1876   * Checks install's file integrity and returns results.
1877   *
1878   * Depending on the given $flags this function will either return an array of
1879   * file statuses, checksums or the digest of the install. It can also return the
1880   * parsed contents of the checksum file.
1881   *
1882   * @param   int $flags Options are INTEGRITY_MD5 | INTEGRITY_STATUS | INTEGRITY_REALPATH | INTEGRITY_DIGEST
1883   * @return  array|bool Array of files and status, or FALSE on error
1884   * @since   4.6.0
1885   * @package Debug
1886   * @example
1887   * print_r(
1888   *     check_file_integrity(INTEGRITY_MD5 | INTEGRITY_REALPATH)
1889   * );
1890   */
1891  
1892  function check_file_integrity($flags = INTEGRITY_STATUS)
1893  {
1894      static $files = null, $files_md5 = array(), $checksum_table = array();
1895  
1896      if ($files === null) {
1897          if ($cs = @file(txpath.'/checksums.txt')) {
1898              $files = array();
1899  
1900              foreach ($cs as $c) {
1901                  if (preg_match('@^(\S+):(?: r?(\S+) | )\(?(.{32})\)?$@', trim($c), $m)) {
1902                      list(, $relative, $r, $md5) = $m;
1903                      $file = realpath(txpath.$relative);
1904                      $checksum_table[$relative] = $md5;
1905  
1906                      if ($file === false) {
1907                          $files[$relative] = INTEGRITY_MISSING;
1908                          $files_md5[$relative] = false;
1909                          continue;
1910                      }
1911  
1912                      if (!is_readable($file)) {
1913                          $files[$relative] = INTEGRITY_NOT_READABLE;
1914                          $files_md5[$relative] = false;
1915                          continue;
1916                      }
1917  
1918                      if (!is_file($file)) {
1919                          $files[$relative] = INTEGRITY_NOT_FILE;
1920                          $files_md5[$relative] = false;
1921                          continue;
1922                      }
1923  
1924                      $files_md5[$relative] = md5_file($file);
1925  
1926                      if ($files_md5[$relative] !== $md5) {
1927                          $files[$relative] = INTEGRITY_MODIFIED;
1928                      } else {
1929                          $files[$relative] = INTEGRITY_GOOD;
1930                      }
1931                  }
1932              }
1933  
1934              if (!get_pref('enable_xmlrpc_server', true)) {
1935                  unset(
1936                      $files_md5['/../rpc/index.php'],
1937                      $files_md5['/../rpc/TXP_RPCServer.php'],
1938                      $files['/../rpc/index.php'],
1939                      $files['/../rpc/TXP_RPCServer.php']
1940                  );
1941              }
1942          } else {
1943              $files_md5 = $files = false;
1944          }
1945      }
1946  
1947      if ($flags & INTEGRITY_DIGEST) {
1948          return $files_md5 ? md5(implode(n, $files_md5)) : false;
1949      }
1950  
1951      if ($flags & INTEGRITY_TABLE) {
1952          return $checksum_table ? $checksum_table : false;
1953      }
1954  
1955      $return = $files;
1956  
1957      if ($flags & INTEGRITY_MD5) {
1958          $return = $files_md5;
1959      }
1960  
1961      if ($return && $flags & INTEGRITY_REALPATH) {
1962          $relative = array();
1963  
1964          foreach ($return as $path => $status) {
1965              $realpath = realpath(txpath.$path);
1966              $relative[!$realpath ? $path : $realpath] = $status;
1967          }
1968  
1969          return $relative;
1970      }
1971  
1972      return $return;
1973  }
1974  
1975  /**
1976   * Assert system requirements.
1977   *
1978   * @access private
1979   */
1980  
1981  function assert_system_requirements()
1982  {
1983      if (version_compare(REQUIRED_PHP_VERSION, PHP_VERSION) > 0) {
1984          txp_die('This server runs PHP version '.PHP_VERSION.'. Textpattern needs PHP version '.REQUIRED_PHP_VERSION.' or better.');
1985      }
1986  
1987      if (!extension_loaded('simplexml')) {
1988          txp_die('This server does not have the required SimpleXML library installed (php-xml). Please install it.');
1989      }
1990  }
1991  
1992  /**
1993   * Get Theme prefs
1994   * Now Textpattern does not support themes. If the setup folder is deleted, it will return an empty array.
1995   */
1996  
1997  function get_prefs_theme()
1998  {
1999      $out = json_decode(txp_get_contents(txpath.'/setup/data/theme.prefs'), true);
2000      if (empty($out)) {
2001          return array();
2002      }
2003  
2004      return $out;
2005  }
2006  
2007  
2008  /**
2009   * Renders an array of available ways to display the date.
2010   * @return array
2011   */
2012  
2013  function txp_dateformats()
2014  {
2015      $dayname = '%A';
2016      $dayshort = '%a';
2017      $daynum = is_numeric(@strftime('%e')) ? '%e' : '%d';
2018      $daynumlead = '%d';
2019      $daynumord = is_numeric(substr(trim(@strftime('%Oe')), 0, 1)) ? '%Oe' : $daynum;
2020      $monthname = '%B';
2021      $monthshort = '%b';
2022      $monthnum = '%m';
2023      $year = '%Y';
2024      $yearshort = '%y';
2025      $time24 = '%H:%M';
2026      $time12 = @strftime('%p') ? '%I:%M %p' : $time24;
2027      $date = @strftime('%x') ? '%x' : '%Y-%m-%d';
2028  
2029      return array(
2030          "$monthshort $daynumord, $time12",
2031          "$daynum.$monthnum.$yearshort",
2032          "$daynumord $monthname, $time12",
2033          "$yearshort.$monthnum.$daynumlead, $time12",
2034          "$dayshort $monthshort $daynumord, $time12",
2035          "$dayname $monthname $daynumord, $year",
2036          "$monthshort $daynumord",
2037          "$daynumord $monthname $yearshort",
2038          "$daynumord $monthnum $year - $time24",
2039          "$daynumord $monthname $year",
2040          "$daynumord $monthname $year, $time24",
2041          "$daynumord. $monthname $year",
2042          "$daynumord. $monthname $year, $time24",
2043          "$year-$monthnum-$daynumlead",
2044          "$year-$daynumlead-$monthnum",
2045          "$date $time12",
2046          "$date",
2047          "$time24",
2048          "$time12",
2049          "$year-$monthnum-$daynumlead $time24",
2050          "since"
2051      );
2052  }

title

Description

title

Description

title

Description

title

title

Body