Show:

File: platform/classes/Q/Utils.php

<?php

/**
 * @module Q
 */

/**
 * Functions for doing various things
 * @class Q_Utils
 */

define('Q_UTILS_CONNECTION_TIMEOUT', 30);
define('Q_UTILS_INTERNAL_TIMEOUT', 1);

class Q_Utils
{
	/**
	 * Converts timestamps to standard UNIX timestamp with seconds.
	 * Accepts timestamps with seconds or milliseconds.
	 * @param $timestamp
	 * @return float
	 */
	static function timestamp($timestamp)
	{
		$timestamp = intval($timestamp);
		return $timestamp > 10000000000 ? round($timestamp / 1000) : $timestamp;
	}

	/**
	 * Generates signature for the data
	 * @method signature
	 * @static
	 * @param {array|string} $data
	 * @param {string} $secret
	 * @return {string}
	 */
	static function signature($data, $secret)
	{
		if (!isset($secret)) {
			throw new Q_Exception("Q_Utils::signature is expecting a \$secret");
		}
		if (is_array($data)) {
			ksort($data);
			$data = http_build_query($data);
			$data = str_replace('+', '%20', $data);
		}
		return self::hmac('sha1', $data, $secret);
	}

	/**
	 * Sign the data
	 * @method sign
	 * @static
	 * @param {array} $data The array of data
	 * @param {array|string} [$fieldKeys] Path of the key under which to save signature
	 * @return {array}
	 */
	static function sign($data, $fieldKeys = null) {
		$secret = Q_Config::get('Q', 'internal', 'secret', null);
		if (isset($secret)) {
			if (!$fieldKeys) {
				$sf = Q_Config::get('Q', 'internal', 'sigField', 'sig');
				$fieldKeys = array("Q.$sf");
			}
			if (is_string($fieldKeys)) {
				$fieldKeys = array($fieldKeys);
			}
			$ref = &$data;
			for ($i=0, $c = count($fieldKeys); $i<$c-1; ++$i) {
				if (!array_key_exists($fieldKeys[$i], $ref)) {
					$ref[ $fieldKeys[$i] ] = array();
				}
				$ref = &$ref[ $fieldKeys[$i] ];
			}
			$ef = end($fieldKeys);
			unset($ref[$ef]);
			$ref[$ef] = Q_Utils::signature($data, $secret);
		}
		return $data;
	}
	
	/**
	 * Calculates hmac
	 * @method hmac
	 * @static
	 * @param {string} $algo
	 * @param {string} $data
	 * @param {string} $key
	 * @param {boolean} [$raw_output=false]
	 * @return {string}
	 */
	static function hmac($algo, $data, $key, $raw_output = false)
	{
		$algo = strtolower($algo);
		$pack = 'H'.strlen(call_user_func($algo, 'test'));
		$size = 64;
		$opad = str_repeat(chr(0x5C), $size);
		$ipad = str_repeat(chr(0x36), $size);

		if (strlen($key) > $size) {
			$key = str_pad(pack($pack, call_user_func($algo, $key)), $size, chr(0x00));
		} else {
			$key = str_pad($key, $size, chr(0x00));
		}

		for ($i = 0; $i < strlen($key) - 1; $i++) {
			$opad[$i] = $opad[$i] ^ $key[$i];
			$ipad[$i] = $ipad[$i] ^ $key[$i];
		}

		$output = call_user_func(
			$algo, 
			$opad.pack($pack, call_user_func($algo, $ipad.$data))
		);

		return ($raw_output) ? pack($pack, $output) : $output;
	}

	/**
	 * Generates random letter sequence
	 * @method unique
	 * @static
	 * @param {integer} [$len=8]
	 * @param {string} [$characters='abcdefghijklmnopqrstuvwxyz'] All the characters from which to construct possible ids
	 * @return {string}
	 */
	static function unique(
		$len = 8, 
		$characters = 'abcdefghijklmnopqrstuvwxyz')
	{
		$characters_len = strlen($characters);
		$result = str_repeat(' ', $len);
		for ($i=0; $i<$len; ++$i) {
			$result[$i] = $characters[mt_rand(0, $characters_len-1)];
		}
		return $result;
	}

	/**
	 * Calculates a hash code from a string, to match String.prototype.hashCode() in Q.js
	 * @static
	 * @param {string} $text
	 * @return {integer}
	 */
	static function hashCode($text)
	{
		$hash = 0;
		$len = strlen($text);
		if (!$len) {
			return $hash;
		}
		for ($i=0; $i<$len; ++$i) {
			$c = ord($text[$i]);
			$hash = $hash % 16777216;
			$hash = (($hash<<5)-$hash)+$c;
			$hash = $hash & $hash; // Convert to 32bit integer
		}
		return $hash;
	}
	
