b2evolution PHP Cross Reference Blogging Systems

Source: /inc/_core/_misc.funcs.php - 6024 lines - 163127 bytes - Summary - Text - Print

Description: This file implements general purpose functions. This file is part of the evoCore framework - {@link http://evocore.net/} See also {@link http://sourceforge.net/projects/evocms/}.

   1  <?php
   2  /**
   3   * This file implements general purpose functions.
   4   *
   5   * This file is part of the evoCore framework - {@link http://evocore.net/}
   6   * See also {@link http://sourceforge.net/projects/evocms/}.
   7   *
   8   * @copyright (c)2003-2014 by Francois Planque - {@link http://fplanque.com/}
   9   * Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
  10   * Parts of this file are copyright (c)2005-2006 by PROGIDISTRI - {@link http://progidistri.com/}.
  11   *
  12   * {@internal License choice
  13   * - If you have received this file as part of a package, please find the license.txt file in
  14   *   the same folder or the closest folder above for complete license terms.
  15   * - If you have received this file individually (e-g: from http://evocms.cvs.sourceforge.net/)
  16   *   then you must choose one of the following licenses before using the file:
  17   *   - GNU General Public License 2 (GPL) - http://www.opensource.org/licenses/gpl-license.php
  18   *   - Mozilla Public License 1.1 (MPL) - http://www.opensource.org/licenses/mozilla1.1.php
  19   * }}
  20   *
  21   * {@internal Open Source relicensing agreement:
  22   * Daniel HAHLER grants Francois PLANQUE the right to license
  23   * Daniel HAHLER's contributions to this file and the b2evolution project
  24   * under any OSI approved OSS license (http://www.opensource.org/licenses/).
  25   *
  26   * PROGIDISTRI S.A.S. grants Francois PLANQUE the right to license
  27   * PROGIDISTRI S.A.S.'s contributions to this file and the b2evolution project
  28   * under any OSI approved OSS license (http://www.opensource.org/licenses/).
  29   * }}
  30   *
  31   * @package evocore
  32   *
  33   * @version $Id: _misc.funcs.php 6136 2014-03-08 07:59:48Z manuel $
  34   */
  35  if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
  36  
  37  
  38  /**
  39   * Dependencies
  40   */
  41  load_funcs('antispam/model/_antispam.funcs.php');
  42  load_funcs('tools/model/_email.funcs.php');
  43  
  44  // @todo sam2kb> Move core functions get_admin_skins, get_filenames, cleardir_r, rmdir_r and some other
  45  // to a separate file, and split files_Module from _core_Module
  46  load_funcs('files/model/_file.funcs.php');
  47  
  48  
  49  /**
  50   * Call a method for all modules in a row
  51   *
  52   * @param string the name of the method which should be called
  53   * @param array params
  54   * @return array[module_name][return value], or NULL if the method doesn't have any return value
  55   */
  56  function modules_call_method( $method_name, $params = NULL )
  57  {
  58      global $modules;
  59  
  60      $result = NULL;
  61  
  62      foreach( $modules as $module )
  63      {
  64          $Module = & $GLOBALS[$module.'_Module'];
  65          if( $params == NULL )
  66          {
  67              $ret = $Module->{$method_name}();
  68          }
  69          else
  70          {
  71              $ret = $Module->{$method_name}( $params );
  72          }
  73          if( isset( $ret ) )
  74          {
  75              $result[$module] = $ret;
  76          }
  77      }
  78  
  79      return $result;
  80  }
  81  
  82  
  83  /**
  84   * @deprecated kept only for plugin backward compatibility (core is being modified to call getters directly)
  85   * To be removed, maybe in b2evo v5.
  86   *
  87   * @return DataObjectCache
  88   */
  89  function & get_Cache( $objectName )
  90  {
  91      global $Plugins;
  92      global $$objectName;
  93  
  94      if( isset( $$objectName ) )
  95      {    // Cache already exists:
  96          return $$objectName;
  97      }
  98  
  99      $func_name = 'get_'.$objectName;
 100  
 101      if( function_exists($func_name) )
 102      {
 103          return $func_name();
 104      }
 105      else
 106      {
 107          debug_die( 'getCache(): Unknown Cache type get function:'.$func_name.'()' );
 108      }
 109  }
 110  
 111  
 112  /**
 113   * Load functions file
 114   */
 115  function load_funcs( $funcs_path )
 116  {
 117      global $inc_path;
 118      require_once $inc_path.$funcs_path;
 119  }
 120  
 121  
 122  /**
 123   * Shutdown function: save HIT and update session!
 124   *
 125   * This is registered in _main.inc.php with register_shutdown_function()
 126   * This is called by PHP at the end of the script.
 127   *
 128   * NOTE: before PHP 4.1 nothing can be echoed here any more, but the minimum PHP requirement for b2evo is PHP 4.3
 129   */
 130  function shutdown()
 131  {
 132      /**
 133       * @var Hit
 134       */
 135      global $Hit;
 136  
 137      /**
 138       * @var Session
 139       */
 140      global $Session;
 141  
 142      global $Settings;
 143      global $Debuglog;
 144  
 145      global $Timer;
 146      global $shutdown_count_item_views;
 147  
 148      // Try forking a background process and let the parent return as fast as possbile.
 149      if( is_callable('pcntl_fork') && function_exists('posix_kill') && defined('STDIN') )
 150      {
 151          if( $pid = pcntl_fork() )
 152              return; // Parent
 153  
 154  		function shutdown_kill()
 155          {
 156              posix_kill(posix_getpid(), SIGHUP);
 157          }
 158  
 159          if ( ob_get_level() )
 160          {    // Discard the output buffer and close
 161              ob_end_clean();
 162          }
 163  
 164          fclose(STDIN);  // Close all of the standard
 165          fclose(STDOUT); // file descriptors as we
 166          fclose(STDERR); // are running as a daemon.
 167  
 168          register_shutdown_function('shutdown_kill');
 169  
 170          if( posix_setsid() < 0 )
 171              return;
 172  
 173          if( $pid = pcntl_fork() )
 174              return;     // Parent
 175  
 176          // Now running as a daemon. This process will even survive
 177          // an apachectl stop.
 178      }
 179  
 180      $Timer->resume('shutdown');
 181  
 182      // echo '*** SHUTDOWN FUNC KICKING IN ***';
 183  
 184      // fp> do we need special processing if we are in CLI mode?  probably earlier actually
 185      // if( ! $is_cli )
 186  
 187      // Note: it might be useful at some point to do special processing if the script has been aborted or has timed out
 188      // connection_aborted()
 189      // connection_status()
 190  
 191      if( ! empty($shutdown_count_item_views) )
 192      { // Increment view counts for Items:
 193          $ItemCache = & get_ItemCache();
 194          foreach( $shutdown_count_item_views as $item_ID )
 195          {
 196              // asimo> Inserted the $item_ID != 0 check, because another way comes unexpected error on preview page
 197              if( !empty($item_ID) && ( $Item = $ItemCache->get_by_ID($item_ID, false) ) ) {
 198                  $Item->count_view( array(
 199                      'allow_multiple_counts_per_page' => false,
 200                  ) );
 201              }
 202          }
 203      }
 204  
 205      // Save the current HIT:
 206      $Hit->log();
 207  
 208      // Update the SESSION:
 209      $Session->dbsave();
 210  
 211      // Get updates here instead of slowing down normal display of the dashboard
 212      load_funcs( 'dashboard/model/_dashboard.funcs.php' );
 213      b2evonet_get_updates();
 214  
 215      // Auto pruning of old HITS, old SESSIONS and potentially MORE analytics data:
 216      if( $Settings->get( 'auto_prune_stats_mode' ) == 'page' )
 217      { // Autopruning is requested
 218          load_class('sessions/model/_hitlist.class.php', 'Hitlist' );
 219          Hitlist::dbprune(); // will prune once per day, according to Settings
 220      }
 221  
 222      // Calling debug_info() here will produce complete data but it will be after </html> hence invalid.
 223      // Then again, it's for debug only, so it shouldn't matter that much.
 224      debug_info();
 225  
 226      // Update the SESSION again, at the very end:
 227      // (e.g. "Debuglogs" may have been removed in debug_info())
 228      $Session->dbsave();
 229  
 230      $Timer->pause('shutdown');
 231  }
 232  
 233  
 234  /***** Formatting functions *****/
 235  
 236  /**
 237   * Format a string/content for being output
 238   *
 239   * @author fplanque
 240   * @todo htmlspecialchars() takes a charset argument, which we could provide ($evo_charset?)
 241   * @param string raw text
 242   * @param string format, can be one of the following
 243   * - raw: do nothing
 244   * - htmlbody: display in HTML page body: allow full HTML
 245   * - entityencoded: Special mode for RSS 0.92: allow full HTML but escape it
 246   * - htmlhead: strips out HTML (mainly for use in Title)
 247   * - htmlattr: use as an attribute: escapes quotes, strip tags
 248   * - formvalue: use as a form value: escapes quotes and < > but leaves code alone
 249   * - text: use as plain-text, e.g. for ascii-mails
 250   * - xml: use in an XML file: strip HTML tags
 251   * - xmlattr: use as an attribute: strips tags and escapes quotes
 252   * @return string formatted text
 253   */
 254  function format_to_output( $content, $format = 'htmlbody' )
 255  {
 256      global $Plugins;
 257  
 258      switch( $format )
 259      {
 260          case 'raw':
 261              // do nothing!
 262              break;
 263  
 264          case 'htmlbody':
 265              // display in HTML page body: allow full HTML
 266              $content = convert_chars($content, 'html');
 267              break;
 268  
 269          case 'urlencoded':
 270              // Encode string to be passed as part of an URL
 271              $content = rawurlencode( $content );
 272              break;
 273  
 274          case 'entityencoded':
 275              // Special mode for RSS 0.92: apply renders and allow full HTML but escape it
 276              $content = convert_chars($content, 'html');
 277              $content = htmlspecialchars( $content, ENT_QUOTES );
 278              break;
 279  
 280          case 'htmlfeed':
 281              // For use in RSS <content:encoded>, allow full HTML + absolute URLs
 282              $content = make_rel_links_abs($content);
 283              $content = convert_chars($content, 'html');
 284              $content = str_replace(']]>', ']]&gt;', $content); // encode CDATA closing tag
 285              break;
 286  
 287          case 'htmlhead':
 288              // Strips out HTML (mainly for use in Title)
 289              $content = strip_tags($content);
 290              $content = convert_chars($content, 'html');
 291              break;
 292  
 293          case 'htmlattr':
 294              // use as an attribute: strips tags and escapes quotes
 295              // TODO: dh> why not just htmlspecialchars?fp> because an attribute can never contain a tag? dh> well, "onclick='return 1<2;'" would get stripped, too. I'm just saying: why mess with it, when we can just use htmlspecialchars.. fp>ok
 296              $content = strip_tags($content);
 297              $content = convert_chars($content, 'html');
 298              $content = str_replace( array('"', "'"), array('&quot;', '&#039;'), $content );
 299              break;
 300  
 301          case 'htmlspecialchars':
 302          case 'formvalue':
 303              // use as a form value: escapes &, quotes and < > but leaves code alone
 304              $content = htmlspecialchars( $content, ENT_QUOTES );  // Handles &, ", ', < and >
 305              break;
 306  
 307          case 'xml':
 308              // use in an XML file: strip HTML tags
 309              $content = strip_tags($content);
 310              $content = convert_chars($content, 'xml');
 311              break;
 312  
 313          case 'xmlattr':
 314              // use as an attribute: strips tags and escapes quotes
 315              $content = strip_tags($content);
 316              $content = convert_chars($content, 'xml');
 317              $content = str_replace( array('"', "'"), array('&quot;', '&#039;'), $content );
 318              break;
 319  
 320          case 'text':
 321              // use as plain-text, e.g. for ascii-mails
 322              $content = strip_tags( $content );
 323              $trans_tbl = get_html_translation_table( HTML_ENTITIES );
 324              $trans_tbl = array_flip( $trans_tbl );
 325              $content = strtr( $content, $trans_tbl );
 326              $content = preg_replace( '/[ \t]+/', ' ', $content);
 327              $content = trim($content);
 328              break;
 329  
 330          default:
 331              debug_die( 'Output format ['.$format.'] not supported.' );
 332      }
 333  
 334      return $content;
 335  }
 336  
 337  
 338  /*
 339   * autobrize(-)
 340   */
 341  function autobrize($content) {
 342      $content = callback_on_non_matching_blocks( $content, '~<code>.+?</code>~is', 'autobrize_callback' );
 343      return $content;
 344  }
 345  
 346  /**
 347   * Adds <br>'s to non code blocks
 348   *
 349   * @param string $content
 350   * @return string content with <br>'s added
 351   */
 352  function autobrize_callback( $content )
 353  {
 354      $content = preg_replace("/<br>\n/", "\n", $content);
 355      $content = preg_replace("/<br \/>\n/", "\n", $content);
 356      $content = preg_replace("/(\015\012)|(\015)|(\012)/", "<br />\n", $content);
 357      return($content);
 358  }
 359  
 360  /*
 361   * unautobrize(-)
 362   */
 363  function unautobrize($content)
 364  {
 365      $content = callback_on_non_matching_blocks( $content, '~<code>.+?</code>~is', 'unautobrize_callback' );
 366      return $content;
 367  }
 368  
 369  /**
 370   * Removes <br>'s from non code blocks
 371   *
 372   * @param string $content
 373   * @return string content with <br>'s removed
 374   */
 375  function unautobrize_callback( $content )
 376  {
 377      $content = preg_replace("/<br>\n/", "\n", $content);   //for PHP versions before 4.0.5
 378      $content = preg_replace("/<br \/>\n/", "\n", $content);
 379      return($content);
 380  }
 381  
 382  /**
 383   * Add leading zeroes to a number when necessary.
 384   *
 385   * @param string The original number.
 386   * @param integer How many digits shall the number have?
 387   * @return string The padded number.
 388   */
 389  function zeroise( $number, $threshold )
 390  {
 391      return str_pad( $number, $threshold, '0', STR_PAD_LEFT );
 392  }
 393  
 394  
 395  /**
 396   * Get a limited text-only excerpt
 397   *
 398   * @param string
 399   * @param int Maximum length
 400   * @return string
 401   */
 402  function excerpt( $str, $maxlen = 200 )
 403  {
 404      // Add spaces
 405      $str = str_replace( array( '<p>', '<br' ), array( ' <p>', ' <br' ), $str );
 406  
 407      // Remove <code>
 408      $str = preg_replace( '#<code>(.+)</code>#i', '', $str );
 409  
 410      // fp> Note: I'm not sure about using 'text' here, but there should definitely be no rendering here.
 411      $str = format_to_output( $str, 'text' );
 412  
 413      // Ger rid of all new lines and Display the html tags as source text:
 414      $str = trim( str_replace( array( "\r", "\n", "\t" ), ' ', $str ) );
 415  
 416      $str = strmaxlen( $str, $maxlen, NULL, 'raw', true );
 417  
 418      return $str;
 419  }
 420  
 421  
 422  /**
 423   * Crop string to maxlen with &hellip; (default tail) at the end if needed.
 424   *
 425   * If $format is not "raw", we make sure to not cut in the middle of an
 426   * HTML entity, so that strmaxlen('1&amp;2', 3, NULL, 'formvalue') will not
 427   * become/stay '1&amp;&hellip;'.
 428   *
 429   * @param string
 430   * @param int Maximum length
 431   * @param string Tail to use, when string gets cropped. Its length gets
 432   *               substracted from the total length (with HTML entities
 433   *               being decoded). Default is "&hellip;" (HTML entity)
 434   * @param string Format, see {@link format_to_output()}
 435   * @param boolean Crop at whitespace, if possible?
 436   *        (any word split at the end will get its head removed)
 437   * @return string
 438   */
 439  function strmaxlen( $str, $maxlen = 50, $tail = NULL, $format = 'raw', $cut_at_whitespace = false  )
 440  {
 441      global $current_charset;
 442  
 443      if( is_null($tail) )
 444      {
 445          $tail = '&hellip;';
 446      }
 447  
 448      $str = rtrim($str);
 449  
 450      if( evo_strlen( $str ) > $maxlen )
 451      {
 452          // Replace all HTML entities by a single char. html_entity_decode for example
 453          // would not handle &hellip;.
 454          $tail_for_length = preg_replace('~&\w+?;~', '.', $tail);
 455          $tail_length = evo_strlen( html_entity_decode($tail_for_length) );
 456          $len = $maxlen-$tail_length;
 457          if( $len < 1 )
 458          { // special case; $tail length is >= $maxlen
 459              $len = 0;
 460          }
 461          $str_cropped = evo_substr( $str, 0, $len );
 462          if( $format != 'raw' )
 463          { // if the format isn't raw we make sure that we do not cut in the middle of an HTML entity
 464              $maxlen_entity = 7; # "&amp;" is 5, min 3!
 465              $str_inspect = evo_substr($str_cropped, 1-$maxlen_entity);
 466              $pos_amp = evo_strpos($str_inspect, '&');
 467              if( $pos_amp !== false )
 468              { // there's an ampersand at the end of the cropped string
 469                  $look_until = $pos_amp;
 470                  $str_cropped_len = evo_strlen($str_cropped);
 471                  if( $str_cropped_len < $maxlen_entity )
 472                  { // we have to look at least for the length of an entity
 473                      $look_until += $maxlen_entity-$str_cropped_len;
 474                  }
 475                  if( strpos(evo_substr($str, $len, $look_until), ';') !== false )
 476                  {
 477                      $str_cropped = evo_substr( $str, 0, $len-evo_strlen($str_inspect)+$pos_amp);
 478                  }
 479              }
 480          }
 481  
 482          if( $cut_at_whitespace )
 483          {
 484              // Get the first character being cut off. Note: we can't use $str[index] in case of utf8 strings!
 485              $first_cut_off_char = evo_substr( $str, evo_strlen( $str_cropped ), 1 );
 486              if( ! ctype_space( $first_cut_off_char ) )
 487              { // first character being cut off is not whitespace
 488                  // Get the chars as an array in case of utf8 charset from the cropped string to be able to get chars by position ( in case of 'iso-8859-1' charset one char is one byte )
 489                  $str_cropped_chars = ( $current_charset == 'iso-8859-1' ) ? $str_cropped : preg_split( '//u', $str_cropped, -1, PREG_SPLIT_NO_EMPTY );
 490                  $i = evo_strlen( $str_cropped );
 491                  while( $i && ! ctype_space( $str_cropped_chars[--$i] ) )
 492                  {}
 493                  if( $i )
 494                  {
 495                      $str_cropped = evo_substr( $str_cropped, 0, $i );
 496                  }
 497              }
 498          }
 499  
 500          $str = format_to_output( rtrim( $str_cropped ), $format );
 501          $str .= $tail;
 502  
 503          return $str;
 504      }
 505      else
 506      {
 507          return format_to_output( $str, $format );
 508      }
 509  }
 510  
 511  
 512  /**
 513   * Crop string to maxwords preserving tags.
 514   *
 515   * @param string
 516   * @param int Maximum number words
 517   * @param mixed array Optional parameters
 518   * @return string
 519   */
 520  function strmaxwords( $str, $maxwords = 50, $params = array() )
 521  {
 522      $params = array_merge( array(
 523              'continued_link' => '',
 524              'continued_text' => '&hellip;',
 525              'always_continue' => false,
 526          ), $params );
 527      $open = false;
 528      $have_seen_non_whitespace = false;
 529      $end = evo_strlen( $str );
 530      for( $i = 0; $i < $end; $i++ )
 531      {
 532          switch( $char = $str[$i] )
 533          {
 534              case '<' :    // start of a tag
 535                  $open = true;
 536                  break;
 537              case '>' : // end of a tag
 538                  $open = false;
 539                  break;
 540  
 541              case ctype_space($char):
 542                  if( ! $open )
 543                  { // it's a word gap
 544                      // Eat any other whitespace.
 545                      while( isset($str[$i+1]) && ctype_space($str[$i+1]) )
 546                      {
 547                          $i++;
 548                      }
 549                      if( isset($str[$i+1]) && $have_seen_non_whitespace )
 550                      { // only decrement words, if there's a non-space char left.
 551                          --$maxwords;
 552                      }
 553                  }
 554                  break;
 555  
 556              default:
 557                  $have_seen_non_whitespace = true;
 558                  break;
 559          }
 560          if( $maxwords < 1 ) break;
 561      }
 562  
 563      // restrict content to required number of words and balance the tags out
 564      $str = balance_tags( evo_substr( $str, 0, $i ) );
 565  
 566      if( $params['always_continue'] || $maxwords == false )
 567      { // we want a continued text
 568          if( $params['continued_link'] )
 569          { // we have a url
 570              $str .= ' <a href="'.$params['continued_link'].'">'.$params['continued_text'].'</a>';
 571          }
 572          else
 573          { // we don't have a url
 574              $str .= ' '.$params['continued_text'];
 575          }
 576      }
 577      // remove empty tags
 578      $str = preg_replace( '~<([\s]+?)[^>]*?></\1>~is', '', $str );
 579  
 580      return $str;
 581  }
 582  
 583  
 584  /**
 585   * Convert all non ASCII chars (except if UTF-8, GB2312 or CP1251) to &#nnnn; unicode references.
 586   * Also convert entities to &#nnnn; unicode references if output is not HTML (eg XML)
 587   *
 588   * Preserves < > and quotes.
 589   *
 590   * fplanque: simplified
 591   * sakichan: pregs instead of loop
 592   */
 593  function convert_chars( $content, $flag = 'html' )
 594  {
 595      global $b2_htmltrans, $evo_charset;
 596  
 597      /**
 598       * Translation of invalid Unicode references range to valid range.
 599       * These are Windows CP1252 specific characters.
 600       * They would look weird on non-Windows browsers.
 601       * If you've ever pasted text from MSWord, you'll understand.
 602       *
 603       * You should not have to change this.
 604       */
 605      static $b2_htmltranswinuni = array(
 606          '&#128;' => '&#8364;', // the Euro sign
 607          '&#130;' => '&#8218;',
 608          '&#131;' => '&#402;',
 609          '&#132;' => '&#8222;',
 610          '&#133;' => '&#8230;',
 611          '&#134;' => '&#8224;',
 612          '&#135;' => '&#8225;',
 613          '&#136;' => '&#710;',
 614          '&#137;' => '&#8240;',
 615          '&#138;' => '&#352;',
 616          '&#139;' => '&#8249;',
 617          '&#140;' => '&#338;',
 618          '&#142;' => '&#382;',
 619          '&#145;' => '&#8216;',
 620          '&#146;' => '&#8217;',
 621          '&#147;' => '&#8220;',
 622          '&#148;' => '&#8221;',
 623          '&#149;' => '&#8226;',
 624          '&#150;' => '&#8211;',
 625          '&#151;' => '&#8212;',
 626          '&#152;' => '&#732;',
 627          '&#153;' => '&#8482;',
 628          '&#154;' => '&#353;',
 629          '&#155;' => '&#8250;',
 630          '&#156;' => '&#339;',
 631          '&#158;' => '&#382;',
 632          '&#159;' => '&#376;'
 633      );
 634  
 635      // Convert highbyte non ASCII/UTF-8 chars to urefs:
 636      if( ! in_array(strtolower($evo_charset), array( 'utf8', 'utf-8', 'gb2312', 'windows-1251') ) )
 637      { // This is a single byte charset
 638          // fp> why do we actually bother doing this:?
 639          $content = preg_replace_callback(
 640              '/[\x80-\xff]/',
 641              create_function( '$j', 'return "&#".ord($j[0]).";";' ),
 642              $content);
 643      }
 644  
 645      // Convert Windows CP1252 => Unicode (valid HTML)
 646      // TODO: should this go to input conversions instead (?)
 647      $content = strtr( $content, $b2_htmltranswinuni );
 648  
 649      if( $flag == 'html' )
 650      { // we can use entities
 651          // Convert & chars that are not used in an entity
 652          $content = preg_replace('/&(?![#A-Za-z0-9]{2,20};)/', '&amp;', $content);
 653      }
 654      else
 655      { // unicode, xml...
 656          // Convert & chars that are not used in an entity
 657          $content = preg_replace('/&(?![#A-Za-z0-9]{2,20};)/', '&#38;', $content);
 658  
 659          // Convert HTML entities to urefs:
 660          $content = strtr($content, $b2_htmltrans);
 661      }
 662  
 663      return( $content );
 664  }
 665  
 666  
 667  /**
 668   * Get number of bytes in $string. This works around mbstring.func_overload, if
 669   * activated for strlen/mb_strlen.
 670   * @param string
 671   * @return int
 672   */
 673  function evo_bytes( $string )
 674  {
 675      $fo = ini_get('mbstring.func_overload');
 676      if( $fo && $fo & 2 && function_exists('mb_strlen') )
 677      { // overloading of strlen is enabled
 678          return mb_strlen( $string, 'ASCII' );
 679      }
 680      return strlen($string);
 681  }
 682  
 683  
 684  /**
 685   * mbstring wrapper for strtolower function
 686   *
 687   * fp> TODO: instead of those "when used" ifs, it would make more sense to redefine
 688   * mb_strtolower beforehand if it doesn"t exist (it would then just be a fallback
 689   * to the strtolower + a Debuglog->add() )
 690   *
 691   * @param string
 692   * @return string
 693   */
 694  function evo_strtolower( $string )
 695  {
 696      global $current_charset;
 697  
 698      if( $current_charset != 'iso-8859-1' && $current_charset != '' && function_exists('mb_strtolower') )
 699      {
 700          return mb_strtolower( $string, $current_charset );
 701      }
 702  
 703      return strtolower($string);
 704  }
 705  
 706  
 707  /**
 708   * mbstring wrapper for strlen function
 709   *
 710   * @param string
 711   * @return string
 712   */
 713  function evo_strlen( $string )
 714  {
 715      global $current_charset;
 716  
 717      if( $current_charset != 'iso-8859-1' && $current_charset != '' && function_exists('mb_strlen') )
 718      {
 719          return mb_strlen( $string, $current_charset );
 720      }
 721  
 722      return strlen($string);
 723  }
 724  
 725  /**
 726   * mbstring wrapper for strpos function
 727   *
 728   * @param string
 729   * @param string
 730   * @return int
 731   */
 732  function evo_strpos( $string , $needle , $offset = null )
 733  {
 734      global $current_charset;
 735  
 736      if( $current_charset != 'iso-8859-1' && $current_charset != '' && function_exists('mb_strpos') )
 737      {
 738          return mb_strpos( $string, $needle, $offset ,$current_charset );
 739      }
 740  
 741      return strpos( $string , $needle , $offset );
 742  }
 743  
 744  
 745  /**
 746   * mbstring wrapper for substr function
 747   *
 748   * @param string
 749   * @param int start position
 750   * @param int string length
 751   * @return string
 752   */
 753  function evo_substr( $string, $start = 0, $length = '#' )
 754  {
 755      global $current_charset;
 756  
 757      if( ! $length )
 758      { // make mb_substr and substr behave consistently (mb_substr returns string for length=0)
 759          return '';
 760      }
 761      if( $length == '#' )
 762      {
 763          $length = evo_strlen($string);
 764      }
 765  
 766      if( $current_charset != 'iso-8859-1' && $current_charset != '' && function_exists('mb_substr') )
 767      {
 768          return mb_substr( $string, $start, $length, $current_charset );
 769      }
 770  
 771      return substr( $string, $start, $length );
 772  }
 773  
 774  
 775  /**
 776   * Split $text into blocks by using $pattern and call $callback on the non-matching blocks.
 777   *
 778   * The non-matching block's text is the first param to $callback and additionally $params gets passed.
 779   *
 780   * This gets used to make links clickable or replace smilies.
 781   *
 782   * E.g., to replace only in non-HTML tags, call it like:
 783   * <code>callback_on_non_matching_blocks( $text, '~<[^>]*>~s', 'your_callback' );</code>
 784   *
 785   * {@internal This function gets tested in misc.funcs.simpletest.php.}}
 786   *
 787   * @param string Text to handle
 788   * @param string Regular expression pattern that defines blocks to exclude.
 789   * @param callback Function name or object/method array to use as callback.
 790   *               Each non-matching block gets passed as first param, additional params may be
 791   *               passed with $params.
 792   * @param array Of additional ("static") params to $callback.
 793   * @return string
 794   */
 795  function callback_on_non_matching_blocks( $text, $pattern, $callback, $params = array() )
 796  {
 797      if( preg_match_all( $pattern, $text, $matches, PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER ) )
 798      {    // $pattern matches, call the callback method on full text except of matching blocks
 799  
 800          // Create an unique string in order to replace all matching blocks temporarily
 801          $unique_replacement = md5( mktime() + rand() );
 802  
 803          $matches_search = array();
 804          $matches_replace = array();
 805          foreach( $matches[0] as $l => $l_matching )
 806          {    // Build arrays with a source code of the matching blocks and with temporary replacement
 807              $matches_source[] = $l_matching[0];
 808              $matches_temp[] = '?'.$l.$unique_replacement.$l.'?';
 809          }
 810  
 811          // Replace all matching blocks with temporary text like '?X219a33da9c1b8f4e335bffc015df8c96X?'
 812          // where X is index of match block in array $matches[0]
 813          // It is used to avoid any changes in the matching blocks
 814          $text = str_ireplace( $matches_source, $matches_temp, $text );
 815  
 816          // Callback:
 817          $callback_params = $params;
 818          array_unshift( $callback_params, $text );
 819          $text = call_user_func_array( $callback, $callback_params );
 820  
 821          // Revert a source code of the matching blocks in content
 822          $text = str_ireplace( $matches_temp, $matches_source, $text );
 823  
 824          return $text;
 825      }
 826  
 827      $callback_params = $params;
 828      array_unshift( $callback_params, $text );
 829      return call_user_func_array( $callback, $callback_params );
 830  }
 831  
 832  
 833  /**
 834   * Replace content outside blocks <code></code> & <pre></pre>
 835   *
 836   * @param array|string Search list
 837   * @param array|string Replace list
 838   * @param string Source content
 839   * @return string Replaced content
 840   */
 841  function replace_content_outcode( $search, $replace, $content, $replace_function_callback = 'replace_content' )
 842  {
 843      if( !empty( $search ) && !empty( $replace ) )
 844      {
 845          if( stristr( $content, '<code' ) !== false || stristr( $content, '<pre' ) !== false )
 846          {    // Call replace_content() on everything outside code/pre:
 847              $content = callback_on_non_matching_blocks( $content,
 848                  '~<(code|pre)[^>]*>.*?</\1>~is',
 849                  $replace_function_callback, array( $search, $replace ) );
 850          }
 851          else
 852          {    // No code/pre blocks, replace on the whole thing
 853              $content = call_user_func( $replace_function_callback, $content, $search, $replace );
 854          }
 855      }
 856  
 857      return $content;
 858  }
 859  
 860  
 861  /**
 862   * Replace content, Used for function callback_on_non_matching_blocks(), because there is different order of params
 863   *
 864   * @param string Source content
 865   * @param array|string Search list
 866   * @param array|string Replace list
 867   * @return string Replaced content
 868   */
 869  function replace_content( $content, $search, $replace )
 870  {
 871      return preg_replace( $search, $replace, $content );
 872  }
 873  
 874  
 875  /**
 876   * Replace content by callback, Used for function callback_on_non_matching_blocks(), because there is different order of params
 877   *
 878   * @param string Source content
 879   * @param array|string Search list
 880   * @param array|string Replace callback
 881   * @return string Replaced content
 882   */
 883  function replace_content_callback( $content, $search, $replace_callback )
 884  {
 885      return preg_replace_callback( $search, $replace_callback, $content );
 886  }
 887  
 888  
 889  /**
 890   * Make links clickable in a given text.
 891   *
 892   * It replaces only text which is not between <a> tags already.
 893   *
 894   * @todo dh> this should not replace links in tags! currently fails for something
 895   *           like '<img src=" http://example.com/" />' (not usual though!)
 896   * fp> I am trying to address this by not replacing anything inside tags
 897   * fp> This should be replaced by a clean state machine (one single variable for current state)
 898   *
 899   * {@internal This function gets tested in misc.funcs.simpletest.php.}}
 900   *
 901   * @param string Text
 902   * @param string Url delimeter
 903   * @param string Callback function name
 904   * @param string Additional attributes for tag <a>
 905   * @return string
 906   */
 907  function make_clickable( $text, $moredelim = '&amp;', $callback = 'make_clickable_callback', $additional_attrs = '' )
 908  {
 909      $r = '';
 910      $inside_tag = false;
 911      $in_a_tag = false;
 912      $in_code_tag = false;
 913      $in_tag_quote = false;
 914      $from_pos = 0;
 915      $i = 0;
 916      $n = strlen($text);
 917  
 918      // Not using callback_on_non_matching_blocks(), because it requires
 919      // wellformed HTML and the implementation below should be
 920      // faster and less memory intensive (tested for some example content)
 921      while( $i < $n )
 922      {    // Go through each char in string... (we will fast forward from tag to tag)
 923          if( $inside_tag )
 924          {    // State: We're currently inside some tag:
 925              switch( $text[$i] )
 926              {
 927                  case '>':
 928                      if( $in_tag_quote )
 929                      { // This is in a quoted string so it doesn't really matter...
 930                          break;
 931                      }
 932                      // end of tag:
 933                      $inside_tag = false;
 934                      $r .= substr($text, $from_pos, $i-$from_pos+1);
 935                      $from_pos = $i+1;
 936                      // $r .= '}';
 937                      break;
 938  
 939                  case '"':
 940                  case '\'':
 941                      // This is the beginning or the end of a quoted string:
 942                      if( ! $in_tag_quote )
 943                      {
 944                          $in_tag_quote = $text[$i];
 945                      }
 946                      elseif( $in_tag_quote == $text[$i] )
 947                      {
 948                          $in_tag_quote = false;
 949                      }
 950                      break;
 951              }
 952          }
 953          elseif( $in_a_tag )
 954          {    // In a link but no longer inside <a>...</a> tag or any other embedded tag like <strong> or whatever
 955              switch( $text[$i] )
 956              {
 957                  case '<':
 958                      if( strtolower(substr($text, $i+1, 3)) == '/a>' )
 959                      {    // Ok, this is the end tag of the link:
 960                          // $r .= substr($text, $from_pos, $i-$from_pos+4);
 961                          // $from_pos = $i+4;
 962                          $i += 4;
 963                          // pre_dump( 'END A TAG: '.substr($text, $from_pos, $i-$from_pos) );
 964                          $r .= substr($text, $from_pos, $i-$from_pos);
 965                          $from_pos = $i;
 966                          $in_a_tag = false;
 967                          $in_tag_quote = false;
 968                      }
 969                      break;
 970              }
 971          }
 972          elseif( $in_code_tag )
 973          {    // In a code but no longer inside <code>...</code> tag or any other embedded tag like <strong> or whatever
 974              switch( $text[$i] )
 975              {
 976                  case '<':
 977                      if( strtolower(substr($text, $i+1, 5)) == '/code' )
 978                      {    // Ok, this is the end tag of the code:
 979                          // $r .= substr($text, $from_pos, $i-$from_pos+4);
 980                          // $from_pos = $i+4;
 981                          $i += 7;
 982                          // pre_dump( 'END A TAG: '.substr($text, $from_pos, $i-$from_pos) );
 983                          $r .= substr($text, $from_pos, $i-$from_pos);
 984                          $from_pos = $i;
 985                          $in_code_tag = false;
 986                          $in_tag_quote = false;
 987                      }
 988                      break;
 989              }
 990          }
 991          else
 992          { // State: we're not currently in any tag:
 993              // Find next tag opening:
 994              $i = strpos($text, '<', $i);
 995              if( $i === false )
 996              { // No more opening tags:
 997                  break;
 998              }
 999  
1000              $inside_tag = true;
1001              $in_tag_quote = false;
1002              // s$r .= '{'.$text[$i+1];
1003  
1004              if( ($text[$i+1] == 'a' || $text[$i+1] == 'A') && ctype_space($text[$i+2]) )
1005              { // opening "A" tag
1006                  $in_a_tag = true;
1007              }
1008  
1009              if( ( substr( $text, $i+1, 4 ) == 'code') )
1010              { // opening "code" tag
1011                  $in_code_tag = true;
1012              }
1013  
1014              // Make the text before the opening < clickable:
1015              if( is_array($callback) )
1016              {
1017                  $r .= $callback[0]->$callback[1]( substr($text, $from_pos, $i-$from_pos), $moredelim, $additional_attrs );
1018              }
1019              else
1020              {
1021                  $r .= $callback( substr($text, $from_pos, $i-$from_pos), $moredelim, $additional_attrs );
1022              }
1023              $from_pos = $i;
1024  
1025              // $i += 2;
1026          }
1027  
1028          $i++;
1029      }
1030  
1031      // the remaining part:
1032      if( $in_a_tag )
1033      { // may happen for invalid html:
1034          $r .= substr($text, $from_pos);
1035      }
1036      else
1037      {    // Make remplacements in the remaining part:
1038          if( is_array($callback) )
1039          {
1040              $r .= $callback[0]->$callback[1]( substr($text, $from_pos), $moredelim, $additional_attrs );
1041          }
1042          else
1043          {
1044              $r .= $callback( substr($text, $from_pos), $moredelim, $additional_attrs );
1045          }
1046      }
1047  
1048      return $r;
1049  }
1050  
1051  
1052  /**
1053   * Callback function for {@link make_clickable()}.
1054   *
1055   * original function: phpBB, extended here for AIM & ICQ
1056   * fplanque restricted :// to http:// and mailto://
1057   * Fixed to not include trailing dot and comma.
1058   *
1059   * fp> I'm thinking of moving this into the autolinks plugin (only place where it's used)
1060   *     and break it up into something more systematic.
1061   *
1062   * @param string Text
1063   * @param string Url delimeter
1064   * @param string Additional attributes for tag <a>
1065   * @return string The clickable text.
1066   */
1067  function make_clickable_callback( $text, $moredelim = '&amp;', $additional_attrs = '' )
1068  {
1069      if( !empty( $additional_attrs ) )
1070      {
1071          $additional_attrs = ' '.trim( $additional_attrs );
1072      }
1073      //return $text;
1074      /*preg_match( '/<code>([.\r\n]+?)<\/code>/i', $text, $matches );
1075      pre_dump( $text, $matches );*/
1076  
1077      $pattern_domain = '([\p{L}0-9\-]+\.[\p{L}0-9\-.\~]+)'; // a domain name (not very strict)
1078      $text = preg_replace(
1079          /* Tblue> I removed the double quotes from the first RegExp because
1080                    it made URLs in tag attributes clickable.
1081                    See http://forums.b2evolution.net/viewtopic.php?p=92073 */
1082          array( '#(^|[\s>\(]|\[url=)(https?|mailto)://([^<>{}\s]+[^.,<>{}\s\]\)])#i',
1083              '#(^|[\s>\(]|\[url=)aim:([^,<\s\]\)]+)#i',
1084              '#(^|[\s>\(]|\[url=)icq:(\d+)#i',
1085              '#(^|[\s>\(]|\[url=)www\.'.$pattern_domain.'((?:/[^<\s]*)?[^.,\s\]\)])#i',
1086              '#(^|[\s>\(]|\[url=)([a-z0-9\-_.]+?)@'.$pattern_domain.'([^.,<\s\]\)]+)#i', ),
1087          array( '$1<a href="$2://$3"'.$additional_attrs.'>$2://$3</a>',
1088              '$1<a href="aim:goim?screenname=$2$3'.$moredelim.'message='.rawurlencode(T_('Hello')).'"'.$additional_attrs.'>$2$3</a>',
1089              '$1<a href="http://wwp.icq.com/scripts/search.dll?to=$2"'.$additional_attrs.'>$2</a>',
1090              '$1<a href="http://www.$2$3$4"'.$additional_attrs.'>www.$2$3$4</a>',
1091              '$1<a href="mailto:$2@$3$4"'.$additional_attrs.'>$2@$3$4</a>', ),
1092          $text );
1093  
1094      return $text;
1095  }
1096  
1097  
1098  /***** // Formatting functions *****/
1099  
1100  /**
1101   * Convert timestamp to MySQL/ISO format.
1102   *
1103   * @param integer UNIX timestamp
1104   * @return string Date formatted as "Y-m-d H:i:s"
1105   */
1106  function date2mysql( $ts )
1107  {
1108      return date( 'Y-m-d H:i:s', $ts );
1109  }
1110  
1111  /**
1112   * Convert a MYSQL date to a UNIX timestamp.
1113   *
1114   * @param string Date formatted as "Y-m-d H:i:s"
1115   * @param boolean true to use GM time
1116   * @return integer UNIX timestamp
1117   */
1118  function mysql2timestamp( $m, $useGM = false )
1119  {
1120      $func = $useGM ? 'gmmktime' : 'mktime';
1121      return $func(substr($m,11,2),substr($m,14,2),substr($m,17,2),substr($m,5,2),substr($m,8,2),substr($m,0,4));
1122  }
1123  
1124  /**
1125   * Convert a MYSQL date -- WITHOUT the time -- to a UNIX timestamp
1126   *
1127   * @param string Date formatted as "Y-m-d"
1128   * @param boolean true to use GM time
1129   * @return integer UNIX timestamp
1130   */
1131  function mysql2datestamp( $m, $useGM = false )
1132  {
1133      $func = $useGM ? 'gmmktime' : 'mktime';
1134      return $func( 0, 0, 0, substr($m,5,2), substr($m,8,2), substr($m,0,4) );
1135  }
1136  
1137  /**
1138   * Format a MYSQL date to current locale date format.
1139   *
1140   * @param string MYSQL date YYYY-MM-DD HH:MM:SS
1141   */
1142  function mysql2localedate( $mysqlstring )
1143  {
1144      return mysql2date( locale_datefmt(), $mysqlstring );
1145  }
1146  
1147  function mysql2localetime( $mysqlstring )
1148  {
1149      return mysql2date( locale_timefmt(), $mysqlstring );
1150  }
1151  
1152  function mysql2localedatetime( $mysqlstring )
1153  {
1154      return mysql2date( locale_datefmt().' '.locale_timefmt(), $mysqlstring );
1155  }
1156  
1157  function mysql2localedatetime_spans( $mysqlstring, $datefmt = NULL, $timefmt = NULL )
1158  {
1159      if( is_null( $datefmt ) )
1160      {
1161          $datefmt = locale_datefmt();
1162      }
1163      if( is_null( $timefmt ) )
1164      {
1165          $timefmt = locale_timefmt();
1166      }
1167  
1168      return '<span class="date">'
1169                      .mysql2date( $datefmt, $mysqlstring )
1170                      .'</span> <span class="time">'
1171                      .mysql2date( $timefmt, $mysqlstring )
1172                      .'</span>';
1173  }
1174  
1175  
1176  /**
1177   * Format a MYSQL date.
1178   *
1179   * @param string enhanced format string
1180   * @param string MYSQL date YYYY-MM-DD HH:MM:SS
1181   * @param boolean true to use GM time
1182   */
1183  function mysql2date( $dateformatstring, $mysqlstring, $useGM = false )
1184  {
1185      $m = $mysqlstring;
1186      if( empty($m) || ($m == '0000-00-00 00:00:00' ) )
1187          return false;
1188  
1189      // Get a timestamp:
1190      $unixtimestamp = mysql2timestamp( $m );
1191  
1192      return date_i18n( $dateformatstring, $unixtimestamp, $useGM );
1193  }
1194  
1195  
1196  /**
1197   * Date internationalization: same as date() formatting but with i18n support.
1198   *
1199   * @todo dh> support for MySQL date format instead of $unixtimestamp? This would simplify callees, where currently mktime() is used.
1200   * @param string enhanced format string
1201   * @param integer UNIX timestamp
1202   * @param boolean true to use GM time
1203   */
1204  function date_i18n( $dateformatstring, $unixtimestamp, $useGM = false )
1205  {
1206      global $month, $month_abbrev, $weekday, $weekday_abbrev, $weekday_letter;
1207      global $localtimenow, $time_difference;
1208  
1209      if( $dateformatstring == 'isoZ' )
1210      { // full ISO 8601 format
1211          $dateformatstring = 'Y-m-d\TH:i:s\Z';
1212      }
1213  
1214      if( $useGM )
1215      { // We want a Greenwich Meridian time:
1216          // TODO: dh> what's the point of the substraction? UNIX timestamp should contain no time_difference in the first place?! Otherwise it should be substracted for !$useGM, too.
1217          // TODO: dh> Why does $useGM do not get the special symbols handling?
1218          $r = gmdate($dateformatstring, ($unixtimestamp - $time_difference));
1219      }
1220      else
1221      { // We want default timezone time:
1222  
1223          /*
1224          Special symbols:
1225              'b': wether it's today (1) or not (0)
1226              'l': weekday
1227              'D': weekday abbrev
1228              'e': weekday letter
1229              'F': month
1230              'M': month abbrev
1231          */
1232  
1233          #echo $dateformatstring, '<br />';
1234  
1235          // protect special symbols, that date() would need proper locale set for
1236          $protected_dateformatstring = preg_replace( '/(?<!\\\)([blDeFM])/', '@@@\\\$1@@@', $dateformatstring );
1237  
1238          #echo $protected_dateformatstring, '<br />';
1239  
1240          $r = date( $protected_dateformatstring, $unixtimestamp );
1241  
1242          if( $protected_dateformatstring != $dateformatstring )
1243          { // we had special symbols, replace them
1244  
1245              $istoday = ( date('Ymd',$unixtimestamp) == date('Ymd',$localtimenow) ) ? '1' : '0';
1246              $datemonth = date('m', $unixtimestamp);
1247              $dateweekday = date('w', $unixtimestamp);
1248  
1249              // replace special symbols
1250              $r = str_replace( array(
1251                          '@@@b@@@',
1252                          '@@@l@@@',
1253                          '@@@D@@@',
1254                          '@@@e@@@',
1255                          '@@@F@@@',
1256                          '@@@M@@@',
1257                          ),
1258                      array( $istoday,
1259                          trim(T_($weekday[$dateweekday])),
1260                          trim(T_($weekday_abbrev[$dateweekday])),
1261                          trim(T_($weekday_letter[$dateweekday])),
1262                          trim(T_($month[$datemonth])),
1263                          trim(T_($month_abbrev[$datemonth])) ),
1264                      $r );
1265          }
1266      }
1267  
1268      return $r;
1269  }
1270  
1271  
1272  /**
1273   * Add given # of days to a timestamp
1274   *
1275   * @param integer timestamp
1276   * @param integer days
1277   * @return integer timestamp
1278   */
1279  function date_add_days( $timestamp, $days )
1280  {
1281      return mktime( date('H',$timestamp), date('m',$timestamp), date('s',$timestamp),
1282                                  date('m',$timestamp), date('d',$timestamp)+$days, date('Y',$timestamp)  );
1283  }
1284  
1285  /**
1286   * Format dates into a string in a way similar to sprintf()
1287   */
1288  function date_sprintf( $string, $timestamp )
1289  {
1290      global $date_sprintf_timestamp;
1291      $date_sprintf_timestamp = $timestamp;
1292  
1293      return preg_replace_callback( '/%\{(.*?)\}/', 'date_sprintf_callback', $string );
1294  }
1295  
1296  function date_sprintf_callback( $matches )
1297  {
1298      global $date_sprintf_timestamp;
1299  
1300      return date_i18n( $matches[1], $date_sprintf_timestamp );
1301  }
1302  
1303  
1304  /**
1305   * Get date name when date was happened
1306   *
1307   * @param integer Timestamp
1308   * @return string Name of date (Today, Yesterday, x days ago, x months ago, x years ago)
1309   */
1310  function date_ago( $timestamp )
1311  {
1312      global $servertimenow;
1313  
1314      $days = floor( ( $servertimenow - $timestamp ) / 86400 );
1315      $months = ceil( $days / 31 );
1316  
1317      if( $days < 1 )
1318      {    // Today
1319          return T_('Today');
1320      }
1321      elseif( $days == 1 )
1322      {    // Yesterday
1323          return T_('Yesterday');
1324      }
1325      elseif( $days > 1 && $days <= 31 )
1326      {    // Days
1327          return sprintf( T_('%s days ago'), $days );
1328      }
1329      elseif( $days > 31 && $months <= 12 )
1330      {    // Months
1331          return sprintf( $months == 1 ? T_('%s month ago') : T_('%s months ago'), $months );
1332      }
1333      else
1334      {    // Years
1335          $years = floor( $months / 12 );
1336          return sprintf( $years == 1 ? T_('%s year ago') : T_('%s years ago'), $years );
1337      }
1338  }
1339  
1340  
1341  /**
1342   * Convert seconds to readable period
1343   *
1344   * @param integer Seconds
1345   * @return string Readable time period
1346   */
1347  function seconds_to_period( $seconds )
1348  {
1349      $periods = array(
1350          array( 31536000, T_('1 year'),   T_('%s years') ), // 365 days
1351          array( 2592000,  T_('1 month'),  T_('%s months') ), // 30 days
1352          array( 86400,    T_('1 day'),    T_('%s days') ),
1353          array( 3600,     T_('1 hour'),   T_('%s hours') ),
1354          array( 60,       T_('1 minute'), T_('%s minutes') ),
1355          array( 1,        T_('1 second'), T_('%s seconds') ),
1356      );
1357  
1358      foreach( $periods as $p_info )
1359      {
1360          $period_value = intval( $seconds / $p_info[0] * 10 ) /10;
1361          if( $period_value >= 1 )
1362          { // Stop on this period
1363              if( $period_value == 1 )
1364              { // One unit of period
1365                  $period_text = $p_info[1];
1366              }
1367              else
1368              { // Two and more units of period
1369                  $period_text = sprintf( $p_info[2], $period_value );
1370              }
1371              break;
1372          }
1373      }
1374  
1375      if( !isset( $period_text ) )
1376      { // 0 seconds
1377          $period_text = sprintf( T_('%s seconds'), 0 );
1378      }
1379  
1380      return $period_text;
1381  }
1382  
1383  
1384  /**
1385   * Converts an ISO 8601 date to MySQL DateTime format.
1386   *
1387   * @param string date and time in ISO 8601 format {@link http://en.wikipedia.org/wiki/ISO_8601}.
1388   * @return string date and time in MySQL DateTime format Y-m-d H:i:s.
1389   */
1390  function iso8601_to_datetime( $iso_date )
1391  {
1392      return preg_replace('#([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(Z|[\+|\-][0-9]{2,4}){0,1}#', '$1-$2-$3 $4:$5:$6', $iso_date);
1393  }
1394  
1395  
1396  /**
1397   * Converts a MySQL DateTime to ISO 8601 date format.
1398   *
1399   * @param string date and time in MySQL DateTime format Y-m-d H:i:s
1400   * @return string date and time in ISO 8601 format {@link http://en.wikipedia.org/wiki/ISO_8601}.
1401   */
1402  function datetime_to_iso8601( $datetime, $useGM = false )
1403  {
1404      $iso_date = mysql2date('U', $datetime);
1405  
1406      if( $useGM )
1407      {
1408          $iso_date = gmdate('Ymd', $iso_date).'T'.gmdate('H:i:s', $iso_date);
1409      }
1410      else
1411      {
1412          $iso_date = date('Ymd', $iso_date).'T'.date('H:i:s', $iso_date);
1413      }
1414  
1415      return $iso_date;
1416  }
1417  
1418  
1419  /**
1420   *
1421   * @param integer year
1422   * @param integer month (0-53)
1423   * @param integer 0 for sunday, 1 for monday
1424   */
1425  function get_start_date_for_week( $year, $week, $startofweek )
1426  {
1427      $new_years_date = mktime( 0, 0, 0, 1, 1, $year );
1428      $weekday = date('w', $new_years_date);
1429      // echo '<br> 1st day is a: '.$weekday;
1430  
1431      // How many days until start of week:
1432      $days_to_new_week = (7 - $weekday + $startofweek) % 7;
1433      // echo '<br> days to new week: '.$days_to_new_week;
1434  
1435      // We now add the required number of days to find the 1st sunday/monday in the year:
1436      //$first_week_start_date = $new_years_date + $days_to_new_week * 86400;
1437      //echo '<br> 1st week starts on '.date( 'Y-m-d H:i:s', $first_week_start_date );
1438  
1439      // We add the number of requested weeks:
1440      // This will fail when passing to Daylight Savings Time: $date = $first_week_start_date + (($week-1) * 604800);
1441      $date = mktime( 0, 0, 0, 1, $days_to_new_week + 1 + ($week-1) * 7, $year );
1442      // echo '<br> week '.$week.' starts on '.date( 'Y-m-d H:i:s', $date );
1443  
1444      return $date;
1445  }
1446  
1447  
1448  
1449  /**
1450   * Get start and end day of a week, based on day f the week and start-of-week
1451   *
1452   * Used by Calendar
1453   *
1454   * @param date
1455   * @param integer 0 for Sunday, 1 for Monday
1456   */
1457  function get_weekstartend( $date, $startOfWeek )
1458  {
1459      while( date('w', $date) <> $startOfWeek )
1460      {
1461          // echo '<br />'.date('Y-m-d H:i:s w', $date).' - '.$startOfWeek;
1462          // mktime is needed so calculations work for DST enabling. Example: March 30th 2008, start of week 0 sunday
1463          $date = mktime( 0, 0, 0, date('m',$date), date('d',$date)-1, date('Y',$date) );
1464      }
1465      // echo '<br />'.date('Y-m-d H:i:s w', $date).' = '.$startOfWeek;
1466      $week['start'] = $date;
1467      $week['end']   = $date + 604800; // 7 days
1468  
1469      // pre_dump( 'weekstartend: ', date( 'Y-m-d', $week['start'] ), date( 'Y-m-d', $week['end'] ) );
1470  
1471      return( $week );
1472  }
1473  
1474  
1475  /**
1476   * Get datetime rounded to lower minute. This is meant to remove seconds and
1477   * leverage MySQL's query cache by having SELECT queries remain identical for 60 seconds instead of just 1.
1478   *
1479   * @param integer UNIX timestamp
1480   * @param string Format (defaults to "Y-m-d H:i"). Use "U" for UNIX timestamp.
1481   */
1482  function remove_seconds($timestamp, $format = 'Y-m-d H:i')
1483  {
1484      return date($format, floor($timestamp/60)*60);
1485  }
1486  
1487  
1488  /**
1489   * Convert from seconds to months, days, hours, minutes and seconds
1490   *
1491   * @param integer duration in seconds
1492   * @return array of [ years, months, days, hours, minutes, seconds ]
1493   */
1494  function get_duration_fields( $duration )
1495  {
1496      $result = array();
1497  
1498      $year_seconds = 31536000; // 1 year
1499      $years = floor( $duration / $year_seconds );
1500      $duration = $duration - $years * $year_seconds;
1501      $result[ 'years' ] = $years;
1502  
1503      $month_seconds = 2592000; // 1 month
1504      $months = floor( $duration / $month_seconds );
1505      $duration = $duration - $months * $month_seconds;
1506      $result[ 'months' ] = $months;
1507  
1508      $day_seconds = 86400; // 1 day
1509      $days = floor( $duration / $day_seconds );
1510      $duration = $duration - $days * $day_seconds;
1511      $result[ 'days' ] = $days;
1512  
1513      $hour_seconds = 3600; // 1 hour
1514      $hours = floor( $duration / $hour_seconds );
1515      $duration = $duration - $hours * $hour_seconds;
1516      $result[ 'hours' ] = $hours;
1517  
1518      $minute_seconds = 60; // 1 minute
1519      $minutes = floor( $duration / $minute_seconds );
1520      $duration = $duration - $minutes * $minute_seconds;
1521      $result[ 'minutes' ] = $minutes;
1522  
1523      $result[ 'seconds' ] = $duration;
1524      return $result;
1525  }
1526  
1527  
1528  /**
1529   * Get a title of duration
1530   *
1531   * @param integer Duration in seconds
1532   * @param array Titles
1533   * @return string Duration title
1534   */
1535  function get_duration_title( $duration, $titles = array() )
1536  {
1537      $titles = array_merge( array(
1538          'year'   => T_('Last %d years'),
1539          'month'  => T_('Last %d months'),
1540          'day'    => T_('Last %d days'),
1541          'hour'   => T_('Last %d hours'),
1542          'minute' => T_('Last %d minutes'),
1543          'second' => T_('Last %d seconds'),
1544          ), $titles );
1545  
1546      $delay_fields = get_duration_fields( $duration );
1547  
1548      if( ! empty( $delay_fields[ 'years' ] ) )
1549      { // Years
1550          return sprintf( $titles['year'], $delay_fields[ 'years' ] );
1551      }
1552      elseif( ! empty( $delay_fields[ 'months' ] ) )
1553      { // Months
1554          return sprintf( $titles['month'], $delay_fields[ 'months' ] );
1555      }
1556      elseif( ! empty( $delay_fields[ 'days' ] ) )
1557      { // Days
1558          return sprintf( $titles['day'], $delay_fields[ 'days' ] );
1559      }
1560      elseif( ! empty( $delay_fields[ 'hours' ] ) )
1561      { // Hours
1562          return sprintf( $titles['hour'], $delay_fields[ 'hours' ] );
1563      }
1564      elseif( ! empty( $delay_fields[ 'minutes' ] ) )
1565      { // Minutes
1566          return sprintf( $titles['minute'], $delay_fields[ 'minutes' ] );
1567      }
1568      else
1569      { // Seconds
1570          return sprintf( $titles['second'], $delay_fields[ 'seconds' ] );
1571      }
1572  }
1573  
1574  
1575  /**
1576   * Validate variable
1577   *
1578   * @param string param name
1579   * @param string validator function name
1580   * @param boolean true if variable value can't be empty
1581   * @param custom error message
1582   * @return boolean true if OK
1583   */
1584  function param_validate( $variable, $validator, $required = false, $custom_msg = NULL )
1585  {
1586      /* Tblue> Note: is_callable() does not check whether a function is
1587       *        disabled (http://www.php.net/manual/en/function.is-callable.php#79151).
1588       */
1589      if( ! is_callable( $validator ) )
1590      {
1591          debug_die( 'Validator function '.$validator.'() is not callable!' );
1592      }
1593  
1594      if( ! isset( $GLOBALS[$variable] ) )
1595      {    // Variable not set, we cannot handle this using the validator function...
1596          if( $required )
1597          {    // Add error:
1598              param_check_not_empty( $variable, $custom_msg );
1599              return false;
1600          }
1601  
1602          return true;
1603      }
1604  
1605      if( $GLOBALS[$variable] === '' && ! $required )
1606      {    // Variable is empty or not set. That's fine since it isn't required:
1607          return true;
1608      }
1609  
1610      $msg = $validator( $GLOBALS[$variable] );
1611  
1612      if( !empty( $msg ) )
1613      {
1614          if( !empty( $custom_msg ) )
1615          {
1616              $msg = $custom_msg;
1617          }
1618  
1619          param_error( $variable, $msg );
1620          return false;
1621      }
1622  
1623      return true;
1624  }
1625  
1626  
1627  /**
1628   * Checks if the param is a decimal number
1629   *
1630   * @param string decimal to check
1631   * @return boolean true if OK
1632   */
1633  function is_decimal( $decimal )
1634  {
1635      return preg_match( '#^[0-9]*(\.[0-9]+)?$#', $decimal );
1636  }
1637  
1638  
1639  /**
1640   * Checks if the param is an integer (no float, e.g. 3.14).
1641   *
1642   * @param string number to check
1643   * @return boolean true if OK
1644   */
1645  function is_number( $number )
1646  {
1647      return preg_match( '#^[0-9]+$#', $number );
1648  }
1649  
1650  
1651  /**
1652   * Check that email address looks valid.
1653   *
1654   * @param string email address to check
1655   * @param string Format to use ('simple', 'rfc')
1656   *    'simple':
1657   *      Single email address.
1658   *    'rfc':
1659   *      Full email address, may include name (RFC2822)
1660   *      - example@example.org
1661   *      - Me <example@example.org>
1662   *      - "Me" <example@example.org>
1663   * @param boolean Return the match or boolean
1664   *
1665   * @return bool|array Either true/false or the match (see {@link $return_match})
1666   */
1667  function is_email( $email, $format = 'simple', $return_match = false )
1668  {
1669      #$chars = "/^([a-z0-9_]|\\-|\\.)+@(([a-z0-9_]|\\-)+\\.)+[a-z]{2,4}\$/i";
1670  
1671      switch( $format )
1672      {
1673          case 'rfc':
1674          case 'rfc2822':
1675              /**
1676               * Regexp pattern converted from: http://www.regexlib.com/REDetails.aspx?regexp_id=711
1677               * Extended to allow escaped quotes.
1678               */
1679              $pattern_email = '/^
1680                  (
1681                      (?>[a-zA-Z\d!\#$%&\'*+\-\/=?^_`{|}~]+\x20*
1682                          |"( \\\" | (?=[\x01-\x7f])[^"\\\] | \\[\x01-\x7f] )*"\x20*)* # Name
1683                      (<)
1684                  )?
1685                  (
1686                      (?!\.)(?>\.?[a-zA-Z\d!\#$%&\'*+\-\/=?^_`{|}~]+)+
1687                      |"( \\\" | (?=[\x01-\x7f])[^"\\\] | \\[\x01-\x7f] )* " # quoted mailbox name
1688                  )
1689                  @
1690                  (
1691                      ((?!-)[a-zA-Z\d\-]+(?<!-)\.)+[a-zA-Z]{2,}
1692                      |
1693                      \[(
1694                          ( (?(?<!\[)\.)(25[0-5] | 2[0-4]\d | [01]?\d?\d) ){4}
1695                          |
1696                          [a-zA-Z\d\-]*[a-zA-Z\d]:( (?=[\x01-\x7f])[^\\\[\]] | \\[\x01-\x7f] )+
1697                      )\]
1698                  )
1699                  (?(3)>) # match ">" if it was there
1700                  $/x';
1701              break;
1702  
1703          case 'simple':
1704          default:
1705              // '/^\S+@[^\.\s]\S*\.[a-z]{2,}$/i'
1706              $pattern_email = '~^(([_a-z0-9-]+)(\.[_a-z0-9-]+)*@([a-z0-9-]+)(\.[a-z0-9-]+)*(\.[a-z]{2,}))$~i';
1707              break;
1708      }
1709  
1710      if( strpos( $email, '@' ) !== false && strpos( $email, '.' ) !== false )
1711      {
1712          if( $return_match )
1713          {
1714              preg_match( $pattern_email, $email, $match );
1715              return $match;
1716          }
1717          else
1718          {
1719              return (bool)preg_match( $pattern_email, $email );
1720          }
1721      }
1722      else
1723      {
1724          return $return_match ? array() : false;
1725      }
1726  }
1727  
1728  
1729  /**
1730   * Checks if the phone number is valid
1731   *
1732   * @param string phone number to check
1733   * @return boolean true if OK
1734   */
1735  function is_phone( $phone )
1736  {
1737      return preg_match( '|^\+?[\-*#/(). 0-9]+$|', $phone );
1738  }
1739  
1740  
1741  /**
1742   * Checks if the url is valid
1743   *
1744   * @param string url to check
1745   * @return boolean true if OK
1746   */
1747  function is_url( $url )
1748  {
1749      if( validate_url( $url, 'posting', false ) )
1750      {
1751          return false;
1752      }
1753  
1754      return true;
1755  }
1756  
1757  
1758  /**
1759   * Checks if the word is valid
1760   *
1761   * @param string word to check
1762   * @return boolean true if OK
1763   */
1764  function is_word( $word )
1765  {
1766      return preg_match( '#^[A-Za-z]+$#', $word );
1767  }
1768  
1769  
1770  /**
1771   * Check if the login is valid (in terms of allowed chars)
1772   *
1773   * @param string login
1774   * @return boolean true if OK
1775   */
1776  function is_valid_login( $login, $force_strict_logins = false )
1777  {
1778      global $Settings;
1779  
1780      $strict_logins = $Settings->get('strict_logins');
1781  
1782      // NOTE: in some places usernames are typed in by other users (messaging) or admins.
1783      // Having cryptic logins with hard to type letters is a PITA.
1784  
1785      // Step 1
1786      // Forbid the following characters in logins
1787      if( preg_match( '~[\'"><@\s]~', $login ) )
1788      {    // WARNING: allowing ' or " or > or < will open security issues!
1789          // NOTE: allowing @ will make some "average" users use their email address (not good for their spam health)
1790          // NOTE: we do not allow whitespace in logins
1791          return false;
1792      }
1793  
1794      // Step 2
1795      if( ($strict_logins || $force_strict_logins) && ! preg_match( '~^[A-Za-z0-9_.]+$~', $login ) )
1796      {    // WARNING: allowing special chars like latin 1 accented chars ( \xDF-\xF6\xF8-\xFF ) will create issues with
1797          // user media directory names (tested on Max OS X) -- Do no allow any of this until we have a clean & safe media dir name generator.
1798  
1799          // fp> TODO: check why a dash '-' prevents renaming the fileroot
1800          return false;
1801      }
1802      elseif( ! $strict_logins )
1803      {    // We allow any character that is not explicitly forbidden in Step 1
1804          // Enforce additional limitations
1805          $login = preg_replace( '|%([a-fA-F0-9][a-fA-F0-9])|', '', $login ); // Kill octets
1806          $login = preg_replace( '/&.+?;/', '', $login ); // Kill entities
1807      }
1808  
1809      // Step 3
1810      // Special case, the login is valid however we forbid it's usage.
1811      // param_check_valid_login() will display a special error message in this case.
1812      if( preg_match( '~^usr_~', $login ) )
1813      {    // Logins cannot start with 'usr_', this prefix is reserved for system use
1814          // We create user media directories for users with non-ASCII logins in format /media/users/usr_55/, where 55 is user ID
1815          return 'usr';
1816      }
1817  
1818      return true;
1819  }
1820  
1821  
1822  /**
1823   * Check if the login is valid (user exists)
1824   *
1825   * @param string login
1826   * @return boolean true if OK
1827   */
1828  function user_exists( $login )
1829  {
1830      global $DB;
1831  
1832      $SQL = new SQL();
1833      $SQL->SELECT( 'COUNT(*)' );
1834      $SQL->FROM( 'T_users' );
1835      $SQL->WHERE( 'user_login = "'.$DB->escape($login).'"' );
1836  
1837      $var = $DB->get_var( $SQL->get() );
1838      return $var > 0 ? true : false; // PHP4 compatibility
1839  }
1840  
1841  
1842  /**
1843   * Are we running on a Windows server?
1844   */
1845  function is_windows()
1846  {
1847      return ( strtoupper(substr(PHP_OS,0,3)) == 'WIN' );
1848  }
1849  
1850  
1851  /**
1852   * Get all "a" tags from the given content
1853   *
1854   * @param string content
1855   * @return array all <a../a> part from the given content
1856   */
1857  function get_atags( $content )
1858  {
1859      $tag = 'a';
1860      $regexp = '{<'.$tag.'[^>]*>(.*?)</'.$tag.'>}';
1861  
1862      preg_match_all( $regexp, $content, $result );
1863      return $result[0];
1864  }
1865  
1866  
1867  /**
1868   * Get all "img" tags from the given content
1869   *
1870   * @param string content
1871   * @return array all <img../img> part from the given content
1872   */
1873  function get_imgtags( $content )
1874  {
1875      $tag = 'img';
1876      $regexp = '{<'.$tag.'[^>]*[ (</'.$tag.'>) | (/>) ]}';
1877  
1878      preg_match_all( $regexp, $content, $result );
1879      return $result[0];
1880  }
1881  
1882  
1883  /**
1884   * Get all urls from the given content
1885   *
1886   * @param string content
1887   * @return array all url from content
1888   */
1889  function get_urls( $content )
1890  {
1891      $regexp = '^(?#Protocol)(?:(?:ht|f)tp(?:s?)\:\/\/|~\/|\/)?(?#Username:Password)(?:\w+:\w+@)?(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2,4}))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:\/(?:[-\w~!$+|.,;=]|%[a-f\d]{2})+)+|\/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,;*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,;*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,;*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,;*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,;*:=]|%[a-f\d]{2})*)?^';
1892  
1893      preg_match_all( $regexp, $content, $result );
1894      return $result[0];
1895  }
1896  
1897  
1898  function xmlrpc_getposttitle($content)
1899  {
1900      global $post_default_title;
1901      if (preg_match('/<title>(.+?)<\/title>/is', $content, $matchtitle))
1902      {
1903          $post_title = $matchtitle[1];
1904      }
1905      else
1906      {
1907          $post_title = $post_default_title;
1908      }
1909      return($post_title);
1910  }
1911  
1912  
1913  /**
1914   * Also used by post by mail
1915   *
1916   * @deprecated by xmlrpc_getpostcategories()
1917   */
1918  function xmlrpc_getpostcategory($content)
1919  {
1920      if (preg_match('/<category>([0-9]+?)<\/category>/is', $content, $matchcat))
1921      {
1922          return $matchcat[1];
1923      }
1924  
1925      return false;
1926  }
1927  
1928  
1929  /**
1930   * Extract categories out of "<category>" tag from $content.
1931   *
1932   * NOTE: w.bloggar sends something like "<category>00000013,00000001,00000004,</category>" to
1933   *       blogger.newPost.
1934   *
1935   * @return false|array
1936   */
1937  function xmlrpc_getpostcategories($content)
1938  {
1939      $cats = array();
1940  
1941      if( preg_match('~<category>(\d+\s*(,\s*\d*)*)</category>~i', $content, $match) )
1942      {
1943          $cats = preg_split('~\s*,\s*~', $match[1], -1, PREG_SPLIT_NO_EMPTY);
1944          foreach( $cats as $k => $v )
1945          {
1946              $cats[$k] = (int)$v;
1947          }
1948      }
1949  
1950      return $cats;
1951  }
1952  
1953  
1954  /*
1955   * xmlrpc_removepostdata(-)
1956   */
1957  function xmlrpc_removepostdata($content)
1958  {
1959      $content = preg_replace('/<title>(.*?)<\/title>/si', '', $content);
1960      $content = preg_replace('/<category>(.*?)<\/category>/si', '', $content);
1961      $content = trim($content);
1962      return($content);
1963  }
1964  
1965  
1966  /**
1967   * Echo the XML-RPC call Result and optionally log into file
1968   *
1969   * @param object XMLRPC response object
1970   * @param boolean true to echo
1971   * @param mixed File resource or == '' for no file logging.
1972   */
1973  function xmlrpc_displayresult( $result, $display = true, $log = '' )
1974  {
1975      if( ! $result )
1976      { // We got no response:
1977          if( $display ) echo T_('No response!')."<br />\n";
1978          return false;
1979      }
1980  
1981      if( $result->faultCode() )
1982      { // We got a remote error:
1983          if( $display ) echo T_('Remote error'), ': ', $result->faultString(), ' (', $result->faultCode(), ")<br />\n";
1984          debug_fwrite($log, $result->faultCode().' -- '.$result->faultString());
1985          return false;
1986      }
1987  
1988      // We'll display the response:
1989      $val = $result->value();
1990      $value = xmlrpc_decode_recurse($result->value());
1991  
1992      if( is_array($value) )
1993      {
1994          $out = '';
1995          foreach($value as $l_value)
1996          {
1997              if( is_array( $l_value ) )
1998              {
1999                  $out .= ' [';
2000                  foreach( $l_value as $lv_key => $lv_val )
2001                  {
2002                      $out .= $lv_key.' => '.( is_array( $lv_val ) ? '{'.implode( '; ', $lv_val ).'}' : $lv_val ).'; ';
2003                  }
2004                  $out .= '] ';
2005              }
2006              else
2007              {
2008                  $out .= ' ['.$l_value.'] ';
2009              }
2010          }
2011      }
2012      else
2013      {
2014          $out = $value;
2015      }
2016  
2017      debug_fwrite($log, $out);
2018  
2019      if( $display ) echo T_('Response').': '.$out."<br />\n";
2020  
2021      return $value;
2022  }
2023  
2024  
2025  /**
2026   * Log the XML-RPC call Result into LOG object
2027   *
2028   * @param object XMLRPC response object
2029   * @param Log object to add messages to
2030   * @return boolean true = success, false = error
2031   */
2032  function xmlrpc_logresult( $result, & $message_Log, $log_payload = true )
2033  {
2034      if( ! $result )
2035      { // We got no response:
2036          $message_Log->add( T_('No response!'), 'error' );
2037          return false;
2038      }
2039  
2040      if( $result->faultCode() )
2041      { // We got a remote error:
2042          $message_Log->add( T_('Remote error').': '.$result->faultString().' ('.$result->faultCode().')', 'error' );
2043          return false;
2044      }
2045  
2046      if( $log_payload )
2047      {
2048          // We got a response:
2049          $value = xmlrpc_decode_recurse($result->value());
2050  
2051          if( is_array($value) )
2052          {
2053              $out = '';
2054              foreach($value as $l_value)
2055              {
2056                  $out .= ' ['.var_export($l_value, true).'] ';
2057              }
2058          }
2059          else
2060          {
2061              $out = $value;
2062          }
2063  
2064          $message_Log->add( T_('Response').': '.$out, 'success' );
2065      }
2066  
2067      return true;
2068  }
2069  
2070  
2071  
2072  function debug_fopen($filename, $mode) {
2073      global $debug;
2074      if ($debug == 1 && ( !empty($filename) ) )
2075      {
2076          $fp = fopen($filename, $mode);
2077          return $fp;
2078      } else {
2079          return false;
2080      }
2081  }
2082  
2083  function debug_fwrite($fp, $string)
2084  {
2085      global $debug;
2086      if( $debug && $fp )
2087      {
2088          fwrite($fp, $string);
2089      }
2090  }
2091  
2092  function debug_fclose($fp)
2093  {
2094      global $debug;
2095      if( $debug && $fp )
2096      {
2097          fclose($fp);
2098      }
2099  }
2100  
2101  
2102  
2103  /**
2104   * Wrap pre tag around {@link var_dump()} for better debugging.
2105   *
2106   * @param $var__var__var__var__,... mixed variable(s) to dump
2107   * @return true
2108   */
2109  function pre_dump( $var__var__var__var__ )
2110  {
2111      global $is_cli;
2112  
2113      #echo 'pre_dump(): '.debug_get_backtrace(); // see where a pre_dump() comes from
2114  
2115      $func_num_args = func_num_args();
2116      $count = 0;
2117  
2118      if( ! empty($is_cli) )
2119      { // CLI, no encoding of special chars:
2120          $count = 0;
2121          foreach( func_get_args() as $lvar )
2122          {
2123              var_dump($lvar);
2124  
2125              $count++;
2126              if( $count < $func_num_args )
2127              { // Put newline between arguments
2128                  echo "\n";
2129              }
2130          }
2131      }
2132      elseif( function_exists('xdebug_var_dump') )
2133      { // xdebug already does fancy displaying:
2134  
2135          // no limits:
2136          $old_var_display_max_children = ini_set('xdebug.var_display_max_children', -1); // default: 128
2137          $old_var_display_max_data = ini_set('xdebug.var_display_max_data', -1); // max string length; default: 512
2138          $old_var_display_max_depth = ini_set('xdebug.var_display_max_depth', -1); // default: 3
2139  
2140          echo "\n<div style=\"padding:1ex;border:1px solid #00f;\">\n";
2141          foreach( func_get_args() as $lvar )
2142          {
2143              xdebug_var_dump($lvar);
2144  
2145              $count++;
2146              if( $count < $func_num_args )
2147              { // Put HR between arguments
2148                  echo "<hr />\n";
2149              }
2150          }
2151          echo '</div>';
2152  
2153          // restore xdebug settings:
2154          ini_set('xdebug.var_display_max_children', $old_var_display_max_children);
2155          ini_set('xdebug.var_display_max_data', $old_var_display_max_data);
2156          ini_set('xdebug.var_display_max_depth', $old_var_display_max_depth);
2157      }
2158      else
2159      {
2160          $orig_html_errors = ini_set('html_errors', 0); // e.g. xdebug would use fancy html, if this is on; we catch (and use) xdebug explicitly above, but just in case
2161  
2162          echo "\n<pre style=\"padding:1ex;border:1px solid #00f;\">\n";
2163          foreach( func_get_args() as $lvar )
2164          {
2165              ob_start();
2166              var_dump($lvar); // includes "\n"; do not use var_export() because it does not detect recursion by design
2167              $buffer = ob_get_contents();
2168              ob_end_clean();
2169              echo htmlspecialchars($buffer);
2170  
2171              $count++;
2172              if( $count < $func_num_args )
2173              { // Put HR between arguments
2174                  echo "<hr />\n";
2175              }
2176          }
2177          echo "</pre>\n";
2178          ini_set('html_errors', $orig_html_errors);
2179      }
2180      evo_flush();
2181      return true;
2182  }
2183  
2184  
2185  /**
2186   * Get a function trace from {@link debug_backtrace()} as html table.
2187   *
2188   * Adopted from {@link http://us2.php.net/manual/de/function.debug-backtrace.php#47644}.
2189   *
2190   * @todo dh> Add support for $is_cli = true (e.g. in case of MySQL error)
2191   *
2192   * @param integer|NULL Get the last x entries from the stack (after $ignore_from is applied). Anything non-numeric means "all".
2193   * @param array After a key/value pair matches a stack entry, this and the rest is ignored.
2194   *              For example, array('class' => 'DB') would exclude everything after the stack
2195   *              "enters" class DB and everything that got called afterwards.
2196   *              You can also give an array of arrays which means that every condition in one of the given array must match.
2197   * @param integer Number of stack entries to include, after $ignore_from matches.
2198   * @return string HTML table
2199   */
2200  function debug_get_backtrace( $limit_to_last = NULL, $ignore_from = array( 'function' => 'debug_get_backtrace' ), $offset_ignore_from = 0 )
2201  {
2202      if( ! function_exists( 'debug_backtrace' ) ) // PHP 4.3.0
2203      {
2204          return 'Function debug_backtrace() is not available!';
2205      }
2206  
2207      $r = '';
2208  
2209      $backtrace = debug_backtrace();
2210      $count_ignored = 0; // remember how many have been ignored
2211      $limited = false;   // remember if we have limited to $limit_to_last
2212  
2213      if( $ignore_from )
2214      {    // we want to ignore from a certain point
2215          $trace_length = 0;
2216          $break_because_of_offset = false;
2217  
2218          for( $i = count($backtrace); $i > 0; $i-- )
2219          {    // Search the backtrace from behind (first call).
2220              $l_stack = & $backtrace[$i-1];
2221  
2222              if( $break_because_of_offset && $offset_ignore_from < 1 )
2223              { // we've respected the offset, but need to break now
2224                  break; // ignore from here
2225              }
2226  
2227              foreach( $ignore_from as $l_ignore_key => $l_ignore_value )
2228              {    // Check if we want to ignore from here
2229                  if( is_array($l_ignore_value) )
2230                  {    // It's an array - all must match
2231                      foreach( $l_ignore_value as $l_ignore_mult_key => $l_ignore_mult_val )
2232                      {
2233                          if( !isset($l_stack[$l_ignore_mult_key]) /* not set with this stack entry */
2234                              || strcasecmp($l_stack[$l_ignore_mult_key], $l_ignore_mult_val) /* not this value (case-insensitive) */ )
2235                          {
2236                              continue 2; // next ignore setting, because not all match.
2237                          }
2238                      }
2239                      if( $offset_ignore_from-- > 0 )
2240                      {
2241                          $break_because_of_offset = true;
2242                          break;
2243                      }
2244                      break 2; // ignore from here
2245                  }
2246                  elseif( isset($l_stack[$l_ignore_key])
2247                      && !strcasecmp($l_stack[$l_ignore_key], $l_ignore_value) /* is equal case-insensitive */ )
2248                  {
2249                      if( $offset_ignore_from-- > 0 )
2250                      {
2251                          $break_because_of_offset = true;
2252                          break;
2253                      }
2254                      break 2; // ignore from here
2255                  }
2256              }
2257              $trace_length++;
2258          }
2259  
2260          $count_ignored = count($backtrace) - $trace_length;
2261  
2262          $backtrace = array_slice( $backtrace, 0-$trace_length ); // cut off ignored ones
2263      }
2264  
2265      $count_backtrace = count($backtrace);
2266      if( is_numeric($limit_to_last) && $limit_to_last < $count_backtrace )
2267      {    // we want to limit to a maximum number
2268          $limited = true;
2269          $backtrace = array_slice( $backtrace, 0, $limit_to_last );
2270          $count_backtrace = $limit_to_last;
2271      }
2272  
2273      $r .= '<div style="padding:1ex; margin-bottom:1ex; text-align:left; color:#000; background-color:#ddf;">
2274                      <h3>Backtrace:</h3>'."\n";
2275      if( $count_backtrace )
2276      {
2277          $r .= '<ol style="font-family:monospace;">';
2278  
2279          $i = 0;
2280          foreach( $backtrace as $l_trace )
2281          {
2282              if( ++$i == $count_backtrace )
2283              {
2284                  $r .= '<li style="padding:0.5ex 0;">';
2285              }
2286              else
2287              {
2288                  $r .= '<li style="padding:0.5ex 0; border-bottom:1px solid #77d;">';
2289              }
2290              $args = array();
2291              if( isset($l_trace['args']) && is_array( $l_trace['args'] ) )
2292              {    // Prepare args:
2293                  foreach( $l_trace['args'] as $l_arg )
2294                  {
2295                      $l_arg_type = gettype($l_arg);
2296                      switch( $l_arg_type )
2297                      {
2298                          case 'integer':
2299                          case 'double':
2300                              $args[] = $l_arg;
2301                              break;
2302                          case 'string':
2303                              $args[] = '"'.strmaxlen(str_replace("\n", '\n', $l_arg), 255, NULL, 'htmlspecialchars').'"';
2304                              break;
2305                          case 'array':
2306                              $args[] = 'Array('.count($l_arg).')';
2307                              break;
2308                          case 'object':
2309                              $args[] = 'Object('.get_class($l_arg).')';
2310                              break;
2311                          case 'resource':
2312                              $args[] = htmlspecialchars((string)$l_arg);
2313                              break;
2314                          case 'boolean':
2315                              $args[] = $l_arg ? 'true' : 'false';
2316                              break;
2317                          default:
2318                              $args[] = $l_arg_type;
2319                      }
2320                  }
2321              }
2322  
2323              $call = "<strong>\n";
2324              if( isset($l_trace['class']) )
2325              {
2326                  $call .= htmlspecialchars($l_trace['class']);
2327              }
2328              if( isset($l_trace['type']) )
2329              {
2330                  $call .= htmlspecialchars($l_trace['type']);
2331              }
2332              $call .= htmlspecialchars($l_trace['function'])."( </strong>\n";
2333              if( $args )
2334              {
2335                  $call .= ' '.implode( ', ', $args ).' ';
2336              }
2337              $call .='<strong>)</strong>';
2338  
2339              $r .= $call."<br />\n";
2340  
2341              $r .= '<strong>';
2342              if( isset($l_trace['file']) )
2343              {
2344                  $r .= "File: </strong> ".$l_trace['file'];
2345              }
2346              else
2347              {
2348                  $r .= '[runtime created function]</strong>';
2349              }
2350              if( isset($l_trace['line']) )
2351              {
2352                  $r .= ' on line '.$l_trace['line'];
2353              }
2354  
2355              $r .= "</li>\n";
2356          }
2357          $r .= '</ol>';
2358      }
2359      else
2360      {
2361          $r .= '<p>No backtrace available.</p>';
2362      }
2363  
2364      // Extra notes, might be to much, but explains why we stopped at some point. Feel free to comment it out or remove it.
2365      $notes = array();
2366      if( $count_ignored )
2367      {
2368          $notes[] = 'Ignored last: '.$count_ignored;
2369      }
2370      if( $limited )
2371      {
2372          $notes[] = 'Limited to'.( $count_ignored ? ' remaining' : '' ).': '.$limit_to_last;
2373      }
2374      if( $notes )
2375      {
2376          $r .= '<p class="small">'.implode( ' - ', $notes ).'</p>';
2377      }
2378  
2379      $r .= "</div>\n";
2380  
2381      return $r;
2382  }
2383  
2384  
2385  /**
2386   * Outputs Unexpected Error message. When in debug mode it also prints a backtrace.
2387   *
2388   * This should be used instead of die() everywhere.
2389   * This should NOT be used instead of exit() anywhere.
2390   * Dying means the application has encontered an unexpected situation,
2391   * i-e: something that should never occur during normal operation.
2392   * Examples: database broken, user changed URL by hand...
2393   *
2394   * @param string Message to output
2395   * @param array Additional params
2396   *        - "status" (Default: '500 Internal Server Error')
2397   */
2398  function debug_die( $additional_info = '', $params = array() )
2399  {
2400      global $debug, $baseurl;
2401      global $log_app_errors, $app_name, $is_cli, $display_errors_on_production;
2402  
2403      $params = array_merge( array(
2404          'status' => '500 Internal Server Error',
2405          ), $params );
2406  
2407      if( $is_cli )
2408      { // Command line interface, e.g. in cron_exec.php:
2409          echo '== '.T_('An unexpected error has occurred!')." ==\n";
2410          echo T_('If this error persists, please report it to the administrator.')."\n";
2411          if( $debug || $display_errors_on_production )
2412          { // Display additional info only in debug mode or when it was explicitly set by display_errors_on_production setting because it can reveal system info to hackers and greatly facilitate exploits
2413              echo T_('Additional information about this error:')."\n";
2414              echo strip_tags( $additional_info )."\n\n";
2415          }
2416      }
2417      else
2418      {
2419          // Attempt to output an error header (will not work if the output buffer has already flushed once):
2420          // This should help preventing indexing robots from indexing the error :P
2421          if( ! headers_sent() )
2422          {
2423              load_funcs('_core/_template.funcs.php');
2424              headers_content_mightcache( 'text/html', 0, '#', false );  // Do NOT cache error messages! (Users would not see they fixed them)
2425              $status_header = $_SERVER['SERVER_PROTOCOL'].' '.$params['status'];
2426              header($status_header);
2427          }
2428  
2429          echo '<div style="background-color: #fdd; padding: 1ex; margin-bottom: 1ex;">';
2430          echo '<h3 style="color:#f00;">'.T_('An unexpected error has occurred!').'</h3>';
2431          echo '<p>'.T_('If this error persists, please report it to the administrator.').'</p>';
2432          echo '<p><a href="'.$baseurl.'">'.T_('Go back to home page').'</a></p>';
2433          echo '</div>';
2434  
2435          if( ! empty( $additional_info ) )
2436          {
2437              echo '<div style="background-color: #ddd; padding: 1ex; margin-bottom: 1ex;">';
2438              if( $debug || $display_errors_on_production )
2439              { // Display additional info only in debug mode or when it was explicitly set by display_errors_on_production setting because it can reveal system info to hackers and greatly facilitate exploits
2440                  echo '<h3>'.T_('Additional information about this error:').'</h3>';
2441                  echo $additional_info;
2442              }
2443              else
2444              {
2445                  echo '<p><i>Enable debugging to get additional information about this error.</i></p>' . get_manual_link('debugging','How to enable debug mode?');
2446              }
2447              echo '</div>';
2448  
2449              // Append the error text to AJAX log if it is AJAX request
2450              global $Ajaxlog;
2451              if( ! empty( $Ajaxlog ) )
2452              {
2453                  $Ajaxlog->add( $additional_info, 'error' );
2454                  $Ajaxlog->display( NULL, NULL, true, 'all',
2455                                  array(
2456                                          'error' => array( 'class' => 'jslog_error', 'divClass' => false ),
2457                                          'note'  => array( 'class' => 'jslog_note',  'divClass' => false ),
2458                                      ), 'ul', 'jslog' );
2459              }
2460          }
2461      }
2462  
2463      if( $log_app_errors > 1 || $debug )
2464      { // Prepare backtrace
2465          $backtrace = debug_get_backtrace();
2466  
2467          if( $log_app_errors > 1 || $is_cli )
2468          {
2469              $backtrace_cli = trim(strip_tags($backtrace));
2470          }
2471      }
2472  
2473      if( $log_app_errors )
2474      { // Log error through PHP's logging facilities:
2475          $log_message = $app_name.' error: ';
2476          if( ! empty($additional_info) )
2477          {
2478              $log_message .= trim( strip_tags($additional_info) );
2479          }
2480          else
2481          {
2482              $log_message .= 'No info specified in debug_die()';
2483          }
2484  
2485          // Get file and line info:
2486          $file = 'Unknown';
2487          $line = 'Unknown';
2488          if( function_exists('debug_backtrace') /* PHP 4.3 */ )
2489          { // get the file and line
2490              foreach( debug_backtrace() as $v )
2491              {
2492                  if( isset($v['function']) && $v['function'] == 'debug_die' )
2493                  {
2494                      $file = isset($v['file']) ? $v['file'] : 'Unknown';
2495                      $line = isset($v['line']) ? $v['line'] : 'Unknown';
2496                      break;
2497                  }
2498              }
2499          }
2500          $log_message .= ' in '.$file.' at line '.$line;
2501  
2502          if( $log_app_errors > 1 )
2503          { // Append backtrace:
2504              // indent after newlines:
2505              $backtrace_cli = preg_replace( '~(\S)(\n)(\S)~', '$1  $2$3', $backtrace_cli );
2506              $log_message .= "\nBacktrace:\n".$backtrace_cli;
2507          }
2508          $log_message .= "\nREQUEST_URI:  ".( isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '-' );
2509          $log_message .= "\nHTTP_REFERER: ".( isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '-' );
2510  
2511          error_log( str_replace("\n", ' / ', $log_message), 0 /* PHP's system logger */ );
2512      }
2513  
2514  
2515      // DEBUG OUTPUT:
2516      if( $debug )
2517      {
2518          if( $is_cli )
2519              echo $backtrace_cli;
2520          else
2521              echo $backtrace;
2522      }
2523  
2524      // EXIT:
2525      if( ! $is_cli )
2526      { // Attempt to keep the html valid (but it doesn't really matter anyway)
2527          echo '</body></html>';
2528      }
2529  
2530      die(1);    // Error code 1. Note: This will still call the shutdown function.
2531  }
2532  
2533  
2534  /**
2535   * Outputs Bad request Error message. When in debug mode it also prints a backtrace.
2536   *
2537   * This should be used when a bad user input is detected.
2538   *
2539   * @param string Message to output (HTML)
2540   */
2541  function bad_request_die( $additional_info = '' )
2542  {
2543      global $debug, $baseurl;
2544  
2545      // Attempt to output an error header (will not work if the output buffer has already flushed once):
2546      // This should help preventing indexing robots from indexing the error :P
2547      if( ! headers_sent() )
2548      {
2549          load_funcs('_core/_template.funcs.php');
2550          headers_content_mightcache( 'text/html', 0, '#', false );        // Do NOT cache error messages! (Users would not see they fixed them)
2551          header_http_response('400 Bad Request');
2552      }
2553  
2554      echo '<div style="background-color: #fdd; padding: 1ex; margin-bottom: 1ex;">';
2555      echo '<h3 style="color:#f00;">'.T_('Bad Request!').'</h3>';
2556      echo '<p>'.T_('The parameters of your request are invalid.').'</p>';
2557      echo '<p>'.T_('If you have obtained this error by clicking on a link INSIDE of this site, please report the bad link to the administrator.').'</p>';
2558      echo '<p><a href="'.$baseurl.'">'.T_('Go back to home page').'</a></p>';
2559      echo '</div>';
2560  
2561      if( !empty( $additional_info ) )
2562      {
2563          echo '<div style="background-color: #ddd; padding: 1ex; margin-bottom: 1ex;">';
2564          if( $debug )
2565          {    // Display additional info only in debug mode because it can reveal system info to hackers and greatly facilitate exploits
2566              echo '<h3>'.T_('Additional information about this error:').'</h3>';
2567              echo $additional_info;
2568          }
2569          else
2570          {
2571              echo '<p><i>Enable debugging to get additional information about this error.</i></p>' . get_manual_link('debugging','How to enable debug mode?');
2572          }
2573          echo '</div>';
2574  
2575          // Append the error text to AJAX log if it is AJAX request
2576          global $Ajaxlog;
2577          if( ! empty( $Ajaxlog ) )
2578          {
2579              $Ajaxlog->add( $additional_info, 'error' );
2580              $Ajaxlog->display( NULL, NULL, true, 'all',
2581                              array(
2582                                      'error' => array( 'class' => 'jslog_error', 'divClass' => false ),
2583                                      'note'  => array( 'class' => 'jslog_note',  'divClass' => false ),
2584                                  ), 'ul', 'jslog' );
2585          }
2586      }
2587  
2588      if( $debug )
2589      {
2590          echo debug_get_backtrace();
2591      }
2592  
2593      // Attempt to keep the html valid (but it doesn't really matter anyway)
2594      echo '</body></html>';
2595  
2596      die(2); // Error code 2. Note: this will still call the shutdown function.
2597  }
2598  
2599  
2600  /**
2601   * Outputs debug info, according to {@link $debug} or $force param. This gets called typically at the end of the page.
2602   *
2603   * @param boolean true to force output regardless of {@link $debug}
2604   * @param boolean true to force clean output (without HTML) regardless of {@link $is_cli}
2605   */
2606  function debug_info( $force = false, $force_clean = false )
2607  {
2608      global $debug, $debug_done, $debug_jslog, $debug_jslog_done, $Debuglog, $DB, $obhandler_debug, $Timer, $ReqHost, $ReqPath, $is_cli;
2609      global $cache_imgsize, $cache_File;
2610      global $Session;
2611      global $db_config, $tableprefix, $http_response_code, $disp, $disp_detail, $robots_index, $robots_follow, $content_type_header;
2612      /**
2613       * @var Hit
2614       */
2615      global $Hit;
2616  
2617      // Detect content-type
2618      $content_type = NULL;
2619      foreach(headers_list() as $header)
2620      {
2621          if( stripos($header, 'content-type:') !== false )
2622          { // content type sent
2623              # "Content-Type:text/html;charset=utf-8" => "text/html"
2624              $content_type = trim(array_shift(explode(';', array_pop(explode(':', $header, 2)))));
2625              break;
2626          }
2627      }
2628  
2629      // ---- Print AJAX Log
2630      if( empty( $debug_jslog_done ) && ( $debug || $debug_jslog ) && $content_type == 'text/html' )
2631      {    // Display debug jslog once
2632          global $rsc_url, $app_version;
2633  
2634          echo '<script type="text/javascript" src="'.$rsc_url.'js/debug_jslog.js"></script>';
2635          echo '<script type="text/javascript" src="'.$rsc_url.'js/jquery/jquery.cookie.min.js"></script>';
2636          $jquery_ui_css_url = url_add_param( $rsc_url.'css/jquery/smoothness/jquery-ui.css', 'v='.$app_version );
2637          echo '<link href="'.$jquery_ui_css_url.'" type="text/css" rel="stylesheet" />';
2638  
2639          $jslog_style_cookies = param_cookie( 'jslog_style', 'string' );
2640          $jslog_styles = array();
2641          if( !empty( $jslog_style_cookies ) )
2642          {    // Get styles only from cookies
2643              $jslog_style_cookies = explode( ';', $jslog_style_cookies );
2644              foreach( $jslog_style_cookies as $jsc => $style )
2645              {
2646                  if( strpos( $style, 'height' ) !== false /*|| ( strpos( $style, 'display' ) !== false && !$debug_jslog )*/ )
2647                  {    // Unset the height param from defined styles ( and the display param if jslog is disabled )
2648                      unset( $jslog_style_cookies[$jsc] );
2649                  }
2650              }
2651              $jslog_styles[] = implode( ';', $jslog_style_cookies );
2652          }
2653          else
2654          {
2655              if( !is_logged_in() )
2656              {    // Align top when evobar is hidden
2657                  $jslog_styles[] = 'top:0';
2658              }
2659              if( $debug_jslog )
2660              {    // Display the jslog
2661                  $jslog_styles[] = 'display:block';
2662              }
2663          }
2664          $jslog_styles = count( $jslog_styles ) > 0 ? ' style="'.implode( ';', $jslog_styles ).'"' : '';
2665  
2666          $close_url = url_add_param( $_SERVER['REQUEST_URI'], 'jslog' );
2667          echo '<div id="debug_ajax_info" class="debug"'.$jslog_styles.'>';
2668          echo '<div class="jslog_titlebar">AJAX Debug log'.get_manual_link('ajax_debug_log').
2669                  action_icon( T_('Close'), 'close', $close_url, NULL, NULL, NULL, array( 'class' => 'jslog_switcher' ) ).
2670              '</div>';
2671          echo '<div id="jslog_container"></div>';
2672          echo '<div class="jslog_statusbar">'.
2673                  '<a href="'.$_SERVER['REQUEST_URI'].'#" class="jslog_clear">'.T_('Clear').'</a>'.
2674              '</div>';
2675          echo '</div>';
2676  
2677          // Make sure debug jslog output only happens once:
2678          $debug_jslog_done = true;
2679      }
2680      // ----
2681  
2682      if( !$force )
2683      {
2684          if( !empty($debug_done))
2685          { // Already displayed!
2686              return;
2687          }
2688  
2689          if( empty($debug) )
2690          { // No debug output desired:
2691              return;
2692          }
2693  
2694          // Do not display, if no content-type header has been sent or it's != "text/html" (debug > 1 skips this)
2695          if( $debug < 2 )
2696          {
2697              if( $content_type != 'text/html' )
2698              {
2699                  return;
2700              }
2701          }
2702      }
2703      //Make sure debug output only happens once:
2704      $debug_done = true;
2705  
2706      // clean output:
2707      $clean = $is_cli || $force_clean;
2708      $printf_format = '| %-45s | %-5s | %-7s | %-5s |';
2709      $table_headerlen = 73;
2710      /* This calculates the number of dashes to print e. g. on the top and
2711       * bottom of the table and after the header, making the table look
2712       * better (looks like the tables of the mysql command line client).
2713       * Normally, the value won't change, so it's hardcoded above. If you
2714       * change the printf() format above, this might be useful.
2715      preg_match_all( '#\d+#', $printf_format, $table_headerlen );
2716      $table_headerlen = array_sum( $table_headerlen[0] ) +
2717                                      strlen( preg_replace( '#[^ \|]+#', '',
2718                                                  $printf_format ) ) - 2;
2719      */
2720  
2721      $ReqHostPathQuery = $ReqHost.$ReqPath.( empty( $_SERVER['QUERY_STRING'] ) ? '' : '?'.$_SERVER['QUERY_STRING'] );
2722  
2723      echo "\n\n\n";
2724      echo ( $clean ? '*** Debug info ***'."\n\n" : '<div class="debug" id="debug_info"><h2>Debug info</h2>' );
2725  
2726      if( !$obhandler_debug )
2727      { // don't display changing items when we want to test obhandler
2728  
2729          // ---------------------------
2730  
2731          echo '<div class="log_container"><div>';
2732  
2733          echo 'HTTP Response code: '.$http_response_code;
2734          echo $clean ? "\n" : '<br />';
2735  
2736          echo '$content_type_header: '.$content_type_header;
2737          echo $clean ? "\n" : '<br />';
2738  
2739          echo '$disp: '.$disp.' -- detail: '.$disp_detail;
2740          echo $clean ? "\n" : '<br />';
2741  
2742          echo '$robots_index: '.$robots_index;
2743          echo $clean ? "\n" : '<br />';
2744  
2745          echo '$robots_follow: '.$robots_follow;
2746          echo $clean ? "\n" : '<br />';
2747  
2748          echo '</div></div>';
2749  
2750          // ================================== DB Summary ================================
2751          if( isset($DB) )
2752          {
2753              echo '<div class="log_container"><div>';
2754              echo $DB->num_queries.' SQL queries executed in '.$Timer->get_duration( 'SQL QUERIES' )." seconds\n";
2755              if( ! $clean )
2756              {
2757                  echo ' &nbsp; <a href="'.$ReqHostPathQuery.'#evo_debug_queries">scroll down to details</a><p>';
2758              }
2759              echo '</div></div>';
2760          }
2761  
2762          // ========================== Timer table ================================
2763          $time_page = $Timer->get_duration( 'total' );
2764          $timer_rows = array();
2765          foreach( $Timer->get_categories() as $l_cat )
2766          {
2767              if( $l_cat == 'sql_query' )
2768              {
2769                  continue;
2770              }
2771              $timer_rows[ $l_cat ] = $Timer->get_duration( $l_cat );
2772          }
2773          // Don't sort to see orginal order of creation
2774          // arsort( $timer_rows );
2775          // ksort( $timer_rows );
2776  
2777          // Remove "total", it will get output as the last one:
2778          $total_time = $timer_rows['total'];
2779          unset($timer_rows['total']);
2780  
2781          $percent_total = $time_page > 0 ? number_format( 100/$time_page * $total_time, 2 ) : '0';
2782  
2783          if( $clean )
2784          {
2785              echo '== Timers =='."\n\n";
2786              echo '+'.str_repeat( '-', $table_headerlen ).'+'."\n";
2787              printf( $printf_format."\n", 'Category', 'Time', '%', 'Count' );
2788              echo '+'.str_repeat( '-', $table_headerlen ).'+'."\n";
2789          }
2790          else
2791          {
2792              echo '<table class="debug_timer"><thead>'
2793                  .'<tr><td colspan="4" class="center">Timers</td></tr>' // dh> TODO: should be TH. Workaround so that tablesorter does not pick it up. Feedback from author requested.
2794                  .'<tr><th>Category</th><th>Time</th><th>%</th><th>Count</th></tr>'
2795                  .'</thead>';
2796  
2797              // Output "total":
2798              echo "\n<tfoot><tr>"
2799                  .'<td>total</td>'
2800                  .'<td class="right red">'.$total_time.'</td>'
2801                  .'<td class="right">'.$percent_total.'%</td>'
2802                  .'<td class="right">'.$Timer->get_count('total').'</td></tr></tfoot>';
2803  
2804              echo '<tbody>';
2805          }
2806  
2807          $table_rows_collapse = array();
2808          foreach( $timer_rows as $l_cat => $l_time )
2809          {
2810              $percent_l_cat = $time_page > 0 ? number_format( 100/$time_page * $l_time, 2 ) : '0';
2811  
2812              if( $clean )
2813              {
2814                  $row = sprintf( $printf_format, $l_cat, $l_time, $percent_l_cat.'%', $Timer->get_count( $l_cat ) );
2815              }
2816              else
2817              {
2818                  $row = "\n<tr>"
2819                      .'<td>'.$l_cat.'</td>'
2820                      .'<td class="right">'.$l_time.'</td>'
2821                      .'<td class="right">'.$percent_l_cat.'%</td>'
2822                      .'<td class="right">'.$Timer->get_count( $l_cat ).'</td></tr>';
2823              }
2824  
2825              // Maybe ignore this row later, but not for clean display.
2826              if( ! $clean && ( $percent_l_cat < 1  ) )
2827              {    // Hide everything that tool less tahn 5% of the time
2828                  $table_rows_collapse[] = $row;
2829              }
2830              else
2831              {
2832                  echo $row."\n";
2833              }
2834          }
2835          $count_collapse = count($table_rows_collapse);
2836          // Collapse ignored rows, allowing to expand them with Javascript:
2837          if( $count_collapse > 5 )
2838          {
2839              echo '<tr><td colspan="4" class="center" id="evo-debuglog-timer-long-header">';
2840              echo '<a href="" onclick="var e = document.getElementById(\'evo-debuglog-timer-long\'); e.style.display = (e.style.display == \'none\' ? \'\' : \'none\'); return false;">+ '.$count_collapse.' queries &lt; 1%</a> </td></tr>';
2841              echo '</tbody>';
2842              echo '<tbody id="evo-debuglog-timer-long" style="display:none;">';
2843          }
2844          echo implode( "\n", $table_rows_collapse )."\n";
2845  
2846          if ( $clean )
2847          { // "total" (done in tfoot for html above)
2848              echo sprintf( $printf_format, 'total', $total_time, $percent_total.'%', $Timer->get_count('total') );
2849              echo '+'.str_repeat( '-', $table_headerlen ).'+'."\n\n";
2850          }
2851          else
2852          {
2853              echo "\n</tbody></table>";
2854  
2855              // add jquery.tablesorter to the "Debug info" table.
2856              global $rsc_uri;
2857              echo '
2858              <script type="text/javascript" src="'.$rsc_uri.'js/jquery/jquery.tablesorter.min.js"></script>
2859              <script type="text/javascript">
2860              (function($){
2861                  var clicked_once;
2862                  jQuery("table.debug_timer th").click( function(event) {
2863                      if( clicked_once ) return; else clicked_once = true;
2864                      jQuery("#evo-debuglog-timer-long tr").appendTo(jQuery("table.debug_timer tbody")[0]);
2865                      jQuery("#evo-debuglog-timer-long-header").remove();
2866                      // click for tablesorter:
2867                      jQuery("table.debug_timer").tablesorter();
2868                      jQuery(event.currentTarget).click();
2869                  });
2870              })(jQuery);
2871              </script>';
2872          }
2873  
2874  
2875          // ================================ Opcode caching ================================
2876          echo '<div class="log_container"><div>';
2877          echo 'Opcode cache: '.get_active_opcode_cache();
2878          echo $clean ? "\n" : '<p>';
2879          echo '</div></div>';
2880  
2881          // ================================ Memory Usage ================================
2882          echo '<div class="log_container"><div>';
2883  
2884          foreach( array( // note: 8MB is default for memory_limit and is reported as 8388608 bytes
2885              'memory_get_usage' => array( 'display' => 'Memory usage', 'high' => 8000000 ),
2886              'memory_get_peak_usage' /* PHP 5.2 */ => array( 'display' => 'Memory peak usage', 'high' => 8000000 ) ) as $l_func => $l_var )
2887          {
2888              if( function_exists( $l_func ) )
2889              {
2890                  $_usage = $l_func();
2891  
2892                  if( $_usage > $l_var['high'] )
2893                  {
2894                      echo $clean ? '[!!] ' : '<span style="color:red; font-weight:bold">';
2895                  }
2896  
2897                  echo $l_var['display'].': '.bytesreadable( $_usage, ! $clean );
2898  
2899                  if( ! $clean && $_usage > $l_var['high'] )
2900                  {
2901                      echo '</span>';
2902                  }
2903                  echo $clean ? "\n" : '<br />';
2904              }
2905          }
2906  
2907          echo 'Len of serialized $cache_imgsize: '.strlen(serialize($cache_imgsize));
2908          echo $clean ? "\n" : '<br />';
2909          echo 'Len of serialized $cache_File: '.strlen(serialize($cache_File));
2910          echo $clean ? "\n" : '<br />';
2911  
2912          echo '</div></div>';
2913      }
2914  
2915  
2916      // DEBUGLOG(s) FROM PREVIOUS SESSIONS, after REDIRECT(s) (with list of categories at top):
2917      if( isset($Session) && ($sess_Debuglogs = $Session->get('Debuglogs')) && ! empty($sess_Debuglogs) )
2918      {
2919          $count_sess_Debuglogs = count($sess_Debuglogs);
2920          if( $count_sess_Debuglogs > 1 )
2921          { // Links to those Debuglogs:
2922              if ( $clean )
2923              {    // kind of useless, but anyway...
2924                  echo "\n".'There are '.$count_sess_Debuglogs.' Debuglogs from redirected pages.'."\n";
2925              }
2926              else
2927              {
2928                  echo '<p>There are '.$count_sess_Debuglogs.' Debuglogs from redirected pages: ';
2929                  for( $i = 1; $i <= $count_sess_Debuglogs; $i++ )
2930                  {
2931                      echo '<a href="'.$ReqHostPathQuery.'#debug_sess_debuglog_'.$i.'">#'.$i.'</a> ';
2932                  }
2933                  echo '</p>';
2934              }
2935          }
2936  
2937          foreach( $sess_Debuglogs as $k => $sess_Debuglog )
2938          {
2939              $log_categories = array( 'error', 'note', 'all' ); // Categories to output (in that order)
2940  
2941              if( $clean )
2942              {
2943                  $log_container_head = "\n".'== Debug messages from redirected page (#'.($k+1).') =='."\n"
2944                                       .'See below for the Debuglog from the current request.'."\n";
2945                  echo format_to_output(
2946                      $sess_Debuglog->display( array(
2947                              'container' => array( 'string' => $log_container_head, 'template' => false ),
2948                              'all' => array( 'string' => '= %s ='."\n\n", 'template' => false ) ),
2949                          '', false, $log_categories, '', 'raw', false ),
2950                      'raw' );
2951              }
2952              else
2953              {
2954                  $log_container_head = '<h3 id="debug_sess_debuglog_'.($k+1).'" style="color:#f00;">Debug messages from redirected page (#'.($k+1).')</h3>'
2955                      // link to real Debuglog:
2956                      .'<p><a href="'.$ReqHostPathQuery.'#debug_debuglog">See below for the Debuglog from the current request.</a></p>';
2957                  $log_cats = array_keys($sess_Debuglog->get_messages( $log_categories )); // the real list (with all replaced and only existing ones)
2958                  $log_head_links = array();
2959  
2960                  foreach( $log_cats as $l_cat )
2961                  {
2962                      $log_head_links[] .= '<a href="'.$ReqHostPathQuery.'#debug_redir_'.($k+1).'_info_cat_'.str_replace( ' ', '_', $l_cat ).'">'.$l_cat.'</a>';
2963                  }
2964                  $log_container_head .= implode( ' | ', $log_head_links );
2965  
2966                  echo format_to_output(
2967                      $sess_Debuglog->display( array(
2968                              'container' => array( 'string' => $log_container_head, 'template' => false ),
2969                              'all' => array( 'string' => '<h4 id="debug_redir_'.($k+1).'_info_cat_%s">%s:</h4>', 'template' => false ) ),
2970                          '', false, $log_categories ),
2971                      'htmlbody' );
2972              }
2973          }
2974  
2975          // Delete logs since they have been displayed...
2976          // EXCEPT if we are redirecting, because in this case we won't see these logs in a browser (only in request debug tools)
2977          // So in that case we want them to move over to the next page...
2978          if( $http_response_code < 300 || $http_response_code >= 400 )
2979          {    // This is NOT a 3xx redirect, assume debuglogs have been seen & delete them:
2980              $Session->delete( 'Debuglogs' );
2981          }
2982      }
2983  
2984  
2985      // CURRENT DEBUGLOG (with list of categories at top):
2986      $log_categories = array( 'error', 'note', 'all' ); // Categories to output (in that order)
2987      $log_container_head = $clean ? ( "\n".'== Debug messages =='."\n" ) : '<h3 id="debug_debuglog">Debug messages</h3>';
2988      if( ! empty($sess_Debuglogs) )
2989      { // link to first sess_Debuglog:
2990          if ( $clean )
2991          {
2992              $log_container_head .= 'See above for the Debuglog(s) from before the redirect.'."\n";
2993          }
2994          else
2995          {
2996              $log_container_head .= '<p><a href="'.$ReqHostPathQuery.'#debug_sess_debuglog_1">See above for the Debuglog(s) from before the redirect.</a></p>';
2997          }
2998      }
2999  
3000      if ( ! $clean )
3001      {
3002          $log_cats = array_keys($Debuglog->get_messages( $log_categories )); // the real list (with all replaced and only existing ones)
3003          $log_head_links = array();
3004          foreach( $log_cats as $l_cat )
3005          {
3006              $log_head_links[] .= '<a href="'.$ReqHostPathQuery.'#debug_info_cat_'.str_replace( ' ', '_', $l_cat ).'">'.$l_cat.'</a>';
3007          }
3008          $log_container_head .= implode( ' | ', $log_head_links );
3009  
3010          echo format_to_output(
3011              $Debuglog->display( array(
3012                      'container' => array( 'string' => $log_container_head, 'template' => false ),
3013                      'all' => array( 'string' => '<h4 id="debug_info_cat_%s">%s:</h4>', 'template' => false ) ),
3014                  '', false, $log_categories ),
3015              'htmlbody' );
3016  
3017          echo '<h3 id="evo_debug_queries">DB</h3>';
3018      }
3019      else
3020      {
3021          echo format_to_output(
3022              $Debuglog->display( array(
3023                      'container' => array( 'string' => $log_container_head, 'template' => false ),
3024                      'all' => array( 'string' => '= %s ='."\n\n", 'template' => false ) ),
3025                  '', false, $log_categories, '', 'raw', false ),
3026              'raw' );
3027  
3028          echo "\n".'== DB =='."\n\n";
3029      }
3030  
3031      if($db_config)
3032      {
3033          if ( ! $clean )
3034          {
3035              echo '<pre>';
3036          }
3037  
3038          echo 'Config DB Username: '.$db_config['user']."\n".
3039              'Config DB Database: '.$db_config['name']."\n".
3040               'Config DB Host: '.(isset($db_config['host']) ? $db_config['host'] : 'unset (localhost)')."\n".
3041               'Config DB tables prefix: '.$tableprefix."\n".
3042               'Config DB connection charset: '.$db_config['connection_charset']."\n";
3043  
3044          echo $clean ? "\n" : '</pre>';
3045      }
3046  
3047      if( !isset($DB) )
3048      {
3049          echo 'No DB object.'.( $clean ? "\n" : '' );
3050      }
3051      else
3052      {
3053          echo '<pre>Current DB charset: '.$DB->connection_charset."</pre>\n";
3054  
3055          $DB->dump_queries( ! $clean );
3056      }
3057  
3058      if ( ! $clean )
3059      {
3060          echo '</div>';
3061      }
3062  }
3063  
3064  
3065  /**
3066   * Check if the current request exceed the post max size limit.
3067   * If too much data was sent add an error message and call header redirect.
3068   */
3069  function check_post_max_size_exceeded()
3070  {
3071      global $Messages;
3072  
3073      if( ( $_SERVER['REQUEST_METHOD'] == 'POST' ) && empty( $_POST ) && empty( $_FILES ) && ( $_SERVER['CONTENT_LENGTH'] > 0 ) )
3074      {
3075          // Check post max size ini setting
3076          $post_max_size = ini_get( 'post_max_size' );
3077  
3078          // Convert post_max_size value to bytes
3079          switch ( substr( $post_max_size, -1 ) )
3080          {
3081              case 'G':
3082                  $post_max_size = $post_max_size * 1024;
3083              case 'M':
3084                  $post_max_size = $post_max_size * 1024;
3085              case 'K':
3086                  $post_max_size = $post_max_size * 1024;
3087          }
3088  
3089          // Add error message and redirect back to the referer url
3090          $Messages->add( sprintf( T_('You have sent too much data (too many large files?) for the server to process (%s sent / %s maximum). Please try again by sending less data/files at a time.'), bytesreadable( $_SERVER['CONTENT_LENGTH'] ), bytesreadable( $post_max_size ) ) );
3091          header_redirect( $_SERVER['HTTP_REFERER'] );
3092          exit(0); // Already exited here
3093      }
3094  }
3095  
3096  
3097  /**
3098   * Prevent email header injection.
3099   */
3100  function mail_sanitize_header_string( $header_str, $close_brace = false )
3101  {
3102      // Prevent injection! (remove everything after (and including) \n or \r)
3103      $header_str = preg_replace( '~(\r|\n).*$~s', '', trim($header_str) );
3104  
3105      if( $close_brace && strpos( $header_str, '<' ) !== false && strpos( $header_str, '>' ) === false )
3106      { // We have probably stripped the '>' at the end!
3107          $header_str .= '>';
3108      }
3109  
3110      return $header_str;
3111  }
3112  
3113  /**
3114   * Encode to RFC 1342 "Representation of Non-ASCII Text in Internet Message Headers"
3115   *
3116   * @param string
3117   * @param string 'Q' for Quoted printable, 'B' for base64
3118   */
3119  function mail_encode_header_string( $header_str, $mode = 'Q' )
3120  {
3121      global $evo_charset;
3122  
3123      /* mbstring way  (did not work for Alex RU)
3124      if( function_exists('mb_encode_mimeheader') )
3125      { // encode subject
3126          $orig = mb_internal_encoding();
3127          mb_internal_encoding('utf-8');
3128          $r = mb_encode_mimeheader( $header_str, 'utf-8', $mode );
3129          mb_internal_encoding($orig);
3130          return $r;
3131      }
3132      */
3133  
3134      if( preg_match( '~[^a-z0-9!*+\-/ ]~i', $header_str ) )
3135      {    // If the string actually needs some encoding
3136          if( $mode == 'Q' )
3137          {    // Quoted printable is best for reading with old/text terminal mail reading/debugging stuff:
3138              $header_str = preg_replace( '#[^a-z0-9!*+\-/ ]#ie', 'sprintf(\'=%02x\', ord(stripslashes(\'$0\')))', $header_str );
3139              $header_str = str_replace( ' ', '_', $header_str );
3140  
3141              $header_str = '=?'.$evo_charset.'?Q?'.$header_str.'?=';
3142          }
3143          else
3144          { // Base 64 -- Alex RU way:
3145              $header_str = '=?'.$evo_charset.'?B?'.base64_encode( $header_str ).'?=';
3146          }
3147      }
3148  
3149      return $header_str;
3150  }
3151  
3152  
3153  /**
3154   * Get setting's value from General or User's settings
3155   *
3156   * @param integer User ID
3157   * @param string Setting ( email | name )
3158   * @return string Setting's value
3159   */
3160  function user_get_notification_sender( $user_ID, $setting )
3161  {
3162      global $Settings;
3163  
3164      $setting_name = 'notification_sender_'.$setting;
3165  
3166      if( empty( $user_ID ) )
3167      {    // Get value from general settings
3168          return $Settings->get( $setting_name );
3169      }
3170  
3171      $UserCache = & get_UserCache();
3172      if( $User = & $UserCache->get_by_ID( $user_ID ) )
3173      {
3174          if( $User->check_status( 'is_validated' ) )
3175          {    // User is Activated or Autoactivated
3176              global $UserSettings;
3177              if( $UserSettings->get( $setting_name, $user_ID ) == '' )
3178              {    // The user's setting is not defined yet
3179                  // Update the user's setting from general setting
3180                  $UserSettings->set( $setting_name, $Settings->get( $setting_name ), $user_ID );
3181                  $UserSettings->dbupdate();
3182              }
3183              else
3184              {    // User has a defined setting; Use this
3185                  return $UserSettings->get( $setting_name, $user_ID );
3186              }
3187          }
3188      }
3189  
3190      return $Settings->get( $setting_name );
3191  }
3192  
3193  
3194  /**
3195   * Sends an email, wrapping PHP's mail() function.
3196   * ALL emails sent by b2evolution must be sent through this function (for consistency and for logging)
3197   *
3198   * {@link $current_locale} will be used to set the charset.
3199   *
3200   * Note: we use a single \n as line ending, though it does not comply to {@link http://www.faqs.org/rfcs/rfc2822 RFC2822}, but seems to be safer,
3201   * because some mail transfer agents replace \n by \r\n automatically.
3202   *
3203   * @todo Unit testing with "nice addresses" This gets broken over and over again.
3204   *
3205   * @param string Recipient email address.
3206   * @param string Recipient name.
3207   * @param string Subject of the mail
3208   * @param string The message text
3209   * @param string From address, being added to headers (we'll prevent injections); see {@link http://securephp.damonkohler.com/index.php/Email_Injection}.
3210   *               Defaults to {@link GeneralSettings::get('notification_sender_email') } if NULL.
3211   * @param string From name.
3212   * @param array Additional headers ( headername => value ). Take care of injection!
3213   * @param integer User ID
3214   * @return boolean True if mail could be sent (not necessarily delivered!), false if not - (return value of {@link mail()})
3215   */
3216  function send_mail( $to, $to_name, $subject, $message, $from = NULL, $from_name = NULL, $headers = array(), $user_ID = NULL )
3217  {
3218      global $servertimenow;
3219  
3220      // Stop a request from the blocked IP addresses
3221      antispam_block_ip();
3222  
3223      global $debug, $app_name, $app_version, $current_locale, $current_charset, $evo_charset, $locales, $Debuglog, $Settings, $demo_mode, $sendmail_additional_params;
3224  
3225      $NL = "\r\n";
3226      $message = str_replace( array( "\r\n", "\r" ), $NL, $message );
3227  
3228      // Replace secret content in the mail logs message body
3229      $message_log = preg_replace( '~\$secret_content_start\$.*\$secret_content_end\$~', '***secret-content-removed***', $message );
3230      // Remove secret content marks from the message
3231      $message = str_replace( array( '$secret_content_start$', '$secret_content_end$' ), '', $message );
3232  
3233      // Memorize email address
3234      $to_email_address = $to;
3235  
3236      if( $demo_mode )
3237      { // Debug mode restriction: Sending email in debug mode is not allowed
3238          return false;
3239      }
3240  
3241      if( !is_array( $headers ) )
3242      { // Make sure $headers is an array
3243          $headers = array( $headers );
3244      }
3245  
3246      if( empty( $from ) )
3247      {
3248          $from = user_get_notification_sender( $user_ID, 'email' );
3249      }
3250  
3251      if( empty( $from_name ) )
3252      {
3253          $from_name = user_get_notification_sender( $user_ID, 'name' );
3254      }
3255  
3256      $return_path = $Settings->get( 'notification_return_path' );
3257  
3258      // Add real name into $from...
3259      if( ! is_windows() )
3260      {    // fplanque: Windows XP, Apache 1.3, PHP 4.4, MS SMTP : will not accept "nice" addresses.
3261          if( !empty( $to_name ) )
3262          {
3263              $to = '"'.mail_encode_header_string($to_name).'" <'.$to.'>';
3264          }
3265          if( !empty( $from_name ) )
3266          {
3267              $from = '"'.mail_encode_header_string($from_name).'" <'.$from.'>';
3268          }
3269      }
3270  
3271      $from = mail_sanitize_header_string( $from, true );
3272      // From has to go into headers
3273      $headers['From'] = $from;
3274      if( !empty( $return_path ) )
3275      {    // Set a return path
3276          $headers['Return-Path'] = $return_path;
3277      }
3278  
3279      // echo 'sending email to: ['.htmlspecialchars($to).'] from ['.htmlspecialchars($from).']';
3280  
3281      $clear_subject = $subject;
3282      $subject = mail_encode_header_string($subject);
3283  
3284      // Convert encoding of message (from internal encoding to the one of the message):
3285      // fp> why do we actually convert to $current_charset?
3286      // dh> I do not remember. Appears to make sense sending it unconverted in $evo_charset.
3287      // asimo> converting the message creates wrong output, no need for conversion, however this needs further investigation
3288      // $message = convert_charset( $message, $current_charset, $evo_charset );
3289  
3290      if( !isset( $headers['Content-Type'] ) )
3291      {    // Specify charset and content-type of email
3292          $headers['Content-Type'] = 'text/plain; charset='.$current_charset;
3293      }
3294      $headers['MIME-Version'] = '1.0';
3295  
3296      $headers['Date'] = gmdate( 'r', $servertimenow );
3297  
3298      // ADDITIONAL HEADERS:
3299      $headers['X-Mailer'] = $app_name.' '.$app_version.' - PHP/'.phpversion();
3300      $ip_list = implode( ',', get_ip_list() );
3301      if( !empty( $ip_list ) )
3302      { // Add X-Remote_Addr param only if its value is not empty
3303          $headers['X-Remote-Addr'] = $ip_list;
3304      }
3305  
3306      // COMPACT HEADERS:
3307      $headerstring = '';
3308      reset( $headers );
3309      while( list( $lKey, $lValue ) = each( $headers ) )
3310      { // Add additional headers
3311          $headerstring .= $lKey.': '.$lValue.$NL;
3312      }
3313  
3314      // Set an additional parameter for the return path:
3315      if( ! empty( $sendmail_additional_params ) )
3316      {
3317          $additional_parameters = str_replace(
3318              array( '$from-address$', '$return-address$' ),
3319              array( $from, ( empty( $return_path ) ? $from : $return_path ) ),
3320              $sendmail_additional_params );
3321      }
3322      else
3323      {
3324          $additional_parameters = '';
3325      }
3326  
3327      if( mail_is_blocked( $to_email_address ) )
3328      { // Check if the email address is blocked
3329          $Debuglog->add( 'Sending mail to &laquo;'.htmlspecialchars( $to_email_address ).'&raquo; FAILED, because this email marked with spam or permanent errors.', 'error' );
3330  
3331          mail_log( $user_ID, $to_email_address, $clear_subject, $message_log, $headerstring, 'blocked' );
3332  
3333          return false;
3334      }
3335  
3336      // SEND MESSAGE:
3337      if( $debug > 1 )
3338      {    // We agree to die for debugging...
3339          if( ! mail( $to, $subject, $message, $headerstring, $additional_parameters ) )
3340          {
3341              mail_log( $user_ID, $to_email_address, $clear_subject, $message_log, $headerstring, 'error' );
3342  
3343              debug_die( 'Sending mail from &laquo;'.htmlspecialchars($from).'&raquo; to &laquo;'.htmlspecialchars($to).'&raquo;, Subject &laquo;'.htmlspecialchars($subject).'&raquo; FAILED.' );
3344          }
3345      }
3346      else
3347      {    // Soft debugging only....
3348          if( ! @mail( $to, $subject, $message, $headerstring, $additional_parameters ) )
3349          {
3350              $Debuglog->add( 'Sending mail from &laquo;'.htmlspecialchars($from).'&raquo; to &laquo;'.htmlspecialchars($to).'&raquo;, Subject &laquo;'.htmlspecialchars($subject).'&raquo; FAILED.', 'error' );
3351  
3352              mail_log( $user_ID, $to_email_address, $clear_subject, $message_log, $headerstring, 'error' );
3353  
3354              return false;
3355          }
3356      }
3357  
3358      $Debuglog->add( 'Sent mail from &laquo;'.htmlspecialchars($from).'&raquo; to &laquo;'.htmlspecialchars($to).'&raquo;, Subject &laquo;'.htmlspecialchars($subject).'&raquo;.' );
3359  
3360      mail_log( $user_ID, $to_email_address, $clear_subject, $message_log, $headerstring, 'ok' );
3361  
3362      return true;
3363  }
3364  
3365  
3366  /**
3367   * Sends an email to User
3368   *
3369   * @param integer Recipient ID.
3370   * @param string Subject of the mail
3371   * @param string Email template name
3372   * @param array Email template params
3373   * @param boolean Force to send this email even if the user is not activated. By default not activated user won't get emails.
3374   *                Pasword reset, and account activation emails must be always forced.
3375   * @param array Additional headers ( headername => value ). Take care of injection!
3376   * @return boolean True if mail could be sent (not necessarily delivered!), false if not - (return value of {@link mail()})
3377   */
3378  function send_mail_to_User( $user_ID, $subject, $template_name, $template_params = array(), $force_on_non_activated = false, $headers = array() )
3379  {
3380      global $UserSettings, $Settings, $current_charset;
3381  
3382      $UserCache = & get_UserCache();
3383      if( $User = $UserCache->get_by_ID( $user_ID ) )
3384      {
3385          if( !$User->check_status( 'can_receive_any_message' ) )
3386          { // user status doesn't allow to receive nor emails nor private messages
3387              return false;
3388          }
3389  
3390          if( !( $User->check_status( 'is_validated' ) || $force_on_non_activated ) )
3391          { // user is not activated and non activated users should not receive emails, unless force_on_non_activated is turned on
3392              return false;
3393          }
3394  
3395          // UserSettings update is not required yet
3396          $update_settings = false;
3397          // Check if a new email to User with the corrensponding email type is allowed
3398          switch( $template_name )
3399          {
3400              case 'account_activate':
3401                  if( $Settings->get( 'validation_process' ) == 'easy' && !$template_params['is_reminder'] )
3402                  { // this is not a notification email
3403                      break;
3404                  }
3405              case 'private_message_new':
3406              case 'private_messages_unread_reminder':
3407              case 'post_new':
3408              case 'comment_new':
3409              case 'account_activated':
3410              case 'account_closed':
3411              case 'account_reported':
3412                  // this is a notificaiton email
3413                  if( !check_allow_new_email( 'notification_email_limit', 'last_notification_email', $User->ID ) )
3414                  { // more notification email is not allowed today
3415                      return false;
3416                  }
3417                  $update_settings = true;
3418                  break;
3419              case 'newsletter':
3420                  // this is a newsletter email
3421                  if( !check_allow_new_email( 'newsletter_limit', 'last_newsletter', $User->ID ) )
3422                  { // more newsletter email is not allowed today
3423                      return false;
3424                  }
3425                  $update_settings = true;
3426                  break;
3427          }
3428  
3429          // Update notification sender's info from General settings
3430          $User->update_sender();
3431  
3432          switch( $UserSettings->get( 'email_format', $User->ID ) )
3433          {    // Set Content-Type from user's setting "Email format"
3434              case 'auto':
3435                  $template_params['boundary'] = 'b2evo-'.md5( rand() );
3436                  $headers['Content-Type'] = 'multipart/mixed; boundary="'.$template_params['boundary'].'"';
3437                  break;
3438              case 'html':
3439                  $headers['Content-Type'] = 'text/html; charset='.$current_charset;
3440                  break;
3441              case 'text':
3442                  $headers['Content-Type'] = 'text/plain; charset='.$current_charset;
3443                  break;
3444          }
3445  
3446          // Get a message text from template file
3447          $message = mail_template( $template_name, $UserSettings->get( 'email_format', $User->ID ), $template_params, $User );
3448  
3449          // Autoinsert user's data
3450          $subject = mail_autoinsert_user_data( $subject, $User );
3451          $message = mail_autoinsert_user_data( $message, $User );
3452  
3453          if( send_mail( $User->email, NULL, $subject, $message, NULL, NULL, $headers, $user_ID ) )
3454          { // email was sent, update last email settings;
3455              if( $update_settings )
3456              { // User Settings need to be updated
3457                  $UserSettings->dbupdate();
3458              }
3459              return true;
3460          }
3461      }
3462  
3463      // No user or email could not be sent
3464      return false;
3465  }
3466  
3467  
3468  /**
3469   * Autoinsert user's data into subject or message of the email
3470   *
3471   * @param string Text
3472   * @param object User
3473   * @return string Text
3474  */
3475  function mail_autoinsert_user_data( $text, $User = NULL )
3476  {
3477      if( !$User )
3478      {    // No user
3479          return $text;
3480      }
3481  
3482      $rpls_from = array( '$login$' , '$email$', '$user_ID$', '$unsubscribe_key$' );
3483      $rpls_to = array( $User->login, $User->email, $User->ID, '$secret_content_start$'.md5( $User->ID.$User->unsubscribe_key ).'$secret_content_end$' );
3484  
3485      return str_replace( $rpls_from, $rpls_to, $text );
3486  }
3487  
3488  
3489  /**
3490   * Get a mail message text by template name
3491   *
3492   * @param string Template name
3493   * @param string Email format ( auto | html | text )
3494   * @param array Params
3495   * @param object User
3496   * @return string Mail message
3497   */
3498  function mail_template( $template_name, $format = 'auto', $params = array(), $User = NULL )
3499  {
3500      global $current_charset, $is_admin_page;
3501  
3502      if( !empty( $params['locale'] ) )
3503      { // Switch to locale for current email template
3504          locale_temp_switch( $params['locale'] );
3505      }
3506  
3507      $value_is_admin_page = $is_admin_page;
3508      // Set TRUE to use gender settings from back office
3509      $is_admin_page = true;
3510  
3511      // Set extension of template
3512      $template_exts = array();
3513      switch( $format )
3514      {
3515          case 'auto':
3516              // $template_exts['non-mime'] = '.txt.php'; // The area that is ignored by MIME-compliant clients
3517              $template_exts['text'] = '.txt.php';
3518              $template_exts['html'] = '.html.php';
3519              $boundary = $params['boundary'];
3520              $boundary_alt = 'b2evo-alt-'.md5( rand() );
3521              $template_headers = array(
3522                      'text' => 'Content-Type: text/plain; charset='.$current_charset,
3523                      'html' => 'Content-Type: text/html; charset='.$current_charset,
3524                  );
3525              break;
3526  
3527          case 'html':
3528              $template_exts['html'] = '.html.php';
3529              break;
3530  
3531          case 'text':
3532              $template_exts['text'] = '.txt.php';
3533              break;
3534      }
3535  
3536      $template_message = '';
3537  
3538      if( isset( $boundary, $boundary_alt ) )
3539      { // Start new boundary content
3540          $template_message .= "\n".'--'.$boundary."\n";
3541          $template_message .= 'Content-Type: multipart/alternative; boundary="'.$boundary_alt.'"'."\n\n";
3542      }
3543  
3544      foreach( $template_exts as $format => $ext )
3545      {
3546          if( isset( $boundary, $boundary_alt ) && $format != 'non-mime' )
3547          { // Start new boundary alt content
3548              $template_message .= "\n".'--'.$boundary_alt."\n";
3549          }
3550  
3551          if( isset( $template_headers[ $format ] ) )
3552          { // Header data for each content
3553              $template_message .= $template_headers[ $format ]."\n\n";
3554          }
3555  
3556          // Get mail template
3557          ob_start();
3558          emailskin_include( $template_name.$ext, $params );
3559          $template_message .= ob_get_clean();
3560  
3561          if( !empty( $User ) )
3562          { // Replace $login$ with gender colored link + icon in HTML format,
3563            //   and with simple login text in PLAIN TEXT format
3564              $user_login = $format == 'html' ? $User->get_colored_login( array( 'mask' => '$avatar$ $login$' ) ) : $User->login;
3565              $template_message = str_replace( '$login$', $user_login, $template_message );
3566          }
3567      }
3568  
3569      if( isset( $boundary, $boundary_alt ) )
3570      { // End all boundary contents
3571          $template_message .= "\n".'--'.$boundary_alt.'--'."\n";
3572          $template_message .= "\n".'--'.$boundary.'--'."\n";
3573      }
3574  
3575      // Return back the value
3576      $is_admin_page = $value_is_admin_page;
3577  
3578      if( !empty( $params['locale'] ) )
3579      { // Restore previous locale
3580          locale_restore_previous();
3581      }
3582  
3583      return $template_message;
3584  }
3585  
3586  
3587  /**
3588   * Include email template from folder /skins_email/custom/ or /skins_email/
3589   *
3590   * @param string Template name
3591   * @param array Params
3592   */
3593  function emailskin_include( $template_name, $params = array() )
3594  {
3595      global $emailskins_path, $is_admin_page, $rsc_url;
3596  
3597      /**
3598      * @var Log
3599      */
3600      global $Debuglog;
3601      global $Timer;
3602  
3603      $timer_name = 'emailskin_include('.$template_name.')';
3604      $Timer->resume( $timer_name );
3605  
3606      $is_customized = false;
3607  
3608      // Try to include custom template firstly
3609      $template_path = $emailskins_path.'custom/'.$template_name;
3610      if( file_exists( $template_path ) )
3611      { // Include custom template file if it exists
3612          $Debuglog->add( 'emailskin_include: '.rel_path_to_base( $template_path ), 'skins' );
3613          require $template_path;
3614          // This template is customized, Don't include standard template
3615          $is_customized = true;
3616      }
3617  
3618      if( !$is_customized )
3619      { // Try to include standard template only if custom template doesn't exist
3620          $template_path = $emailskins_path.$template_name;
3621          if( file_exists( $template_path ) )
3622          { // Include standard template file if it exists
3623              $Debuglog->add( 'emailskin_include: '.rel_path_to_base( $template_path ), 'skins' );
3624              require $template_path;
3625          }
3626      }
3627  
3628      $Timer->pause( $timer_name );
3629  }
3630  
3631  
3632  /**
3633   * If first parameter evaluates to true printf() gets called using the first parameter
3634   * as args and the second parameter as print-pattern
3635   *
3636   * @param mixed variable to test and output if it's true or $disp_none is given
3637   * @param string printf-pattern to use (%s gets replaced by $var)
3638   * @param string printf-pattern to use, if $var is numeric and > 1 (%s gets replaced by $var)
3639   * @param string printf-pattern to use if $var evaluates to false (%s gets replaced by $var)
3640   */
3641  function disp_cond( $var, $disp_one, $disp_more = NULL, $disp_none = NULL )
3642  {
3643      if( is_numeric($var) && $var > 1 )
3644      {
3645          printf( ( $disp_more === NULL ? $disp_one : $disp_more ), $var );
3646          return true;
3647      }
3648      elseif( $var )
3649      {
3650          printf( $disp_one, $var );
3651          return true;
3652      }
3653      else
3654      {
3655          if( $disp_none !== NULL )
3656          {
3657              printf( $disp_none, $var );
3658              return false;
3659          }
3660      }
3661  }
3662  
3663  
3664  /**
3665   * Create IMG tag for an action icon.
3666   *
3667   * @param string TITLE text (IMG and A link)
3668   * @param string icon code for {@link get_icon()}
3669   * @param string URL where the icon gets linked to (empty to not wrap the icon in a link)
3670   * @param string word to be displayed after icon (if no icon gets displayed, $title will be used instead!)
3671   * @param integer 1-5: weight of the icon. The icon will be displayed only if its weight is >= than the user setting threshold.
3672   *                     Use 5, if it's a required icon - all others could get disabled by the user. (Default: 4)
3673   * @param integer 1-5: weight of the word. The word will be displayed only if its weight is >= than the user setting threshold.
3674   *                     (Default: 1)
3675   * @param array Additional attributes to the A tag. The values must be properly encoded for html output (e.g. quotes).
3676   *        It may also contain these params:
3677   *         - 'use_js_popup': if true, the link gets opened as JS popup. You must also pass an "id" attribute for this!
3678   *         - 'use_js_size': use this to override the default popup size ("500, 400")
3679   *         - 'class': defaults to 'action_icon', if not set; use "" to not use it
3680   * @param array Attributes for the icon
3681   * @return string The generated action icon link.
3682   */
3683  function action_icon( $title, $icon, $url, $word = NULL, $icon_weight = NULL, $word_weight = NULL, $link_attribs = array(), $icon_attribs = array() )
3684  {
3685      global $UserSettings;
3686  
3687      $link_attribs['href'] = $url;
3688      $link_attribs['title'] = $title;
3689  
3690      if( is_null($icon_weight) )
3691      {
3692          $icon_weight = 4;
3693      }
3694      if( is_null($word_weight) )
3695      {
3696          $word_weight = 1;
3697      }
3698  
3699      if( ! isset($link_attribs['class']) )
3700      {
3701          $link_attribs['class'] = 'action_icon';
3702      }
3703  
3704      if( get_icon( $icon, 'rollover' ) )
3705      {
3706          if( empty($link_attribs['class']) )
3707          {
3708              $link_attribs['class'] = 'rollover';
3709          }
3710          else
3711          {
3712              $link_attribs['class'] .= ' rollover';
3713          }
3714  
3715          if( get_icon( $icon, 'sprite' ) )
3716          { // Set class "rollover_sprite" If image uses sprite
3717              $link_attribs['class'] .= '_sprite';
3718          }
3719      }
3720      //$link_attribs['class'] .= $icon != '' ? ' '.$icon : ' noicon';
3721  
3722      // "use_js_popup": open link in a JS popup
3723      // TODO: this needs to be rewritten with jQuery instead
3724      if( false && ! empty($link_attribs['use_js_popup']) )
3725      {
3726          $popup_js = 'var win = new PopupWindow(); win.setUrl( \''.$link_attribs['href'].'\' ); win.setSize(  ); ';
3727  
3728          if( isset($link_attribs['use_js_size']) )
3729          {
3730              if( ! empty($link_attribs['use_js_size']) )
3731              {
3732                  $popup_size = $link_attribs['use_js_size'];
3733              }
3734          }
3735          else
3736          {
3737              $popup_size = '500, 400';
3738          }
3739          if( isset($popup_size) )
3740          {
3741              $popup_js .= 'win.setSize( '.$popup_size.' ); ';
3742          }
3743          $popup_js .= 'win.showPopup(\''.$link_attribs['id'].'\'); return false;';
3744  
3745          if( empty( $link_attribs['onclick'] ) )
3746          {
3747              $link_attribs['onclick'] = $popup_js;
3748          }
3749          else
3750          {
3751              $link_attribs['onclick'] .= $popup_js;
3752          }
3753          unset($link_attribs['use_js_popup']);
3754          unset($link_attribs['use_js_size']);
3755      }
3756  
3757      $display_icon = empty( $UserSettings ) ? false : ($icon_weight >= $UserSettings->get('action_icon_threshold'));
3758      $display_word = empty( $UserSettings ) ? false : ($word_weight >= $UserSettings->get('action_word_threshold'));
3759  
3760      $a_body = '';
3761  
3762      if( $display_icon || ! $display_word )
3763      {    // We MUST display an action icon in order to make the user happy:
3764          // OR we default to icon because the user doesn't want the word either!!
3765  
3766          $icon_attribs = array_merge( array(
3767                  'title' => $title
3768              ), $icon_attribs );
3769  
3770          if( $icon_s = get_icon( $icon, 'imgtag', $icon_attribs, true ) )
3771          {
3772              $a_body .= $icon_s;
3773          }
3774          else
3775          { // fallback to word
3776              $display_word = true;
3777          }
3778      }
3779  
3780      if( $display_word )
3781      {    // We MUST display an action word in order to make the user happy:
3782  
3783          if( $display_icon )
3784          { // We already have an icon, display a SHORT word:
3785              if( !empty($word) )
3786              {    // We have provided a short word:
3787                  $a_body .= $word;
3788              }
3789              else
3790              {    // We fall back to alt:
3791                  $a_body .= get_icon( $icon, 'legend' );
3792              }
3793          }
3794          else
3795          {    // No icon display, let's display a LONG word/text:
3796              $a_body .= trim( $title, ' .!' );
3797          }
3798  
3799          // Add class "hoverlink" for icon with text
3800          $link_attribs['class'] .= ' hoverlink';
3801      }
3802  
3803  
3804      // NOTE: We do not use format_to_output with get_field_attribs_as_string() here, because it interferes with the Results class (eval() fails on entitied quotes..) (blueyed)
3805      return '<a'.get_field_attribs_as_string( $link_attribs, false ).'>'.$a_body.'</a>';
3806  }
3807  
3808  
3809  /**
3810   * Get properties of an icon.
3811   *
3812   * Note: to get a file type icon, use {@link File::get_icon()} instead.
3813   *
3814   * @uses get_icon_info()
3815   * @param string icon for what? (key)
3816   * @param string what to return for that icon ('imgtag', 'alt', 'legend', 'file', 'url', 'size' {@link imgsize()})
3817   * @param array additional params
3818   *   - 'class' => class name when getting 'imgtag',
3819   *   - 'size' => param for 'size',
3820   *   - 'title' => title attribute for 'imgtag'
3821   * @param boolean true to include this icon into the legend at the bottom of the page (works for 'imgtag' only)
3822   * @return mixed False on failure, string on success.
3823   */
3824  function get_icon( $iconKey, $what = 'imgtag', $params = NULL, $include_in_legend = false )
3825  {
3826      global $admin_subdir, $Debuglog, $use_strict;
3827      global $conf_path;
3828      global $rsc_path, $rsc_uri;
3829  
3830      if( ! function_exists('get_icon_info') )
3831      {
3832          require_once $conf_path.'_icons.php';
3833      }
3834  
3835      $icon = get_icon_info($iconKey);
3836      if( ! $icon )
3837      {
3838          $Debuglog->add('No image defined for '.var_export( $iconKey, true ).'!', 'icons');
3839          return false;
3840      }
3841  
3842      if( !isset( $icon['file'] ) && $what != 'imgtag' )
3843      {
3844          $icon['file'] = 'icons/icons_sprite.png';
3845      }
3846  
3847      switch( $what )
3848      {
3849          case 'rollover':
3850              if( isset( $icon['rollover'] ) )
3851              {    // Image has rollover available
3852                  return $icon['rollover'];
3853              }
3854              return false;
3855              /* BREAK */
3856  
3857  
3858          case 'file':
3859              return $rsc_path.$icon['file'];
3860              /* BREAK */
3861  
3862  
3863          case 'alt':
3864              if( isset( $icon['alt'] ) )
3865              { // alt tag from $map_iconfiles
3866                  return $icon['alt'];
3867              }
3868              else
3869              { // fallback to $iconKey as alt-tag
3870                  return $iconKey;
3871              }
3872              /* BREAK */
3873  
3874  
3875          case 'legend':
3876              if( isset( $icon['legend'] ) )
3877              { // legend tag from $map_iconfiles
3878                  return $icon['legend'];
3879              }
3880              else
3881              if( isset( $icon['alt'] ) )
3882              { // alt tag from $map_iconfiles
3883                  return $icon['alt'];
3884              }
3885              else
3886              { // fallback to $iconKey as alt-tag
3887                  return $iconKey;
3888              }
3889              /* BREAK */
3890  
3891  
3892          case 'class':
3893              if( isset($icon['class']) )
3894              {
3895                  return $icon['class'];
3896              }
3897              else
3898              {
3899                  return '';
3900              }
3901              /* BREAK */
3902  
3903          case 'url':
3904              return $rsc_uri.$icon['file'];
3905              /* BREAK */
3906  
3907          case 'size':
3908              if( !isset( $icon['size'] ) )
3909              {
3910                  $Debuglog->add( 'No iconsize for ['.$iconKey.']', 'icons' );
3911  
3912                  $icon['size'] = imgsize( $rsc_path.$icon['file'] );
3913              }
3914  
3915              switch( $params['size'] )
3916              {
3917                  case 'width':
3918                      return $icon['size'][0];
3919  
3920                  case 'height':
3921                      return $icon['size'][1];
3922  
3923                  case 'widthxheight':
3924                      return $icon['size'][0].'x'.$icon['size'][1];
3925  
3926                  case 'width':
3927                      return $icon['size'][0];
3928  
3929                  case 'string':
3930                      return 'width="'.$icon['size'][0].'" height="'.$icon['size'][1].'"';
3931  
3932                  default:
3933                      return $icon['size'];
3934              }
3935              /* BREAK */
3936  
3937  
3938          case 'xy':
3939              if( isset( $icon['xy'] ) )
3940              { // Return data for style property "background-position"
3941                  return "-".$icon['xy'][0]."px -".$icon['xy'][1]."px";
3942              }
3943              return false;
3944  
3945  
3946          case 'sprite':
3947              if( isset( $icon['xy'] ) )
3948              {    // Image uses spite file
3949                  return true;
3950              }
3951              return false;
3952              /* BREAK */
3953  
3954  
3955          case 'imgtag':
3956              if( ! isset( $icon['file'] ) )
3957              { // Use span tag with sprite instead of img
3958                  $styles = array();
3959  
3960                  if( isset( $params['xy'] ) )
3961                  { // Get background position from params
3962                      $styles[] = "background-position: ".$params['xy'][0]."px ".$params['xy'][1]."px";
3963                      unset( $params['xy'] );
3964                  }
3965                  else if( isset( $icon['xy'] ) )
3966                  { // Set background position in the icons_sprite.png
3967                      $styles[] = "background-position: -".$icon['xy'][0]."px -".$icon['xy'][1]."px";
3968                  }
3969  
3970                  if( isset( $params['size'] ) )
3971                  { // Get sizes from params
3972                      $icon['size'] = $params['size'];
3973                      unset( $params['size'] );
3974                  }
3975                  if( isset( $icon['size'] ) )
3976                  { // Set width & height
3977                      if( $icon['size'][0] != 16 )
3978                      {
3979                          $styles[] = "width: ".$icon['size'][0]."px";
3980                      }
3981                      if( $icon['size'][1] != 16 )
3982                      {
3983                          $styles[] = "height: ".$icon['size'][1]."px; line-height: ".$icon['size'][1]."px";
3984                      }
3985                  }
3986  
3987                  if( isset( $params['style'] ) )
3988                  { // Get styles from params
3989                      $styles[] = $params['style'];
3990                  }
3991                  if( count( $styles ) > 0 )
3992                  {
3993                      $params['style'] = implode( '; ', $styles);
3994                  }
3995  
3996                  if( ! isset( $params['title'] ) )
3997                  {    // Use 'alt' for 'title'
3998                      if( isset( $params['alt'] ) )
3999                      {
4000                          $params['title'] = $params['alt'];
4001                          unset( $params['alt'] );
4002                      }
4003                      else if( ! isset( $params['alt'] ) && isset( $icon['alt'] ) )
4004                      {
4005                          $params['title'] = $icon['alt'];
4006                      }
4007                  }
4008  
4009                  if( isset( $params['class'] ) )
4010                  {    // Get class from params
4011                      $params['class'] = 'icon '.$params['class'];
4012                  }
4013                  else
4014                  {    // Set default class
4015                      $params['class'] = 'icon';
4016                  }
4017  
4018                  // Add all the attributes:
4019                  $params = get_field_attribs_as_string( $params, false );
4020  
4021                  $r = '<span'.$params.'>&nbsp;</span>';
4022              }
4023              else
4024              { // Use img tag
4025                  $r = '<img src="'.$rsc_uri.$icon['file'].'" ';
4026  
4027                  if( !$use_strict )
4028                  {    // Include non CSS fallbacks - transitional only:
4029                      $r .= 'border="0" align="top" ';
4030                  }
4031  
4032                  // Include class (will default to "icon"):
4033                  if( ! isset( $params['class'] ) )
4034                  {
4035                      if( isset($icon['class']) )
4036                      {    // This icon has a class
4037                          $params['class'] = $icon['class'];
4038                      }
4039                      else
4040                      {
4041                          $params['class'] = '';
4042                      }
4043                  }
4044  
4045                  // Include size (optional):
4046                  if( isset( $icon['size'] ) )
4047                  {
4048                      $r .= 'width="'.$icon['size'][0].'" height="'.$icon['size'][1].'" ';
4049                  }
4050  
4051                  // Include alt (XHTML mandatory):
4052                  if( ! isset( $params['alt'] ) )
4053                  {
4054                      if( isset( $icon['alt'] ) )
4055                      { // alt-tag from $map_iconfiles
4056                          $params['alt'] = $icon['alt'];
4057                      }
4058                      else
4059                      { // $iconKey as alt-tag
4060                          $params['alt'] = $iconKey;
4061                      }
4062                  }
4063  
4064                  // Add all the attributes:
4065                  $r .= get_field_attribs_as_string( $params, false );
4066  
4067                  // Close tag:
4068                  $r .= '/>';
4069  
4070  
4071                  if( $include_in_legend && ( $IconLegend = & get_IconLegend() ) )
4072                  { // This icon should be included into the legend:
4073                      $IconLegend->add_icon( $iconKey );
4074                  }
4075              }
4076              return $r;
4077              /* BREAK */
4078  
4079          case 'noimg':
4080              // Include size (optional):
4081              if( isset( $icon['size'] ) )
4082              {
4083                  $params['size'] = $icon['size'];
4084              }
4085              $params['style'] = 'margin: 0 2px';
4086  
4087              return get_icon( 'pixel', 'imgtag', $params );
4088              /* BREAK */
4089              /*
4090              $blank_icon = get_icon_info('pixel');
4091  
4092              $r = '<img src="'.$rsc_uri.$blank_icon['file'].'" ';
4093  
4094              // TODO: dh> add this only for !$use_strict, like above?
4095              // Include non CSS fallbacks (needed by bozos... and basic skin):
4096              $r .= 'border="0" align="top" ';
4097  
4098              // Include class (will default to "noicon"):
4099              if( ! isset( $params['class'] ) )
4100              {
4101                  if( isset($icon['class']) )
4102                  {    // This icon has a class
4103                      $params['class'] = $icon['class'];
4104                  }
4105                  else
4106                  {
4107                      $params['class'] = 'no_icon';
4108                  }
4109              }
4110  
4111              // Include size (optional):
4112              if( isset( $icon['size'] ) )
4113              {
4114                  $r .= 'width="'.$icon['size'][0].'" height="'.$icon['size'][1].'" ';
4115              }
4116  
4117              // Include alt (XHTML mandatory):
4118              if( ! isset( $params['alt'] ) )
4119              {
4120                  $params['alt'] = '';
4121              }
4122  
4123              // Add all the attributes:
4124              $r .= get_field_attribs_as_string( $params, false );
4125  
4126              // Close tag:
4127              $r .= '/>';
4128  
4129              return $r;*/
4130              /* BREAK */
4131      }
4132  }
4133  
4134  
4135  /**
4136   * @param string date (YYYY-MM-DD)
4137   * @param string time
4138   */
4139  function form_date( $date, $time = '' )
4140  {
4141      return substr( $date.'          ', 0, 10 ).' '.$time;
4142  }
4143  
4144  
4145  /**
4146   * Get list of client IP addresses from REMOTE_ADDR and HTTP_X_FORWARDED_FOR,
4147   * in this order. '' is used when no IP could be found.
4148   *
4149   * @param boolean True, to get only the first IP (probably REMOTE_ADDR)
4150   * @return array|string Depends on first param.
4151   */
4152  function get_ip_list( $firstOnly = false )
4153  {
4154      $r = array();
4155  
4156      if( !empty( $_SERVER['REMOTE_ADDR'] ) )
4157      {
4158          foreach( explode( ',', $_SERVER['REMOTE_ADDR'] ) as $l_ip )
4159          {
4160              $l_ip = trim($l_ip);
4161              if( !empty($l_ip) )
4162              {
4163                  $r[] = $l_ip;
4164              }
4165          }
4166      }
4167  
4168      if( !empty($_SERVER['HTTP_X_FORWARDED_FOR']) )
4169      { // IP(s) behind Proxy - this can be easily forged!
4170          foreach( explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] ) as $l_ip )
4171          {
4172              $l_ip = trim($l_ip);
4173              if( !empty($l_ip) && $l_ip != 'unknown' )
4174              {
4175                  $r[] = $l_ip;
4176              }
4177          }
4178      }
4179  
4180      if( !isset( $r[0] ) )
4181      { // No IP found.
4182          $r[] = '';
4183      }
4184  
4185      return $firstOnly ? $r[0] : $r;
4186  }
4187  
4188  
4189  /**
4190   * Get the base domain (without protocol and any subdomain) of an URL.
4191   *
4192   * Gets a max of 3 domain parts (x.y.tld)
4193   *
4194   * @param string URL
4195   * @return string the base domain (may become empty, if found invalid)
4196   */
4197  function get_base_domain( $url )
4198  {
4199      global $evo_charset;
4200  
4201      //echo '<p>'.$url;
4202      // Chop away the http part and the path:
4203      $domain = preg_replace( '~^([a-z]+://)?([^:/#]+)(.*)$~i', '\\2', $url );
4204  
4205      if( empty($domain) || preg_match( '~^(\d+\.)+\d+$~', $domain ) )
4206      {    // Empty or All numeric = IP address, don't try to cut it any further
4207          return $domain;
4208      }
4209  
4210      //echo '<br>'.$domain;
4211  
4212      // Get the base domain up to 3 levels (x.y.tld):
4213      // NOTE: "_" is not really valid, but for Windows it is..
4214      // NOTE: \w includes "_"
4215  
4216      // convert URL to IDN:
4217      $domain = idna_encode($domain);
4218  
4219      $domain_pattern = '~ ( \w (\w|-|_)* \. ){0,2}   \w (\w|-|_)* $~ix';
4220      if( ! preg_match( $domain_pattern, $domain, $match ) )
4221      {
4222          return '';
4223      }
4224      $base_domain = convert_charset(idna_decode($match[0]), $evo_charset, 'UTF-8');
4225  
4226      // Remove any www. prefix:
4227      $base_domain = preg_replace( '~^www\.~i', '', $base_domain );
4228  
4229      //echo '<br>'.$base_domain.'</p>';
4230  
4231      return $base_domain;
4232  }
4233  
4234  
4235  /**
4236   * Generate a valid key of size $length.
4237   *
4238   * @param integer length of key
4239   * @param string chars to use in generated key
4240   * @return string key
4241   */
4242  function generate_random_key( $length = 32, $keychars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' )
4243  {
4244      $key = '';
4245      $rnd_max = strlen($keychars) - 1;
4246  
4247      for( $i = 0; $i < $length; $i++ )
4248      {
4249          $key .= $keychars{mt_rand(0, $rnd_max)}; // get a random character out of $keychars
4250      }
4251  
4252      return $key;
4253  }
4254  
4255  
4256  /**
4257   * Generate a random password with no ambiguous chars
4258   *
4259   * @param integer length of password
4260   * @return string password
4261   */
4262  function generate_random_passwd( $length = NULL )
4263  {
4264      // fp> NOTE: do not include any characters that would make autogenerated passwords ambiguous
4265      // 1 (one) vs l (L) vs I (i)
4266      // O (letter) vs 0 (digit)
4267  
4268      if( empty($length) )
4269      {
4270          $length = rand( 8, 14 );
4271      }
4272  
4273      return generate_random_key( $length, 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789' );
4274  }
4275  
4276  
4277  function is_create_action( $action )
4278  {
4279      $action_parts = explode( '_', $action );
4280  
4281      switch( $action_parts[0] )
4282      {
4283          case 'new':
4284          case 'new_switchtab':
4285          case 'copy':
4286          case 'create':    // we return in this state after a validation error
4287              return true;
4288  
4289          case 'edit':
4290          case 'edit_switchtab':
4291          case 'update':    // we return in this state after a validation error
4292          case 'delete':
4293          // The following one's a bit far fetched, but can happen if we have no sheet display:
4294          case 'unlink':
4295          case 'view':
4296              return false;
4297  
4298          default:
4299              debug_die( 'Unhandled action in form: '.strip_tags($action_parts[0]) );
4300      }
4301  }
4302  
4303  
4304  /**
4305   * Generate a link that toggles display of an element on clicking.
4306   *
4307   * @todo Provide functionality to make those links accessible without JS (using GET parameter)
4308   * @uses toggle_display_by_id() (JS)
4309   * @param string ID (html) of the link
4310   * @param string ID (html) of the target to toggle displaying
4311   * @return string
4312   */
4313  function get_link_showhide( $link_id, $target_id, $text_when_displayed, $text_when_hidden, $display_hidden = true )
4314  {
4315      $html = "<a id='$link_id' href='#' onclick='return toggle_display_by_id(\"$link_id\", \"$target_id\", \""
4316          .jsspecialchars( $text_when_displayed ).'", "'.jsspecialchars( $text_when_hidden ).'")\'>'
4317          .( $display_hidden ? $text_when_hidden : $text_when_displayed )
4318          .'</a>';
4319  
4320      return $html;
4321  }
4322  
4323  
4324  /**
4325   * Escape a string to be used in Javascript.
4326   *
4327   * @param string
4328   * @return string
4329   */
4330  function jsspecialchars($s)
4331  {
4332      $r = str_replace(
4333          array(  '\\', '"', "'" ),
4334          array( '\\\\', '\"', "\'" ),
4335          $s );
4336      return htmlspecialchars($r, ENT_QUOTES);
4337  }
4338  
4339  
4340  /**
4341   * Compact a date in a number keeping only integer value of the string
4342   *
4343   * @param string date
4344   */
4345  function compact_date( $date )
4346  {
4347      return preg_replace( '#[^0-9]#', '', $date );
4348  }
4349  
4350  
4351  /**
4352   * Decompact a date in a date format ( Y-m-d h:m:s )
4353   *
4354   * @param string date
4355   */
4356  function decompact_date( $date )
4357  {
4358      $date0 = $date;
4359  
4360      return  substr($date0,0,4).'-'.substr($date0,4,2).'-'.substr($date0,6,2).' '
4361                                  .substr($date0,8,2).':'.substr($date0,10,2).':'.substr($date0,12,2);
4362  }
4363  
4364  /**
4365   * Check the format of the phone number param and
4366   * format it in a french number if it is.
4367   *
4368   * @param string phone number
4369   */
4370  function format_phone( $phone, $hide_country_dialing_code_if_same_as_locale = true )
4371  {
4372      global $CountryCache;
4373  
4374      $dialing_code = NULL;
4375  
4376      if( substr( $phone, 0, 1 ) == '+' )
4377      {    // We have a dialing code in the phone, so we extract it:
4378          $dialing_code = $CountryCache->extract_country_dialing_code( substr( $phone, 1 ) );
4379      }
4380  
4381      if( !is_null( $dialing_code ) && ( locale_dialing_code() == $dialing_code )
4382              && $hide_country_dialing_code_if_same_as_locale )
4383      {    // The phone dialing code is same as locale and we want to hide it in this case
4384          if( ( strlen( $phone ) - strlen( $dialing_code ) ) == 10 )
4385          {    // We can format it like a french phone number ( 0x.xx.xx.xx.xx )
4386              $phone_formated = format_french_phone( '0'.substr( $phone, strlen( $dialing_code )+1 ) );
4387          }
4388          else
4389          { // ( 0xxxxxxxxxxxxxx )
4390              $phone_formated = '0'.substr( $phone, strlen( $dialing_code )+1 );
4391          }
4392  
4393      }
4394      elseif( !is_null( $dialing_code ) )
4395      {    // Phone has a dialing code
4396          if( ( strlen( $phone ) - strlen( $dialing_code ) ) == 10 )
4397          { // We can format it like a french phone number with the dialing code ( +dialing x.xx.xx.xx.xx )
4398              $phone_formated = '+'.$dialing_code.format_french_phone( ' '.substr( $phone, strlen( $dialing_code )+1 ) );
4399          }
4400          else
4401          { // ( +dialing  xxxxxxxxxxx )
4402              $phone_formated = '+'.$dialing_code.' '.substr( $phone, strlen( $dialing_code )+1 );
4403          }
4404      }
4405      else
4406      {
4407          if( strlen( $phone ) == 10 )
4408          { //  We can format it like a french phone number ( xx.xx.xx.xx.xx )
4409              $phone_formated = format_french_phone( $phone );
4410          }
4411          else
4412          {    // We don't format phone: TODO generic format phone ( xxxxxxxxxxxxxxxx )
4413              $phone_formated = $phone;
4414          }
4415      }
4416  
4417      return $phone_formated;
4418  }
4419  
4420  
4421  /**
4422   * Format a string in a french phone number
4423   *
4424   * @param string phone number
4425   */
4426  function format_french_phone( $phone )
4427  {
4428      return substr($phone, 0 , 2).'.'.substr($phone, 2, 2).'.'.substr($phone, 4, 2)
4429                      .'.'.substr($phone, 6, 2).'.'.substr($phone, 8, 2);
4430  }
4431  
4432  
4433  /**
4434   * Generate a link to a online help resource.
4435   * testing the concept of online help (aka webhelp).
4436   * this function should be relocated somewhere better if it is taken onboard by the project
4437   *
4438   * @todo replace [?] with icon,
4439   * @todo write url suffix dynamically based on topic and language
4440   *
4441   * QUESTION: launch new window with javascript maybe?
4442   * @param string Topic
4443   *        The topic should be in a format like [\w]+(/[\w]+)*, e.g features/online_help.
4444   * @param string link text, leave it NULL to get link with manual icon
4445   * @return string
4446   */
4447  function get_manual_link( $topic, $link_text = NULL )
4448  {
4449      global $Settings, $current_locale, $app_shortname, $app_version;
4450  
4451      if( $Settings->get('webhelp_enabled') )
4452      {
4453          // $manual_url = 'http://manual.b2evolution.net/redirect/'.str_replace(' ','_',strtolower($topic)).'?lang='.$current_locale.'&amp;app='.$app_shortname.'&amp;version='.$app_version;
4454          // fp> TODO: this below is a temmporary hack while we work on the new manual:
4455          $manual_url = 'http://b2evolution.net/man/'.str_replace('_','-',strtolower($topic));
4456  
4457          if( $link_text == NULL )
4458          {
4459              $webhelp_link = action_icon( T_('Open relevant page in online manual'), 'manual', $manual_url, T_('Manual'), 5, 1, array( 'target' => '_blank', 'style' => 'vertical-align:top' ) );
4460          }
4461          else
4462          {
4463              $webhelp_link = '<a href="'.$manual_url.'" target = "_blank">'.$link_text.'</a>';
4464          }
4465  
4466          return ' '.$webhelp_link;
4467      }
4468      else
4469      {
4470          return '';
4471      }
4472  }
4473  
4474  
4475  /**
4476   * Build a string out of $field_attribs, with each attribute
4477   * prefixed by a space character.
4478   *
4479   * @param array Array of field attributes.
4480   * @param boolean Use format_to_output() for the attributes?
4481   * @return string
4482   */
4483  function get_field_attribs_as_string( $field_attribs, $format_to_output = true )
4484  {
4485      $r = '';
4486      foreach( $field_attribs as $l_attr => $l_value )
4487      {
4488          if( $l_value === NULL )
4489          { // don't generate empty attributes (it may be NULL if we pass 'value' => NULL as field_param for example, because isset() does not match it!)
4490              // sam2kb> what about alt="" how do we handle this?
4491              // I've removed the "=== ''" check now. Should not do any harm. IIRC NULL is what we want to avoid here.
4492              continue;
4493          }
4494  
4495          if( $format_to_output )
4496          {
4497              $r .= ' '.$l_attr.'="'.htmlspecialchars($l_value).'"';
4498          }
4499          else
4500          {
4501              $r .= ' '.$l_attr.'="'.$l_value.'"';
4502          }
4503      }
4504  
4505      return $r;
4506  }
4507  
4508  
4509  /**
4510   * Is the current page an admin/backoffice page?
4511   *
4512   * @return boolean
4513   */
4514  function is_admin_page()
4515  {
4516      global $is_admin_page;
4517  
4518      return isset($is_admin_page) && $is_admin_page === true; // check for type also, because of register_globals!
4519  }
4520  
4521  
4522  /**
4523   * Does the given url require logged in user
4524   *
4525   * @param string url
4526   * @param boolean set true to also check if url is login screen or not
4527   * @return boolean
4528   */
4529  function require_login( $url, $check_login_screen )
4530  {
4531      global $Settings;
4532      if( preg_match( '#/admin.php([&?].*)?$#', $url ) )
4533      { // admin always require logged in user
4534          return true;
4535      }
4536  
4537      if( $check_login_screen &&  preg_match( '#/login.php([&?].*)?$#', $url ) )
4538      {
4539          return true;
4540      }
4541  
4542      $disp_names = 'threads|messages|contacts';
4543      if( !$Settings->get( 'allow_anonymous_user_list' ) )
4544      {
4545          $disp_names .= '|users';
4546      }
4547      if( !$Settings->get( 'allow_anonymous_user_profiles' ) )
4548      {
4549          $disp_names .= '|user';
4550      }
4551      if( $check_login_screen )
4552      {
4553          $disp_names .= '|login';
4554      }
4555      if( preg_match( '#disp=('.$disp_names.')#', $url ) )
4556      { // $url require logged in user
4557          return true;
4558      }
4559  
4560      return false;
4561  }
4562  
4563  
4564  /**
4565   * Implode array( 'x', 'y', 'z' ) to something like 'x, y and z'. Useful for displaying list to the end user.
4566   *
4567   * If there's one element in the table, it is returned.
4568   * If there are at least two elements, the last one is concatenated using $implode_last, while the ones before are imploded using $implode_by.
4569   *
4570   * @todo dh> I don't think using entities/HTML as default for $implode_last is sane!
4571   *           Use "&" instead and make sure that the output for HTML is HTML compliant..
4572   * @todo Support for locales that have a different kind of enumeration?!
4573   * @return string
4574   */
4575  function implode_with_and( $arr, $implode_by = ', ', $implode_last = ' &amp; ' )
4576  {
4577      switch( count($arr) )
4578      {
4579          case 0:
4580              return '';
4581  
4582          case 1:
4583              $r = array_shift($arr);
4584              return $r;
4585  
4586          default:
4587              $r = implode( $implode_by, array_slice( $arr, 0, -1 ) )
4588                  .$implode_last.array_pop( $arr );
4589              return $r;
4590      }
4591  }
4592  
4593  
4594  /**
4595   * Display an array as a list:
4596   *
4597   * @param array
4598   * @param string
4599   * @param string
4600   * @param string
4601   * @param string
4602   * @param string
4603   */
4604  function display_list( $items, $list_start = '<ul>', $list_end = '</ul>', $item_separator = '',
4605                                                  $item_start = '<li>', $item_end = '</li>', $force_hash = NULL, $max_items = NULL, $link_params = array() )
4606  {
4607      if( !is_null($max_items) && $max_items < 1 )
4608      {
4609          return;
4610      }
4611  
4612      if( !empty( $items ) )
4613      {
4614          echo $list_start;
4615          $count = 0;
4616          $first = true;
4617  
4618          foreach( $items as $item )
4619          {    // For each list item:
4620  
4621              $link = resolve_link_params( $item, $force_hash, $link_params );
4622              if( empty( $link ) )
4623              {
4624                  continue;
4625              }
4626  
4627              $count++;
4628              if( $count>1 )
4629              {
4630                  echo $item_separator;
4631              }
4632              echo $item_start.$link.$item_end;
4633  
4634              if( !is_null($max_items) && $count >= $max_items )
4635              {
4636                  break;
4637              }
4638          }
4639          echo $list_end;
4640      }
4641  }
4642  
4643  
4644  /**
4645   * Credits stuff.
4646   */
4647  function display_param_link( $params )
4648  {
4649      echo resolve_link_params( $params );
4650  }
4651  
4652  
4653  /**
4654   * Resolve a link based on params (credits stuff)
4655   *
4656   * @param array
4657   * @param integer
4658   * @param array
4659   * @return string
4660   */
4661  function resolve_link_params( $item, $force_hash = NULL, $params = array() )
4662  {
4663      global $current_locale;
4664  
4665      // echo 'resolve link ';
4666  
4667      if( is_array( $item ) )
4668      {
4669          if( isset( $item[0] ) )
4670          {    // Older format, which displays the same thing for all locales:
4671              return generate_link_from_params( $item, $params );
4672          }
4673          else
4674          {    // First get the right locale:
4675              // echo $current_locale;
4676              foreach( $item as $l_locale => $loc_item )
4677              {
4678                  if( $l_locale == substr( $current_locale, 0, strlen($l_locale) ) )
4679                  {    // We found a matching locale:
4680                      //echo "[$l_locale/$current_locale]";
4681                      if( is_array( $loc_item[0] ) )
4682                      {    // Randomize:
4683                          $loc_item = hash_link_params( $loc_item, $force_hash );
4684                      }
4685  
4686                      return generate_link_from_params( $loc_item, $params );
4687                  }
4688              }
4689              // No match found!
4690              return '';
4691          }
4692      }
4693  
4694      // Super old format:
4695      return $item;
4696  }
4697  
4698  
4699  /**
4700   * Get a link line, based url hash combined with probability percentage in first column
4701   *
4702   * @param array of arrays
4703   * @param display for a specific hash key
4704   */
4705  function hash_link_params( $link_array, $force_hash = NULL )
4706  {
4707      global $ReqHost, $ReqPath, $ReqURI;
4708  
4709      static $hash;
4710  
4711      if( !is_null($force_hash) )
4712      {
4713          $hash = $force_hash;
4714      }
4715      elseif( !isset($hash) )
4716      {
4717          $key = $ReqHost.$ReqPath;
4718  
4719          global $Blog;
4720          if( !empty($Blog) && strpos( $Blog->get_setting('single_links'), 'param_' ) === 0 )
4721          {    // We are on a blog that doesn't even have clean URLs for posts
4722              $key .= $ReqURI;
4723          }
4724  
4725          $hash = 0;
4726          for( $i=0; $i<strlen($key); $i++ )
4727          {
4728              $hash += ord($key[$i]);
4729          }
4730          $hash = $hash % 100 + 1;
4731  
4732          // $hash = rand( 1, 100 );
4733          global $debug, $Debuglog;
4734          if( $debug )
4735          {
4736              $Debuglog->add( 'Hash key: '.$hash, 'request' );
4737          }
4738      }
4739      //    echo "[$hash] ";
4740  
4741      foreach( $link_array as $link_params )
4742      {
4743          // echo '<br>'.$hash.'-'.$link_params[ 0 ];
4744          if( $hash <= $link_params[ 0 ] )
4745          {    // select this link!
4746              // pre_dump( $link_params );
4747              array_shift( $link_params );
4748              return $link_params;
4749          }
4750      }
4751      // somehow no match, return 1st element:
4752      $link_params = $link_array[0];
4753      array_shift( $link_params );
4754      return $link_params;
4755  }
4756  
4757  
4758  /**
4759   * Generate a link from params (credits stuff)
4760   *
4761   * @param array
4762   * @param array
4763   */
4764  function generate_link_from_params( $link_params, $params = array() )
4765  {
4766      $url = $link_params[0];
4767      if( empty( $url ) )
4768      {
4769          return '';
4770      }
4771  
4772      // Make sure we are not missing any param:
4773      $params = array_merge( array(
4774              'type'        => 'link',
4775              'img_url'     => '',
4776              'img_width'   => '',
4777              'img_height'  => '',
4778              'title'       => '',
4779              'target'      => '_blank',
4780          ), $params );
4781  
4782      $text = $link_params[1];
4783      if( is_array($text) )
4784      {
4785          $text = hash_link_params( $text );
4786          $text = $text[0];
4787      }
4788      if( empty( $text ) )
4789      {
4790          return '';
4791      }
4792  
4793      $r = '<a href="'.$url.'"';
4794  
4795      if( !empty($params['target'] ) )
4796      {
4797          $r .= ' target="'.$params['target'].'"';
4798      }
4799  
4800      if( $params['type'] == 'img' )
4801      {
4802          return $r.' title="'.$params['title'].'"><img src="'.$params['img_url'].'" alt="'
4803                          .$text.'" title="'.$params['title'].'" width="'.$params['img_width'].'" height="'.$params['img_height']
4804                          .'" border="0" /></a>';
4805      }
4806  
4807      return $r.'>'.$text.'</a>';
4808  }
4809  
4810  
4811  /**
4812   * Send a result as javascript
4813   * automatically includes any Messages ( @see Log::display() )
4814   * no return from function as it terminates processing
4815   *
4816   * @author Yabba
4817   *
4818   * @todo dh> Move this out into some more specific (not always included) file.
4819   *
4820   * @param array $methods javascript funtions to call with array of parameters
4821   *        format : 'function_name' => array( param1, parm2, param3 )
4822   * @param boolean $send_as_html Wrap the script into an html page with script tag; default is to send as js file
4823   * @param string $target prepended to function calls : blank or window.parent
4824   */
4825  function send_javascript_message( $methods = array(), $send_as_html = false, $target = '' )
4826  {
4827      // lets spit out any messages
4828      global $Messages;
4829      ob_start();
4830      $Messages->display();
4831      $output = ob_get_clean();
4832  
4833      // set target
4834      $target = ( $target ? $target : param( 'js_target', 'string' ) );
4835      if( $target )
4836      {    // add trailing [dot]
4837          $target = trim( $target, '.' ).'.';
4838      }
4839  
4840      // target should be empty or window.parent.
4841      if( $target && $target != 'window.parent.' )
4842      {
4843          debug_die( 'Unexpected javascript target' );
4844      }
4845  
4846      if( $output )
4847      {    // we have some messages
4848          $output = $target.'DisplayServerMessages( \''.format_to_js( $output ).'\');'."\n";
4849      }
4850  
4851      if( !empty( $methods ) )
4852      {    // we have a methods to call
4853          foreach( $methods as $method => $param_list )
4854          {    // loop through each requested method
4855              $params = array();
4856              if( !is_array( $param_list ) )
4857              {    // lets make it an array
4858                  $param_list = array( $param_list );
4859              }
4860              foreach( $param_list as $param )
4861              {    // add each parameter to the output
4862                  if( !is_numeric( $param ) )
4863                  {    // this is a string, quote it
4864                      $param = '\''.format_to_js( $param ).'\'';
4865                  }
4866                  $params[] = $param;// add param to the list
4867              }
4868              // add method and parameters
4869              $output .= $target.$method.'('.implode( ',', $params ).');'."\n";
4870          }
4871      }
4872  
4873      if( $send_as_html )
4874      {    // we want to send as a html document
4875          headers_content_mightcache( 'text/html', 0 );        // Do NOT cache interactive communications.
4876          echo '<html><head></head><body><script type="text/javascript">'."\n";
4877          echo $output;
4878          echo '</script></body></html>';
4879      }
4880      else
4881      {    // we want to send as js
4882          headers_content_mightcache( 'text/javascript', 0 );        // Do NOT cache interactive communications.
4883          echo $output;
4884      }
4885  
4886      exit(0);
4887  }
4888  
4889  
4890  /**
4891   * Basic tidy up of strings
4892   *
4893   * @author Yabba
4894   * @author Tblue
4895   *
4896   * @param string $unformatted raw data
4897   * @return string formatted data
4898   */
4899  function format_to_js( $unformatted )
4900  {
4901      return str_replace( array(
4902                              '\'',
4903                              '\n',
4904                              '\r',
4905                              '\t',
4906                              "\n",
4907                              "\r",
4908                          ),
4909                          array(
4910                              '\\\'',
4911                              '\\\\n',
4912                              '\\\\r',
4913                              '\\\\t',
4914                              '\n',
4915                              '\r',
4916                          ), $unformatted );
4917  }
4918  
4919  
4920  /**
4921   * Get available cort oprions for items
4922   *
4923   * @return array key=>name
4924   */
4925  function get_available_sort_options()
4926  {
4927      return array(
4928          'datestart'       => T_('Date issued (Default)'),
4929          'order'           => T_('Order (as explicitly specified)'),
4930          //'datedeadline' => T_('Deadline'),
4931          'title'           => T_('Title'),
4932          'datecreated'     => T_('Date created'),
4933          'datemodified'    => T_('Date last modified'),
4934          'last_touched_ts' => T_('Date last touched'),
4935          'urltitle'        => T_('URL "filename"'),
4936          'priority'        => T_('Priority'),
4937          'views'           => T_('Views'),
4938          'RAND'            => T_('Random order!'),
4939      );
4940  }
4941  
4942  
4943  /**
4944   * Get available cort oprions for blogs
4945   *
4946   * @return array key=>name
4947   */
4948  function get_coll_sort_options()
4949  {
4950      return array(
4951          'ID'           => T_('Blog ID (Default)'),
4952          'name'         => T_('Name'),
4953          'shortname'    => T_('Short name'),
4954          'tagline'      => T_('Tagline'),
4955          'description'  => T_('Description'),
4956          'urlname'      => T_('URL "filename"'),
4957          'RAND'         => T_('Random order!'),
4958      );
4959  }
4960  
4961  
4962  /**
4963   * Converts array to form option list
4964   *
4965   * @param integer|array selected key
4966   * @param boolean provide a choice for "none" with value ''
4967   * @return string
4968   */
4969  function array_to_option_list( $array, $default = '', $allow_none = false )
4970  {
4971      if( !is_array( $default ) )
4972      {
4973          $default = array( $default );
4974      }
4975  
4976      $r = '';
4977  
4978      if( $allow_none )
4979      {
4980          $r .= '<option value="'.$this->none_option_value.'"';
4981          if( empty($default) ) $r .= ' selected="selected"';
4982          $r .= '>'.format_to_output($this->none_option_text).'</option>'."\n";
4983      }
4984  
4985      foreach( $array as $k=>$v )
4986      {
4987          $r .=  '<option value="'.format_to_output($k,'formvalue').'"';
4988          if( in_array( $k, $default ) ) $r .= ' selected="selected"';
4989          $r .= '>';
4990          $r .= format_to_output( $v, 'htmlbody' );
4991          $r .=  '</option>'."\n";
4992      }
4993  
4994      return $r;
4995  }
4996  
4997  
4998  /**
4999   * Get a value from a volatile/lossy cache.
5000   *
5001   * @param string key
5002   * @param boolean success (by reference)
5003   * @return mixed True in case of success, false in case of failure. NULL, if no backend is available.
5004   */
5005  function get_from_mem_cache($key, & $success )
5006  {
5007      global $Timer;
5008  
5009      $Timer->resume('get_from_mem_cache', false);
5010  
5011      if( function_exists('apc_fetch') )
5012          $r = apc_fetch( $key, $success );
5013      elseif( function_exists('xcache_get') && ini_get('xcache.var_size') > 0 )
5014          $r = xcache_get($key);
5015      elseif( function_exists('eaccelerator_get') )
5016          $r = eaccelerator_get($key);
5017  
5018      if( ! isset($success) )
5019      { // set $success for implementation that do not set it itself (only APC does so)
5020          $success = isset($r);
5021      }
5022      if( ! $success )
5023      {
5024          $r = NULL;
5025  
5026          global $Debuglog;
5027          $Debuglog->add('No caching backend available for reading "'.$key.'".', 'cache');
5028      }
5029  
5030      $Timer->pause('get_from_mem_cache', false);
5031      return $r;
5032  }
5033  
5034  
5035  /**
5036   * Set a value to a volatile/lossy cache.
5037   *
5038   * There's no guarantee that the data is still available, since e.g. old
5039   * values might get purged.
5040   *
5041   * @param string key
5042   * @param mixed Data. Objects would have to be serialized.
5043   * @param int Time to live (seconds). Default is 0 and means "forever".
5044   * @return mixed
5045   */
5046  function set_to_mem_cache($key, $payload, $ttl = 0)
5047  {
5048      global $Timer;
5049  
5050      $Timer->resume('set_to_mem_cache', false);
5051  
5052      if( function_exists('apc_store') )
5053          $r = apc_store( $key, $payload, $ttl );
5054      elseif( function_exists('xcache_set') && ini_get('xcache.var_size') > 0 )
5055          $r = xcache_set( $key, $payload, $ttl );
5056      elseif( function_exists('eaccelerator_put') )
5057          $r = eaccelerator_put( $key, $payload, $ttl );
5058      else {
5059          global $Debuglog;
5060          $Debuglog->add('No caching backend available for writing "'.$key.'".', 'cache');
5061          $r = NULL;
5062      }
5063  
5064      $Timer->pause('set_to_mem_cache', false);
5065  
5066      return $r;
5067  }
5068  
5069  
5070  /**
5071   * Remove a given key from the volatile/lossy cache.
5072   *
5073   * @param string key
5074   * @return boolean True on success, false on failure. NULL if no backend available.
5075   */
5076  function unset_from_mem_cache($key)
5077  {
5078      if( function_exists('apc_delete') )
5079          return apc_delete( $key );
5080  
5081      if(