1<?php 2/** 3 * @package Habari 4 * 5 */ 6 7/** 8 * Habari Utility Class 9 * 10 */ 11class Utils 12{ 13 public static $debug_defined = false; 14 15 /** 16 * Utils constructor 17 * This class should not be instantiated. 18 */ 19 private function __construct() 20 { 21 } 22 23 /** 24 * function get_params 25 * Returns an associative array of parameters, whether the input value is 26 * a querystring or an associative array. 27 * @param mixed An associative array or querystring parameter list 28 * @return array An associative array of parameters 29 */ 30 public static function get_params( $params ) 31 { 32 if ( is_array( $params ) || $params instanceof ArrayObject || $params instanceof ArrayIterator ) { 33 return $params; 34 } 35 $paramarray = array(); 36 parse_str( $params, $paramarray ); 37 return $paramarray; 38 } 39 40 /** 41 * function end_in_slash 42 * Forces a string to end in a single slash 43 * @param string A string, usually a path 44 * @return string The string with the slash added or extra slashes removed, but with one slash only 45 */ 46 public static function end_in_slash( $value ) 47 { 48 return rtrim( $value, '\\/' ) . '/'; 49 } 50 51 /** 52 * function redirect 53 * Redirects the request to a new URL 54 * @param string $url The URL to redirect to, or omit to redirect to the current url 55 * @param boolean $continue Whether to continue processing the script (default false for security reasons, cf. #749) 56 */ 57 public static function redirect( $url = '', $continue = false ) 58 { 59 if ( $url == '' ) { 60 $url = Controller::get_full_url(); 61 } 62 header( 'Location: ' . $url, true, 302 ); 63 64 if ( ! $continue ) exit; 65 } 66 67 /** 68 * function atomtime 69 * Returns RFC-3339 time from a time string or integer timestamp 70 * @param mixed A string of time or integer timestamp 71 * @return string An RFC-3339 formatted time 72 */ 73 public static function atomtime( $t ) 74 { 75 if ( ! is_numeric( $t ) ) { 76 $t = strtotime( $t ); 77 } 78 $vdate = date( DATE_ATOM, $t ); 79 // If the date format used for timezone was O instead of P... 80 if ( substr( $vdate, -3, 1 ) != ':' ) { 81 $vdate = substr( $vdate, 0, -2 ) . ':' . substr( $vdate, -2, 2 ); 82 } 83 return $vdate; 84 } 85 86 /** 87 * function nonce 88 * Returns a random 12-digit hex number 89 */ 90 public static function nonce() 91 { 92 return sprintf( '%06x', rand( 0, 16776960 ) ) . sprintf( '%06x', rand( 0, 16776960 ) ); 93 } 94 95 /** 96 * function WSSE 97 * returns an array of tokens used for WSSE authentication 98 * http://www.xml.com/pub/a/2003/12/17/dive.html 99 * http://www.sixapart.com/developers/atom/protocol/atom_authentication.html 100 * @param String a nonce 101 * @param String a timestamp 102 * @return Array an array of WSSE authentication elements 103 */ 104 public static function WSSE( $nonce = '', $timestamp = '' ) 105 { 106 if ( '' === $nonce ) { 107 $nonce = Utils::crypt( Options::get( 'GUID' ) . Utils::nonce() ); 108 } 109 if ( '' === $timestamp ) { 110 $timestamp = date( 'c' ); 111 } 112 $user = User::identify(); 113 $wsse = array( 114 'nonce' => $nonce, 115 'timestamp' => $timestamp, 116 'digest' => base64_encode( pack( 'H*', sha1( $nonce . $timestamp . $user->password ) ) ) 117 ); 118 return $wsse; 119 } 120 121 /** 122 * function stripslashes 123 * Removes slashes from escaped strings, including strings in arrays 124 */ 125 public static function stripslashes( $value ) 126 { 127 if ( is_array( $value ) ) { 128 $value = array_map( array( 'Utils', 'stripslashes' ), $value ); 129 } 130 elseif ( !empty( $value ) && is_string( $value ) ) { 131 $value = stripslashes( $value ); 132 } 133 return $value; 134 } 135 136 /** 137 * function addslashes 138 * Adds slashes to escape strings, including strings in arrays 139 */ 140 public static function addslashes( $value ) 141 { 142 if ( is_array( $value ) ) { 143 $value = array_map( array( 'Utils', 'addslashes' ), $value ); 144 } 145 else if ( !empty( $value ) && is_string( $value ) ) { 146 $value = addslashes( $value ); 147 } 148 return $value; 149 } 150 151 /** 152 * function de_amp 153 * Returns & entities in a URL querystring to their previous & glory, for use in redirects 154 * @param string $value A URL, maybe with a querystring 155 */ 156 public static function de_amp( $value ) 157 { 158 $url = InputFilter::parse_url( $value ); 159 $url[ 'query' ] = str_replace( '&', '&', $url[ 'query' ] ); 160 return InputFilter::glue_url( $url ); 161 } 162 163 /** 164 * function revert_magic_quotes_gpc 165 * Reverts magicquotes_gpc behavior 166 */ 167 public static function revert_magic_quotes_gpc() 168 { 169 /* We should only revert the magic quotes once per page hit */ 170 static $revert = true; 171 if ( get_magic_quotes_gpc() && $revert ) { 172 $_GET = self::stripslashes( $_GET ); 173 $_POST = self::stripslashes( $_POST ); 174 $_COOKIE = self::stripslashes( $_COOKIE ); 175 $revert = false; 176 } 177 } 178 179 /** 180 * function quote_spaced 181 * Adds quotes around values that have spaces in them 182 * @param string A string value that might have spaces 183 * @return string The string value, quoted if it has spaces 184 */ 185 public static function quote_spaced( $value ) 186 { 187 return ( strpos( $value, ' ' ) === false ) ? $value : '"' . $value . '"'; 188 } 189 190 /** 191 * function implode_quoted 192 * Behaves like the implode() function, except it quotes values that contain spaces 193 * @param string A separator between each value 194 * @param array An array of values to separate 195 * @return string The concatenated string 196 */ 197 public static function implode_quoted( $separator, $values ) 198 { 199 if ( ! is_array( $values ) ) { 200 $values = array(); 201 } 202 $values = array_map( array( 'Utils', 'quote_spaced' ), $values ); 203 return implode( $separator, $values ); 204 } 205 206 /** 207 * Returns a string of question mark parameter 208 * placeholders. 209 * 210 * Useful when building, for instance, an IN() list for SQL 211 * 212 * @param count Number of placeholders to put in the string 213 * @return string Placeholder string 214 */ 215 public static function placeholder_string( $count ) 216 { 217 if ( Utils::is_traversable( $count ) ) { 218 $count = count( $count ); 219 } 220 return rtrim( str_repeat( '?,', $count ), ',' ); 221 } 222 223 /** 224 * function archive_pages 225 * Returns the number of pages in an archive using the number of items per page set in options 226 * @param integer Number of items in the archive 227 * @param integer Number of items per page 228 * @returns integer Number of pages based on pagination option. 229 */ 230 public static function archive_pages( $item_total, $items_per_page = null ) 231 { 232 if ( $items_per_page ) { 233 return ceil( $item_total / $items_per_page ); 234 } 235 return ceil( $item_total / Options::get( 'pagination' ) ); 236 } 237 238 /** 239 * Used with array_map to create an array of PHP stringvar-style search/replace strings using optional pre/postfixes 240 * <code> 241 * $mapped_values= array_map(array('Utils', 'map_array'), $values); 242 * </code> 243 * @param string $value The value to wrap 244 * @param string $prefix The prefix for the returned value 245 * @param string $postfix The postfix for the returned value 246 * @return string The wrapped value 247 */ 248 public static function map_array( $value, $prefix = '{$', $postfix = '}' ) 249 { 250 return $prefix . $value . $postfix; 251 } 252 253 /** 254 * Helper function used by debug() 255 * Not for external use. 256 */ 257 public static function debug_reveal( $show, $hide, $debugid, $close = false ) 258 { 259 $reshow = $restyle = $restyle2 = ''; 260 if ( $close ) { 261 $reshow = "onclick=\"debugtoggle('debugshow-{$debugid}');debugtoggle('debughide-{$debugid}');return false;\""; 262 $restyle = "<span class=\"utils__block\">"; 263 $restyle2 = "</span>"; 264 } 265 return "<span class=\"utils__arg\"><a href=\"#\" id=\"debugshow-{$debugid}\" onclick=\"debugtoggle('debugshow-{$debugid}');debugtoggle('debughide-{$debugid}');return false;\">$show</a><span style=\"display:none;\" id=\"debughide-{$debugid}\" {$reshow} >{$restyle}$hide{$restyle2}</span></span>"; 266 } 267 268 /** 269 * Outputs a call stack with parameters, and a dump of the parameters passed. 270 * @params mixed Any number of parameters to output in the debug box. 271 */ 272 public static function debug() 273 { 274 $debugid = md5( microtime() ); 275 $tracect = 0; 276 277 $fooargs = func_get_args(); 278 echo "<div class=\"utils__debugger\">"; 279 if ( !self::$debug_defined ) { 280 $output = "<script type=\"text/javascript\"> 281 debuggebi = function(id) {return document.getElementById(id);} 282 debugtoggle = function(id) {debuggebi(id).style.display = debuggebi(id).style.display=='none'?'inline':'none';} 283 </script> 284 <style type=\"text/css\"> 285 .utils__debugger{background-color:#550000;border:1px solid red;text-align:left;} 286 .utils__debugger pre{margin:5px;background-color:#000;overflow-x:scroll} 287 .utils__debugger pre em{color:#dddddd;} 288 .utils__debugger table{background-color:#770000;color:white;width:100%;} 289 .utils__debugger tr{background-color:#000000;} 290 .utils__debugger td{padding-left: 10px;vertical-align:top;white-space: pre;font-family:Courier New,Courier,monospace;} 291 .utils__debugger .utils__odd{background:#880000;} 292 .utils__debugger .utils__arg a{color:#ff3333;} 293 .utils__debugger .utils__arg span{display:none;} 294 .utils__debugger .utils__arg span span{display:inline;} 295 .utils__debugger .utils__arg span .utils__block{display:block;background:#990000;margin:0px 2em;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:9px;padding:5px;} 296 </style> 297 "; 298 echo $output; 299 self::$debug_defined = true; 300 } 301 if ( function_exists( 'debug_backtrace' ) ) { 302 $output = "<table>"; 303 $backtrace = array_reverse( debug_backtrace(), true ); 304 $odd = ''; 305 $tracect = 0; 306 foreach ( $backtrace as $trace ) { 307 $file = $line = $class = $type = $function = ''; 308 $args = array(); 309 extract( $trace ); 310 if ( isset( $class ) ) $fname = $class . $type . $function; else $fname = $function; 311 if ( !isset( $file ) || $file=='' ) $file = '[Internal PHP]'; else $file = basename( $file ); 312 $odd = $odd == '' ? 'class="utils__odd"' : ''; 313 $output .= "<tr {$odd}><td>{$file} ({$line}):</td><td>{$fname}("; 314 $comma = ''; 315 foreach ( (array)$args as $arg ) { 316 $tracect++; 317 $argout = print_r( $arg, 1 ); 318 $output .= $comma . Utils::debug_reveal( gettype( $arg ), htmlentities( $argout ), $debugid . $tracect, true ); 319 $comma = ', '; 320 } 321 $output .= ");</td></tr>"; 322 } 323 $output .= "</table>"; 324 echo Utils::debug_reveal( '<small>Call Stack</small>', $output, $debugid ); 325 } 326 echo "<pre style=\"color:white;\">"; 327 foreach ( $fooargs as $arg1 ) { 328 echo '<em>' . gettype( $arg1 ) . '</em> '; 329 if ( gettype( $arg1 ) == 'boolean' ) { 330 echo htmlentities( var_export( $arg1 ) ) . '<br>'; 331 } 332 else { 333 echo htmlentities( print_r( $arg1, true ) ) . "<br>"; 334 } 335 } 336 echo "</pre></div>"; 337 } 338 339 /** 340 * Outputs debug information like ::debug() but using Firebug's Console. 341 * @params mixed Any number of parameters to output in the debug box. 342 */ 343 public static function firedebug() 344 { 345 $fooargs = func_get_args(); 346 $output = "<script type=\"text/javascript\">\nif (window.console){\n"; 347 $backtrace = array_reverse( debug_backtrace(), true ); 348 $output .= Utils::firebacktrace( $backtrace ); 349 350 foreach ( $fooargs as $arg1 ) { 351 $output .= "console.info(\"%s: %s\", \"" . gettype( $arg1 ) . "\""; 352 $output .= ", \"" . str_replace( "\n", '\n', addslashes( print_r( $arg1, 1 ) ) ) . "\");\n"; 353 } 354 $output .= "console.groupEnd();\n}\n</script>"; 355 echo $output; 356 } 357 358 /** 359 * Utils::firebacktrace() 360 * 361 * @param array $backtrace An array of backtrace details from debug_backtrace() 362 * @return string Javascript output that will display the backtrace in the Firebug console. 363 */ 364 public static function firebacktrace( $backtrace ) 365 { 366 $output = ''; 367 extract( end( $backtrace ) ); 368 if ( isset( $class ) ) $fname = $class . $type . $function; else $fname = $function; 369 if ( !isset( $file ) || $file=='' ) $file = '[Internal PHP]'; else $file = basename( $file ); 370 $output .= "console.group(\"%s(%s): %s(…)\", \"" . basename( $file ) . "\", \"{$line}\", \"{$fname}\");\n"; 371 foreach ( $backtrace as $trace ) { 372 $file = $line = $class = $type = $function = ''; 373 $args = array(); 374 extract( $trace ); 375 if ( isset( $class ) ) $fname = $class . $type . $function; else $fname = $function; 376 if ( !isset( $file ) || $file=='' ) $file = '[Internal PHP]'; else $file = basename( $file ); 377 378 $output .= "console.group(\"%s(%s): %s(%s)\", \"{$file}\", \"{$line}\", \"{$fname}\", \""; 379 380 $output2 = $comma = $argtypes = ''; 381 foreach ( (array)$args as $arg ) { 382 $argout = str_replace( "\n", '\n', addslashes( print_r( $arg, 1 ) ) ); 383 //$output .= $comma . Utils::debug_reveal( gettype($arg), htmlentities($argout), $debugid . $tracect, true ); 384 $argtypes .= $comma . gettype( $arg ); 385 $output2 .= "console.log(\"$argout\");\n"; 386 $comma = ', '; 387 } 388 $argtypes = trim( $argtypes ); 389 $output .= "{$argtypes}\");\n{$output2}"; 390 $output .= "console.groupEnd();\n"; 391 //$output .= ");</td></tr>"; 392 } 393 return $output; 394 } 395 396 /** 397 * Crypt a given password, or verify a given password against a given hash. 398 * 399 * @todo Enable best algo selection after DB schema change. 400 * 401 * @param string $password the password to crypt or verify 402 * @param string $hash (optional) if given, verify $password against $hash 403 * @return crypted password, or boolean for verification 404 */ 405 public static function crypt( $password, $hash = null ) 406 { 407 if ( $hash == null ) { 408 return self::ssha512( $password, $hash ); 409 } 410 elseif ( strlen( $hash ) > 3 ) { // need at least {, } and a char :p 411 // verify 412 if ( $hash{0} == '{' ) { 413 // new hash from the block 414 $algo = strtolower( substr( $hash, 1, strpos( $hash, '}', 1 ) - 1 ) ); 415 switch ( $algo ) { 416 case 'sha1': 417 case 'ssha': 418 case 'ssha512': 419 case 'md5': 420 return self::$algo( $password, $hash ); 421 default: 422 Error::raise( sprintf( _t( 'Unsupported digest algorithm "%s"' ), $algo ) ); 423 return false; 424 } 425 } 426 else { 427 // legacy sha1 428 return ( sha1( $password ) == $hash ); 429 } 430 } 431 else { 432 Error::raise( _t( 'Invalid hash' ) ); 433 } 434 } 435 436 /** 437 * Crypt or verify a given password using SHA. 438 * 439 * Passwords should not be stored using this method, but legacy systems might require it. 440 */ 441 public static function sha1( $password, $hash = null ) 442 { 443 $marker = '{SHA1}'; 444 if ( $hash == null ) { 445 return $marker . sha1( $password ); 446 } 447 else { 448 return ( sha1( $password ) == substr( $hash, strlen( $marker ) ) ); 449 } 450 } 451 452 /** 453 * Crypt or verify a given password using MD5. 454 * 455 * Passwords should not be stored using this method, but legacy systems might require it. 456 */ 457 public static function md5( $password, $hash = null ) 458 { 459 $marker = '{MD5}'; 460 if ( $hash == null ) { 461 return $marker . md5( $password ); 462 } 463 else { 464 return ( md5( $password ) == substr( $hash, strlen( $marker ) ) ); 465 } 466 } 467 468 /** 469 * Crypt or verify a given password using SSHA. 470 * Implements the {Seeded,Salted}-SHA algorithm as per RfC 2307. 471 * 472 * @param string $password the password to crypt or verify 473 * @param string $hash (optional) if given, verify $password against $hash 474 * @return crypted password, or boolean for verification 475 */ 476 public static function ssha( $password, $hash = null ) 477 { 478 $marker = '{SSHA}'; 479 if ( $hash == null ) { // encrypt 480 // create salt (4 byte) 481 $salt = ''; 482 for ( $i = 0; $i < 4; $i++ ) { 483 $salt .= chr( mt_rand( 0, 255 ) ); 484 } 485 // get digest 486 $digest = sha1( $password . $salt, true ); 487 // b64 for storage 488 return $marker . base64_encode( $digest . $salt ); 489 } 490 else { // verify 491 // is this a SSHA hash? 492 if ( ! substr( $hash, 0, strlen( $marker ) ) == $marker ) { 493 Error::raise( _t( 'Invalid hash' ) ); 494 return false; 495 } 496 // cut off {SSHA} marker 497 $hash = substr( $hash, strlen( $marker ) ); 498 // b64 decode 499 $hash = base64_decode( $hash ); 500 // split up 501 $digest = substr( $hash, 0, 20 ); 502 $salt = substr( $hash, 20 ); 503 // compare 504 return ( sha1( $password . $salt, true ) == $digest ); 505 } 506 } 507 508 /** 509 * Crypt or verify a given password using SSHA512. 510 * Implements a modified version of the {Seeded,Salted}-SHA algorithm 511 * from RfC 2307, using SHA-512 instead of SHA-1. 512 * 513 * Requires the new hash*() functions. 514 * 515 * @param string $password the password to crypt or verify 516 * @param string $hash (optional) if given, verify $password against $hash 517 * @return crypted password, or boolean for verification 518 */ 519 public static function ssha512( $password, $hash = null ) 520 { 521 $marker = '{SSHA512}'; 522 if ( $hash == null ) { // encrypt 523 $salt = ''; 524 for ( $i = 0; $i < 4; $i++ ) { 525 $salt .= chr( mt_rand( 0, 255 ) ); 526 } 527 $digest = hash( 'sha512', $password . $salt, true ); 528 return $marker . base64_encode( $digest . $salt ); 529 } 530 else { // verify 531 if ( ! substr( $hash, 0, strlen( $marker ) ) == $marker ) { 532 Error::raise( _t( 'Invalid hash' ) ); 533 return false; 534 } 535 $hash = substr( $hash, strlen( $marker ) ); 536 $hash = base64_decode( $hash ); 537 $digest = substr( $hash, 0, 64 ); 538 $salt = substr( $hash, 64 ); 539 return ( hash( 'sha512', $password . $salt, true ) == $digest ); 540 } 541 } 542 543 /** 544 * Return an array of date information 545 * Just like getdate() but also returns 0-padded versions of day and month in mday0 and mon0 546 * @param integer $timestamp A unix timestamp 547 * @return array An array of date data 548 */ 549 public static function getdate( $timestamp ) 550 { 551 $info = getdate( $timestamp ); 552 $info[ 'mon0' ] = substr( '0' . $info[ 'mon' ], -2, 2 ); 553 $info[ 'mday0' ] = substr( '0' . $info[ 'mday' ], -2, 2 ); 554 return $info; 555 } 556 557 /** 558 * Return a formatted date/time trying to use strftime() AND date() 559 * @param string $format The format for the date. If it contains non-escaped percent signs, it uses strftime(), otherwise date() 560 * @param integer $timestamp The unix timestamp of the time to format 561 * @return string The formatted time 562 */ 563 public static function locale_date( $format, $timestamp ) 564 { 565 $matches = preg_split( '/((?<!\\\\)%[a-z]\\s*)/iu', $format, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); 566 $output = ''; 567 foreach ( $matches as $match ) { 568 if ( $match{0} == '%' ) { 569 $output .= strftime( $match, $timestamp ); 570 } 571 else { 572 $output .= date( $match, $timestamp ); 573 } 574 } 575 return $output; 576 } 577 578 /** 579 * Return a sanitized slug, replacing non-alphanumeric characters to dashes 580 * @param string $string The string to sanitize. Non-alphanumeric characters will be replaced by dashes 581 * @param string $separator The slug separator, '-' by default 582 * @return string The sanitized slug 583 */ 584 public static function slugify( $string, $separator = '-' ) 585 { 586 // Decode HTML entities 587 // Replace non-alphanumeric characters to dashes. Exceptions: %, _, - 588 // Note that multiple separators are collapsed automatically by the preg_replace. 589 // Convert all characters to lowercase. 590 // Trim spaces on both sides. 591 $slug = rtrim( MultiByte::strtolower( preg_replace( '/[^\p{L}\p{N}_]+/u', $separator, preg_replace( '/\p{Po}/u', '', html_entity_decode( $string ) ) ) ), $separator ); 592 // Let people change the behavior. 593 $slug = Plugins::filter( 'slugify', $slug, $string ); 594 595 return $slug; 596 } 597 598 /** 599 * Create an HTML select tag with options and a current value 600 * 601 * @param string $name The name and id of the select control 602 * @param array $options An associative array of values to use as the select options 603 * @param string $current The value of the currently selected option 604 * @param array $properties An associative array of additional properties to assign to the select control 605 * @return string The select control markup 606 */ 607 public static function html_select( $name, $options, $current = null, $properties = array() ) 608 { 609 $output = '<select id="' . $name . '" name="' . $name . '"'; 610 foreach ( $properties as $key => $value ) { 611 $output .= " {$key}=\"{$value}\""; 612 } 613 $output .= ">\n"; 614 foreach ( $options as $value => $text ) { 615 $output .= '<option value="' . $value . '"'; 616 if ( $current == (string)$value ) { 617 $output .= ' selected="selected"'; 618 } 619 $output .= '>' . $text . "</option>\n"; 620 } 621 $output .= '</select>'; 622 return $output; 623 } 624 625 /** 626 * Creates one or more HTML checkboxes 627 * @param string The name of the checkbox element. If there are 628 * multiple checkboxes for the same name, this method will 629 * automatically apply "[]" at the end of the name 630 * @param array An array of checkbox options. Each element should be 631 * an array containing "name" and "value". If the checkbox 632 * should be checked, it should have a "checked" element. 633 * @return string The HTML of the checkboxes 634 */ 635 public static function html_checkboxes( $name, $options ) 636 { 637 $output = ''; 638 $multi = false; 639 if ( count( $options > 1 ) ) { 640 $multi = true; 641 } 642 foreach ( $options as $option ) { 643 $output .= '<input type="checkbox" id="' . $option[ 'name' ] . '" name="' . $option[ 'name' ]; 644 if ( $multi ) { 645 $output .= '[]'; 646 } 647 $output .= '" value="' . $option[ 'value' ] . '"'; 648 if ( isset( $option[ 'checked' ] ) ) { 649 $output .= ' checked'; 650 } 651 $output .= '>'; 652 } 653 return $output; 654 } 655 656 /** 657 * Trims longer phrases to shorter ones with elipsis in the middle 658 * @param string The string to truncate 659 * @param integer The length of the returned string 660 * @param bool Whether to place the ellipsis in the middle (true) or 661 * at the end (false) 662 * @return string The truncated string 663 */ 664 public static function truncate( $str, $len = 10, $middle = true ) 665 { 666 // make sure $len is a positive integer 667 if ( ! is_numeric( $len ) || ( 0 > $len ) ) { 668 return $str; 669 } 670 // if the string is less than the length specified, bail out 671 if ( MultiByte::strlen( $str ) <= $len ) { 672 return $str; 673 } 674 675 // okay. Shuold we place the ellipse in the middle? 676 if ( $middle ) { 677 // yes, so compute the size of each half of the string 678 $len = round( ( $len - 3 ) / 2 ); 679 // and place an ellipse in between the pieces 680 return MultiByte::substr( $str, 0, $len ) . '…' . MultiByte::substr( $str, -$len ); 681 } 682 else { 683 // no, the ellipse goes at the end 684 $len = $len - 3; 685 return MultiByte::substr( $str, 0, $len ) . '…'; 686 } 687 } 688 689 /** 690 * Check the PHP syntax of the specified code. 691 * Performs a syntax (lint) check on the specified code testing for scripting errors. 692 * 693 * @param string $code The code string to be evaluated. It does not have to contain PHP opening tags. 694 * @return bool Returns true if the lint check passed, and false if the link check failed. 695 */ 696 public static function php_check_syntax( $code, &$error = null ) 697 { 698 $b = 0; 699 700 foreach ( token_get_all( $code ) as $token ) { 701 if ( is_array( $token ) ) { 702 $token = token_name( $token[0] ); 703 } 704 switch ( $token ) { 705 case 'T_CURLY_OPEN': 706 case 'T_DOLLAR_OPEN_CURLY_BRACES': 707 case 'T_CURLY_OPENT_VARIABLE': // This is not documented in the manual. (11.05.07) 708 case '{': 709 ++$b; 710 break; 711 case '}': 712 --$b; 713 break; 714 } 715 } 716 717 if ( $b ) { 718 $error = _t( 'Unbalanced braces.' ); 719 return false; // Unbalanced braces would break the eval below 720 } 721 else { 722 ob_start(); // Catch potential parse error messages 723 $display_errors = ini_set( 'display_errors', 'on' ); // Make sure we have something to catch 724 $error_reporting = error_reporting( E_ALL ^ E_NOTICE ); 725 $code = eval( ' if (0){' . $code . '}' ); // Put $code in a dead code sandbox to prevent its execution 726 ini_set( 'display_errors', $display_errors ); // be a good citizen 727 error_reporting( $error_reporting ); 728 $error = ob_get_clean(); 729 730 return false !== $code; 731 } 732 } 733 734 /** 735 * Check the PHP syntax of (and execute) the specified file. 736 * 737 * @see Utils::php_check_syntax() 738 */ 739 public static function php_check_file_syntax( $file, &$error = null ) 740 { 741 // Prepend and append PHP opening tags to prevent eval() failures. 742 $code = ' ?>' . file_get_contents( $file ) . '<?php '; 743 744 return self::php_check_syntax( $code, $error ); 745 } 746 747 /** 748 * Replacement for system glob that returns an empty array if there are no results 749 * 750 * @param string $pattern The glob() file search pattern 751 * @param integer $flags Standard glob() flags 752 * @return array An array of result files, or an empty array if no results found 753 */ 754 public static function glob( $pattern, $flags = 0 ) 755 { 756 if ( ! defined( 'GLOB_NOBRACE' ) || ! ( ( $flags & GLOB_BRACE ) == GLOB_BRACE ) ) { 757 // this platform supports GLOB_BRACE out of the box or GLOB_BRACE wasn't requested 758 $results = glob( $pattern, $flags ); 759 } 760 elseif ( ! preg_match_all( '/\{.*?\}/', $pattern, $m ) ) { 761 // GLOB_BRACE used, but this pattern doesn't even use braces 762 $results = glob( $pattern, $flags ^ GLOB_BRACE ); 763 } 764 else { 765 // pattern uses braces, but platform doesn't support GLOB_BRACE 766 $braces = array(); 767 foreach ( $m[0] as $raw_brace ) { 768 $braces[ preg_quote( $raw_brace ) ] = '(?:' . str_replace( ',', '|', preg_quote( substr( $raw_brace, 1, -1 ), '/' ) ) . ')'; 769 } 770 $new_pattern = preg_replace( '/\{.*?\}/', '*', $pattern ); 771 $pattern = preg_quote( $pattern, '/' ); 772 $pattern = str_replace( '\\*', '.*', $pattern ); 773 $pattern = str_replace( '\\?', '.', $pattern ); 774 $regex = '/' . str_replace( array_keys( $braces ), array_values( $braces ), $pattern ) . '/'; 775 $results = preg_grep( $regex, Utils::glob( $new_pattern, $flags ^ GLOB_BRACE ) ); 776 } 777 778 if ( $results === false ) $results = array(); 779 return $results; 780 } 781 782 /** 783 * Produces a human-readable size string. 784 * For example, converts 12345 into 12.34KB 785 * 786 * @param integer $bytesize Number of bytes 787 * @return string Human-readable string 788 */ 789 public static function human_size( $bytesize ) 790 { 791 $sizes = array( 792 ' bytes', 793 'KiB', 794 'MiB', 795 'GiB', 796 'TiB', 797 'PiB' 798 ); 799 $tick = 0; 800 $max_tick = count( $sizes ) - 1; 801 while ( $bytesize > 1024 && $tick < $max_tick ) { 802 $tick++; 803 $bytesize /= 1024; 804 } 805 806 return sprintf( '%0.2f%s', $bytesize, $sizes[ $tick ] ); 807 } 808 809 /** 810 * Convert a single non-array variable into an array with that one element 811 * 812 * @param mixed $element Some value, either an array or not 813 * @return array Either the original array value, or the passed value as the single element of an array 814 */ 815 public static function single_array( $element ) 816 { 817 if ( !is_array( $element ) ) { 818 return array( $element ); 819 } 820 return $element; 821 } 822 823 /** 824 * Return the mimetype of a file 825 * 826 * @param string $filename the path of a file 827 * @return string The mimetype of the file. 828 */ 829 public static function mimetype( $filename ) 830 { 831 $mimetype = null; 832 if ( function_exists( 'finfo_open' ) ) { 833 $finfo = finfo_open( FILEINFO_MIME ); 834 $mimetype = finfo_file( $finfo, $filename ); 835 /* FILEINFO_MIME Returns the mime type and mime encoding as defined by RFC 2045. 836 * So only return the mime type, not the encoding. 837 */ 838 if ( ( $pos = strpos( $mimetype, ';' ) ) !== false ) { 839 $mimetype = substr( $mimetype, 0, $pos ); 840 } 841 finfo_close( $finfo ); 842 } 843 844 if ( empty( $mimetype ) ) { 845 $pi = pathinfo( $filename ); 846 switch ( strtolower( $pi[ 'extension' ] ) ) { 847 // hacky, hacky, kludge, kludge... 848 case 'jpg': 849 case 'jpeg': 850 $mimetype = 'image/jpeg'; 851 break; 852 case 'gif': 853 $mimetype = 'image/gif'; 854 break; 855 case 'png': 856 $mimetype = 'image/png'; 857 break; 858 case 'mp3': 859 $mimetype = 'audio/mpeg3'; 860 break; 861 case 'wav': 862 $mimetype = 'audio/wav'; 863 break; 864 case 'mpg': 865 case 'mpeg': 866 $mimetype = 'video/mpeg'; 867 break; 868 case 'swf': 869 $mimetype = 'application/x-shockwave-flash'; 870 break; 871 } 872 } 873 $mimetype = Plugins::filter( 'get_mime_type', $mimetype, $filename ); 874 return $mimetype; 875 } 876 877 /** 878 * Returns a trailing slash or a string, depending on the value passed in 879 * 880 * @param mixed $value A trailing string value 881 * @return string A slash if true, the value if value passed, emptystring if false 882 */ 883 public static function trail( $value = false ) 884 { 885 if ( $value === true ) { 886 return '/'; 887 } 888 elseif ( $value ) { 889 return $value; 890 } 891 return ''; 892 } 893 894 /** 895 * Send email 896 * 897 * @param string $to The destination address 898 * @param string $subject The subject of the message 899 * @param string $message The message itself 900 * @param array $headers An array of key=>value pairs for additional email headers 901 * @param string $parameters Additional parameters to mail() 902 * @return boolean True if sending the message succeeded 903 */ 904 public static function mail( $to, $subject, $message, $headers = array(), $parameters = '' ) 905 { 906 $mail = array( 907 'to' => $to, 908 'subject' => $subject, 909 'message' => $message, 910 'headers' => $headers, 911 'parameters' => $parameters, 912 ); 913 $mail = Plugins::filter( 'mail', $mail ); 914 915 $handled = false; 916 $handled = Plugins::filter( 'send_mail', $handled, $mail ); 917 if ( $handled ) { 918 return true; 919 } 920 else { 921 $additional_headers = array(); 922 foreach ( $headers as $header_key => $header_value ) { 923 $header_key = trim( $header_key ); 924 $header_value = trim( $header_value ); 925 if ( strpos( $header_key . $header_value, "\n" ) === false ) { 926 $additional_headers[] = "{$header_key}: {$header_value}"; 927 } 928 } 929 $additional_headers = implode( "\r\n", $additional_headers ); 930 } 931 return mail( $to, $subject, $message, $additional_headers, $parameters ); 932 } 933 934 /** 935 * Create a random password of a specific length 936 * 937 * @param integer $length Length of the password, if not provded, 10 938 * @return string A random password 939 */ 940 public static function random_password( $length = 10 ) 941 { 942 $password = ''; 943 $character_set = '1234567890!@#$^*qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVNBM'; 944 $data = str_split( $character_set ); 945 for ( $i = 0; $i < $length; $i++ ) { 946 $password .= $data[rand( 1, strlen( $character_set ) ) - 1]; 947 } 948 return $password; 949 } 950 951 /** 952 * Does a bitwise OR of all the numbers in an array 953 * @param array $input An array of integers 954 * @return int The bitwise OR of the input array 955 */ 956 public static function array_or( $input ) 957 { 958 return array_reduce( $input, array( 'Utils', 'ror' ), 0 ); 959 } 960 961 /** 962 * Helper function for array_or 963 */ 964 public static function ror( $v, $w ) 965 { 966 return $v |= $w; 967 } 968 969 /** 970 * Checks whether the correct HTTP method was used for the request 971 * 972 * @param array $expected Expected HTTP methods for the request 973 */ 974 public static function check_request_method( $expected ) 975 { 976 if ( !in_array( $_SERVER['REQUEST_METHOD'], $expected ) ) { 977 if ( in_array( $_SERVER['REQUEST_METHOD'], array( 'GET', 'HEAD', 'POST', 'PUT', 'DELETE' ) ) ) { 978 header( 'HTTP/1.1 405 Method Not Allowed', true, 405 ); 979 } 980 else { 981 header( 'HTTP/1.1 501 Method Not Implemented', true, 501 ); 982 } 983 header( 'Allow: ' . implode( ',', $expected ) ); 984 exit; 985 } 986 } 987 988 /** 989 * Returns a regex pattern equivalent to the given glob pattern 990 * 991 * @return string regex pattern with '/' delimiter 992 */ 993 public static function glob_to_regex( $glob ) 994 { 995 $pattern = $glob; 996 // braces need more work 997 $braces = array(); 998 if ( preg_match_all( '/\{.*?\}/', $pattern, $m ) ) { 999 foreach ( $m[0] as $raw_brace ) { 1000 $braces[ preg_quote( $raw_brace ) ] = '(?:' . str_replace( ',', '|', preg_quote( substr( $raw_brace, 1, -1 ), '/' ) ) . ')'; 1001 } 1002 } 1003 $pattern = preg_quote( $pattern, '/' ); 1004 $pattern = str_replace( '\\*', '.*', $pattern ); 1005 $pattern = str_replace( '\\?', '.', $pattern ); 1006 $pattern = str_replace( array_keys( $braces ), array_values( $braces ), $pattern ); 1007 return '/' . $pattern . '/'; 1008 } 1009 1010 /** 1011 * Return the port used for a specific URL scheme 1012 * 1013 * @param string $scheme The scheme in question 1014 * @return integer the port used for the scheme 1015 */ 1016 public static function scheme_ports( $scheme = null ) 1017 { 1018 $scheme_ports = array( 1019 'ftp' => 21, 1020 'ssh' => 22, 1021 'telnet' => 23, 1022 'http' => 80, 1023 'pop3' => 110, 1024 'nntp' => 119, 1025 'news' => 119, 1026 'irc' => 194, 1027 'imap3' => 220, 1028 'https' => 443, 1029 'nntps' => 563, 1030 'imaps' => 993, 1031 'pop3s' => 995, 1032 ); 1033 if ( is_null( $scheme ) ) { 1034 return $scheme_ports; 1035 } 1036 return $scheme_ports[ $scheme ]; 1037 } 1038 1039 /** 1040 * determines if the given that is travesable in foreach 1041 * 1042 * @param mixed $data 1043 * @return bool 1044 */ 1045 public static function is_traversable( $data ) 1046 { 1047 return ( is_array( $data ) || ( $data instanceof Traversable && $data instanceof Countable ) ); 1048 } 1049 1050 /** 1051 * Get the remote IP address, but try and take into account users who are 1052 * behind proxies, whether they know it or not. 1053 * @return The client's IP address. 1054 */ 1055 public static function get_ip( $default = '0.0.0.0' ) 1056 { 1057 // @todo in particular HTTP_X_FORWARDED_FOR could be a comma-separated list of IPs that have handled it, the client being the left-most. we should handle that... 1058 $keys = array( 'HTTP_CLIENT_IP', 'HTTP_FORWARDED', 'HTTP_X_FORWARDED', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_CLUSTER_CLIENT_IP', 'REMOTE_ADDR' ); 1059 1060 $return = ''; 1061 foreach ( $keys as $key ) { 1062 if ( isset( $_SERVER[ $key ] ) ) { 1063 $return = $_SERVER[ $key ]; 1064 } 1065 } 1066 1067 // make sure whatever IP we got was valid 1068 $valid = filter_var( $return, FILTER_VALIDATE_IP ); 1069 1070 if ( $valid === false ) { 1071 return $default; 1072 } 1073 else { 1074 return $return; 1075 } 1076 1077 } 1078 1079 /** 1080 * Call htmlspecialchars() with the correct flags and encoding, 1081 * without double escaping strings. 1082 * See http://php.net/manual/en/function.htmlspecialchars.php for details on the parameters 1083 * and purpose of the function. 1084 * 1085 * @todo Should htmlspecialchars_decode() be used instead of html_entity_decode()? 1086 * 1087 * @param $string. string. The string to escape 1088 * @param $quote_flag. integer. Sets what quotes and doublequotes are escaped 1089 * @param $encoding. string. The encoding of the passed string 1090 * 1091 * @return The escaped string 1092 */ 1093 public static function htmlspecialchars( $string, $quote_flag = ENT_COMPAT, $encoding = 'UTF-8' ) 1094 { 1095 return htmlspecialchars( html_entity_decode( $string, ENT_QUOTES, $encoding ), $quote_flag, $encoding ); 1096 } 1097 1098 /** 1099 * Convenience function to find a usable PCRE regular expression 1100 * delimiter for a particular string. (I.e., some character that 1101 * *isn't* found in the string.) 1102 * 1103 * @param $string. string. The string for which to find a delimiter. 1104 * @param $choices. string. Delimiters from which to choose one. 1105 * @param $encoding. string. The encoding of the passed string 1106 * 1107 * @return A valid regex delimiter, or null if none of the choices work. 1108 */ 1109 public static function regexdelim( $string, $choices = null ) 1110 { 1111 /* 1112 * Supply some default possibilities for delimiters if we 1113 * weren't given an explicit list. 1114 */ 1115 if ( ! isset( $choices ) ) { 1116 $choices = sprintf( '%c%c%c%c%c%c%c', 1117 167, /* § */ 1118 164, /* ¤ */ 1119 165, /* ¥ */ 1120 ord( '`' ), 1121 ord( '~' ), 1122 ord( '%' ), 1123 ord( '#' ) 1124 ); 1125 } 1126 $a_delims = str_split( $choices ); 1127 /* 1128 * Default condition is 'we didn't find one.' 1129 */ 1130 $delim = null; 1131 /* 1132 * Check for each possibility by scanning the text for it. 1133 * If it isn't found, it's a valid choice, so break out of the 1134 * loop. 1135 */ 1136 foreach ( $a_delims as $tdelim ) { 1137 if ( ! strstr( $string, $tdelim ) ) { 1138 $delim = $tdelim; 1139 break; 1140 } 1141 } 1142 return $delim; 1143 } 1144 1145 /** 1146 * Create a list of html element attributes from an associative array 1147 * 1148 * @param array $attrs An associative array of parameters 1149 * @return string The parameters turned into a string of tag attributes 1150 */ 1151 public static function html_attr($attrs) 1152 { 1153 $out = ''; 1154 foreach($attrs as $key => $value) { 1155 if($value != '') { 1156 $out .= ($out == '' ? '' : ' ') . $key . '="' . Utils::htmlspecialchars($value) . '"'; 1157 } 1158 } 1159 return $out; 1160 } 1161 1162 /** 1163 * Get a list of the PHP ini settings relevant to Habari 1164 * 1165 * @return Array The relevant PHP ini settings as array of strings 1166 */ 1167 public static function get_ini_settings() 1168 { 1169 $settings = array(); 1170 $keys = array( 1171 'safe_mode', 1172 'open_basedir', 1173 'display_errors', 1174 'session.gc_probability', 1175 'session.gc_maxlifetime', 1176 'error_reporting', 1177 'memory_limit', 1178 ); 1179 foreach($keys as $key ) { 1180 $val = ini_get( $key ); 1181 if ( $val === false ) { 1182 $settings[] = $key . ': ' . _t( 'Not set' ); 1183 } 1184 else { 1185 $settings[] = $key . ': ' . ( strlen( $val ) ? $val : '0' ); 1186 } 1187 } 1188 return $settings; 1189 } 1190} 1191?> 1192