	/**
	 * Some basic obfuscation to thwart scrapers from getting emails, phone numbers, etc.
	 * @static
	 * @method obfuscate
	 * @param {string} $text The text to obfuscate
	 * @param {string} [$key="blah"] Some key to use for obfuscation
	 * @return {text}
	 */
	static function obfuscate($text, $key = 'blah')
	{
		$len = strlen($text);
		$len2 = strlen($key);
		$result = '';
		for ($i=0; $i<$len; ++$i) {
			$j = $i % $len2;
			$diff = self::ord($text[$i]) - self::ord($key[$j]);
			$result .= ($diff < 0 ? '1' : '0') . self::chr(abs($diff));
		}
		return $result;
	}
	
	/**
	 * Like ord but handles utf-8 encoding
	 * @static
	 * @method ord
	 * @param {string} $text
	 * @return {integer}
	 */
	static function ord($text) { 
	    $k = mb_convert_encoding($text, 'UCS-2LE', 'UTF-8'); 
	    $k1 = ord(substr($k, 0, 1)); 
	    $k2 = ord(substr($k, 1, 1)); 
	    return $k2 * 256 + $k1; 
	}
	
	/**
	 * Like chr but handles utf-8 encoding
	 * @static
	 * @method chr
	 * @param {integer} $intval
	 * @return {string}
	 */
	static function chr($intval) {
		return mb_convert_encoding(pack('n', $intval), 'UTF-8', 'UTF-16BE');
	}

	/**
	 * Normalizes text by converting it to lower case, and
	 * replacing all non-accepted characters with underscores.
	 * @method normalize
	 * @static
	 * @param {string} $text The text to normalize
	 * @param {string} [$replacement='_'] A string to replace one or more unacceptable characters.
	 *  You can also change this default using the config Db/normalize/replacement
	 * @param {string} [$characters=null] Defaults to '/[^A-Za-z0-9]+/'. A regexp characters that are not acceptable.
	 *  You can also change this default using the config Db/normalize/characters
	 * @param {integer} [$numChars=200] Defaults to 200, maximum length of normalized string
	 * @param {boolean} [$keepCaseIntact=false] If true, doesn't convert to lowercase
	 */
	static function normalize(
		$text,
		$replacement = '_',
		$characters = null,
		$numChars = 200,
		$keepCaseIntact = false)
	{
		if (!isset($characters)) {
			$characters = '/[^A-Za-z0-9]+/';
			if (class_exists('Q_Config')) {
				$characters = Q_Config::get('Db', 'normalize', 'characters', $characters);
			}
		}
		if (!$numChars) {
			$numChars = 200;
		}
		if (!isset($replacement)) {
			$replacement = '_';
			if (class_exists('Q_Config')) {
				$replacement = Q_Config::get('Db', 'normalize', 'replacement', $replacement);
			}
		}
		if (!$keepCaseIntact) {
			$text = mb_strtolower($text, 'UTF-8');
		}
		$result = preg_replace($characters, $replacement, $text);
		if (mb_strlen($result) > $numChars) {
			$result = substr($result, 0, $numChars - 11) . '_' 
					  . self::hashCode(substr($result, $numChars - 11));
		}
		return $result;
	}
	
	/**
	 * Hashes text in a standard way. It uses md5, which is fast and irreversible,
	 * so it's good for things like indexes, but not for obscuring information.
	 * @method hash
	 * @static
	 * @param {string} $test
	 * @return {string}
	 */
	static function hash($text)
	{
		return md5(Db::normalize($text));
	}

	/**
	 * Cache-timing-safe variant of ord()
	 *
	 * @internal You should not use this directly from another application
	 *
	 * @param string $chr
	 * @return int
	 * @throws TypeError
	*/
	public static function chrToInt($chr)
	{
		/* Type checks: */
		if (!is_string($chr)) {
			throw new TypeError('Argument 1 must be a string, ' . gettype($chr) . ' given.');
		}
		/** @var array<int, int> $chunk */
		$chunk = unpack('C', $chr);
		return (int) ($chunk[1]);
	}

    /**
	 * Safe string length
	 *
	 * @internal You should not use this directly from another application
	 *
	 * @ref mbstring.func_overload
	 *
	 * @param string $str
	 * @return int
	*/
	public static function strlen($str)
	{
		return (int) (
			self::isMbStringOverride()
				? mb_strlen($str, '8bit')
				: strlen($str)
		);
	}

	/**
	 * Returns whether or not mbstring.func_overload is in effect.
	 *
	 * @internal You should not use this directly from another application
	 *
	 * @return bool
	*/
	protected static function isMbStringOverride()
	{
		static $mbstring = null;

		if ($mbstring === null) {
			$mbstring = extension_loaded('mbstring')
			&&
			(ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING);
		}
		/** @var bool $mbstring */
		return $mbstring;
	}

