/inc/util.inc.php (e47b8fcfe9cb10d112a95e207155056781b648b8) (76450 bytes) (mode 100644) (type blob)

<?php
require_once(__DIR__ . '/prof.inc.php');
require_once(__DIR__ . '/log.inc.php');

set_error_handler("rg_error_handler");
register_shutdown_function("rg_error_shutdown");

define('RG_SOCKET_NO_WAIT', 0x01);

define('RG_LOCK_BLOCK', 1 << 0);

if (!isset($rg_util_debug))
	$rg_util_debug = FALSE;
if (strcmp(getenv('ROCKETGIT_UTIL_DEBUG'), '1') == 0)
	$rg_util_debug = TRUE;

$rg_util_error = "";
$rg_php_err = '-';

function rg_util_set_error($str)
{
	global $rg_util_error;
	$rg_util_error = $str;
	rg_log('util_set_error: ' . $str);
}

function rg_util_error()
{
	global $rg_util_error;
	return $rg_util_error;
}

/*
 * This array will keep all registered function for templates
 */
$rg_template_functions = array();

/*
 *
 */
function rg_php_err()
{
	global $rg_php_err;

	//TODO: we are using our error handler!
	//$a = error_get_last();
	//if ($a === NULL) {
		$ret = $rg_php_err;
		$rg_php_err = '-';
	//} else {
	//	$ret = $a['message'];
	//	error_clear_last();
	//}

	return $ret;
}

/*
 * Register a function to be called when a @@func:func_name:var@@ construction
 * is found in a template.
 * Please note that the function is called once per template.
 */
function rg_template_func($name, $real_func_name)
{
	global $rg_template_functions;

	$rg_template_functions[$name] = $real_func_name;
}

function sha512($m)
{
	return hash('sha512', $m);
}

/*
 * TODO: 8192KiB must be 8MiB
 */
function rg_1024($v)
{
	if ($v <= 9999)
		return number_format($v) . 'B';
	$v /= 1024;

	if ($v <= 9999)
		return number_format($v) . "KiB";
	$v /= 1024;

	if ($v <= 9999)
		return number_format($v) . "MiB";
	$v /= 1024;

	if ($v <= 9999)
		return number_format($v) . "GiB";
	$v /= 1024;

	if ($v <= 9999)
		return number_format($v) . "TiB";
	$v /= 1024;

	return number_format($v) . "PiB";
}

/*
 * Transforms a kilo/mega/giga/tera into bytes
 * Example: 8M -> 8388608
 */
function rg_mega2bytes($s)
{
	$r = intval($s);
	if (stristr($s, 'k'))
		return $r * 1024;
	if (stristr($s, 'm'))
		return $r * 1024 * 1024;
	if (stristr($s, 'g'))
		return $r * 1024 * 1024 * 1024;
	if (stristr($s, 't'))
		return $r * 1024 * 1024 * 1024 * 1024;

	return $r;
}

/*
 * Returns a binary string of random bytes
 */
function rg_random_bytes($len)
{
	static $buf = '';
	static $buf_len = 0;

	rg_prof_start('random_bytes');

	$ret = FALSE;

	if ($len > $buf_len) {
		$f = @fopen('/dev/urandom', 'r');
		if ($f === NULL)
			rg_fatal('cannot open urandom');

		while ($len > $buf_len) {
			$r = @fread($f, max(512, $len));
			if ($r === FALSE)
				rg_fatal('cannot read from urandom');

			$buf .= $r;
			$buf_len += strlen($r);
		}

		fclose($f);
	}

	$ret = substr($buf, $len);
	$buf = substr($buf, $len);
	$buf_len -= $len;

	rg_prof_end('random_bytes');
	return $ret;
}

/*
 * Unique ID generator
 */
function rg_id($len)
{
	rg_prof_start('id');

	$len2 = intval(($len + 1) / 2);
	$id = rg_random_bytes($len2);
	$id = bin2hex($id);
	$id = substr($id, 0, $len);

	rg_prof_end('id');
	return $id;
}

/*
 * Locks a file
 */
$_lock = array();
function rg_lock($file, $flags)
{
	global $_lock;
	global $rg_lock_dir;

	if (!isset($rg_lock_dir))
		$rg_lock_dir = "/var/lib/rocketgit/locks";

	if (strncmp($file, '/', 1) == 0) // full path lock
		$lock_file = $file;
	else
		$lock_file = $rg_lock_dir . '/' . $file;

	// Double locking?
	if (isset($_lock[$file])) {
		rg_util_set_error('double locking error [' . $lock_file . ']');
		return FALSE;
	}

	$f = @fopen($lock_file, "w");
	if ($f === FALSE) {
		rg_util_set_error('cannot open lock [' . $lock_file . ']: '
			. rg_php_err());
		return FALSE;
	}

	$x = LOCK_EX | LOCK_NB;
	if ($flags & RG_LOCK_BLOCK)
		$x &= ~LOCK_NB;
	if (!@flock($f, $x, $wouldblock)) {
		if ($wouldblock == 1)
			rg_util_set_error('lock already locked [' . $lock_file . ']');
		else
			rg_util_set_error('cannot lock [' . $lock_file . ']: '
				. rg_php_err());
		fclose($f);
		return FALSE;
	}

	fwrite($f, getmypid() . "\n");

	$_lock[$file] = $f;
	rg_log_debug('Lock acquired [' . $file . ']');

	return TRUE;
}

function rg_lock_or_exit($file)
{
	if (rg_lock($file, 0) === FALSE)
		exit(0);
}

function rg_unlock($file)
{
	global $_lock;

	if (!isset($_lock[$file])) {
		rg_internal_error('Lock not taken [' . $file . ']');
		return FALSE;
	}

	fclose($_lock[$file]);
	rg_log_debug('Lock unlocked [' . $file . ']');
	unset($_lock[$file]);
}

/*
 * Returns information about memory usage of the OS
 */
function rg_memory()
{
	$r = @file('/proc/meminfo');
	if ($r === FALSE)
		return FALSE;

	$ret = array();
	$found = 0;
	foreach ($r as $line) {
		$t = explode(':', $line);
		$k = rtrim($t[0], ' ');
		$t = explode(' ', trim($t[1]));
		$v = $t[0];

		if (strcmp($k, 'MemTotal') == 0) {
			$ret['total'] = $v;
			$found++;
		} else if (strcmp($k, 'MemAvailable') == 0) {
			$ret['avail'] = $v;
			$found++;
		}

		if ($found == 2)
			break;
	}

	return $ret;
}

/*
 * Taks a list (example: 0-4,6-7) and return exploded elements (0,1,2,3,4,6,7)
 */
function rg_list_expand($s)
{
	$t = explode(',', trim($s));
	$ret = array();
	$i = 0;
	while (isset($t[$i])) {
		$x = explode('-', $t[$i]);
		if (!isset($x[1])) {
			$ret[] = $t[$i];
		} else {
			for ($j = $x[0]; $j <= $x[1]; $j++)
				$ret[] = $j;
		}
		$i++;
	}

	return $ret;
}

/*
 * Returns the number of online cores
 */
function rg_cores()
{
	$r = @file_get_contents('/sys/devices/system/cpu/online');
	if ($r === FALSE)
		return 1;

	$online_cores = rg_list_expand($r);
	return count($online_cores);
}

/*
 * Returns the load of the system multiplied by 100 and divided by the number of cores
 * Example: 4 cores, load 8 will return 2
 */
function rg_load()
{
	// Number of cores
	$r = @file_get_contents('/proc/loadavg');
	if ($r === FALSE)
		return 0;

	$t = explode(' ', $r);
	$v = 100 * $t[0];
	return intval($v / rg_cores());
}

/*
 * Outputs a string to browser, XSS safe
 * Thanks OWASP!
 */
