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 &amp; 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( '&amp;', '&', $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(&hellip;)\", \"" . 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 ) . '&hellip;' . 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 ) . '&hellip;';
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