	/**
	 * A polyfill for hash_equals
	 * @param string $a
	 * @param string $b
	 *
	 * @return bool
	*/
	public static function hashEquals($a, $b)
	{
		if (is_callable('hash_equals')) {
			// PHP 5.6
			return hash_equals($a, $b);
		}
		try {
			if (class_exists('ParagonIE_Sodium_Core_Util')) {
				// sodium_compat
				try {
					return ParagonIE_Sodium_Core_Util::hashEquals($a, $b);
				} catch (SodiumException $ex) {

				}
			}
			// Home-grown polyfill:
			$d = 0;
			/** @var int $len */
			$len = self::strlen($a);
			if ($len !== self::strlen($b)) {
				return false;
			}
			for ($i = 0; $i < $len; ++$i) {
				$d |= self::chrToInt($a[$i]) ^ self::chrToInt($b[$i]);
			}

			if ($d !== 0) {
				return false;
			}

			return $a === $b;
		} catch (TypeError $ex) {
			// Safe bet: Fail closed
			return false;
		}
	}

	/**
	 * Get the lines from a csv file
	 * @method csvLines
	 * @param {string} $input
	 * @param {string} [$enclosure='"']
	 * @param {string} [$escape="\\"]
	 * @return array
	 *
	 */
	static function csvLines($input, $enclosure = '"', $escape = "\\")
	{
		$result = array();
		$lines = str_getcsv($input, "\r", $enclosure, $escape);
		foreach ($lines as $line) {
			if ($line and $line[0] === "\n") {
				$line = substr($line, 1);
			}
		}
		return $result;
	}

	/**
	 * Generates a Universally Unique IDentifier, version 4.
	 * This function generates a truly random UUID.
	 * @method uuid
	 * @static
	 * @see http://tools.ietf.org/html/rfc4122#section-4.4
	 * @see http://en.wikipedia.org/wiki/UUID
	 * @return {string} A UUID, made up of 32 hex digits and 4 hyphens.
	 */
	static function uuid() {
		
		if (!self::$urand) {
			self::$urand = @fopen ( '/dev/urandom', 'rb' );
		}

		$pr_bits = false;
		if (is_callable('random_bytes')) {
			$pr_bits .= random_bytes(16);
		} elseif (is_resource ( self::$urand )) {
			$pr_bits .= @fread ( self::$urand, 16 );
		}
		if (! $pr_bits) {
			$fp = @fopen ( '/dev/urandom', 'rb' );
			if ($fp !== false) {
				$pr_bits .= @fread ( $fp, 16 );
				@fclose ( $fp );
			} else {
				// If /dev/urandom isn't available (eg: in non-unix systems), use mt_rand().
				$pr_bits = "";
				for($cnt = 0; $cnt < 16; $cnt ++) {
					$pr_bits .= chr ( mt_rand ( 0, 255 ) );
				}
			}
		}
		$time_low = bin2hex ( substr ( $pr_bits, 0, 4 ) );
		$time_mid = bin2hex ( substr ( $pr_bits, 4, 2 ) );
		$time_hi_and_version = bin2hex ( substr ( $pr_bits, 6, 2 ) );
		$clock_seq_hi_and_reserved = bin2hex ( substr ( $pr_bits, 8, 2 ) );
		$node = bin2hex ( substr ( $pr_bits, 10, 6 ) );
		
		/**
		 * Set the four most significant bits (bits 12 through 15) of the
		 * time_hi_and_version field to the 4-bit version number from
		 * Section 4.1.3.
		 * @see http://tools.ietf.org/html/rfc4122#section-4.1.3
		 */
		$time_hi_and_version = hexdec ( $time_hi_and_version );
		$time_hi_and_version = $time_hi_and_version >> 4;
		$time_hi_and_version = $time_hi_and_version | 0x4000;
		
		/**
		 * Set the two most significant bits (bits 6 and 7) of the
		 * clock_seq_hi_and_reserved to zero and one, respectively.
		 */
		$clock_seq_hi_and_reserved = hexdec ( $clock_seq_hi_and_reserved );
		$clock_seq_hi_and_reserved = $clock_seq_hi_and_reserved >> 2;
		$clock_seq_hi_and_reserved = $clock_seq_hi_and_reserved | 0x8000;
		
		return sprintf ( '%08s-%04s-%04x-%04x-%012s', $time_low, $time_mid, $time_hi_and_version, $clock_seq_hi_and_reserved, $node );
	}
	
	/**
	 * Takes parts of a string
	 * @method parts
	 * @static
	 * @param {string} $source the string to split by the separator
	 * @param {integer} $ofset just like in array_slice
	 * @param {integer} $length just like in array_slice
	 * @param {string} $separator the separator, defaults to '/'
	 * @return {string} the extracted parts, joined together again by the separator
	 */
	static function parts($source, $offset, $length = null, $separator = '/')
	{
		return implode($separator, array_slice(explode($separator, $source), $offset, $length));
	}

	static function socket($ip, $port, &$errno, &$errstr, $timeout = null)
	{
		if (isset(self::$sockets[$ip][$port])) {
			return self::$sockets[$ip][$port];
		}
		return self::$sockets[$ip][$port] = @fsockopen($ip, $port, $errno, $errstr, $timeout);
	}
	