function rg_xss_safe($str)
{
	return htmlspecialchars($str,
		ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}

/*
 * Builds URLs
 */
function rg_re_url($area)
{
	return $area;
}

function rg_re_userpage($ui)
{
	if (isset($ui['uid']) && ($ui['uid'] == 0))
		return '';

	if (!isset($ui['organization'])) {
		rg_internal_error("rg_re_userpage called with wrong ui (no org)!");
		rg_log("ui: " . print_r($ui, TRUE));
		exit(1);
	}

	$prefix = '';
	if ($ui['organization'] == 0)
		$prefix = '/user';

	$s = $prefix . '/' . rawurlencode($ui['username']);

	return rg_re_url($s);
}

function rg_re_repopage($ui, $repo_name)
{
	$s = rg_re_userpage($ui) . "/" . rawurlencode($repo_name);

	return rg_re_url($s);
}

function rg_re_bugpage($ui, $repo_name, $bug_id)
{
	return rg_re_repopage($ui, $repo_name) . "/bug/" . $bug_id;
}

/*
 * Builds a host[+port]
 */
function rg_base_url_host_port($hostname, $port, $default_port)
{
	if ($port != $default_port)
		return $hostname . ':' . $port;

	return $hostname;
}

/*
 * Builds a correct URL to refer to the current virtualhost
 * @http(s)_allow: '0' if not allowed, FALSE if unknown, else port number
 * Note: at least one of http_allow or https_allow will be set to a port number.
 */
function rg_base_url_build($hostname, $http_allow, $https_allow, $user, $pass)
{
	// We are forced to use something if we cannot get them from cache/db
	if (($hostname === FALSE) || empty($hostname))
		$hostname = php_uname('n');

	if (!empty($user))
		$add = rawurlencode($user) . ':' . rawurlencode($pass) . '@';
	else
		$add = '';

	// Prefer httpS
	if (intval($https_allow) > 0)
		return 'https://' . $add
			. rg_base_url_host_port($hostname, $https_allow, 443);

	return 'http://' . $add
		. rg_base_url_host_port($hostname, $http_allow, 80);
}

function rg_re_repo_ssh($organization, $user, $repo)
{
	global $rg_ssh_host;
	global $rg_ssh_port;

	if ($rg_ssh_port == 22)
		$port = "";
	else
		$port = ":" . $rg_ssh_port;

	$prefix = "";
	if ($organization == 0)
		$prefix = "/user";

	return "ssh://rocketgit@" . $rg_ssh_host . $port
		. $prefix . "/" . rawurlencode($user) . "/" . rawurlencode($repo);
}

function rg_re_repo_git($organization, $user, $repo)
{
	global $rg_git_host;
	global $rg_git_port;

	if ($rg_git_port == 9418)
		$port = "";
	else
		$port = ":" . $rg_git_port;

	$prefix = "";
	if ($organization == 0)
		$prefix = "/user";

	return "git://" . $rg_git_host . $port
		. $prefix . "/" . rawurlencode($user) . "/" . rawurlencode($repo);
}

function rg_re_repo_http($organization, $user, $repo)
{
	$prefix = '';
	if ($organization == 0)
		$prefix = '/user';

	return $prefix . "/"
		. rawurlencode($user) . "/" . rawurlencode($repo);
}

function rg_var_get($name)
{
	if (isset($_SERVER[$name]))
		$ret = $_SERVER[$name];
	else if (isset($_POST[$name]))
		$ret = $_POST[$name];
	else if (isset($_GET[$name]))
		$ret = $_GET[$name];
	else
		return FALSE;

	return str_replace("\r", "", $ret);
}

function rg_var_is_set($name)
{
	$c = rg_var_get($name);
	if ($c === FALSE)
		return 0;

	return 1;
}

function rg_var_str($name)
{
	$c = rg_var_get($name);
	if ($c === FALSE)
		return '';

	return $c;
}

function rg_var_str_nocr($name)
{
	$k = array("\n", "\r");
	$v = array('', '');
	return str_replace($k, $v, rg_var_str($name));
}

function rg_var_str_list($name)
{
	$a = trim(rg_var_str($name));
	$a = str_replace("\n", ' ', $a);
	$a = str_replace("\r", ' ', $a);
	$a = str_replace("\t", ' ', $a);
	$old = '';
	while (strcmp($old, $a) != 0) {
		$old = $a;
		$a = str_replace('  ', ' ', $a);
	}

	return $a;
}

function rg_var_int($name)
{
	$r = rg_var_str($name);

	if (is_array($r)) {
		$ret2 = array();
		foreach ($r as $k => $v)
			$ret2[$k] = sprintf("%d", $v);
		return $ret2;
	}
	return sprintf("%d", $r);
}

function rg_var_uint($name)
{
	$r = rg_var_str($name);

	if (is_array($r)) {
		$ret2 = array();
		foreach ($r as $k => $v)
			$ret2[$k] = sprintf("%u", $v);
		return $ret2;
	}
	return sprintf("%u", $r);
}

function rg_var_bool($name)
{
	$r = rg_var_str($name);
	if (strcmp($r, '1') == 0)
		return 1;

	return 0;
}

function rg_var_email($name)
{
	return trim(rg_var_str_nocr($name));
}

/*
 * Allow only @re chars
 */
function rg_var_re($name, $re)
{
	$a = rg_var_str($name);
	return preg_replace('/[^' . $re . ']/', '', $a);
}

/*
 * Extract a cookie from $_COOKIE
 */
function rg_var_cookie_re($name, $re)
{
	if (!isset($_COOKIE[$name]))
		return "";

	return preg_replace($re, '', $_COOKIE[$name]);
}

/*
 * Gets data from request and transforms it into an array
 * Usefull for checkboxes.
 * (a2s = array2string)
 */
function rg_var_a2s($var)
{
	$r = rg_var_str($var);
	if (!is_array($r))
		return $r;

	$ret = '';
	foreach ($r as $s => $junk)
		$ret .= $s;

	return $ret;
}

/*
 * Enforce chars in a name. It is used for user and repo.
 * Returns: -1 on error, 0 = no match, 1 = match
 */
function rg_chars_allow($name, $allowed_regexp, &$invalid)
{
	// Modifiers: u = UTF8, D = PCRE_DOLLAR_ENDONLY
	// http://php.net/manual/en/reference.pcre.pattern.modifiers.php
	$r = preg_match('/^[' . $allowed_regexp . ']*$/uD', $name);
	if ($r === FALSE) {
		$m = 'cannot match [' . $allowed_regexp
			. '] in [' . $name . ']';
		rg_internal_error($m);
		rg_util_set_error($m);
		return -1;
	}

	if ($r !== 1) {
		$invalid = preg_replace('/[' . $allowed_regexp . ']/', '', $name);
		rg_util_set_error('chars_allow: [' . $name . ']'
			. ' does not match [' . $allowed_regexp . ']');
		return 0;
	}

	return 1;
}

/*
 * Deletes a folder and the files inside it
 */
function rg_rmdir($dir)
{
	if (!is_dir($dir))
		return TRUE;

	$scan = glob($dir . "/*");
	if ($scan === FALSE) {
		rg_util_set_error("invalid pattern [$dir/*]");
		return FALSE;
	}

	if (count($scan) > 0) {
		$all_good = TRUE;
		foreach ($scan as $junk => $path) {
			if (is_dir($path)) {
				$r = rg_rmdir($path);
				if ($r !== TRUE)
					return FALSE;

				continue;
			}

			if (!unlink($path)) {
				rg_util_set_error('cannot remove path: ' . rg_php_err());
				return FALSE;
			}
		}
	}

	if (!rmdir($dir)) {
		rg_util_set_error('cannot remove main dir: ' . rg_php_err());
		return FALSE;
	}

	return TRUE;
}

/*
 * Lookup a path in the current theme with fallback to default
 * Returns the correct path
 */
function rg_theme_resolve_path($path)
{
	global $rg_theme, $rg_theme_dir;

	$url = "/themes/" . $rg_theme . "/" . $path;
	$xfile = $rg_theme_dir . "/" . $rg_theme . "/" . $path;
	if (!is_file($xfile))
		$url = "/themes/default/" . $path;

	return $url;
}

/*
 * Loads a file if exists, else return ""
 */
function rg_file_get_contents($f)
{
	if (!file_exists($f))
		return "";

	$c = @file_get_contents($f);
	if ($c === FALSE) {
		rg_internal_error('Could not load file: ' . rg_php_err() . '.');
		return "";
	}

	return $c;
}

/*
 * Merges an array (a) into another (src), using a namespace
 * Protects modifiers (HTML: etc.).
 */
function rg_array_merge($src, $namespace, $a)
{
	$ret = $src;

	if (!empty($namespace))
		if (!isset($ret[$namespace]))
			$ret[$namespace] = array();

	foreach ($a as $k => $v) {
		if (empty($namespace))
			$ret[$k] = $v;
		else
			$ret[$namespace][$k] = $v;
	}

	return $ret;
}

/*
 * Returns the members of the array, excluding arrays
 */
function rg_array_filter_arrays($a)
{
	$ret = array();
	foreach ($a as $k => $v) {
		if (is_array($v))
			continue;
		$ret[$k] = $v;
	}

	return $ret;
}

/*
 * Performs a lookup of a var of type 'a::b::c' into an array and
 * returns FALSE or the value
 */
function rg_template_tree_lookup($var, &$data, $xss_protection)
{
	$tree = &$data;
	$t = explode('::', $var);
	$v = array_pop($t);
	foreach ($t as $token) {
		if (!isset($tree[$token]))
			return FALSE;

		$tree = &$tree[$token];
	}

	// We prefer the HTML version
	$hv = 'HTML:' . $v;
	if (isset($tree[$hv]))
		return $tree[$hv];

	if (isset($tree[$v])) {
		if ($xss_protection)
			return rg_xss_safe($tree[$v]);
		else
			return $tree[$v];
	}

	return FALSE;
}

/*
 * Evaluates a condition
 * Returns FALSE on error, 0 for TRUE and 1 for FALSE
 */
function rg_template_eval_cond($cond, &$data)
{
	while (strpos($cond, '  '))
		$cond = str_replace('  ', ' ', $cond);

	$t = explode(' ', $cond);
	if (count($t) != 3) {
		rg_util_set_error('invalid condition: ' . $cond
			. ' (not 3 tokens)');
		return FALSE;
	}

	$left = rg_template_string($t[0], 0, $data, FALSE);
	$op = $t[1];
	$right = rg_template_string($t[2], 0, $data, FALSE);

	if (strcmp($op, '==') == 0)
		return strcmp($left, $right) == 0 ? 1 : 0;

	if (strcmp($op, '!=') == 0)
		return strcmp($left, $right) != 0 ? 1 : 0;

	$ileft = intval($left);
	$iright = intval($right);

	if (strcmp($op, '>=') == 0)
		return $ileft >= $iright ? 1 : 0;

	if (strcmp($op, '>') == 0)
		return $ileft > $iright ? 1 : 0;

	if (strcmp($op, '<=') == 0)
		return $ileft <= $iright ? 1 : 0;

	if (strcmp($op, '<') == 0)
		return $ileft < $iright ? 1 : 0;

	rg_util_set_error('unknown condition: ' . $cond);
	return FALSE;
}

/*
 * Finds matching }} for an {{
 * We assume @off points to the byte after '{{'
 * Returns the offset of the byte before '}}'
 */
function rg_template_find_closing(&$s, $off)
{
	$nesting_level = 0;
	while (1) {
		$end = strpos($s, '}}', $off);
		if ($end === FALSE)
			return -1;

		$start = strpos($s, '{{', $off);
		if (($start === FALSE) || ($start >= $end)) {
			if ($nesting_level == 0)
				return $end - 1;

			$nesting_level--;
			$off = $end + 2;
		} else {
			$nesting_level++;
			$off = $start + 2;
		}
	}
}

/*
 * "Decodes" an 'if', returning 'true_start/end' and false_start/end'
 * s + off must point after ')'
 * Returns -1 on error, 0 on success
 */
function rg_template_find_true_and_false(&$s, $off, &$true_start, &$true_end,
	&$false_start, &$false_end)
{
	//rg_log_enter("DEBUG: template_find_true_and_false s+off=[" . substr($s, $off) . "]");

	$true_start = strpos($s, '{{', $off);
	if ($true_start === FALSE) {
		//rg_log_debug('no \'{{\'!');
		//rg_log_exit();
		return -1;
	}
	$true_start += 2;

	if (strncmp(substr($s, $true_start, 1), "\n", 1) == 0) {
		//rg_log_debug('true starts with CR, remove it');
		$true_start++;
	}

	$true_end = rg_template_find_closing($s, $true_start);
	if ($true_end == -1) {
		//rg_log_debug('no true_end!');
		//rg_log_exit();
		return -1;
	}
	//rg_log_debug('true_start=' . $true_start
	//	. ' true_end=' . $true_end . ' [' . substr($s, $true_end, 3) . '...]'
	//	. ' true=[' . substr($s, $true_start, $true_end - $true_start + 1) . ']');

	// We try to detect if we have an else
	$false_start = -1; $false_end = -1;
	$x = strpos($s, '{{', $true_end);
	if ($x !== FALSE) {
		$gap = substr($s, $true_end + 3, $x - $true_end - 3);
		$gap = trim($gap);
		//rg_log_debug('gap = [' . $gap . ']');
		if (empty($gap)) {
			$false_start = $x + 2;
			if (strncmp(substr($s, $false_start, 1), "\n", 1) == 0) {
				//rg_log_debug('false starts with CR, remove it');
				$false_start++;
			}
			$false_end = rg_template_find_closing($s, $x + 2);
			//rg_log_debug('false=[' . substr($s, $false_start, $false_end - $false_start + 1) . ']');
		} else {
			//rg_log_debug('gap prevents parsing stuff as false, we have only true part');
		}
	} else {
		//rg_log_debug('cannot find \'{{\'');
	}

	//rg_log_exit();
	return 0;
}

/*
 * Helper for rg_tempalte_string to deal with 'if's
 * Returns how many bytes used from string @s in @next
 */
function rg_template_string_if(&$s, $off, &$data, &$next, $xss_protection)
{
	rg_prof_start("template_string_if");
	//rg_log_enter("DEBUG: template_string_if s+off=[" . substr($s, $off) . "]");

	$ret = '';
	$next = $off;

	$off += 5; /* skip '@@if(' */
	$pos = strpos($s, ')', $off);
	if ($pos === FALSE) {
		rg_util_set_error('no closing \')\' in [' . substr($s, $off) . ']');
		rg_log_exit();
		rg_prof_end("template_string_if");
		return '';
	}

	$cond = substr($s, $off, $pos - $off); $off = $pos + 1;
	$eval_cond = rg_template_eval_cond($cond, $data);
	if ($eval_cond === FALSE) {
		rg_prof_end('template_string_if');
		return -1;
	}

	// TODO: Between ')' and '{{' must be only space, else ignore anything??

	$r = rg_template_find_true_and_false($s, $off, $true_start, $true_end,
		$false_start, $false_end);
	if ($r == -1) {
		rg_util_set_error('no if skeleton found [' . substr($s, $off) . ']');
		//rg_log_exit();
		rg_prof_end("template_string_if");
		return -1;
	}

	$x = '';
	if ($eval_cond === 1) {
		$x = substr($s, $true_start, $true_end - $true_start + 1);
	} else {
		if ($false_start != -1)
			$x = substr($s, $false_start, $false_end - $false_start + 1);
	}
	//rg_log_debug('x=[' . $x . ']');

	$ret .= rg_template_string($x, 0, $data, $xss_protection);

	if ($false_start != -1)
		$next = $false_end + 3;
	else
		$next = $true_end + 3;

	if (strncmp(substr($s, $next, 1), "\n", 1) == 0)
		$next++;

	//rg_log_debug('next: [' . substr($s, $next) . ']');
	//rg_log_exit();
	rg_prof_end("template_string_if");
	return $ret;
}

/*
 * Replace all known variables in string @s
 * Example @data: a->a2->a3, b->b2; @s='@@a::a2@@ @@b@@' => 'a3 b2'
 * @off - offset in @s (performance reasons)
 * @xss_protection - TRUE if you want to apply rg_xss_safe on the value of vars
 */
function rg_template_string(&$s, $off, &$data, $xss_protection)
{
	global $rg_template_functions;

	rg_prof_start('template_string');
	//rg_log_enter("DEBUG: template_string: s+off=[" . substr($s, $off) . "]");

	$ret = '';
	while (strlen(substr($s, $off, 1)) == 1) {
		//rg_log_debug('template_string: s+off=[' . substr($s, $off) . ']');
		$pos = strpos($s, '@@', $off);
		if ($pos === FALSE) {
			$ret .= substr($s, $off);
			break;
		}
		$var_start = $pos + 2;

		// copy everything before '@@'
		$ret .= substr($s, $off, $pos - $off);
		//rg_log_debug('after copy all before @@, ret=[' . $ret . ']');
		$off = $pos;

		$s2 = substr($s, $off, 5);
		if (strcmp($s2, '@@if(') == 0) {
			$r = rg_template_string_if($s, $off, $data, $next,
				$xss_protection);
			if ($r == -1) {
				$ret .= 'Cannot evaluate condition: ' . rg_xss_safe(rg_util_error());
				break;
			}
			$ret .= $r;
			$off = $next;
			continue;
		}

		$off += 2; /* skip start '@@' */
		$pos2 = strpos($s, '@@', $off);
		if ($pos2 === FALSE) {
			// We have only start '@@'
			$ret .= substr($s, $off);
			break;
		}
		$var_end = $pos2 - 1;
		$off = $pos2 + 2;

		$var = substr($s, $var_start, $var_end - $var_start + 1);
		//rg_log_debug('var=[' . $var . ']');

		$value = rg_template_tree_lookup($var, $data, $xss_protection);
		if ($value === FALSE) {
			$value = '@@' . $var . '@@';
			if (strncmp($var, 'IMG:', 4) == 0) {
				$path = substr($var, 4);
				$path = rg_template_string($path, 0, $data, $xss_protection);
				//rg_log_debug('found an img tag path=[' . $path . ']');
				$value = rg_theme_resolve_path($path);
			} else if (strncmp($var, 'FUNC:', 5) == 0) {
				$rest = substr($var, 5);
				//rg_log_debug('found a function call rest=[' . $rest . ']');
				$fpos = strpos($rest, ':');
				if ($fpos === FALSE) {
					// no params
					$func = $rest;
					$_param = '';
				} else {
					$func = substr($rest, 0, $fpos);
					$_param = substr($rest, $fpos + 1);
				}

				//rg_log_debug('func=[' . $func . '] _param=[' . $_param . ']');

				// out var may be with '@@'
				$param = rg_template_string($_param, 0, $data, $xss_protection);

				if (isset($rg_template_functions[$func])) {
					if (strncmp($param, '@@', 2) == 0) {
						$param = substr($param, 2);
						$param = substr($param, 0, -2);
					}
					$r = $rg_template_functions[$func]($param);
					//rg_debug() && rg_log('  func returned [' . $r . ']');
					$value = $r['value'];
					if ($xss_protection && ($r['html'] !== 1))
						$value = rg_xss_safe($value);
				}
			} else if (strncmp($var, 'URL_ESCAPE:', 11) == 0) {
				$rest = substr($var, 11);
				$value = rg_template_tree_lookup($rest, $data, FALSE /*xss*/);
				$value = rawurlencode($value);
			} else if (strncmp($var, 'ESCAPE_SHELL_ARG:', 17) == 0) {
				$rest = substr($var, 17);
				$value = rg_template_tree_lookup($rest, $data, FALSE /*xss*/);
				$value = escapeshellarg($value);
			} else if (strncmp($var, 'TEMPLATE:', 9) == 0) {
				$path = substr($var, 9);
				rg_log_debug('found a template path=[' . $path . ']');
				$value = rg_template($path, $data, $xss_protection);
			} else if (strncmp($var, 'SET:', 4) == 0) {
				$rest = substr($var, 4);
				rg_log_debug('found a set rest=[' . $rest . ']');
				$_t = explode('=', $rest, 2);
				if (isset($_t[1]))
					$data[$_t[0]] = $_t[1];
				$value = '';
			} else if (strcmp($var, 'DUMP') == 0) {
				$value = rg_xss_safe(print_r($data, TRUE));
			} else {
				// You do not want to activate next line because of spurious lookups
				rg_log('WARN: var [' . $var . '] not found');
			}
		}
		//rg_log_debug('var=[' . $var . '] value=[' . $value . ']');
		$ret .= $value;
	}

	//rg_log_debug('ret=[' . $ret . ']');
	//rg_log_exit();
	rg_prof_end('template_string');
	return $ret;
}

/*
 * Loads a template from disk without replacing vars or acting on 'ifs'
 */
function rg_template_blind($file)
{
	global $rg_theme_dir;
	global $rg_theme;

	rg_prof_start('template_blind');
	//rg_log_enter('template_blind: ' . $file);

	$ret = '';
	while (1) {
		$xfile = $rg_theme_dir . "/" . $rg_theme . "/" . $file;
		if (!is_file($xfile)) {
			$xfile = $rg_theme_dir . "/default/" . $file;
			if (!is_file($xfile))
				$xfile = $file;
				if (!is_file($xfile)) {
					rg_internal_error('Cannot find ' . $file . '!');
					break;
				}
		}

		$ret = rg_file_get_contents($xfile);
		break;
	}

	rg_prof_end('template_blind');
	return $ret;
}

/*
 * Loads a template from disk and replase all known variables
 * @xss_protection - TRUE if you want to apply rg_xss_safe on the value of vars
 */
function rg_template($file, &$data, $xss_protection)
{
	rg_prof_start('template');
	rg_log_enter('template: ' . $file);

	$ret = '';
	while (1) {
		$body = rg_template_blind($file);
		if (empty($body))
			break;

		$ret = rg_template_string($body, 0, $data, $xss_protection);
		break;
	}

	//rg_log_debug('rg_template returns [' . $ret . ']');
	rg_log_exit();
	rg_prof_end('template');
	return $ret;
}

/*
 * Builds a html output based on a template with header, footer and line
 * @data - in array of data for every out line: index 0 is line 1, index 1 is line 2...
 * @more - ?
 */
function rg_template_table($dir, &$data, $more)
{
	global $rg_theme_dir;
	global $rg_theme;

	rg_prof_start('template_table');
	//rg_log_debug('template_table: ' . $dir
	//	. ' data: ' . print_r($data, TRUE));

	$xdir = $rg_theme_dir . "/" . $rg_theme . "/" . $dir;
	if (!is_dir($xdir)) {
		rg_log("$xdir not found.");
		$xdir = $rg_theme_dir . "/default/" . $dir;
		rg_log("Using [$xdir]");
	}

	if (!is_array($data) || empty($data)) {
		$ret = rg_template($xdir . "/nodata.html", $more, TRUE /*xss*/);
		rg_prof_end('template_table');
		return $ret;
	}

	$head = rg_template($xdir . "/header.html", $more, TRUE /* xss */);
	$foot = rg_template($xdir . "/footer.html", $more, TRUE /* xss */);

	$line = rg_file_get_contents($xdir . "/line.html");

	$body = '';
	foreach ($data as $index => $info) {
		$more2 = array_merge($more, $info);
		$more2['index'] = rg_xss_safe($index);
		$body .= rg_template_string($line, 0, $more2, TRUE /*xss*/);
	}

	rg_prof_end('template_table');
	return $head . $body . $foot;
}

/*
 * Outputs a numbered list
 */
function rg_template_list($c)
{
	$a = explode("\n", $c);
	if (count($a) == 0)
		return "";

	$ret = "";
	$i = 1;
	$add = "";
	foreach ($a as $line) {
		$ret .= $add . $i . "&nbsp;" . rg_xss_safe($line);
		$add = "<br />";
		$i++;
	}

	return $ret;
}

/*
 * Show errors using a template
 */
function rg_template_errmsg($a)
{
	if (empty($a))
		return "";

	rg_log('errmsg: ' . rg_array2string($a));

	$b = array();
	foreach ($a as $junk => $err)
		$b[] = array("error" => $err);
	return rg_template_table("errmsg", $b, array());
}

/*
 * Show a warning using a template
 */
function rg_warning($msg)
{
	if (empty($msg))
		return "";

	rg_log("Warning: $msg");

	$x = array("msg" => $msg);
	return rg_template("warning.html", $x, TRUE /* xss */);
}

/*
 * Show an OK message using a template
 * TODO: OBSOLETE? Because we want the files to be in templates?
 */
function rg_ok($msg)
{
	if (empty($msg))
		return "";

	$x = array("msg" => $msg);
	return rg_template("ok.html", $x, TRUE /* xss */);
}

/*
 * Helper for reading input from a fd for rg_exec2.
 * Used as a 'cb_output' function.
 */
function rg_exec2_helper_read_fd($index, &$info)
{
	$ret = -1;
	while (1) {
		$r = @fread($info['cb_output_fd'], 16 * 4096);
		if ($r === FALSE) {
			$err = 'cannot read from cb_output_fd: ' . rg_php_err();
			rg_internal_error($err);
			break;
		}

		$len = strlen($r);
		if ($len === 0) {
			fclose($info['cb_output_fd']);
			$ret = 0;
			break;
		}

		$info['out_buf'] .= $r;
		$ret = $len;
		break;
	}

	return $ret;
}

/*
 * Helper for decompressing input for rg_exec2.
 * On error, it returns FALSE and $err contains the error message.
 */
function rg_exec2_helper_gzip_in(&$info)
{
	global $rg_util_debug;

	$rg_util_debug &&
	rg_log('exec2_helper_gzip_in...');

	if (empty($info['out_buf']))
		return TRUE;

	if (!isset($info['gzip_in_ctx'])) {
		$info['gzip_in_ctx'] = inflate_init(ZLIB_ENCODING_GZIP);
		if ($info['gzip_in_ctx'] === FALSE) {
			$err = 'cannot initialize context';
			return FALSE;
		}

		$info['gzip_in_bytes'] = 0;
	}

	$do_exit = FALSE;
	do {
		$rg_util_debug &&
		rg_log_debug('  calling inflate_add with buf[0-31]=' . substr($info['out_buf'], 0, 32) . (!empty($info['out_buf']) ? '' : ' and ZLIB_FINISH'));
		if (!empty($info['out_buf']))
			$r = inflate_add($info['gzip_in_ctx'], $info['out_buf']);
		else
			$r = inflate_add($info['gzip_in_ctx'], $info['out_buf'], ZLIB_FINISH);
		if ($r === FALSE) {
			$err = 'error in decompression: ' . rg_php_err();
			rg_internal_error($err);
			return FALSE;
		}

		$s = inflate_get_status($info['gzip_in_ctx']);
		if ($s === ZLIB_STREAM_END) {
			$rg_util_debug && rg_log('  Got ZLIB_STREAM_END; set out_buf_done to 1 and do_exit to TRUE');
			$info['out_buf'] = ''; // because zlib may finish before consuming all input
			$info['out_buf_done'] = 1;
			unset($info['gzip_in_ctx']);
			$do_exit = TRUE;
		} else if ($s === ZLIB_OK) {
			$rg_util_debug && rg_log('  Got ZLIB_OK');

			$rl = inflate_get_read_len($info['gzip_in_ctx']);
			if ($rl === FALSE) {
				$err = 'cannot retrieve read_len';
				return FALSE;
			}
			$rg_util_debug &&
			rg_log_debug('  gzip_in_bytes=' . $info['gzip_in_bytes'] . ' read_len=' . $rl);

			$diff = $rl - $info['gzip_in_bytes'];
			$info['gzip_in_bytes'] = $rl;
			$info['out_buf'] = substr($info['out_buf'], $diff);

			if (!empty($r))
				$do_exit = TRUE;
		} else {
			unset($info['gzip_in_ctx']);
			if ($s === FALSE)
				rg_log_debug('  status is FALSE');
			else if ($s === ZLIB_BUF_ERROR)
				rg_log_debug('  status is ZLIB_BUF_ERROR');
			else
				rg_log_debug('  status is unknown [' . $s . ']');
			return FALSE;
		}

		$rg_util_debug &&
		rg_log_debug('  decompression produced: [' . $r . '] out_buf=' . $info['out_buf']);
	} while (!$do_exit);

	$info['out_buf_real'] .= $r;

	return TRUE;
}

/*
 * Helper for rg_exec to init the structure
 */
function rg_exec2_helper_init(&$info)
{
	$info['last_activity'] = 0;
	$info['last_errmsg'] = '';
	$info['start_ts'] = time();
	$info['last_failure'] = 0;
	if (!isset($info['out_buf']))
		$info['out_buf'] = '';
	$info['out_buf_real'] = '';
	$info['in_buf'] = '';
	$info['err_buf'] = '';
	$info['done'] = FALSE;
	$info['started'] = 0;

	if (!isset($info['idle_time']))
		$info['idle_time'] = 5;

	if (isset($info['input_fd']))
		$info['input_fd_done'] = 0;
	else
		$info['input_fd_done'] = 1;

	if (isset($info['cb_output']))
		$info['cb_output_done'] = 0;
	else
		$info['cb_output_done'] = 1;

	// TODO: what this is doing, rename it to something better
	if (!isset($info['out_buf_done']))
		$info['out_buf_done'] = 1;

	// m2p = 'me' to 'program'
	$info['m2p_bytes'] = 0;

	// w2m = 'web' to 'me'
	$info['w2m_bytes'] = 0;
}

/*
 * Helper for rg_exec to close a stream
 */
function rg_exec2_helper_close_stream($index, $fd, &$info, $stream)
{
	global $rg_util_debug;

	$rg_util_debug &&
	rg_log_enter($index . ' DEBUG: exec2_helper_close_stream: stream ' . $stream);

	if ($stream == -1) {
		$info['input_fd_done'] = 1;
		unset($info['input_fd']);
	} else {
		unset($info['pipes'][$stream]);
	}

	$r = @fclose($fd);
	if ($r === FALSE)
		rg_log($index . ' Error closing stream ' . $stream
			. ' (' . rg_php_err() . ')');

	if (($stream == 0) && isset($info['input_fd'])) {
		$rg_util_debug &&
		rg_log($index . ' DEBUG: closing stream 0 =>'
			. ' closing input_fd ' . $info['input_fd']);
		rg_exec2_helper_close_stream($index, $info['input_fd'], $info, -1);
	}

	$rg_util_debug &&
	rg_log_exit();
}

/*
 * Tries to procude data to be sent to the peer process
 */
function rg_exec2_helper_populate_out_buf($index, &$info)
{
	global $rg_util_debug;

	$rg_util_debug &&
	rg_log_enter($index . ': exec2_helper_populate_out_buf');

	// cb_output can populate $info['out_buf']
	if ($info['cb_output_done'] == 0) {
		$r = $info['cb_output']($index, $info);
		if ($r == -1) {
			// TODO
		} else if ($r == 0) {
			$info['cb_output_done'] = 1;
		} else {
			$info['w2m_bytes'] += $r;
		}
	}

	if (!isset($info['out_buf_helper'])) {
		$info['out_buf_real'] .= $info['out_buf'];
		$info['out_buf'] = '';
	} else {
		$info['out_buf_helper']($info);
	}

	$rg_util_debug &&
	rg_log_exit();
}

/*
 * This will replace rg_exec function
 * Returns the array of the commands with the last status.
 */
function rg_exec2($a)
{
	global $rg_util_debug;

	$_id = ''; $_add = '';
	foreach ($a['cmds'] as $_junk => $_i) {
		$_id .= $_add . '[' . $_i['cmd'] . ']';
		$_add = ' ';
	}

	rg_prof_start('exec2 ' . $_id);
	rg_log_enter('exec2 ' . $_id);

	//$rg_util_debug &&
	//rg_log_ml('DEBUG: a: ' . print_r($a, TRUE));

	$ret = array();
	$ret['ok'] = 0;
	$ret['cmds'] = $a['cmds'];

	// some status initialization
	$s = array();
	foreach ($ret['cmds'] as $cmd => &$info) {
		$info['stopped'] = TRUE;
		$info['wait_for_stop'] = FALSE;
		rg_exec2_helper_init($info);
	}

	while (1) {
		// check if all commands are started
		$now = time();
		$rx = array(); $wx = array();
		$lut = array();
		$done = TRUE;
		foreach ($ret['cmds'] as $index => &$info) {
			if ($info['done'])
				continue;

			if ($info['started'] == 0) {
				// nothing
			} else if (empty($info['pipes'])) {
				$rg_util_debug &&
				rg_log($index . ' DEBUG: All streams are closed.'
					. ' Set wait_for_stop to TRUE');
				$info['wait_for_stop'] = TRUE;
			} else {
				$rg_util_debug &&
				rg_log_ml($index . ' DEBUG: Cannot set wait_for_stop to'
					. ' true because something is not closed; pipes:'
					. ' ' . rg_array2string($info['pipes']));
			}

			if ($info['wait_for_stop'] == TRUE) {
				$rg_util_debug &&
				rg_log($index . ' DEBUG: needs stop is TRUE!');

				for ($i = 0; $i <= 2; $i++)
					if (isset($info['pipes'][$i]))
						fclose($info['pipes'][$i]);

				if (isset($info['input_fd'])) {
					@fclose($info['input_fd']);
					unset($info['input_fd']);
				}

				$info['ps'] = proc_get_status($info['a']);
				$rg_util_debug &&
				rg_log_ml($index . ' DEBUG: info[ps]: '
					. print_r($info['ps'], TRUE));

				if ($info['ps']['running'] !== FALSE) {
					$done = FALSE;
					continue;
				}

				@proc_close($info['a']);
				$info['exitcode'] = $info['ps']['exitcode'];

				if (($info['exitcode'] != 0) && empty($info['last_errmsg']))
					$info['last_errmsg'] = 'child exited with'
						. ' error code ' . $info['exitcode'];

				if (isset($info['cb_finish']))
					$info['cb_finish']($index, $info, $info['exitcode']);

				unset($info['pipes']);
				unset($info['a']);

				$info['wait_for_stop'] = FALSE;
				$info['stopped'] = TRUE;

				if (isset($info['restart_delay'])) {
					rg_exec2_helper_init($info);
				} else {
					$rg_util_debug &&
					rg_log($index . ' DEBUG: no restart flag => done = TRUE');
					$info['done'] = TRUE;
				}
			}

			$rg_util_debug &&
			rg_log($index . ' DEBUG:'
				. ' info[done]=' . ($info['done'] === TRUE ? 'true' : 'false'));
			if ($info['done'])
				continue;

			$rg_util_debug &&
			rg_log($index . ' DEBUG: set done to FALSE');
			$done = FALSE;

			if ($info['started'] == 0) {
				$restart_delay = isset($info['restart_delay']) ? $info['restart_delay'] : 0;

				if ($info['last_failure'] + $restart_delay > $now)
					continue;

				$rg_util_debug &&
				rg_log($index . ' DEBUG: Running [' . $info['cmd'] . ']');
				$desc = array(
					0 => array('pipe', 'r'),
					1 => array('pipe', 'w'),
					2 => array("pipe", "w")
				);

				$info['a'] = proc_open($info['cmd'], $desc, $info['pipes']);
				if ($info['a'] === FALSE) {
					$info['last_errmsg'] = 'cannot do proc_open';
					if (isset($info['cb_error']))
						$info['cb_error']($index, $info, $info['last_errmsg']);
					$info['last_failure'] = $now;
					continue;
				}

				$rg_util_debug &&
				rg_log($index . ' DEBUG: proc_open pipes: '
					. rg_array2string($info['pipes']));

				$info['stopped'] = FALSE;
				$info['started'] = time();
			}

			if ($info['stopped']) {
				$rg_util_debug &&
				rg_log($index . ' DEBUG: stopped is TRUE');
				continue;
			}

			rg_exec2_helper_populate_out_buf($index, $info);

			// Check idle
			if (isset($info['cb_idle'])
				&& ($info['idle_time'] > 0)
				&& ($info['last_activity'] > 0)
				&& ($info['last_activity'] + $info['idle_time'] <= time())) {
				$rg_util_debug &&
				rg_log($index . ' DEBUG: IDLE:'
					. ' last_activity=' . $info['last_activity']
					. ' now=' . time());
				$info['cb_idle']($index, $info);
			}

			if (!empty($info['out_buf_real'])) {
				$rg_util_debug &&
				rg_log($index . ' DEBUG: out_buf_real not empty =>'
					. ' enable write notification!');

				$wx[] = $info['pipes'][0];
			} else if (($info['input_fd_done'] == 1)
				&& ($info['out_buf_done'] == 1)
				&& (isset($info['pipes'][0]))) {
				$rg_util_debug &&
				rg_log($index . ' DEBUG: empty(out_buf_real)'
					. ' && input_fd_done == 1'
					. ' && out_buf_done == 1'
					. ': close output ' . $info['pipes'][0]);
				rg_exec2_helper_close_stream($index,
					$info['pipes'][0], $info, 0);
			}

			if (isset($info['pipes'][1])) $rx[] = $info['pipes'][1];
			if (isset($info['pipes'][2])) $rx[] = $info['pipes'][2];
			if (isset($info['input_fd'])) $rx[] = $info['input_fd'];

			for ($i = 0; $i <= 2; $i++) {
				if (!isset($info['pipes'][$i]))
					continue;

				$k = intval($info['pipes'][$i]);
				$lut[$k] = array('index' => $index,
					'stream' => $i);
			}

			if (isset($info['input_fd'])) {
				$k = intval($info['input_fd']);
				$lut[$k] = array('index' => $index,
					'stream' => -1);
			}
		}
		if ($done) {
			$rg_util_debug &&
			rg_log('DEBUG: No work to do anymore! Exit!');
			$ret['ok'] = 1;
			break;
		}

		if (empty($rx) && empty($wx)) {
			$rg_util_debug &&
			rg_log('DEBUG: No r/w events; sleeping...');
			sleep(1);
			continue;
		}

		$revents = $rx;
		$wevents = $wx;
		$ex = NULL;
		$rg_util_debug && rg_log('DEBUG: before stream_select:'
			. (empty($revents) ? '' : ' revents:' . rg_array2string($revents))
			. (empty($wevents) ? '' : ' wevents:' . rg_array2string($wevents)));
		$r = stream_select($revents, $wevents, $ex, 0, 100 * 1000);
		if ($r === FALSE) {
			$ret['errmsg'] = "cannot select";
			break;
		}

		$rg_util_debug && (!empty($revents) || !empty($wevents) || !empty($ex)) &&
			rg_log('DEBUG: after stream_select:'
			. (empty($revents) ? '' : ' revents:' . rg_array2string($revents))
			. (empty($wevents) ? '' : ' wevents:' . rg_array2string($wevents))
			. (empty($ex) ? '' : ' ex:' . rg_array2string($ex)));

		if ($r === 0)
			continue;

		foreach ($wevents as $fd) {
			$ifd = intval($fd);
			$index = $lut[$ifd]['index'];
			$rg_util_debug &&
			rg_log($index . ' DEBUG: write event on ifd ' . $ifd);

			$info = &$ret['cmds'][$index];

			rg_exec2_helper_populate_out_buf($index, $info);

			if (empty($info['out_buf_real'])) {
				$rg_util_debug &&
				rg_log_ml($index . ' DEBUG: out_buf_real is empty!'
					. ' info: ' . print_r($info, TRUE));
				continue;
			}

			//$rg_util_debug &&
			//rg_log($index . ' DEBUG: sending: [' . $info['out_buf_real'] . ']');
			$r = @fwrite($fd, $info['out_buf_real']);
			if ($r === FALSE) {
				$info['last_errmsg'] = 'cannot write';
				if (isset($info['cb_error']))
					$info['cb_error']($index, $info, $info['last_errmsg']);
				else
					rg_log($index . ' fwrite returned FALSE');
				$info['wait_for_stop'] = TRUE;
				continue;
			}
			$info['out_buf_real'] = substr($info['out_buf_real'], $r);
			$info['m2p_bytes'] += $r;
		}

		foreach ($revents as $fd) {
			$ifd = intval($fd);
			$index = $lut[$ifd]['index'];
			$stream = $lut[$ifd]['stream'];
			$rg_util_debug &&
			rg_log($index . ' DEBUG: read event on ifd ' . $ifd
				. ', stream=' . $stream . '!');

			$info = &$ret['cmds'][$index];

			$_d = @fread($fd, 32 * 4096);
			if ($_d === FALSE) {
				$rg_util_debug &&
				rg_log($index . ' DEBUG: ifd ' . $ifd . ' fread returned FALSE');
				$info['last_errmsg'] = 'cannot read';
				if (isset($info['cb_error']))
					$info['cb_error']($index, $info, $info['last_errmsg']);
				$info['wait_for_stop'] = TRUE;
				continue;
			}

			if (empty($_d)) {
				$rg_util_debug &&
				rg_log($index . ' DEBUG: ifd ' . $ifd . ' returned no data.');
				rg_exec2_helper_close_stream($index, $fd, $info, $stream);
				continue;
			}

			$rg_util_debug &&
			rg_log($index . ' DEBUG: fread [stream ' . $stream . '] returned ['
				. $_d . ']');

			$info['last_activity'] = time();

			switch ($stream) {
			case -1: $info['out_buf'] .= $_d; break;
			case 1: $info['in_buf'] .= $_d; break;
			case 2: $info['err_buf'] .= $_d; break;
			}

			if (isset($info['cb_input']))
				$info['cb_input']($index, $info, $stream);
		}

		foreach ($ret['cmds'] as $index => &$info) {
			if (!isset($info['cb_tick']))
				continue;

			if ($info['done'])
				continue;

			$info['cb_tick']($index, $info);
		}
	}

	rg_log_exit();
	rg_prof_end('exec2 ' . $_id);
	return $ret;
}

/*
 * Helper for rg_exec
 */
function rg_exec_helper_data($index, &$a, $stream)
{
	switch ($stream) {
	case 1: $a['my_data_out']($a['in_buf']); $a['in_buf'] = ''; break;
	case 1: $a['my_err_out']($a['err_buf']); $a['err_buf'] = ''; break;
	}
}

/*
 * Helper for rg_exec
 */
function rg_exec_helper_output($index, &$a)
{
	return $a['my_data_in']();
}

/*
 * Execute $cmd and returns the output as a string, binary safe
 * @input: some data to be sent to the process and received as stdin
 * @cb_stdin - call back used to pass data frou our stdin to the process
 *	ifa @cb_stdin is false, it is not called; if is 0, STDIN will be used.
 * @cb_stdout - call back called when there is something to be send to our stdout
 *	if @cb_stdout is FALSE, stdout output will be returned in $ret['data']
 * cb_stderr - call back called when there is something to be sent to our stderr
 *	if @cb_stderr is FALSE, stderr output will be returned in $ret['stderr']
 */
function rg_exec($cmd, $input, $cb_stdin, $cb_stdout, $cb_stderr)
{
	$ret = array();
	$ret['ok'] = 0;
	while(1) {
		$ret['errmsg'] = '';
		$ret['code'] = 65000; // fake code
		$ret['data'] = '';
		$ret['stderr'] = '';

		$a = array('cmds' => array(
				'cmd1' => array(
					'cmd' => $cmd,
					'out_buf' => $input
					)
				)
			);

		if ($cb_stdin === 0) {
			$a['cmds']['cmd1']['input_fd'] = @fopen('php://stdin', 'rb');
			if ($a['cmds']['cmd1']['input_fd'] === FALSE) {
				$ret['errmsg'] = 'cannot open stdin: ' . rg_php_err();
				break;
			}
		} else if ($cb_stdin !== FALSE) {
			$a['cmds']['cmd1']['my_data_in'] = $cb_stdin;
			$a['cmds']['cmd1']['cb_output'] = 'rg_exec_helper_output';
		}

		if ($cb_stdout !== FALSE) {
			$a['cmds']['cmd1']['my_data_out'] = $cb_stdout;
			$a['cmds']['cmd1']['cb_input'] = 'rg_exec_helper_data';
		}

		if ($cb_stderr !== FALSE) {
			$a['cmds']['cmd1']['my_err_out'] = $cb_stderr;
			$a['cmds']['cmd1']['cb_input'] = 'rg_exec_helper_data';
		}

		$r = rg_exec2($a);
		if ($r['ok'] != 1) {
			$ret['errmsg'] = $r['errmsg'];
			break;
		}

		$ret['code'] = $r['cmds']['cmd1']['exitcode'];
		$ret['data'] = $r['cmds']['cmd1']['in_buf'];
		$ret['stderr'] = $r['cmds']['cmd1']['err_buf'];
		$ret['errmsg'] = $r['cmds']['cmd1']['last_errmsg'];

		if ($ret['code'] == 0)
			$ret['ok'] = 1;
		break;
	}

	return $ret;
}

/*
 * Force browser to redirect to another page
 */
function rg_redirect($url)
{
	rg_log("Redirecting to [$url]");
	header("Location: $url");
	exit(0);
}

/*
 * Force browser to redirect to another page, using a HTML header
 */
function rg_redirect_html($seconds, $url)
{
	global $rg;

	$rg['rg_redirect_html'] = 1;
	$rg['rg_redirect_html_seconds'] = $seconds;
	$rg['rg_redirect_html_url'] = $url;
}

/*
 * Transforms strange chars to hexa
 */
function rg_callback_hexa($matches)
{
	$n = pack("a*", $matches[0]);
	$tmp = unpack("H*", $n);
	return "[" . $tmp[1] . "]";
}

/*
 * Transforms an array into a string
 */
function rg_array2string_avoid($a, $avoid)
{
	$what = array('/[^\pL\pN\pP\pS ]/uU');

	if (!is_array($a))
		return preg_replace_callback($what, 'rg_callback_hexa', $a);

	if (empty($a))
		return '';

	$ret = '';
	$add = '';
	foreach ($a as $k => $v) {
		if (in_array($k, $avoid)) {
			$s = '-';
		} else if (is_array($v)) {
			$s = rg_array2string_short($v);
		} else if (is_resource($v)) {
			$s = 'RES';
		} else if (is_object($v)) {
			$s = 'OBJ';
		} else if (is_string($v)) {
			$s = preg_replace_callback($what, 'rg_callback_hexa', $v);
		} else if (is_int($v)) {
			$s = $v;
		} else {
			$s = '?';
		}

		$ret .= $add . $k . '=[' . $s . ']';
		$add = ' ';
	}

	return $ret;
}

function rg_array2string_short($a)
{
	$f = array('gpg_pub_key', 'gpg_priv_key', 'rgfs_key', 'ssh_key',
		'key', 'last_output', 'client_ca_cert');
	return rg_array2string_avoid($a, $f);
}

/*
 * Transforms an array into a string
 */
function rg_array2string($a)
{
	return rg_array2string_avoid($a, array());
}

/*
 * Load files from a folder using a pattern for match
 */
function rg_dir_load_pattern($dir, $pattern)
{
	$ret = FALSE;
	if (!file_exists($dir)) {
		rg_util_set_error("$dir does not exists");
		return $ret;
	}

	$d = @scandir($dir);
	if ($d === FALSE) {
		rg_util_set_error("cannot scan dir $dir (" . rg_php_err() . ")");
		return $ret;
	}

	$ret = array();
	foreach ($d as $file) {
		if ((strcmp($file, ".") == 0) || (strcmp($file, "..") == 0))
			continue;

		if (preg_match('/' . $pattern . '/uD', $file) !== 1)
			continue;

		$ret[] = $file;
	}

	return $ret;
}

/*
 * Load all files from a folder
 */
function rg_dir_load($dir)
{
	return rg_dir_load_pattern($dir, ".*");
}

/*
 * Recursive dir load (used for references)
 */
function rg_dir_load_deep($dir)
{
	$ret = array();

	if (is_file($dir))
		return array($dir);

	$d = rg_dir_load($dir);
	if ($d === FALSE)
		return FALSE;

	foreach ($d as $obj) {
		if (is_dir($dir . "/" . $obj)) {
			$c = rg_dir_load_deep($dir . "/" . $obj);
			foreach ($c as $obj2)
				$ret[] = $obj . "/" . $obj2;
		} else {
			$ret[] = $obj;
		}
	}

	return $ret;
}

/*
 * Copy a fs tree to another place
 */
function rg_copy_tree($src, $dst, $mask)
{
	rg_prof_start("copy_tree");
	rg_log_enter("copy_tree($src, $dst, mask=$mask)");

	$ret = FALSE;
	while (1) {
		if (!is_dir($dst)) {
			$r = @mkdir($dst, $mask, TRUE);
			if ($r !== TRUE) {
				rg_log('ERROR: Cannot mkdir: ' . rg_php_err() . '.');
				break;
			}
		}

		$d = rg_dir_load($src);
		if ($d === FALSE)
			break;

		$err = FALSE;
		foreach ($d as $obj) {
			if (is_dir($src . "/" . $obj)) {
				if (!is_dir($dst . "/" . $obj)) {
					$r = @mkdir($dst . "/" . $obj, $mask);
					if ($r !== TRUE) {
						rg_log('ERROR: Cannot mkdir: '
							. rg_php_err() . '.');
						$err = TRUE;
						break;
					}
				}

				$r = rg_copy_tree($src . "/" . $obj, $dst . "/" . $obj, $mask);
				if ($r !== TRUE) {
					$err = TRUE;
					break;
				}
			} else {
				$r = @copy($src . "/" . $obj, $dst . "/" . $obj);
				if ($r !== TRUE) {
					rg_log("ERROR: Cannot copy file (" . rg_php_err() . ").");
					$err = TRUE;
					break;
				}
			}
		}

		if (!$err)
			$ret = TRUE;
		break;
	}

	rg_log_exit();
	rg_prof_end("copy_tree");
	return $ret;
}

/*
 * Recursively deletes a tree
 */
function rg_del_tree($dst)
{
	rg_prof_start('del_tree');

	$ret = FALSE;
	while (1) {
		if (!is_dir($dst)) {
			$ret = TRUE;
			break;
		}

		$d = rg_dir_load($dst);
		// TODO: here, I cannot distinguish between error and dir_not_exists
		if ($d === FALSE) {
			rg_log('DEBUG: Cannot load dir ' . $dst);
			break;
		}

		$err = FALSE;
		foreach ($d as $obj) {
			if (is_dir($dst . '/' . $obj)) {
				$r = rg_del_tree($dst . '/' . $obj);
				if ($r !== TRUE) {
					$err = TRUE;
					break;
				}
			} else {
				$r = @unlink($dst . '/' . $obj);
				if ($r !== TRUE) {
					rg_util_set_error('cannot delete file: ' . rg_php_err());
					$err = TRUE;
					break;
				}
			}
		}

		$r = @rmdir($dst);
		if ($r !== TRUE) {
			rg_util_set_error('cannot delete dir: ' . rg_php_err());
			$err = TRUE;
			break;
		}

		if (!$err)
			$ret = TRUE;
		break;
	}

	rg_prof_end('del_tree');
	return $ret;
}

/*
 * Called by PHP in case of error
 */
function rg_error_handler($no, $str, $file, $line)
{
	global $rg_php_err;

	if ($no == 0)
		return;

	$rg_php_err = $str;

	// call was prepended with '@'
	// PHP7 returned 0
	$er = error_reporting();
	//echo 'DEBUG: er=' . base_convert($er, 10, 16) . ' no=' . $no . ' ' . $str . "\n";
	if (($er == 0) || !($er & $no))
		return;

	$msg = "PHP ERROR: $file:$line: $str (errno=$no)";
	rg_error_core($msg);

	if ($no == E_ERROR)
		die();

	$key = md5($msg);
	$rg_error_seen[$key] = 1;

	return FALSE;
}

/*
 * Shutdown function to log fatal errors
 */
function rg_error_shutdown()
{
	$a = error_get_last();
	if ($a === NULL)
		return;

	rg_error_handler($a['type'], $a['message'], $a['file'], $a['line']);
}

/*
 * YYYY-MM-DD -> timestamp
 */
function rg_date2ts($s)
{
	rg_log("rg_date2ts s=[$s]");

	if (strlen($s) != 10)
		return FALSE;

	$f = explode("-", $s);

	if (count($f) != 3)
		return FALSE;

	return gmmktime(0, 0, 0, $f[1], $f[2], $f[0]);
}

/*
 * YYYY-MM-DD -> timestamp (last second of the day)
 */
function rg_date2ts_last_second($s)
{
	rg_log("rg_date2ts_last_second s=[$s]");

	if (strlen($s) != 10)
		return FALSE;

	$f = explode("-", $s);

	if (count($f) != 3)
		return FALSE;

	return gmmktime(0, 0, 0, $f[1], $f[2] + 1, $f[0]) - 1;
}

/*
 * Special implode, with prefix/postfix
 */
function rg_implode($prefix, $a, $postfix)
{
	if (!is_array($a))
		return $a;

	if (empty($a))
		return "";

	$ret = array();
	foreach ($a as $index => $data)
		$ret[] = $prefix . $data;

	return implode($postfix, $ret);
}

/*
 * Here we will cache the connections
 */
$rg_socket_cache = array();

/*
 * Receives buffers and test if @wait string is present.
 * @timeout - in miliseconds, NULL=forever, 0=no_wait
 */
function rg_socket_recv_wait($socket, $wait, $timeout)
{
	rg_prof_start('socket_recv_wait');

	if ($timeout === NULL) {
		$tv_sec = NULL;
		$tv_usec = NULL;
		$limit = 0;
	} else if ($timeout == 0) {
		$tv_sec = 0;
		$tv_usec = 0;
		$limit = 0;
	} else {
		$tv_sec = intval($timeout / 1000);
		$tv_usec = ($timeout % 1000) * 1000;
		$limit = microtime(TRUE) + $timeout / 1000;
	}

	$ret_buf = '';
	$ret = FALSE;
	while (1) {
		$reads = array($socket); $writes = array(); $ex = array();
		$r = @socket_select($reads, $writes, $ex, $tv_sec, $tv_usec);
		if ($r === FALSE) {
			rg_log('Cannot select(' . socket_strerror(socket_last_error()) . ')!');
			break;
		}

		if ($r === 0) { // timeout
			rg_log('Timeout reading from socket!');
			break;
		}

		if (!in_array($socket, $reads)) {
			rg_log('Select returned > 0 and my socket is not in reads');
			break;
		}

		$r = @socket_recv($socket, $buf, 32 * 4096, 0);
		if ($r === FALSE) {
			rg_log('Cannot receive: ' . socket_strerror(socket_last_error()));
			break;
		}
		if ($r === 0) {
			rg_log('Cannot receive (zero)');
			break;
		}

		//rg_log("Received [$buf]");
		$ret_buf .= $buf;

		$pos = strpos($buf, $wait);
		if ($pos !== FALSE) {
			$ret = $ret_buf;
			break;
		}

		$t = microtime(TRUE);
		if ($t > $limit)
			break;

		$tv_sec = intval($t - $limit);
		$tv_usec = intval(($t - $limit) * 1000) % 1000;
	}

	rg_prof_end('socket_recv_wait');
	return $ret;
}

/*
 * Sends a full buffer
 * TODO: Take timeout in consideration.
 */
function rg_socket_send($socket, $buf)
{
	rg_prof_start('socket_send');

	$ret = FALSE;
	$len = strlen($buf);
	$off = 0;
	while (1) {
		$r = @socket_send($socket, substr($buf, $off), $len - $off, 0);
		if ($r === FALSE) {
			rg_log('Could not send data to socket ('
				. socket_strerror(socket_last_error()) . ')!');
			break;
		}
		//rg_log("Sent $r bytes (" . substr($buf, $off, $r) . ").");

		$len -= $r; $off += $r;
		if ($len == 0) {
			$ret = TRUE;
			break;
		}
	}

	rg_prof_end('socket_send');
	return $ret;
}

/*
 * Connects to a socket, send @buf and returns the answer.
 * @timeout: NULL=forever, 0=no_wait
 * @tries - how many time to retry if it fails
 */
function rg_socket($path, $buf, $timeout, $tries, $flags)
{
	global $rg_socket_cache;

	rg_prof_start('socket');

	$ret = FALSE;
	while ($tries > 0) {
		if (isset($rg_socket_cache[$path])) {
			$socket = $rg_socket_cache[$path];
		} else {
			rg_prof_start('socket-connect');
			$socket = @socket_create(AF_UNIX, SOCK_STREAM, 0);
			if ($socket === FALSE) {
				rg_log("Could not create socket (" . socket_strerror(socket_last_error()) . ")!");
				break;
			}

			while ($tries > 0) {
				$r = @socket_connect($socket, $path);
				if ($r === FALSE) {
					$tries--;
					usleep(50 * 1000);
					continue;
				}

				break;
			}
			rg_prof_end('socket-connect');
			if ($r === FALSE) {
				rg_log('Could not connect the socket [' . $path . ']'
					. '(' . socket_strerror(socket_last_error()) . ')!');
				break;
			}

			$rg_socket_cache[$path] = $socket;
		}

		$r = rg_socket_send($socket, $buf);
		if ($r !== TRUE) {
			socket_close($socket);
			unset($rg_socket_cache[$path]);
			continue;
		}

		if ($flags & RG_SOCKET_NO_WAIT) {
			//rg_log('We do not have to wait. Exit.');
			$ret = '';
			break;
		}

		$ret = rg_socket_recv_wait($socket, "\n", $timeout);
		if ($ret === FALSE) {
			socket_close($socket);
			unset($rg_socket_cache[$path]);
			break;
		}

		break;
	}

	rg_prof_end("socket");
	return $ret;
}

/*
 * Check if referer matchces current website
 */
function rg_valid_referer()
{
	$ref0 = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : "";

	// If not provided, we can do nothing about
	if (empty($ref0))
		return TRUE;

	// TODO: are we sure we want to strip the port?
	// TODO: are we sure we want to check the referer?
	$ref = preg_replace('|^http(s)?://|', '', $ref0);
	$ref = preg_replace('|/.*$|', '', $ref); // remove URI
	//$ref = preg_replace('|:.*$|', '', $ref); // remove port

	$we = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : "";

	if (strcasecmp($we, $ref) == 0)
		return TRUE;

	rg_security_violation_no_exit("invalid referer for form submission"
		. " we=[$we] ref=[$ref] ref0=[$ref0]");
	return FALSE;
}

/*
 * Returns the human formatted time based on a number of seconds
 * @diff - time in seconds
 */
function rg_human_time_interval($diff)
{
	$ret = ($diff % 60) . 's';
	$rest = intval($diff / 60);
	if ($rest == 0)
		return $ret;

	$ret = ($diff % 60) . 'm' . $ret;
	$rest = intval($diff / 60);
	if ($rest == 0)
		return $ret;

	$ret = ($diff % 24) . 'h' . $ret;
	$rest = intval($diff / 24);
	if ($rest == 0)
		return $ret;

	$ret = ($diff % 12) . 'm' . $ret;
	$rest = intval($diff / 12);
	if ($rest == 0)
		return $ret;

	$ret = $rest . 'y' . $ret;

	return $ret;
}

/*
 * Returns the age of an object
 */
function rg_age($ts)
{
	$diff = time() - $ts;
	return rg_human_time_interval($diff);
}

/*
 * Returns a path to a temporary directory.
 * Timeout parameter will instruct the clean task when it can be removed.
 * Use 0 for timeout_in_minutes to not clean that temp dir/file.
 * TODO: The clean procedure is not done yet!
 */
function rg_tmp_path($name, $timeout_in_minutes)
{
	global $rg_state_dir;

	$p = $rg_state_dir . '/tmp/' . $name;
	if ($timeout_in_minutes > 0)
		@file_put_contents($p . '.meta', time() + $timeout_in_minutes * 60);

	return $p;
}

/*
 * Returns a random file name into the temporary area
 */
function rg_tmp_path_random($prefix)
{
	global $rg_state_dir;

	return $rg_state_dir . '/tmp/' . $prefix . rg_id(10);
}

/*
 * Creates and stores content into a temporary file and returns the name
 */
function rg_tmp_file($file, $content)
{
	global $rg_state_dir;

	$final_name = $rg_state_dir . '/tmp/' . $file;
	$r = @file_put_contents($final_name, $content);
	if ($r === FALSE)
		return FALSE;
	// TODO: log error messege?

	return $final_name;
}

/*
 * Function to short the certificates
 */
function rg_cert_short($s)
{
	if (empty($s))
		return 'n/a';

	if (strlen($s) < 12)
		return $s;

	$s = str_replace('-----BEGIN CERTIFICATE-----', '', $s);
	$s = str_replace('-----END CERTIFICATE-----', '', $s);
	$s = trim($s);

	return substr($s, 0, 4) . '...' . substr($s, -4, 4);
}

/*
 * Remove ::ffff: prefix
 */
function rg_fix_ip($ip)
{
	if (strncasecmp($ip, "::ffff:", 7) == 0)
		$ip = substr($ip, 7);

	return $ip;
}

/*
 * Escape json fields - TODO - for now do nothing
 */
function rg_json_escape($s)
{
	return json_encode($s);
}

/*
 * Escapes texts inside XML tags
 * Example: <a>&</a> -> <a>&amp;</a>
 */
function fn_xml_escape($s)
{
	return strtr($s,
		array(
			'<' => '&lt;',
			'>' => '&gt;',
			'"' => '&quot;',
			'\'' => '&apos;',
			'&' => '&amp;',
		)
	);
}

/*
 * Removes from a string all non-alpha characters
 */
function rg_force_alpha($s)
{
	return preg_replace('/[^A-Za-z]/', '', $s);
}

/*
 * Removes from a string all non-alpha/num and _ characters
 */
function rg_force_alphanum($s)
{
	return preg_replace('/[^A-Za-z0-9_]/', '', $s);
}

/*
 * Dummy function to be used in rg_exec callback
 */
function rg_echo($s)
{
	echo $s;
}

/*
 * Function used to register login functions
 */
$rg_login_functions = array();
function rg_register_login_function($f)
{
	global $rg_login_functions;

	$rg_login_functions[] = $f;
}

/*
 * Quote some valid UTF-8 but not printable chars
 */
function rg_utf8_convert_non_printable($a)
{
	$ret = '';

	$len = strlen($a);
	for ($i = 0; $i < $len; $i++) {
		$c = ord($a[$i]);

		if (($c <= 6) || (($c >= 0x0e) && ($c <= 0x1f)) || ($c == 0x7f)) {
			$ret .= chr(0x5c) . sprintf('%o', $c);
			continue;
		}

		switch ($c) {
		case 0x07: /* \a */ $ret .= chr(0x5c) . 'a'; break;
		case 0x08: /* \b */ $ret .= chr(0x5c) . 'b'; break;
		case 0x09: /* \t */ $ret .= chr(0x5c) . 't'; break;
		case 0x0a: /* \n */ $ret .= chr(0x5c) . 'n'; break;
		case 0x0b: /* \v */ $ret .= chr(0x5c) . 'v'; break;
		case 0x0c: /* \f */ $ret .= chr(0x5c) . 'f'; break;
		case 0x0d: /* \r */ $ret .= chr(0x5c) . 'r'; break;
		default: $ret .= $a[$i];
		}
	}

	return $ret;
}

/*
 * Returns the number of bytes of the next UTF-8 char
 * Returns a negative number if the sequence is not correct, else, positive.
 * The value is the number of bytes.
 * Taken from: https://stackoverflow.com/questions/1473441/check-to-see-if-a-string-is-encoded-as-utf-8
 */
function rg_utf8_test($a)
{
	$len = strlen($a);
	$state = 0; /* 0 = unknown, 1 = valid, -1 = invalid */
	$pos = 0;
	while ($pos < $len) {
		$c = ord($a[$pos]);
		//rg_log('c=' . $c . '(' . $a[$pos] . ') pos=' . $pos . ' state=' . $state);

		if ($c < 0x80)
			$n = 0;
		else if (($c & 0xE0) == 0xC0)
			$n = 1;
		else if (($c & 0xF0) == 0xE0)
			$n = 2;
		else if (($c & 0xF8) == 0xF0)
			$n = 3;
		else if (($c & 0xFC) == 0xF8)
			$n = 4;
		else if (($c & 0xFE) == 0xFC)
			$n = 5;
		else if ($state === 0) {
			//rg_log('  DEBUG: invalid byte, unk -> bad state');
			$state = -1;
			$pos++;
			continue;
		} else if ($state === 1) {
			//rg_log('  DEBUG: invalid byte, last state good, return pos');
			return $pos;
		} else if ($state === -1) {
			//rg_log('  DEBUG: already in bad state');
			$pos++;
			continue;
		}
		//rg_log('  DEBUG: n=' . $n);

		if (1 + $pos + $n > $len) {
			//rg_log('  DEBUG: too little bytes left');
			if ($state !== 1) {
				//rg_log('  DEBUG: state != 1; return - (1 + pos)');
				return - (1 + $pos);
			}

			//rg_log('  DEBUG: state is good, return pos');
			return $pos;
		}

		// Validate next chars
		for ($i = 0; $i < $n; $i++) {
			$x = ord($a[1 + $pos + $i]);
			if (($x & 0xC0) != 0x80) {
				if ($state !== 1) {
					//rg_log('  DEBUG: invalid body, state != 1; return - (1 + pos)');
					return - (1 + $pos);
				}

				//rg_log('  DEBUG: invalid body, state == 1; return pos');
				return $pos;
			}
		}

		if ($state === -1) {
			//rg_log('DEBUG: found valid bytes: return -pos');
			return -$pos;
		}

		if ($state === 0) {
			//rg_log('  DEBUG: enter good state');
			$state = 1;
		}

		$pos += 1 + $n;
	}

	return $state * $pos;
}

/*
 * Converts an string to a valid representation.
 * Mostly used to display strange filenames.
 * The string may be not be UTF-8 valid.
 * Example: "a\xc8a" -> 'a\310a'
 * Example: "\xffț" -> '\377ț'
 */
function rg_visible_string($a)
{
	$ret = '';
	$len = strlen($a);
	while ($len > 0) {
		//rg_log('DEBUG: a: ' . $a);
		$r = rg_utf8_test($a);
		//rg_log('DEBUG: utf8_test returned ' . $r);
		if ($r > 0) {
			$ret .= rg_utf8_convert_non_printable(substr($a, 0, $r));
			$skip = $r;
		} else {
			$ret .= rg_git_quote(substr($a, 0, -$r));
			$skip = -$r;
		}

		$a = substr($a, $skip);
		$len -= $skip;
		//rg_log('DEBUG: ret=' . $ret);
	}

	return $ret;
}

/*
 * Get latest bytes from a file
 * Note: bytes not chars
 */
function rg_file_get_tail($f, $bytes)
{
	$size = @filesize($f);
	if ($size === FALSE)
		return '';

	if ($size <= $bytes)
		return @file_get_contents($f);

	$r = @file_get_contents($f, FALSE, null, -$bytes, $bytes);
	if ($r === FALSE)
		return '';

	$pos = strpos($r, "\n");
	if ($pos === FALSE)
		return $r;

	return substr($r, $pos + 1);
}

/*
 * Unserialize JSON or php 'serialize'
 */
function rg_unserialize($s)
{
	$c = substr($s, 0, 1);
	if ((strcmp($c, '{') == 0) || (strcmp($c, '"') == 0)
		|| (strcmp($c, '[') == 0)) {
		$r = @json_decode($s, TRUE);
		if ($r !== NULL)
			return $r;

		$m = 'cannot decode json: ' . json_last_error_msg();
		rg_internal_error($m);
		rg_util_set_error($m);
		return FALSE;
	}

	$x = substr($s, 1, 1);
	if (strcmp($x, ':') == 0) {
		$r = @unserialize($s);
		if ($r !== FALSE)
			return $r;
	}

	return $s;
}

/*
 * Serialize data
 */
function rg_serialize($a)
{
	$r = @json_encode($a);
	if ($r === FALSE) {
		$m = 'error encoding json: ' . json_last_error_msg();
		rg_internal_error($m);
		rg_util_set_error($m);
		return FALSE;
	}

	return $r;
}

/*
 * Compress data for web (zlib)
 */
function rg_gzencode($c, &$orig_len, &$comp_len)
{
	rg_prof_start('gzencode');
	$orig_len = strlen($c);

	$ret = @gzencode($c, 9);
	if ($ret === FALSE)
		rg_fatal('cannot gzencode');

	$comp_len = strlen($ret);
	rg_log('COMPRESSION: orig=' . $orig_len
		. ' comp=' . $comp_len
		. ' ratio=' . sprintf('%.1f', $orig_len / $comp_len));

	rg_prof_end('gzencode');
	return $ret;
}

/*
 * Output data to client
 */
function rg_web_output($c, $disable)
{
	$comp_len = 0;

	if ($disable === FALSE) {
		$acc = isset($_SERVER['HTTP_ACCEPT_ENCODING']) ? $_SERVER['HTTP_ACCEPT_ENCODING'] : '';
		if (stristr($acc, 'gzip')) {
			$c = rg_gzencode($c, $orig_len, $comp_len);
			header('Content-Encoding: gzip');
			header('Content-Length: ' . $comp_len);
		}
	}

	if ($comp_len == 0)
		$comp_len = strlen($c);

	echo $c;

	return $comp_len;
}

/*
 * Replaces a string with another, recursively in arrays
 */
function rg_str_replace($keys, $values, $a, $level)
{
	if ($level > 100) {
		rg_internal_error('array too deep');
		return FALSE;
	}

	if (is_string($a))
		return str_replace($keys, $values, $a);

	if (!is_array($a))
		return $a;

	if (is_array($a)) {
		foreach ($a as $k => $v)
			$a[$k] = rg_str_replace($keys, $values, $v, $level + 1);
		return $a;
	}

	return FALSE;
}

/*
 * Transforms a nested array into a flat one
 */
function rg_array_flat($prefix, $a)
{
	$ret = array();

	foreach ($a as $k => $v) {
		if (!is_array($v)) {
			$ret[$prefix . $k] = $v;
			continue;
		}

		$ret = array_merge($ret, rg_array_flat($prefix . $k . '::', $v));
	}

	return $ret;
}

/*
 * Because array_splice destroys the index!
 */
function rg_array_top($a, $nr)
{
	if ($nr == 0)
		return $a;

	$ret = array();
	$i = 0;
	foreach ($a as $k => $v) {
		if ($i++ == $nr)
			break;
		$ret[$k] = $v;
	}

	return $ret;
}

/*
 * Save a file (safe version)
 */
function rg_save_plain($file, $a)
{
	$r = @file_put_contents($file . '.tmp', $a);
	if ($r === FALSE) {
		rg_internal_error('Cannot save file [' . $file . '.tmp]: ' . rg_php_err());
		return FALSE;
	}

	$r = @rename($file . '.tmp', $file);
	if ($r === FALSE) {
		@unlink($file . '.tmp');
		rg_internal_error('Cannot rename: ' . rg_php_err());
		return FALSE;
	}

	return TRUE;
}

/*
 * Save a serialized array
 */
function rg_save($file, $a)
{
	return rg_save_plain($file, rg_serialize($a));
}

function rg_is_abuser(string $ua)
{
	if (stristr($ua, 'facebookexternalhit/')) return TRUE;
	if (stristr($ua, 'gptbot/')) return TRUE;
	if (stristr($ua, 'GoogleOther')) return TRUE;

	return FALSE;
}

/*
 * Returns TRUE if we detect that the User-Agent seems a bot
 */
function rg_is_bot($ua)
{
	if (empty($ua)) return FALSE;

	if (stristr($ua, 'bot/')) return TRUE;
	if (stristr($ua, 'bot;')) return TRUE;
	if (stristr($ua, 'crawler')) return TRUE;

	if (strstr($ua, ' Adsbot/')) return TRUE;
	if (strstr($ua, ' AhrefsBot')) return TRUE;
	if (strstr($ua, ' aiHitBot/')) return TRUE;
	if (strstr($ua, ' Amazonbot/')) return TRUE;
	if (strstr($ua, 'Applebot/')) return TRUE;
	if (strstr($ua, 'AWeb')) return TRUE;
	if (strstr($ua, ' Babya Discoverer ')) return TRUE;
	if (strstr($ua, ' Baiduspider/')) return TRUE;
	if (strstr($ua, ' Barkrowler/')) return TRUE;
	if (strstr($ua, 'BananaBot/')) return TRUE;
	if (strstr($ua, ' bingbot')) return TRUE;
	if (strstr($ua, ' BLEXBot/')) return TRUE;
	if (strstr($ua, ' Bytespider')) return TRUE;
	if (strstr($ua, 'CATExplorador')) return TRUE;
	if (strstr($ua, 'CCBot/')) return TRUE;
	if (strstr($ua, ' CensysInspect/')) return TRUE;
	if (strstr($ua, 'CISPA Webcrawler ')) return TRUE;
	if (strstr($ua, 'clark-crawler2/')) return TRUE;
	if (strstr($ua, 'Cloud mapping experiment')) return TRUE;
	if (strstr($ua, ' Cliqzbot/')) return TRUE;
	if (strstr($ua, ' coccocbot-web')) return TRUE;
	if (strstr($ua, ' DataForSeoBot/')) return TRUE;
	if (strstr($ua, 'dcrawl/')) return TRUE;
	if (strstr($ua, ' DNSResearchBot')) return TRUE;
	if (strstr($ua, 'DomainStatsBot/')) return TRUE;
	if (strstr($ua, ' DotBot')) return TRUE;
	if (strstr($ua, ' DuckDuckGo')) return TRUE;
	if (strstr($ua, 'e.ventures Investment Crawler ')) return TRUE;
	if (strstr($ua, 'facebookexternalhit/')) return TRUE;
	if (strstr($ua, 'Facebot')) return TRUE;
	if (strstr($ua, 'GarlikCrawler/')) return TRUE;
	if (strstr($ua, 'Gigabot ')) return TRUE;
	if (strstr($ua, ' Googlebot')) return TRUE;
	if (strstr($ua, 'googlebot')) return TRUE;
	if (strstr($ua, 'Googlebot-Image/')) return TRUE;
	if (strstr($ua, 'Googlebot-News')) return TRUE;
	if (strstr($ua, 'Googlebot-Video/')) return TRUE;
	if (strstr($ua, 'GoScraper')) return TRUE;
	if (strstr($ua, 'HappyWing')) return TRUE;
	if (strstr($ua, 'ichiro/')) return TRUE;
	if (strstr($ua, 'IonCrawl')) return TRUE;
	if (strstr($ua, 'Internet-structure-research-project-bot')) return TRUE;
	if (strstr($ua, 'Lawinsiderbot/')) return TRUE;
	if (strstr($ua, 'LightspeedSystemsCrawler')) return TRUE;
	if (strstr($ua, ' Linguee Bot ')) return TRUE;
	if (strstr($ua, 'ltx71 ')) return TRUE;
	if (strstr($ua, ' MaCoCu;')) return TRUE;
	if (strstr($ua, ' Mail.RU_Bot')) return TRUE;
	if (strstr($ua, ' MauiBot ')) return TRUE;
	if (strstr($ua, 'search.marginalia.nu')) return TRUE;
	if (strstr($ua, ' MegaIndex.ru/')) return TRUE;
	if (strstr($ua, 'meta-externalagent/')) return TRUE;
	if (strstr($ua, ' MJ12bot')) return TRUE;
	if (strstr($ua, ' MojeekBot')) return TRUE;
	if (strstr($ua, 'msnbot-media/')) return TRUE;
	if (strstr($ua, ' MTRobot/')) return TRUE;
	if (strstr($ua, ' NetcraftSurveyAgent/')) return TRUE;
	if (strstr($ua, 'NewsGator FetchLinks extension/')) return TRUE;
	if (strstr($ua, 'netEstate NE Crawler ')) return TRUE;
	if (strstr($ua, 'nu.marginalia.wmsa.edge-crawler')) return TRUE;
	if (strstr($ua, 'OpenSearch@MPDL ')) return TRUE;
	if (strstr($ua, ' PageThing ')) return TRUE;
	if (strstr($ua, 'PageThing.com')) return TRUE;
	if (strstr($ua, 'paloaltonetworks.com')) return TRUE;
	if (strstr($ua, 'Pandalytics')) return TRUE;
	if (strstr($ua, 'panscient.com')) return TRUE;
	if (strstr($ua, ' PetalBot')) return TRUE;
	if (strstr($ua, 'pimeyes.com crawler')) return TRUE;
	if (strstr($ua, ' proximic;')) return TRUE;
	if (strstr($ua, 'Re-re Studio ')) return TRUE;
	if (strstr($ua, 'Robot Terminator ')) return TRUE;
	if (strstr($ua, 'rpmlint/')) return TRUE;
	if (strstr($ua, 'SaaSHub')) return TRUE;
	if (strstr($ua, ' SemanticScholarBot')) return TRUE;
	if (strstr($ua, 'Screaming Frog SEO Spider')) return TRUE;
	if (strstr($ua, ' Seekport Crawler')) return TRUE;
	if (strstr($ua, ' SemrushBot')) return TRUE;
	if (strstr($ua, ' SEOkicks;')) return TRUE;
	if (strstr($ua, 'serpstatbot/')) return TRUE;
	if (strstr($ua, ' SeznamBot')) return TRUE;
	if (strstr($ua, 'Sidetrade indexer bot')) return TRUE;
	if (strstr($ua, 'Slackbot-LinkExpanding ')) return TRUE;
	if (strstr($ua, 'Sogou web spider')) return TRUE;
	if (strstr($ua, 'TelegramBot ')) return TRUE;
	if (strstr($ua, 'The Knowledge AI')) return TRUE;
	if (strstr($ua, 'TprAdsTxtCrawler')) return TRUE;
	if (strstr($ua, 'TurnitinBot ')) return TRUE;
	if (strstr($ua, 'Twitterbot/')) return TRUE;
	if (strstr($ua, 'TinyTestBot')) return TRUE;
	if (strstr($ua, ' VelenPublicWebCrawler/')) return TRUE;
	if (strstr($ua, 'Wappalyzer')) return TRUE;
	if (strstr($ua, ' webtechbot;')) return TRUE;
	if (strstr($ua, 'http://webmeup-crawler.com/')) return TRUE;
	if (strstr($ua, 'Xenu Link Sleuth/')) return TRUE;
	if (strstr($ua, 'XYZ Spider')) return TRUE;
	if (strstr($ua, 'yacybot ')) return TRUE;
	if (strstr($ua, '/yacy.net/')) return TRUE;
	if (strstr($ua, ' YandexBot/')) return TRUE;
	if (strstr($ua, ' YandexImages/')) return TRUE;
	if (strstr($ua, ' YisouSpider')) return TRUE;
	if (strstr($ua, 'ZoomBot ')) return TRUE;
	if (strstr($ua, 'ZoominfoBot ')) return TRUE;

	return FALSE;
}

/*
 * Makes clickable all segments of an URL
 */
function rg_url_segments($base, $url)
{
	$ret = '';
	$a = explode('/', $url);

	// ROOT
	$ret .= '<a href="' . $base . '">ROOT</a>';

	if (empty($url))
		return $ret;

	$p = '';
	foreach ($a as $v) {
		$p .= '/' . rawurlencode($v);
		$ret .= ' / ' . '<a href="' . $base . $p . '">'
			. rg_xss_safe($v) . '</a>';
	}

	return $ret;
}

/*
 * Transform a FS path into an URL escaping all path segments
 * Example: /a b/c/d -> /a%20b/c/d
 */
function rg_path2url($path)
{
	$ret = '';
	$a = explode('/', $path);
	$r = array();
	foreach ($a as $v)
		$r[] = rawurlencode($v);

	return implode('/', $r);
}

/*
 * Check if a file (relative to @root) is inside a directory and return
 * 'stat' info if TRUE.
 */
function rg_path_validate($root, $path)
{
	rg_log_enter('path_validate root=' . $root . ' path=' . $path);

	$ret = FALSE;
	do {
		$last = substr($root, -1);
		if (strcmp($last, '/') != 0)
			$root .= '/';

		$a = array();
		$a['realpath'] = @realpath($root . $path);
		if ($a['realpath'] === FALSE) {
			rg_util_set_error('path ' . $root . $path . ' does not exists');
			break;
		}

		if (strncmp($a['realpath'], $root, strlen($root)) != 0) {
			rg_util_set_error('path [' . $path . '] is trying to escape'
				. ' root [' . $root . ']');
			break;
		}

		$a['stat'] = @stat($a['realpath']);
		if ($a['stat'] === FALSE) {
			rg_util_set_error('cannot stat ' . $a['realpath']
				. ': ' . rg_php_err());
			break;
		}

		$ret = $a;
	} while (0);

	rg_log_exit();
	return $ret;
}

/*
 * Load a set of files. Used to load jobs.
 */
function rg_load_files($dir, $pattern, $id_field)
{
	$ret = FALSE;
	while (1) {
		$l = rg_dir_load_pattern($dir, $pattern);
		if ($l === FALSE)
			break;

		$list = array();
		$error = FALSE;
		foreach ($l as $f) {
			$c = @file_get_contents($dir . '/' . $f);
			if ($c === FALSE) {
				rg_util_set_error('cannot load data: ' . rg_php_err());
				$error = TRUE;
				break;
			}

			$d = @json_decode($c, TRUE);
			if ($d === NULL) {
				rg_util_set_error('cannot decode job file ' . $f . ': ' . json_last_error_msg());
				$error = TRUE;
				break;
			}

			$id = $d[$id_field];
			$list[$id] = $d;
		}
		if ($error)
			break;

		$ret = $list;
		break;
	}

	return $ret;
}

/*
 * Computing the size of a directory (helper)
 */
function rg_dir_size_helper($dir, &$icache)
{
	$tree = array('dirs' => array());
	$ret = FALSE;
	while (1) {
		$d = @opendir($dir);
		if ($d === FALSE) {
			rg_util_set_error('cannot open dir: ' . rg_php_err());
			break;
		}

		$error = FALSE;
		while (($f = readdir($d)) !== FALSE) {
			if (strcmp($f, ".") == 0)
				continue;

			if (strcmp($f, "..") == 0)
				continue;

			$s = @stat($dir . '/' . $f);
			if ($s === FALSE) {
				rg_util_set_error('cannot stat: ' . rg_php_err());
				$error = TRUE;
				break;
			}

			if (($s['mode'] & 0040000) == 0040000) { // dir
				$v = rg_dir_size_helper($dir . '/' . $f, $icache);
				if ($v === FALSE) {
					$error = TRUE;
					break;
				}
			} else if (($s['mode'] & 0100000) == 0100000) { // regular file
				$k = $s['dev'] . '-' . $s['ino'];
				if (!isset($icache[$k]))
					$icache[$k] = array(
						'links' => $s['nlink'],
						'size' => $s['size'],
						'blocks' => $s['blocks']
					);
				$v = $k;
			}

			$tree['dirs'][$f] = $v;
		}
		closedir($d);

		if (empty($tree['dirs']))
			unset($tree['dirs']);

		if ($error === FALSE)
			$ret = $tree;
		break;
	}

	return $ret;
}

function rg_dir_size_helper2(&$tree, $icache)
{
	// Step two, now we can compute the correct size
	$tree['blocks'] = 0;
	$tree['size'] = 0;
	if (!isset($tree['dirs'])) // empty dir
		return $tree;

	foreach ($tree['dirs'] as $k => &$o) {
		if (is_array($o)) { // dir
			//echo '  dir [' . $k . ']: ' . rg_array2string($o);
			$x = rg_dir_size_helper2($o, $icache);
			//echo '    ret: ' . rg_array2string($x) . "\n";
			$tree['size'] += $x['size'];
			$tree['blocks'] += $x['blocks'];
		} else { // file
			$p = $icache[$o];
			//echo '  file [' . $k . ']: icache: ' . rg_array2string($p) . "\n";
			$tree['size'] += intval($p['size'] / $p['links']);
			$tree['blocks'] += intval($p['blocks'] / $p['links']);
			unset($tree['dirs'][$k]);
		}
	}
	unset($o);

	if (empty($tree['dirs']))
		unset($tree['dirs']);

	return $tree;
}

/*
 * Computing the size of a directory
 */
function rg_dir_size($dir)
{
	rg_prof_start('dir_size');

	$ret = FALSE;
	while (1) {
		$r = file_exists($dir);
		if ($r === FALSE) {
			$ret = array('blocks' => 0, 'size' => 0);
			break;
		}

		// This will allow us to count correctly the size of the dir
		$icache = array();

		$ret = rg_dir_size_helper($dir, $icache);
		if ($ret === FALSE)
			break;

		//rg_log_ml('dir_size: ' . print_r($ret));
		//rg_log_ml('icache: ' . print_r($icache));

		rg_dir_size_helper2($ret, $icache);
		unset($icache);
		break;
	}

	rg_prof_end('dir_size');
	return $ret;
}

/*
 * Creates all needed directories in a path to a file (NOT a dir)
 */
function rg_create_dirs($path, $mode)
{
	$ret = array('ok' => 0);
	do {
		$d = dirname($path);
		if (strcmp($d, '/') == 0) {
			$ret['ok'] = 1;
			break;
		}

		if (strcmp($d, '.') != 0) {
			// Try to create the parent
			$ret = rg_create_dirs($d, $mode);
			if ($ret['ok'] != 1)
				break;
		}

		$r = @stat($d);
		if ($r !== FALSE) {
			$ret['ok'] = 1;
			break;
		}

		$r = @mkdir($d, $mode);
		if ($r === FALSE) {
			$ret['errmsg'] = rg_php_err();
			break;
		}

		$ret['ok'] = 1;
	} while (0);

	return $ret;
}

$_ip = '';
function rg_ip_set($ip)
{
	global $_ip;

	$_ip = rg_fix_ip($ip);
}

function rg_ip()
{
	global $_ip;

	return $_ip;
}

$_t = @file_get_contents('/home/rocketgit/rg_debug');
$rg_debug = ($_t !== FALSE) ? intval($_t) : 0;
function rg_debug_set($level)
{
	global $rg_debug;

	$rg_debug |= intval($level);
	rg_log('DEBUG: rg_debug set to ' . $rg_debug);
}

function rg_debug()
{
	global $rg_debug;

	return $rg_debug;
}

$rg_debug_html = array();
function rg_debug_html_set($k, $v)
{
	global $rg_debug_html;

	if (rg_debug() == 0)
		return;

	//rg_log_debug('rg_debug_html_set called [' . $k . '] [' . rg_array2string($v) . ']');
	$rg_debug_html[$k] = $v;
}

/* Functions for template system */
function rg_debug_html($junk)
{
	global $rg_debug_html;

	if (rg_debug() == 0)
		return array('value' => '', 'html' => 1);

        $ret = array(
        	'html' => 1, // not really html
        	'value' => '<!-- --rg_debug_html-- '
	        	. json_encode($rg_debug_html)
        		. ' --rg_debug_html-- -->'
        );

	return $ret;
}
rg_template_func('RG_DEBUG_HTML', 'rg_debug_html');

/*
 * Loads a .sha256 hash file or generate one
 * TODO: test if file is more recent than hash file!
 */
function rg_hash_load($hash, $f)
{
	$ret = array('ok' => 0);
	do {
		$r = @stat($f);
		if ($r === FALSE) {
			$ret['errmsg'] = 'cannot stat file';
			break;
		}
		$ret['size'] = $r['size'];

		$mtime = $r['mtime'];

		$r = @file_get_contents($f . '.' . $hash);
		if ($r !== FALSE) {
			$x = explode(' ', $r);
			if (isset($x[1]) && ($mtime == $x[1])) {
				$ret['hash'] = $x[0];
				$ret['ok'] = 1;
				break;
			}
		}

		$r = hash_file($hash, $f);
		if ($r === FALSE) {
			$ret['errmsg'] = 'cannot create hash';
			break;
		}
		$ret['hash'] = $r;
		$ret['ok'] = 1;

		@file_put_contents($f . '.' . $hash, $ret['hash']);
		// we ignore errors here
	} while (0);

	return $ret;
}

/*
 * XZ compress a file
 */
function rg_xz_compress($f, $extra)
{
	rg_log_enter('xz_compress: f=' . $f);

	$ret = array('ok' => 0);
	do {
		$a = array(
			'cmds' => array(
				'cmd1' => array(
					'cmd' => 'xz ' . $extra . ' ' . escapeshellarg($f)
				)
			)
		);

		$r = rg_exec2($a);
		if (($r['ok'] != 1) || ($r['cmds']['cmd1']['exitcode'] != 0)) {
			$ret['errmsg'] = $r['cmds']['cmd1']['last_errmsg']
				. ' [' . $r['cmds']['cmd1']['err_buf'];
			break;
		}

		$ret['ok'] = 1;
	} while (0);

	rg_log_exit();
	return $ret;
}

/*
 * Remove dir2 from dir2 to obtain a relative path
 */
function rg_dir_cut($dir1, $dir2)
{
	return substr($dir1, strlen($dir2));
}



Mode Type Size Ref File
100644 blob 9 f3c7a7c5da68804a1bdf391127ba34aed33c3cca .exclude
100644 blob 108 acc2186b1d357966e09df32afcea14933f5f0c78 .gitignore
100644 blob 375 1f425bcd2049c526744d449511094fc045ceac74 AUTHORS
100644 blob 2375 07d4a2277dd1c385e2562a9ac7c636bcdd084c18 History.txt
100644 blob 34520 dba13ed2ddf783ee8118c6a581dbf75305f816a3 LICENSE
100644 blob 3632 f216d8f6ca7180c095ee4fcbe96d8fc2ca2b0dee Makefile.in
100644 blob 5325 96c40d868ce10b715299085ccffb30f96a730cf3 README
100644 blob 194316 464d80c60ed62ecbf5b91ff26e747163b7a3b656 TODO
100644 blob 1294 f22911eb777f0695fcf81ad686eac133eb11fcc4 TODO-plans
100644 blob 203 a2863c67c3da44126b61a15a6f09738c25e0fbe0 TODO.perf
100644 blob 967 56bbaa7c937381fb10a2907b6bbe056ef8cc824a TODO.vm
040000 tree - 21928e906ad2907a55c2e81c2a8b0502b586b8a0 artwork
100644 blob 5328 d5be4cc3f15d059ad8d267d800c602e9774816a8 compare.csv
100755 blob 30 92c4bc48245c00408cd7e1fd89bc1a03058f4ce4 configure
040000 tree - 66648b9a1193c863332c5e0487b723223dbdc1a4 debian
040000 tree - 98aa7eab5457a649ed3c683caa22699a1f44e138 docker
040000 tree - f67d3605efbd6422a8acdd953578991139266391 docs
100755 blob 18252 e2438615edba7066a730ed6a796a5302263f1f37 duilder
100644 blob 536 1431ab7fdd572e4815f0b15facc04a6b047a0dd5 duilder.conf
040000 tree - 73914e7faf49474a12b29e936ae77006254cc1a1 hooks
040000 tree - ca371e380febf1e280eca9bd0a5875f07b9cd102 inc
040000 tree - e255ce234c3993998edc12bc7e93fff555376eda misc
100644 blob 6380 ae703ff087613ba62a559f2d637a6785b50fd29c rocketgit.spec
040000 tree - 150931608e051754ede7f677e94d3b4025ad7b3b root
040000 tree - 19d974acea12eaedf6dd233d2997183b3f84f1a6 samples
040000 tree - 598183d3781803f4d6014853bf75f2848be67b29 scripts
040000 tree - 454044f7e286fe13ec18598fce6b613190f52e5e selinux
100755 blob 256 462ccd108c431f54e380cdac2329129875a318b5 spell_check.sh
040000 tree - d9260d3cf0d6490be720312893600a8041bf991b techdocs
040000 tree - 1697fd6dc35e069d2d16a6c262c4ddc84abd0566 tests
040000 tree - e810d7397575886ef495708d571eb3675f6928ba tools
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/catalinux/rocketgit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/catalinux/rocketgit

Clone this repository using git:
git clone git://git.rocketgit.com/user/catalinux/rocketgit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main