	/**
 	 * Sends a post and returns right away.
 	 * In the url being called, make sure it ignores user aborts.
	 * For example, in PHP call ignore_user_abort(true) at the top of that script.
	 * @method postAsync
	 * @static
	 * @param {string|array} $uri The url to post to
	 * @param {array} $params An associative array of params
	 * @param {string} [$user_agent=null] The user-agent string to send. 
	 *  If null, is replaced by default of "Mozilla/5.0 ..."
	 *  If false, not sent.
	 * @param {integer} [$timeout=Q_UTILS_CONNECTION_TIMEOUT]
	 * @param {boolean} [$throwIfRefused=false] Pass true here to throw an exception whenever Node process is not running or refuses the request
	 * @param {boolean} [$closeSocket=false] Pass true to close the socket after sending. The default is to do HTTP pipelining.
	 * @return {boolean} Returns whether the post succeeded.
	 */
	static function postAsync(
		$uri,
		$params,
		$user_agent = null,
		$timeout = Q_UTILS_CONNECTION_TIMEOUT,
		$throwIfRefused = false,
		$closeSocket = false)
	{
		if (!is_array($params)) {
			throw new Exception("\$params must be an array");
		}
		$post_string = http_build_query($params);
		
		$headers = array();
		$ip = null;
		if (is_array($uri)) {
			$url = $uri[0];
			if (isset($uri[1])) {
				$ip = $uri[1];
			}
		} else {
			$url = $uri;
		}
		$parts = parse_url($url);		
		$host = $parts['host'];
		if (!isset($ip)) $ip = $host;
		$request_uri = isset($parts['path']) ? $parts['path'] : '';
		if (!empty($parts['query'])) $request_uri .= "?".$parts['query'];
		$port = !empty($parts['port']) ? ':'.$parts['port'] : '';
		$url = $parts['scheme']."://".$ip.$port.$request_uri;

		if (empty($parts['path'])) $parts['path'] = '/';
		$headers[] = "POST " . $parts['path'] . " HTTP/1.1";
		$headers[] = "Host: ".$host;
		$headers[] = "Content-Type: application/x-www-form-urlencoded";
		$headers[] = "Content-Length: " . strlen($post_string) . "";
		if ($user_agent !== false) {
			if (is_null($user_agent)) {
				$user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9';
			}
			$headers[] ="User-Agent: $user_agent";
		}
		$out = implode("\r\n", $headers);
		$out .= "\r\nConnection: " . ($closeSocket ? 'Close' : 'Keep-Alive');
		$out .= "\r\n\r\n";
		if (isset($post_string))
			$out .= $post_string;

		$port = isset($parts['port']) ? $parts['port'] : 80;
		$fp = self::socket($ip, $port, $errno, $errstr, $timeout);
		if (!$fp) {
			if ($throwIfRefused) {
				$app = Q::app();
				throw new Q_Exception("PHP couldn't open a socket to " . $url . " (" . $errstr . ") Go to scripts/$app and run node $app.js");
			}
			return false;
		}
		$result = (fwrite($fp, $out) !== false);
		$result = $result && fflush($fp);
		$result = $result && fclose($fp);
		self::$sockets[$ip][$port] = null;
		return $result;
	}

	/**
	 * Issues a POST request, and returns the response
	 * @method post
	 * @static
	 * @param {string|array} $url The URL to post to
	 *  This can also be an array of ($url, $ip) to send the request
	 *  to a particular IP, while retaining the hostname and request URI
	 * @param {array|string} $data The data content to post or an array of ($field => $value) pairs
	 * @param {string} [$user_agent=null] The user-agent string to send. Defaults to Mozilla.
	 * @param {string} [$follow_redirects=true] Whether to follow redirects when getting a response.
	 * @param {string} [$header=null] Optional string to replace the entire POST header
	 * @return {string} The response.
	 * 
	 * **NOTE:** *The function waits for it, which might take a while!*
	 */
	static function post ($url, $data, $user_agent = null, $follow_redirects = true, $header = null, $conn_t = 120, $res_t = 120)
	{
		return Q_Utils::request('POST', $url, $data, $user_agent, $follow_redirects, $header, $conn_t, $res_t);
	}

	/**
	 * Issues a GET request, and returns the response
	 * @method get
	 * @static
	 * @param {string|array} $url The URL to post to
	 *  This can also be an array of ($url, $ip) to send the request
	 *  to a particular IP, while retaining the hostname and request URI
	 * @param {string} [$user_agent=null] The user-agent string to send. Defaults to Mozilla.
	 * @param {string} [$follow_redirects=true] Whether to follow redirects when getting a response.
	 * @param {string} [$header=null] Optional string to replace the entire GET header
	 * @return {string} The response.
	 * 
	 * **NOTE:** *The function waits for it, which might take a while!*
	 */
	static function get ($url, $user_agent = null, $follow_redirects = true, $header = null)
	{
		return Q_Utils::request('GET', $url, null, $user_agent, $follow_redirects, $header);
	}

	/**
	 * Issues an http request, and returns the response
	 * @method request
	 * @static
	 * @private
	 * @param {string} $method The http method to use
	 * @param {string|array} $url The URL to request
	 *  This can also be an array of ($url, $ip) to send the request
	 *  to a particular IP, while retaining the hostname and request URI
	 * @param {array|string} $data The data content to post or an array of ($field => $value) pairs
	 * @param {string} [$user_agent=null] The user-agent string to send. Defaults to Mozilla.
	 * @param {string} [$follow_redirects=true] Whether to follow redirects when getting a response.
	 * @param {string} [$header=null] Optional string to replace the entire header
	 * @return {string} The response.
	 * 
	 * **NOTE:** *The function waits for it, which might take a while!*
	 */
	private static function request($method, $uri, $data, $user_agent = null, $follow_redirects = true, $header = null, $conn_t = 120, $res_t = 120)
	{
		$method = strtoupper($method);
		if (!isset($user_agent))
			$user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9';

		$ip = null;
		if (is_array($uri)) {
			$url = $uri[0];
			if (isset($uri[1])) $ip = $uri[1];
		} else $url = $uri;
		$parts = parse_url($url);		
		$host = $parts['host'];
		if (!isset($ip)) $ip = $host;
		$request_uri = $parts['path'];
//		if (!empty($parts['query'])) $request_uri .= "?".$parts['query'];
		$port = isset($parts['port']) ? ':'.$parts['port'] : '';
		$url = $parts['scheme']."://".$ip.$port.$request_uri;

		// NOTE: this works for http(s) only
		$headers = array("Host: ".$host);

		if (is_array($data)) {
			$data = http_build_query($data, null, '&');
		}
		if (!is_string($data)) {
			$data = '';
		}

		if (function_exists('curl_init')) {
			// Use CURL if installed...
			$ch = curl_init();
			curl_setopt_array($ch, array(
				CURLOPT_USERAGENT => $user_agent,

				CURLOPT_RETURNTRANSFER => true,	 // return web page
				CURLOPT_HEADER		 => false,	// don't return headers
				CURLOPT_FOLLOWLOCATION => true,	 // follow redirects
				CURLOPT_ENCODING	   => "",	   // handle all encodings
				CURLOPT_AUTOREFERER	=> true,	 // set referer on redirect
				CURLOPT_CONNECTTIMEOUT => $conn_t,	  // timeout on connect
				CURLOPT_TIMEOUT		=> $res_t,	  // timeout on response
				CURLOPT_MAXREDIRS	  => 10,	   // stop after 10 redirects
			));
			switch ($method) {
				case 'POST':
					curl_setopt_array($ch, array(
						CURLOPT_URL => $url,
						CURLOPT_POSTFIELDS => $data,
						CURLOPT_POST => true
					));
					break;
				case 'GET':
					// default method for cURL
					curl_setopt($ch, CURLOPT_URL, "$url?$data");
					break;
				case 'PUT':
					// not supported
				case 'DELETE':
					// not supported
				default:
					throw new Q_Exception("Unknown request method '$method'");
			}
			if (!empty($headers)) {
				curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
			}
			$result = curl_exec($ch);
			curl_close($ch);
		} else {
			// Non-CURL based version...
			if (!isset($header)) {
				if ($data) $headers[] = "Content-type: application/x-www-form-urlencoded";
				$headers[] = "User-Agent: $user_agent";
				if ($data) $headers[] = "Content-length: " . strlen($data);
				$header = implode("\r\n", $headers);
			}
			$context = stream_context_create(array(
				'http' => array(
					'method' => $method,
					'header' => $header,
					'content' => $data,
					'max_redirects' => 10,
					'timeout'	   => $res_t
			)));
			$sock = fopen($url, 'rb', false, $context);
			if ($sock) {
				$result = '';
				while (! feof($sock))
					$result .= fgets($sock, 4096);
				fclose($sock);
			}
		}
		return $result;
	}

	/**
	 * Queries a server externally to the specified handler. Expects json array with 
	 * either ['slots']['data'] or ['error'] fields filled
	 * @method queryExternal
	 * @static
	 * @param {string} $handler the handler to call
	 * @param {array} [$data=array()] Associative array of data of the message to send.
	 * @param {string|array} [$url=null] and url to query. Default to 'Q/web/appRootUrl' config value
	 * @return {mixed} The response from the server
	 */
	static function queryExternal($handler, $data = array(), $url = null)
	{
		if (!is_array($data)) {
			throw new Q_Exception_WrongType(array('field' => 'data', 'type' => 'array'));
		}
		$data['Q.ajax'] = 'json';
		$data['Q.slotNames'] = 'data';

		if ((!isset($url)) && !($url = Q_Config::get('Q', 'web', 'appRootUrl', false)))
			throw new Q_Exception("Root URL is not defined in Q_Utils::queryExternal");

		if (is_array($url)) {
			$server = array();
			$server[] = "{$url[0]}/action.php/$handler";
			if (isset($url[1])) $server[] = $url[1];
		} else {
			$server = "$url/action.php/$handler";
		}

		$result = json_decode(self::post($server, self::sign($data)), null, true, null, Q_UTILS_CONNECTION_TIMEOUT, Q_UTILS_CONNECTION_TIMEOUT, true);
		
		// TODO: check signature of returned data

		if (isset($result['errors'])) {
			throw new Q_Exception($result['errors']);
		}
		return isset($result['slots']['data']) ? $result['slots']['data'] : null;
	}

	/**
	 * Sends a query to Node.js internal server and gets the response
	 * This method shall make communications behind firewal
	 * @method queryInternal
	 * @static
	 * @param {string} $handler is used as 'Q/method' while querying $url 
	 * @param {array} [$data=array()] Associative array of data of the message to send.
	 * @param {string|array} [$url=null] and url to query. Default to 'Q/nodeInternal' config value and path '/Q_Utils/query'
	 * @return {mixed} The response from the server
	 */
	static function queryInternal($handler, $data = array(), $url = null)
	{
		if (!is_array($data)) {
			throw new Q_Exception_WrongType(array('field' => 'data', 'type' => 'array'));
		}

		if (!isset($url)) {
			$nodeh = Q_Config::get('Q', 'nodeInternal', 'host', null);
			$nodep = Q_Config::get('Q', 'nodeInternal', 'port', null);
			$url = $nodep && $nodeh ? "http://$nodeh:$nodep" : false;
		}

		if (!$url) {
			throw new Q_Exception("Q_Utils::queryInternal: the nodeInternal config is missing");
		}
		
		if (is_array($url)) {
			$server = array();
			$server[] = "{$url[0]}/$handler";
			if (isset($url[1])) $server[] = $url[1];
		} else {
			$server = "$url/$handler";
		}

		$result = Q::json_decode(self::post(
			$server, self::sign($data), null, true, null, 
			Q_UTILS_INTERNAL_TIMEOUT, Q_UTILS_INTERNAL_TIMEOUT
		), true);

		// TODO: check signature of returned data

		// delete the above line to throw on error
		if (isset($result['errors'])) {
			$msg = is_array($result['errors'])
				? reset($result['errors'])
				: $result['errors'];
			throw new Q_Exception($msg);
		}
		return isset($result['data']) ? $result['data'] : null;
	}
	
	/**
	 * Sends asynchronous internal message to Node.js
	 *  If "Q.clientId" is in $_REQUEST, adds it into the data
	 * @method sendToNode
	 * @static
	 * @param {array} $data Associative array of data of the message to send.
	 *  It should contain the key "Q/method" so Node can decide what to do with the message.
	 * @param {string|array} [$url=null] and url to query. Default to 'Q/nodeInternal' config value and path '/Q/node'
	 * @param {boolean} [$throwIfRefused=false] Pass true here to throw an exception whenever Node process is not running or refuses the request
	 */
	static function sendToNode($data, $url = null, $throwIfRefused = false)
	{
		if (!is_array($data)) {
			throw new Q_Exception_WrongType(array('field' => 'data', 'type' => 'array'));
		}
		if (empty($data['Q/method'])) {
			throw new Q_Exception_RequiredField(array('field' => 'Q/method'));
		}
		
		$clientId = Q_Request::special('clientId', null);
		if (isset($clientId)) {
			$data['Q.clientId'] = $clientId;
		}
		
		// The following hook may modify the url
		/**
		 * @event Q/Utils/sendToNode {before}
		 * @param {array} data
		 * @param {string|array} 'url'
		 */
		Q::event('Q/Utils/sendToNode', array('data' => $data, 'url' => $url), 'before');

		if (!$url) {
			$nodeh = Q_Config::get('Q', 'nodeInternal', 'host', null);
			$nodep = Q_Config::get('Q', 'nodeInternal', 'port', null);
			$url = $nodep && $nodeh ? "http://$nodeh:$nodep/Q/node" : false;
		}

		if (!$url) {
			$result = false;
		} else {
			// Should we switch to sending JSON over TCP?
			$result = Q_Utils::postAsync(
				$url, self::sign($data), null, 
				Q_UTILS_INTERNAL_TIMEOUT, $throwIfRefused
			);
		}
		return $result;
//		if (!$result) {
//			throw new Q_Exception_SendingToNode(array('method' => $data['Q/method']));
//		}
	}
	
	/**
	 * Unserializes session stored in PHP 5.2 and 5.3 format
	 * @method unserializeSession
	 * @static
	 * @param {string} $val
	 * @return {array}
	 */
	static function unserializeSession($val)
	{
		$result = array();

		// prefixing with semicolon to make it easier to write the regular expression
		$val = ';' . $val;

		// regularexpression to find the keys
		$keyreg = '/;([^|{}"]+)\|/';

		// find all keys
		$matches = array();
		preg_match_all($keyreg, $val, $matches);

		// only go further if we found some keys
		if (isset($matches[1])) {
			$keys = $matches[1];

			// find the values by splitting the input on the key regular expression
			$values = preg_split($keyreg, $val);

			// unshift the first value since it's always empty (due to our semicolon prefix)
			if (count($values) > 1) {
				array_shift($values);
			}

			// combine the $keys and $values
			$result = array_combine($keys, $values);
		}

		return $result;
	}

	/**
	 * Returns base url for node.js requests
	 * @method nodeUrl
	 * @static
	 * @throws {Q_Exception_MissingConfig} If node host or port are not defined
	 */
	static function nodeUrl () {
		$url = Q_Config::get('Q', 'node', 'url', null);
		if (isset($url)) {
			return Q_Uri::interpolateUrl($url);
		}
		$host = Q_Config::get('Q', 'node', 'host', null);
		$port = Q_Config::get('Q', 'node', 'port', null);
		if (!isset($port) || !isset($host)) {
			return null;
		}
		$https = Q_Config::get('Q', 'node', 'https', false);
		$s = $https ? 's' : '';
		return "http$s://$host:$port";
	}
	
	/**
	 * Returns path option for socket.io connection
	 * @method socketPath
	 * @static
	 * @throws {Q_Exception_MissingConfig} If node host or port are not defined
	 */
	static function socketPath () {
		return Q_Config::get('Q', 'node', 'socket', 'path', '/socket.io');
	}

	/**
	 * Copies a file or directory from path to another. May overwrite existing files.
	 * @method copy
	 * @static
	 * @param {string} $source
	 * @param {string} $dest
	 * @throws {Q_Exception_MissingConfig} If node host or port are not defined
	 */
	static function copy($source, $dest) {
		
		if (file_exists($dest) and (is_dir($source) xor is_dir($dest))) {
			throw new Q_Exception("Q_Utils::copy doesn't work if one parameter is a file and one is a directory");
		}

		$dir = opendir($source);
		@mkdir($dest);
		while (false !== ( $file = readdir($dir)) ) {
			if (($file != '.') && ($file != '..')) {
				if ( is_dir($source.DS.$file) ) {
					self::copy($source.DS.$file, $dest.DS.$file);
				} else {
					copy($source . DS . $file,$dest.DS.$file);
				}
			}
		}
		closedir($dir);
		
	}

	/**
	 * Checks whether the path can be used for writing files by the current session
	 * @method canWriteToPath
	 * @static
	 * @param {string} $path The path to check
	 * @param {mixed} [$throwIfNotWritable=false] Defaults to false.
	 * Set to true to throw a Q_Exception_CantWriteToPath if no user is logged in.
	 * Set to null to skip firing the "before" event, thereby skipping custom access checks.
	 * The null value is useful for when the filename is generated by the app, not the user.
	 * @param {boolean} [$mkdirIfMissing] Defaults to false.
	 * Pass true here to make a directory at the specified path, if it's writeable but missing.
	 * Pass a string here to override the umask before making the directory.
	 * @return {boolean}
	 */
	static function canWriteToPath(
		$path, 
		$throwIfNotWritable = false, 
		$mkdirIfMissing = false
	) {
		if (isset($throwIfNotWritable)) {
			$result = Q::event(
				"Q/Utils/canWriteToPath",
				compact('path', 'throwIfNotWritable', 'mkdirIfMissing'),
				'before'
			);
			if (isset($result)) {
				if (!$result and $throwIfNotWritable) {
					throw new Q_Exception_CantWriteToPath(compact('path', 'mkdirIfMissing'));
				}
				return $result;
			}
		}
		$paths = array(APP_FILES_DIR);
		foreach (Q_Config::get('Q', 'plugins', array()) as $plugin) {
			$c = strtoupper($plugin).'_PLUGIN_FILES_DIR';
			if (defined($c)) {
				$paths[] = constant($c);
			}
		}
		$paths[] = Q_FILES_DIR;
		if (strpos($path, "../") === false
		and strpos($path, "..".DS) === false) {
			foreach ($paths as $p) {
				$len = strlen($p);
				if (strncmp($path, $p, $len) === 0) {
					// we can write to this path
					if ($mkdirIfMissing and !file_exists($path)) {
						$mask = is_string($mkdirIfMissing)
							? umask($mkdirIfMissing)
							: umask(0000);
						if (!@mkdir($path, 0777, true)) {
							throw new Q_Exception_FilePermissions(array(
								'action' => 'create',
								'filename' => $path,
								'recommendation' => ' Please set your files directory to be writable.'
							));
						}
						umask($mask);
					}
					return true;
				}
			}
		}
		if ($throwIfNotWritable) {
			throw new Q_Exception_CantWriteToPath(compact('path', 'mkdirIfMissing'));
		}
		return false;
	}
	
	static function colored($text, $foreground_color = null, $background_color = null)
	{
		if (!$foreground_color and !$background_color) {
			return $text;
		}
		static $foreground_colors = array(
			'black' => '0;30',
			'dark_gray' => '1;30',
			'blue' => '0;34',
			'light_blue' => '1;34',
			'green' => '0;32',
			'light_green' => '1;32',
			'cyan' => '0;36',
			'light_cyan' => '1;36',
			'red' => '0;31',
			'light_red' => '1;31',
			'purple' => '0;35',
			'light_purple' => '1;35',
			'brown' => '0;33',
			'yellow' => '1;33',
			'light_gray' => '0;37',
			'white' => '1;37'
		);
		static $background_colors = array(
			'black' => '40',
			'red' => '41',
			'green' => '42',
			'yellow' => '43',
			'blue' => '44',
			'magenta' => '45',
			'cyan' => '46',
			'light_gray' => '47'
		);
		$colored_string = "";
		if (isset($foreground_colors[$foreground_color])) {
			$colored_string .= "\033[" . $foreground_colors[$foreground_color] . "m";
		}
		if (isset($background_colors[$background_color])) {
			$colored_string .= "\033[" . $background_colors[$background_color] . "m";
		}
		return $colored_string .  $text . "\033[0m";
	}
	
	static function cp ($src, $dest)
	{
		if (is_file($src)) {
			return copy($src, $dest);
		}
		if (!is_dir($src)) {
			return false;
		}
		@mkdir($dest);
		foreach(scandir($src) as $file) {
			if( $file == "." || $file == ".." ) {
				continue;
			}
			if( is_dir( $src.DS.$file ) ) {
				self::cp( $src.DS.$file, $dest.DS.$file );
			} else {
				copy( $src.DS.$file, $dest.DS.$file );
			}
		}
		return true;
	}
	
	/**
	 * Create a symlink
	 * @method symlink
	 * @static
	 * @param {string} $target
	 * @param {string} $link
	 * @param {boolean} [$skipIfExists=false]
	 * @return {boolean} true if link was created, false if it already exists
	 * @throws Q_Exception if link could not be created
	 */
	static function symlink($target, $link, $skipIfExists = false)
	{
		// Make sure destination directory exists
		if(!file_exists(dirname($link))) {
			$mask = umask(Q_Config::get('Q', 'internal', 'umask', 0000));
			mkdir(dirname($link), 0777, true);
			umask($mask);
		}

		$is_win = (substr(strtolower(PHP_OS), 0, 3) === 'win');

		if(is_dir($link) && !$is_win && !is_link($link)) {
			echo Q_Utils::colored(
				"[WARN] Symlink '$link' (target: '$target') was not created".PHP_EOL, 
				'red', 'yellow'
			);
			return;
		}

		if (file_exists($link)) {
			if ($skipIfExists) {
				return false;
			}
			if ($is_win && is_dir($link)) {
				rmdir($link);
			} else if (is_link($link)) {
				unlink($link);
			}
		}

		if ($is_win) {
			exec('mklink /j "' . $link . '" "' . $target . '"');
		} else {
			@symlink($target, $link);
		}
		
		if (!file_exists($link)) {
			throw new Q_Exception("Link $link to target $target was not created");
		}
	}
	
	/**
	 * Used to split ids into one or more segments, in order to store millions
	 * of files under a directory, without running into limits of various filesystems
	 * on the number of files in a directory.
	 * Consider using Amazon S3 or another service for uploading files in production.
	 * @method splitId
	 * @static
	 * @param {string} $id the id to split
	 * @param {integer} [$lengths=3] the lengths of each segment (the last one can be smaller)
	 * @param {string} [$delimiter=DIRECTORY_SEPARATOR] the delimiter to put between segments
	 * @param {string} [$internalDelimiter='/'] the internal delimiter, if it is set then only the last part is split, and instances of internalDelimiter are replaced by delimiter
	 * @return {string} the segments, delimited by the delimiter
	 */
	static function splitId($id, $lengths = 3, $delimiter = DIRECTORY_SEPARATOR, $internalDelimiter = '/')
	{
		if (!$internalDelimiter) {
			return implode($delimiter, str_split($id, $lengths));
		}
		$parts = explode($internalDelimiter, $id);
		$last = array_pop($parts);
		$prefix = $parts ? (implode($delimiter, $parts) . $delimiter) : '';
		return $prefix . implode($delimiter, str_split($last, $lengths));
	}

	/**
	 * Normalize paths to use DS, used mostly on Windows
	 * @method normalizePath
	 * @static
	 * @param {string|array} $path the path or paths to normalize
	 */
	static function normalizePath (&$path)
	{
		$symbol = (DS === '/') ? '\\' : '/';
		switch (gettype($path)) {
			case "string":
				$path = str_replace($symbol, DS, $path);
				break;
			case "array":
				array_walk($path, function (&$item, $key, $symbol) {
					$item = str_replace($symbol, DS, $item);
				}, $symbol);
				break;
		}
	}

	protected static $urand;
	protected static $sockets = array();
}