<?php
require_once(__DIR__ . '/util2.inc.php');
require_once(__DIR__ . '/log.inc.php');
require_once(__DIR__ . '/sql.inc.php');
require_once(__DIR__ . '/user.inc.php');
require_once(__DIR__ . '/git.inc.php');
require_once(__DIR__ . '/rights.inc.php');
require_once(__DIR__ . '/prof.inc.php');
require_once(__DIR__ . '/mail.inc.php');
require_once(__DIR__ . '/events.inc.php');
require_once(__DIR__ . '/webhooks.inc.php');
require_once(__DIR__ . '/format.inc.php');
$rg_repo_refs_rights = array(
"F" => "Fetch",
"P" => "Push",
"H" => "Anonymous push",
"S" => "Create annotated tag",
"n" => "Delete annotated tag",
"Y" => "Create un-annotated tag",
"U" => "Modify un-annotated tag",
"u" => "Delete un-annotated tag",
"C" => "Create branch",
"D" => "Delete branch",
"O" => "Non fast-forwards",
"M" => "Merge commits",
"W" => "Bad whitespace"
);
$rg_repo_path_rights = array(
"P" => "Push",
"W" => "Bad whitespace"
);
$rg_repo_rights = array(
'A' => 'Access repo',
'a' => 'Access bug tracker',
'B' => 'Add bugs',
'C' => 'Close bugs',
'D' => 'Delete repo',
'd' => 'Delete bugs',
'E' => 'Create/edit repo',
'G' => 'Grant rights',
'g' => 'Revoke rights',
'K' => 'Lock repo',
'r' => 'Reopen bugs',
't' => 'Access artifacts',
'T' => 'Delete artifacts',
'W' => 'Manage hooks'
);
// TODO: default rights should go into conf file?
// TODO: better move all config to database (modulo db conn info)?
rg_rights_register("repo_refs", $rg_repo_refs_rights, "FMH",
"rg_repo_compare_refs", "rg_repo_rights_inject");
rg_rights_register("repo_path", $rg_repo_path_rights, "P",
"rg_repo_compare_paths", "rg_repo_rights_inject");
rg_rights_register("repo", $rg_repo_rights, 'AaBt',
FALSE, "rg_repo_rights_inject");
/*
* Helper to obtain the next id (used for bugs and mr)
*/
function rg_repo_next_id($db, $field, $repo_id)
{
rg_prof_start('repo_next_id');
rg_log_enter('repo_next_id: field=' . $field . ' repo_id=' . $repo_id);
$next_id = FALSE;
while (1) {
$params = array('repo_id' => $repo_id);
$sql = 'UPDATE repos SET last_' . $field . '_id'
. ' = last_' . $field . '_id + 1'
. ' WHERE repo_id = @@repo_id@@'
. ' RETURNING last_' . $field . '_id AS next_id';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error('cannot get/update max (' . rg_sql_error() . ')');
break;
}
$rows = rg_sql_num_rows($res);
if ($rows > 0)
$row = rg_sql_fetch_array($res);
rg_sql_free_result($res);
if ($rows == 0) {
rg_repo_set_error('seems that repo does not exists!');
break;
}
$next_id = $row['next_id'];
$key = 'repo_by_id' . '::' . $repo_id . '::'
. 'last_' . $field . '_id';
rg_cache_set($key, $next_id, RG_SOCKET_NO_WAIT);
break;
}
rg_log_debug('next_id=' . $next_id);
rg_log_exit();
rg_prof_end('repo_next_id');
return $next_id;
}
/*
* Improve repo info
*/
function rg_repo_cosmetic($db, &$row)
{
global $rg_ssh_port, $rg_git_port;
// Because this field was added later and the cache may not have it
if (!isset($row['template']))
$row['template'] = '';
if (!empty($row['description'])) {
$_a = rg_xss_safe($row['description']);
$row['HTML:description_nlbr'] = nl2br($_a);
if (mb_strlen($row['description']) > 100)
$row['HTML:description_short_nlbr'] = nl2br(mb_substr($_a, 0, 100)) . '...';
else
$row['HTML:description_short_nlbr'] = $row['HTML:description_nlbr'];
} else {
$row['HTML:description_nlbr'] = 'No description available';
$row['HTML:description_short_nlbr'] = $row['HTML:description_nlbr'];
}
if (isset($row['itime']))
$row['HTML:itime_nice'] = gmdate('Y-m-d', $row['itime']);
$_ui = rg_user_info($db, $row['uid'], '', '');
if ($_ui['exists'] == 1) {
$row['owner'] = $_ui['username'];
$row['url_user'] = rg_re_userpage($_ui);
$row['url_repo'] = rg_re_repopage($_ui, $row['name']);
$row['clone_url_ssh'] = rg_re_repo_ssh($_ui['organization'],
$_ui['username'], $row['name']);
$row['clone_url_git'] = rg_re_repo_git($_ui['organization'],
$_ui['username'], $row['name']);
$row['clone_url_http'] = rg_base_url($db, '', '')
. rg_re_repo_http($_ui['organization'],
$_ui['username'], $row['name']);
}
$row['master_name'] = '-';
if ($row['master'] > 0) {
$_mi = rg_repo_info($db, $row['master'], 0, '');
if ($_mi['exists'] == 1)
$row['master_name'] = $_mi['name'];
}
if (isset($row['disk_used_mb']))
$row['disk_used'] = rg_1024($row['disk_used_mb'] * 1024 * 1024);
if (empty($row['main_branch']))
$row['main_branch'] = 'main';
if (empty($row['hash']))
$row['hash'] = 'sha1';
}
/*
* Returns info about a repo
* If you want to lookup by repo_id or uid/repo_name
*/
function rg_repo_info($db, $repo_id, $uid, $repo_name)
{
rg_prof_start('repo_info');
$ret['ok'] = 0;
$ret['exists'] = 0;
while(1) {
$params = array("uid" => $uid,
"repo_id" => $repo_id,
"repo_name" => $repo_name);
if ($repo_id > 0) {
$c = rg_cache_get('repo_by_id' . '::' . $repo_id);
if (($c !== FALSE) && isset($c['repo_id'])) {
$ret = $c;
rg_repo_cosmetic($db, $ret);
break;
}
$sql = "SELECT * FROM repos WHERE repo_id = @@repo_id@@";
} else if (!empty($repo_name)) {
$x_repo_id = rg_cache_get("repo_by_name::$uid::$repo_name");
if ($x_repo_id !== FALSE) {
$ret = rg_repo_info($db, $x_repo_id, $uid, "");
break;
}
$sql = "SELECT * FROM repos WHERE uid = @@uid@@"
. " AND name = @@repo_name@@";
} else {
rg_repo_set_error("no repo_id or user/repo specified!");
break;
}
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error("cannot query (" . rg_sql_error() . ")");
break;
}
$rows = rg_sql_num_rows($res);
if ($rows > 0)
$ret = rg_sql_fetch_array($res);
rg_sql_free_result($res);
if (($rows == 0) && ($repo_id == 0)) {
// Repo not found, maybe it was renamed
$_repo_id = rg_repo_lookup_by_old_name($db, $uid, $repo_name);
if ($_repo_id === FALSE)
break;
if ($_repo_id > 0) {
$ret = rg_repo_info($db, $_repo_id, 0, "");
break;
}
}
$ret['ok'] = 1;
if ($rows > 0) {
$ret['exists'] = 1;
} else {
$ret['exists'] = 0;
}
//rg_log_ml("CHECK: ret=" . print_r($ret, TRUE));
if ($ret['exists'] == 1) {
rg_cache_set('repo_by_id' . '::' . $ret['repo_id'],
$ret, RG_SOCKET_NO_WAIT);
rg_cache_set("repo_by_name::$uid::" . $ret['name'],
$ret['repo_id'], RG_SOCKET_NO_WAIT);
}
if ($rows > 0)
rg_repo_cosmetic($db, $ret);
break;
}
rg_prof_end("repo_info");
return $ret;
}
/*
* Function used to inject rights for a obj_id/type combination
* It will be called from rg_rights_get
* @out - the output array where we want to put rights
*/
function rg_repo_rights_inject($db, $obj_id, $type, $owner, $uid)
{
rg_log_enter("repo_rights_inject: obj_id=$obj_id type=$type"
. " owner=$owner uid=$uid");
$ret = array();
while (1) {
$a = array();
$a['type'] = $type;
$a['obj_id'] = $obj_id;
$a['uid'] = 0; // TODO: not clear here what to put!
$a['itime'] = 0;
$a['misc'] = "";
$a['prio'] = 0;
$a['who'] = $owner; // TODO: not clear if correct/good
$a['right_id'] = 0;
$a['can_be_deleted'] = 0;
$a['ip'] = '';
$a['description'] = "Autogenerated";
if ($uid > 0) {
$ui = rg_user_info($db, $uid, "", "");
if ($ui['exists'] != 1)
break;
if ($ui['is_admin'] == 1) {
$a['uid'] = $owner;
$a['rights'] = rg_rights_all($type);
$a['description'] = 'Autogenerated (you are admin)';
$ret[] = $a;
}
}
$ri = rg_repo_info($db, $obj_id, 0, "");
if ($ri['exists'] != 1) {
rg_log("repo $obj_id does not exists");
break;
}
// Here, rights to inject, even if repo is not public
if (strcmp($type, "repo_path") == 0) {
$a['uid'] = 0;
$a['prio'] = 30001;
$a['rights'] = "PW"; // push + whitespace
$a['description'] = 'Autogenerated (sane default)';
$ret[] = $a;
}
// No rights to inject if is not public
if ($ri['public'] != 1)
break;
if (strcmp($type, "repo") == 0) {
$a['uid'] = 0;
$a['rights'] = 'Aat';
$a['description'] = 'Autogenerated (repo is public)';
$ret[] = $a;
} else if (strcmp($type, "repo_refs") == 0) {
$a['uid'] = 0;
$a['prio'] = 1;
$a['rights'] = "F"; // Fetch
$a['description'] = 'Autogenerated (repo is public)';
$ret[] = $a;
$a['uid'] = 0;
$a['prio'] = 30001;
$a['rights'] = "H"; // push + whitespace
$a['description'] = 'Autogenerated (sane default)';
$ret[] = $a;
} else if (strcmp($type, "repo_path") == 0) {
} else {
rg_log("type $type not used. bug?");
break;
}
break;
}
rg_log_exit();
return $ret;
}
/*
* Bring a ref to a canonical format (refs/x/name)
* TODO: move to git.inc?
*/
function rg_repo_ref_canon($ref)
{
if (strncmp($ref, "refs/", 5) == 0) {
// do nothing
} else if (strncmp($ref, "/refs/", 6) == 0) {
$ref = substr($ref, 1);
} else {
$ref = "refs/heads/" . $ref;
}
return $ref;
}
/*
* Compare function for repo names
*/
function rg_repo_compare_names($a, $b)
{
if (empty($a))
return TRUE;
$qmisc = str_replace('`', '', $a);
$ret = preg_match('`^' . $qmisc . '`uD', $b);
rg_log('repo_compare_names: ret=' . $ret . ' a=' . $a . ' b=' . $b
. ' [qmisc=' . $qmisc . '] => '
. ($ret === 1 ? 'match' : 'no match'));
return $ret === 1;
}
/*
* Compare function for refs
*/
function rg_repo_compare_refs($misc, $ref)
{
rg_prof_start("repo_compare_refs");
if (empty($misc))
return TRUE;
$misc = rg_repo_ref_canon($misc);
$ref = rg_repo_ref_canon($ref);
// seems that by doing escape, nothing will match!
//$qmisc = preg_quote($misc, '/');
$qmisc = str_replace('`', '', $misc);
$ret = preg_match('`^' . $qmisc . '`uD', $ref);
rg_log("repo_compare_refs: ret=$ret misc=$misc ref=$ref [qmisc=$qmisc] => "
. ($ret === 1 ? "match" : "no match"));
rg_prof_end("repo_compare_refs");
return $ret === 1;
}
/*
* Compare function for paths
*/
function rg_repo_compare_paths($misc, $path)
{
rg_prof_start("repo_compare_paths");
$qmisc = preg_quote($misc, '/');
$ret = preg_match('/' . $qmisc . '/uD', $path);
rg_log("repo_compare_paths: misc=$misc path=$path => " . ($ret === 1 ? "T" : "F"));
rg_prof_end("repo_compare_paths");
return $ret === 1;
}
/*
* Remove "refs/heads/" when showing rights on web
* TODO: don't know where to call it. Maybe in rg_rights_load?
* TODO: also, register this kind of function.
*/
function rg_repo_ref_nice($ref)
{
if (strncmp($ref, "refs/heads/", 11) == 0)
$ref = substr($ref, 11);
return $ref;
}
// Repo history categories
define('REPO_CAT_CREATE', 1);
define('REPO_CAT_CLONED', 2);
define('REPO_CAT_PUSH', 3);
define('REPO_CAT_RENAME', 4);
define('REPO_CAT_UPDATE', 5);
define('REPO_CAT_BUG_ADDED', 10);
define('REPO_CAT_BUG_CLOSED', 11);
define('REPO_CAT_GIT_ATAG_CREATE', 20);
define('REPO_CAT_GIT_ATAG_DELETE', 21);
define('REPO_CAT_GIT_ATAG_UPDATE', 22);
define('REPO_CAT_GIT_UTAG_CREATE', 30);
define('REPO_CAT_GIT_UTAG_DELETE', 31);
define('REPO_CAT_GIT_UTAG_UPDATE', 32);
define('REPO_CAT_GIT_BRANCH_DELETE', 40);
define('REPO_CAT_GIT_BRANCH_UPDATE', 41);
define('REPO_CAT_GIT_BRANCH_CREATE', 42);
define('REPO_CAT_GIT_BRANCH_ANON_PUSH', 43);
define('REPO_CAT_LOCK', 50);
define('REPO_CAT_UNLOCK', 51);
$rg_repo_error = '';
function rg_repo_set_error($str)
{
global $rg_repo_error;
$rg_repo_error = $str;
rg_log('repo_set_error: ' . $str);
}
function rg_repo_error()
{
global $rg_repo_error;
return $rg_repo_error;
}
/*
* Events functions
*/
$rg_repo_functions = array(
3000 => "rg_repo_event_new",
3001 => "rg_repo_event_del",
3002 => "rg_repo_event_update",
3003 => "rg_repo_event_notify_user",
3004 => "rg_repo_event_symlink_by_name",
3005 => "rg_repo_event_storage_create",
3006 => "rg_repo_history_insert",
3007 => 'rg_repo_event_push',
// new new style
'repo_event_new' => 'rg_repo_event_new',
'repo_event_del' => 'rg_repo_event_del',
'repo_event_update' => 'rg_repo_event_update',
'repo_event_notify_user' => 'rg_repo_event_notify_user',
'repo_event_symlink_by_name' => 'rg_repo_event_symlink_by_name',
'repo_event_storage_create' => 'rg_repo_event_storage_create',
'repo_history_insert' => 'rg_repo_history_insert',
'repo_event_push' => 'rg_repo_event_push'
);
rg_event_register_functions($rg_repo_functions);
/*
* Event for adding a new repo
*/
function rg_repo_event_new($db, $event)
{
$ret = array();
$event['op'] = "new";
$event['source'] = 'repo_event_new';
$x = $event;
$x['category'] = 'repo_event_storage_create';
$x['prio'] = 20;
$x['notification'] .= "-git";
$ret[] = $x;
$x = $event;
$x['category'] = 'repo_event_symlink_by_name';
$x['prio'] = 200;
$x['notification'] .= "-symlink";
$ret[] = $x;
$x = $event;
$x['category'] = 'repo_event_notify_user';
$x['prio'] = 100;
$x['notification'] .= "-notify";
$ret[] = $x;
// webhook
$x = $event;
$x['category'] = 'wh_send';
$x['prio'] = 50;
$x['wh_event'] = 'C'; // see rg_wh_events array
$ret[] = $x;
// add a history entry
$x = $event;
$x['category'] = 'repo_history_insert';
$x['prio'] = 50;
$ret[] = $x;
// TODO: notify watchers of the user
return $ret;
}
/*
* Event for deleting repo
*/
function rg_repo_event_del($db, $event)
{
$ret = array();
$event['op'] = "del";
// notify user
$ret[] = array_merge($event,
array(
'category' => 'repo_event_notify_user',
'source' => 'repo_event_del',
'prio' => 100)
);
// TODO: notify watchers
return $ret;
}
/*
* Make a symlink by name (by_name/name -> ../by_id/xx/xx/xx/xx/xxxxxxxx.git)
* TODO: why return may be an array?!
*/
function rg_repo_event_symlink_by_name($db, $e)
{
rg_prof_start("repo_event_symlink_by_name");
$id_path = rg_repo_path_by_id($e['ui_login']['uid'], $e['ri']['repo_id']);
$id_path_rel = rg_repo_path_by_id_rel($e['ui_login']['uid'], $e['ri']['repo_id']);
$new_path = rg_repo_path_by_name($e['ui_login']['uid'], $e['ri']['name']);
$ret = FALSE;
while (1) {
// Check if we already did the rename
if (file_exists($new_path)) {
if (!is_link($new_path)) {
rg_internal_error("$new_path is not a link");
break;
}
$v = readlink($new_path);
if ($v === FALSE) {
rg_internal_error("cannot read link $new_path");
break;
}
rg_log("new_path points to [$v]");
if (strcmp($id_path_rel, $v) == 0) {
// Link already done
$ret = array();
break;
}
// Seems that new_path points to other place
$r = rename($new_path, $new_path . ".BOGUS." . time());
if ($r !== TRUE) {
rg_internal_error("cannot rename bogus");
break;
}
}
// Create links' parent
$new_path_parent = dirname($new_path);
if (!is_dir($new_path_parent)) {
$r = mkdir($new_path_parent, 0700, TRUE);
if ($r === FALSE) {
rg_repo_set_error("cannot create links' parent");
break;
}
}
// Now, the new name is free, do the link
$r = symlink($id_path_rel, $new_path);
if ($r !== TRUE) {
rg_internal_error("cannot symlink $id_path -> $new_path (" . rg_php_err() . ")!");
break;
}
$ret = array();
break;
}
rg_prof_end("repo_event_symlink_by_name");
return $ret;
}
/*
* Creates git dir storage
*/
function rg_repo_event_storage_create($db, $e)
{
rg_prof_start("repo_event_storage_create");
rg_log_enter("repo_event_storage_create: e=" . rg_array2string($e));
$ret = FALSE;
while (1) {
$by_id_path = rg_repo_path_by_id($e['ui_login']['uid'], $e['ri']['repo_id']);
if (!is_dir($by_id_path)) {
if (mkdir($by_id_path, 0700, TRUE) === FALSE) {
rg_repo_set_error("could not create folder $dst");
break;
}
}
if ($e['ri']['master'] == 0) {
$r = rg_git_init($by_id_path, $e['ri']['main_branch'],
$e['ri']['hash']);
if ($r === FALSE) {
rg_repo_set_error("cannot init master"
. " (" . rg_git_error() . ")");
break;
}
} else {
$mi = rg_repo_info($db, $e['ri']['master'], 0, "");
if ($mi['exists'] != 1) {
rg_repo_set_error("cannot find master"
. " (" . rg_repo_error() . ")");
break;
}
$master_by_id_path = rg_repo_path_by_id($mi['uid'],
$mi['repo_id']);
$r = rg_git_clone($master_by_id_path, $by_id_path);
if ($r === FALSE) {
rg_repo_set_error("could not create repo"
. " (" . rg_git_error() . ")");
break;
}
}
$r = rg_repo_event_symlink_by_name($db, $e);
if ($r === FALSE)
break;
$r = rg_repo_git_done($db, $e['ri']['repo_id']);
if ($r !== TRUE)
break;
$ret = array();
break;
}
rg_log_exit();
rg_prof_end("repo_event_storage_create");
return $ret;
}
/*
* Event for renaming a repo
*/
function rg_repo_event_update($db, $event)
{
$ret = array();
$event['op'] = "update";
$event['source'] = 'repo_event_update';
// make symlink by name
$ret[] = array_merge($event,
array('category' => 'repo_event_symlink_by_name', 'prio' => 200));
// notify user
$ret[] = array_merge($event,
array('category' => 'repo_event_notify_user', 'prio' => 100));
// TODO: notify watchers
return $ret;
}
/*
* User notification
*/
function rg_repo_event_notify_user($db, $event)
{
rg_prof_start("repo_event_notify_user");
$ret = FALSE;
while (1) {
$r = rg_mail_template("mail/user/repo/" . $event['op'], $event);
if ($r === FALSE)
break;
$ret = array();
break;
}
rg_prof_end("repo_event_notify_user");
return $ret;
}
/*
* Inserts an event into repo_history table
* TODO: we do not know yet if update-ref succeded.
*/
function rg_repo_history_insert($db, $event)
{
rg_prof_start("repo_history_insert");
rg_log_enter("repo_history_insert: event=" . rg_array2string($event));
$ret = FALSE;
while (1) {
$now = time();
$params = array("now" => $now,
"repo_id" => $event['ri']['repo_id'],
"uid" => $event['ui_login']['uid'],
"cat" => $event['history_category'],
"mess" => $event['history_message']);
$sql = "INSERT INTO repo_history_" . gmdate("Y_m", $now)
. " (itime, uid, repo_id, category, message)"
. " VALUES (@@now@@, @@uid@@, @@repo_id@@, @@cat@@, @@mess@@)";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE)
break;
rg_sql_free_result($res);
$ret = array();
break;
}
rg_log_exit();
rg_prof_end("repo_history_insert");
return $ret;
}
/*
* Event for pushing into a repo
*/
function rg_repo_event_push($db, $event)
{
$ret = array();
// TODO: we must be sure here that the update-ref finished with success
// TODO: load references and check them?
// 'post-update' hook knows if it succeeded or not. How to use it?
$event['source'] = 'repo_event_push';
// notify user
if (0) { // TODO
$x = $event;
$x['category'] = 'repo_event_notify_user';
$x['prio'] = 100;
$x['notification'] .= "-notify";
$ret[] = $x;
}
// TODO: notify watchers?
// webhook
$x = $event;
$x['category'] = 'wh_send';
$x['wh_event'] = 'P'; // push
$ret[] = $x;
// TODO: notify watchers of the user
return $ret;
}
/*
* Returns last events from repo_history
* @category: 0 for any
* @number: number of entries
* @min_time: limit the search to earlier than min_time timestamp
*/
function rg_repo_history_load($db, $repo_id, $category, $number, $min_time)
{
rg_prof_start("repo_history_load");
rg_log_enter('repo_history_load: repo_id=' . $repo_id . ' category=' . $category
. ', number=' . $number . ' min_time=' . $min_time);
$ret = FALSE;
while (1) {
$now = time();
$category_sql = "";
if ($category > 0)
$category_sql = " AND category = " . $category;
$limit_sql = "";
if ($number > 0)
$limit_sql = " LIMIT " . $number;
else
$limit_sql = " LIMIT 50";
$time_sql = '';
if ($min_time > 0)
$time_sql = ' AND itime >= ' . $min_time;
$sql = 'SELECT itime, category, message, uid'
. ' FROM repo_history'
. ' WHERE repo_id = ' . $repo_id
. $category_sql
. $time_sql
. ' ORDER BY itime DESC'
. $limit_sql;
$res = rg_sql_query($db, $sql);
if ($res === FALSE)
break;
$ret = array();
while (($row = rg_sql_fetch_array($res))) {
$row['username'] = 'n/a';
if ($row['uid'] > 0) {
$ui = rg_user_info($db, $row['uid'], '', '');
if ($ui['exists'] == 1)
$row['username'] = $ui['username'];
}
if ($row['itime'] == 0)
$row['itime_text'] = "N/A";
else
$row['itime_text'] = gmdate("Y-m-d H:i", $row['itime']);
$ret[] = $row;
}
rg_sql_free_result($res);
break;
}
rg_log_exit();
rg_prof_end("repo_history_load");
return $ret;
}
/*
* Enforce name
*/
function rg_repo_ok($repo)
{
global $rg_repo_allow;
global $rg_repo_min_len;
global $rg_repo_max_len;
if (empty($repo)) {
rg_repo_set_error("invalid repository name (empty)");
return FALSE;
}
if (strcmp(substr($repo, 0, 1), '-') == 0) {
rg_repo_set_error('invalid repository name'
. ' (cannot start with minus)');
return FALSE;
}
if (strpos($repo, ':') !== FALSE) {
rg_repo_set_error('invalid repository name'
. ' (cannot contain :)');
return FALSE;
}
if (rg_chars_allow($repo, $rg_repo_allow, $invalid) !== 1) {
rg_repo_set_error("invalid repository name"
. " (invalid chars: '$invalid')");
return FALSE;
}
if (preg_match('/\.\./', $repo) !== 0) {
rg_repo_set_error("invalid repository name (..)");
return FALSE;
}
$len = mb_strlen($repo);
if ($len < $rg_repo_min_len) {
rg_repo_set_error("repository name is too short"
. " (minimum $rg_repo_min_len < $len)");
return FALSE;
}
if ($len > $rg_repo_max_len) {
rg_repo_set_error("repository name is too long"
. " (maximum $rg_repo_max_len > $len)");
return FALSE;
}
return TRUE;
}
/*
* Returns the relative path to a repository based on id
*/
function rg_repo_path_by_id_rel($uid, $repo_id)
{
return "../by_id/" . $repo_id . ".git";
}
/*
* Returns the path to a repository based on id
*/
function rg_repo_path_by_id($uid, $repo_id)
{
return rg_user_path_by_id($uid) . "/repos/by_id/" . $repo_id . ".git";
}
/*
* Returns the path to a artifacts based on id
*/
function rg_repo_artifacts_path_by_id($uid, $repo_id)
{
return rg_user_path_by_id($uid) . '/repos/by_id/' . $repo_id . '.a';
}
/*
* Returns the path to a repository based on name
*/
function rg_repo_path_by_name($uid, $repo_name)
{
return rg_user_path_by_id($uid) . "/repos/by_name/" . $repo_name . ".git";
}
/*
* Delete a repo
*/
function rg_repo_delete($db, $repo_id, $ui_login)
{
rg_prof_start("repo_delete");
rg_log_enter("repo_delete: uid=" . $ui_login['uid'] . ", repo_id=$repo_id");
$ret = FALSE;
while (1) {
// TODO: Transaction?
$ri = rg_repo_info($db, $repo_id, 0, "");
if ($ri['ok'] != 1)
break;
if ($ri['exists'] != 1) {
rg_repo_set_error("Repository does not exists.");
break;
}
// Only mark it as such, deletion will happen in background
$params = array('repo_id' => $repo_id, 'now' => time());
$sql = "UPDATE repos SET deleted = @@now@@"
. " WHERE repo_id = @@repo_id@@";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error("Cannot delete (" . rg_sql_error() . ")");
break;
}
rg_sql_free_result($res);
$event = array(
'category' => 'repo_event_del',
'source' => 'repo_delete',
'prio' => 50,
'ui_login' => $ui_login,
'ri' => array(
'name' => $ri['name'],
'repo_id' => $repo_id
)
);
$r = rg_event_add($db, $event);
if ($r !== TRUE) {
rg_repo_set_error("cannot add event"
. " (" . rg_event_error() . ")");
break;
}
rg_event_signal_daemon("", 0);
rg_cache_unset('repo_by_id::' . $repo_id, RG_SOCKET_NO_WAIT);
rg_cache_unset('repo_by_name::' . $ui_login['uid']
. '::' . $ri['name'], RG_SOCKET_NO_WAIT);
rg_cache_unset('user' . '::' . $ui_login['uid'] . '::' . 'has_repos',
RG_SOCKET_NO_WAIT);
$ret = TRUE;
break;
}
rg_log_exit();
rg_prof_end("repo_delete");
return $ret;
}
/*
* Lookup in db the old name, so we can redirect the user to the new name
*/
function rg_repo_lookup_by_old_name($db, $uid, $old_name)
{
rg_prof_start("repo_lookup_by_old_name");
rg_log_enter("repo_lookup_by_old_name: uid=$uid old_name=$old_name");
$ret = FALSE;
while (1) {
$c = rg_cache_get("repo_by_old_name::$uid::$old_name");
if ($c !== FALSE) {
$ret = $c;
break;
}
$params = array("uid" => $uid, "old_name" => $old_name);
$sql = "SELECT repo_id FROM repos_renames"
. " WHERE uid = @@uid@@"
. " AND old_name = @@old_name@@";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error("cannot lookup old name ("
. rg_sql_error() . ")");
break;
}
$rows = rg_sql_num_rows($res);
if ($rows > 0)
$row = rg_sql_fetch_array($res);
rg_sql_free_result($res);
if ($rows == 0)
$ret = 0;
else
$ret = $row['repo_id'];
rg_cache_set("repo_by_old_name::$uid::$old_name", $ret, RG_SOCKET_NO_WAIT);
break;
}
rg_log_exit();
rg_prof_end("repo_lookup_by_old_name");
return $ret;
}
/*
* Add a rename to the database
*/
function rg_repo_insert_rename($db, $uid, $repo_id, $old_name)
{
rg_prof_start("repo_insert_rename");
rg_log_enter("repo_insert_rename: uid=$uid repo_id=$repo_id old_name=$old_name");
$ret = FALSE;
while (1) {
// Check if already exists in the database
$r = rg_repo_lookup_by_old_name($db, $uid, $old_name);
if ($r === FALSE)
break;
$params = array("repo_id" => $repo_id,
"uid" => $uid,
"old_name" => $old_name,
"now" => time());
if ($r > 0) {
$sql = "UPDATE repos_renames"
. " SET repo_id = @@repo_id@@"
. " WHERE uid = @@uid@@"
. " AND old_name = @@old_name@@";
} else {
$sql = "INSERT INTO repos_renames (uid, old_name"
. ", repo_id, itime)"
. " VALUES (@@uid@@, @@old_name@@, @@repo_id@@"
. ", @@now@@)";
}
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error("cannot link with old_name in db"
. " (" . rg_sql_error() . ")");
break;
}
rg_sql_free_result($res);
rg_cache_set("repo_by_name::$uid::$old_name", $repo_id, RG_SOCKET_NO_WAIT);
$ret = TRUE;
break;
}
rg_log_exit();
rg_prof_end("repo_insert_rename");
return $ret;
}
/*
* Creates/updates a repository
* @ui_login - info of the user doing the update.
* TODO: Warning, it may not be the owner.
* TODO: where do we validate if the user has enough public/private slots?
*/
function rg_repo_edit($db, $ui_login, &$new)
{
rg_prof_start("repo_edit");
rg_log_enter('repo_edit: login_uid=' . $ui_login['uid']
. ' new=' . rg_array2string($new));
// TODO: test if user is allowed to add a repository
// TODO: test if user did not cross the limit for number of repos
$ret = FALSE;
while (1) {
if (rg_repo_ok($new['name']) !== TRUE)
break;
if ($new['repo_id'] == 0) {
// Check if name is already taken
$ri = rg_repo_info($db, 0, $ui_login['uid'],
$new['name']);
if ($ri['ok'] != 1)
break;
if ($ri['exists'] == 1) {
rg_repo_set_error('name already taken;'
. ' choose a different one');
break;
}
} else {
// Test if repo_id is valid
// TODO: also here we must test if we have a dup name!
$ri = rg_repo_info($db, $new['repo_id'],
$ui_login['uid'], '');
if ($ri['ok'] != 1)
break;
if ($ri['exists'] != 1) {
rg_repo_set_error('repo ' . $new['repo_id']
. ' does not exists.');
break;
}
}
$renamed = 0;
if ($new['repo_id'] > 0) {
// Check if the user renamed the repo
if (strcmp($new['name'], $ri['name']) != 0) {
$renamed = 1;
$r = rg_repo_insert_rename($db, $ui_login['uid'],
$new['repo_id'], $ri['name']);
if ($r !== TRUE)
break;
}
}
//TODO: master may be not accessible to this user. check.
// Small fixes
$new['itime'] = time();
$new['uid'] = $ui_login['uid'];
if (!isset($new['main_branch']))
$new['main_branch'] = 'main';
if ($new['repo_id'] == 0) {
$new['deleted'] = 0;
$new['disk_used_mb'] = 0;
$new['git_dir_done'] = 0;
$new['last_bug_id'] = 0;
$new['last_mr_id'] = 0;
$sql = "INSERT INTO repos (uid, master, name"
. ", itime, max_commit_size, description"
. ", git_dir_done, public, license, template"
. ", main_branch, hash)"
. " VALUES (@@uid@@, @@master@@, @@name@@"
. ", @@itime@@, @@max_commit_size@@"
. ", @@description@@, 0, @@public@@"
. ", @@license@@, @@template@@, @@main_branch@@"
. ", @@hash@@)"
. " RETURNING repo_id";
} else {
$sql = "UPDATE repos SET name = @@name@@"
. ", max_commit_size = @@max_commit_size@@"
. ", description = @@description@@"
. ", public = @@public@@"
. ", license = @@license@@"
. ", template = @@template@@"
. ", main_branch = @@main_branch@@"
. ", hash = @@hash@@"
. " WHERE repo_id = @@repo_id@@";
}
$res = rg_sql_query_params($db, $sql, $new);
if ($res === FALSE) {
rg_repo_set_error("cannot update: " . rg_sql_error());
break;
}
if ($new['repo_id'] == 0) {
$row = rg_sql_fetch_array($res);
if ($row === FALSE) {
rg_repo_set_error('cannot fetch row');
break;
}
}
rg_sql_free_result($res);
if ($new['repo_id'] == 0) {
$cat = 'repo_event_new';
$hcat = REPO_CAT_CREATE;
$hmess = "Repository has been created";
$notification = 'repo_create-' . $ui_login['uid']
. '-' . $row['repo_id'];
$old_description = "";
$new['repo_id'] = $row['repo_id'];
} else {
$cat = 'repo_event_update';
$hcat = REPO_CAT_UPDATE;
$hmess = "Repository has been updated";
$notification = "";
$old_description = $ri['description'];
// TODO: we should log what changed
}
$event = array(
'category' => $cat,
'source' => 'repo_edit',
'prio' => 50,
'notification' => $notification,
'ui_login' => $ui_login,
'history_category' => $hcat,
'history_message' => $hmess);
$event = rg_array_merge($event, 'ri_old', $ri);
$new['url'] = rg_base_url($db, '', '')
. rg_re_repopage($ui_login, $new['name']);
$event = rg_array_merge($event, 'ri', $new);
$event['ri_old']['description_md5'] = md5($old_description);
$event['ri']['description_md5'] = md5($new['description']);
$r = rg_event_add($db, $event);
if ($r !== TRUE) {
rg_repo_set_error("cannot add event"
. " (" . rg_event_error() . ")");
break;
}
$new['ok'] = 1;
$new['exists'] = 1;
rg_cache_merge("repo_by_id::" . $new['repo_id'], $new,
RG_SOCKET_NO_WAIT);
rg_cache_set("repo_by_name::" . $ui_login['uid'] . "::"
. $new['name'], $new['repo_id'], RG_SOCKET_NO_WAIT);
rg_event_signal_daemon('', 0);
$ret = TRUE;
break;
}
rg_log_exit();
rg_prof_end("repo_edit");
return $ret;
}
/*
* Returns 1 if user has at least one repo
* Used to go directly to 'create' page instead to 'list'
*/
function rg_repo_have($db, $uid)
{
rg_prof_start('repo_have');
$ret = 1;
while (1) {
$key = 'user' . '::' . $uid . '::' . 'has_repos';
$c = rg_cache_get($key);
if ($c !== FALSE) {
$ret = $c;
break;
}
$params = array('uid' => $uid);
$sql = 'SELECT 1 FROM repos WHERE uid = @@uid@@ LIMIT 1';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE)
break;
$rows = rg_sql_num_rows($res);
rg_sql_free_result($res);
$ret = $rows;
rg_cache_set($key, $ret, RG_SOCKET_NO_WAIT);
break;
}
rg_prof_end('repo_have');
return $ret;
}
/*
* List repositories
*/
function rg_repo_list_query($db, $url, $sql, $params, $ui_login)
{
rg_prof_start("repo_list_query");
rg_log("repo_list_query: url=$url, sql=$sql...");
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error("cannot list by query (" . rg_sql_error() . ")");
return FALSE;
}
$d = array();
while (($row = rg_sql_fetch_array($res))) {
$_line = array();
foreach ($row as $k => $v)
$_line[$k] = $v;
$_ui = rg_user_info($db, $row['uid'], "", "");
if ($_ui['exists'] != 1) {
rg_log("uid " . $row['uid']
. " associated with this repo not found");
continue;
}
rg_repo_cosmetic($db, $_line);
$d[] = $_line;
}
rg_sql_free_result($res);
rg_prof_end("repo_list_query");
return rg_template_table('repo/list', $d, $ui_login);
}
/*
* List repos of page user 'ui_login'.
* @uid - owner of the to be listed repos
*/
function rg_repo_list($db, $rg, $url, $uid, $limit)
{
rg_log('repo_list: url=' . $url . ' uid=' . $uid);
$ui_login = rg_ui_login();
$params = array(
'uid' => $uid,
'login_uid' => $ui_login['uid']);
if ($uid > 0)
$add = ' AND uid = @@uid@@';
else
$add = ' AND uid != @@login_uid@@';
// TODO: also admin must be able to see them?
if (($ui_login['uid'] == 0) || ($ui_login['uid'] != $uid))
$add .= ' AND public = 1';
if ($limit > 0)
$add_limit = ' LIMIT ' . $limit;
else
$add_limit = '';
$sql = 'SELECT * FROM repos'
. ' WHERE deleted = 0'
. $add
. ' ORDER BY name'
. $add_limit;
return rg_repo_list_query($db, $url, $sql, $params, $ui_login);
}
/*
* Search in all repositories owned by 'ui_login' or public
* We need to exclude private repositories.
*/
function rg_repo_search($db, $ui_login, $q)
{
rg_prof_start("repo_search");
rg_log("repo_search: q=[$q]");
$admin = 0;
if (isset($ui_login['admin']) && ($ui_login['admin'] == 1))
$admin = 1;
$params = array("q" => "%" . $q . "%",
'uid' => $ui_login['uid']);
$sql = "SELECT * FROM repos"
. " WHERE deleted = 0"
. " AND (uid = @@uid@@ OR public = 1 OR " . $admin . " = 1)"
. " AND (name ILIKE @@q@@ OR description ILIKE @@q@@)"
. " ORDER BY master, name"
. " LIMIT 50";
$r = rg_repo_list_query($db, "", $sql, $params, $ui_login);
rg_prof_end("repo_search");
return $r;
}
/*
* Computes the sizes of a repository
*/
function rg_repo_size($uid, $repo_id)
{
$git_path = rg_repo_path_by_id($uid, $repo_id);
$git = rg_dir_size($git_path);
if ($git === FALSE)
return FALSE;
//rg_log_debug('XXX: git:' . print_r($git, TRUE));
$git_size = intval($git['blocks'] / 2 / 1024);
$artifact_path = rg_repo_artifacts_path_by_id($uid, $repo_id);
$a = rg_dir_size($artifact_path);
if ($a === FALSE)
return FALSE;
//rg_log_debug('XXX: a:' . print_r($a, TRUE));
$a_size = intval($a['blocks'] / 2 / 1024);
return array(
'disk_used_mb' => $git_size + $a_size,
'git_mb' => $git_size,
'artifacts_mb' => $a_size
);
}
/*
* Mark a git repo as done
*/
function rg_repo_git_done($db, $repo_id)
{
rg_prof_start("repo_git_done");
rg_log_enter("repo_git_done: repo_id=$repo_id...");
$ret = FALSE;
while (1) {
$params = array("repo_id" => $repo_id);
rg_cache_merge('repo_by_id::' . $repo_id,
array('git_dir_done' => 1), RG_SOCKET_NO_WAIT);
$sql = "UPDATE repos SET git_dir_done = 1"
. " WHERE repo_id = @@repo_id@@";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error("Cannot query (" . rg_sql_error() . ")");
break;
}
rg_sql_free_result($res);
$ret = TRUE;
break;
}
rg_log_exit();
rg_prof_end("repo_git_done");
return $ret;
}
/*
* Returns the status of a repo
*/
function rg_repo_lock_status($db, $repo_id)
{
rg_prof_start('repo_lock_status');
rg_log_enter('repo_lock_status');
$ret = array();
$ret['ok'] = 0;
while (1) {
$key = 'locks' . '::' . 'repos' . '::' . $repo_id;
$c = rg_cache_get($key);
if ($c !== FALSE) {
$ret = $c;
$ret['ok'] = 1;
break;
}
$params = array('repo_id' => $repo_id);
$sql = 'SELECT * FROM repo_locks'
. ' WHERE repo_id = @@repo_id@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error('cannot get lock status');
break;
}
$rows = rg_sql_num_rows($res);
if ($rows == 0) {
$ret['status'] = 0;
} else {
$ret = rg_sql_fetch_array($res);
$ret['status'] = 1;
}
rg_sql_free_result($res);
$v = $ret;
unset($v['ok']);
rg_cache_set($key, $v, RG_SOCKET_NO_WAIT);
$ret['ok'] = 1;
break;
}
rg_log_exit();
rg_prof_end('repo_lock_status');
return $ret;
}
/*
* Locks/unlocks a repo
* @lock: 1 = lock, 0 = unlock
*/
function rg_repo_lock($db, $repo_id, $uid, $lock, $reason)
{
rg_prof_start('repo_lock');
rg_log_enter('repo_lock');
$ret = FALSE;
while (1) {
$si = rg_repo_lock_status($db, $repo_id);
if ($si['ok'] != 1)
break;
// If we already are in the same state as the passed one
if ($si['status'] == $lock) {
$ret = TRUE;
break;
}
$params = array(
'itime' => time(),
'repo_id' => $repo_id,
'uid' => $uid,
'reason' => $reason);
if ($lock == 1)
$sql = 'INSERT INTO repo_locks (repo_id, uid, itime)'
. ' VALUES (@@repo_id@@, @@uid@@, @@itime@@)';
else
$sql = 'DELETE FROM repo_locks'
. ' WHERE repo_id = @@repo_id@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error('db lock/unlock error');
break;
}
rg_sql_free_result($res);
$key = 'locks' . '::' . 'repos' . '::' . $repo_id;
if ($lock == 1) {
$params['status'] = 1;
rg_cache_set($key, $params, RG_SOCKET_NO_WAIT);
} else {
rg_cache_unset($key, RG_SOCKET_NO_WAIT);
}
$h = array('ri' => array(), 'ui_login' => array());
$h['ri']['repo_id'] = $repo_id;
$h['ui_login']['uid'] = $uid;
$h['history_category'] = $lock == 1 ? REPO_CAT_LOCK : REPO_CAT_UNLOCK;
$h['history_message'] = 'Repository ' .
($lock == 1 ? 'locked' : 'unlocked') . ': ' . $reason;
rg_repo_history_insert($db, $h);
$ret = TRUE;
break;
}
rg_log_exit();
rg_prof_end('repo_lock');
return $ret;
}
/*
* Loads data for graphs
* @unit - interval on which a sum is made
*/
function rg_repo_data($db, $type, $start, $end, $unit, $mode)
{
$params = array('start' => $start, 'end' => $end);
switch ($type) {
case 'create_repo':
$q = 'SELECT itime FROM repos'
. ' WHERE itime >= @@start@@ AND itime <= @@end@@';
break;
case 'create_bug':
$q = 'SELECT itime FROM bugs'
. ' WHERE itime >= @@start@@ AND itime <= @@end@@';
break;
case 'history':
$q = 'SELECT itime FROM repo_history'
. ' WHERE itime >= @@start@@ AND itime <= @@end@@';
break;
default:
rg_internal_error('invalid type');
return FALSE;
}
$ret = rg_graph_query($db, $start, $end, $unit, $mode, $q, $params, '');
if ($ret === FALSE)
rg_repo_set_error(rg_graph_error());
return $ret;
}
/*
* High level function for Repo -> Admin -> Rights -> Repo/Refs/Path rights menu.
*/
function rg_repo_admin_rights($db, $rg, $type)
{
rg_log('rg_repo_admin_rights type=' . $type);
$obj_id = $rg['ri']['repo_id'];
if (rg_rights_allow2($db, 'repo', $obj_id, 'G', '') !== TRUE)
return rg_template('rights/deny.html', $rg, TRUE /*xss*/);
$owner = $rg['ri']['uid'];
return rg_rights_high_level($db, $rg, 'repo', $type, $owner, $obj_id);
}
/*
* High level function for repo deletion
*/
function rg_repo_admin_delete($db, $rg)
{
$ret = "";
$obj_id = $rg['ri']['repo_id'];
if (rg_rights_allow2($db, 'repo', $obj_id, 'D', '') !== TRUE)
return rg_template("user/repo/delete/deny.html", $rg, TRUE /* xss */);
$are_you_sure = rg_var_uint("are_you_sure");
$errmsg = array();
$show_form = 1;
while (1) {
if ($rg['doit'] != 1)
break;
if ($are_you_sure == 0) {
$ret .= rg_template("user/repo/delete/no.html", $rg, TRUE /* xss */);
$show_form = 0;
break;
}
if (!rg_valid_referer()) {
$errmsg[] = "invalid referer; try again";
break;
}
if (!rg_token_valid($db, $rg, 'repo_admin_delete', FALSE)) {
$errmsg[] = "invalid token; try again";
break;
}
rg_log_ml("CHECK: rg: " . print_r($rg, TRUE));
$ui_login = rg_ui_login();
$r = rg_repo_delete($db, $rg['ri']['repo_id'], $ui_login);
if ($r === FALSE) {
$errmsg[] = rg_repo_error();
break;
}
$ret .= rg_template("user/repo/delete/done.html", $rg, TRUE /* xss */);
$show_form = 0;
// TODO: shouldn't we invalidate the cache?
break;
}
if ($show_form == 1) {
$rg['HTML:errmsg'] = rg_template_errmsg($errmsg);
$rg['rg_form_token'] = rg_token_get($db, $rg, 'repo_admin_delete');
$ret .= rg_template("user/repo/delete/sure.html", $rg, TRUE /* xss */);
}
return $ret;
}
/*
* Easy obtain repo name
*/
function rg_repo_nice($db, $repo_id)
{
$ri = rg_repo_info($db, $repo_id, 0, '');
if ($ri['exists'] == 1)
return $ri['name'];
return 'n/a';
}
/*
* High level function creating/editing a repo
*/
function rg_repo_edit_high_level($db, &$rg)
{
rg_log_enter('repo_edit_high_level');
$ret = "";
$errmsg = array();
$load_form = TRUE;
while (1) {
if ($rg['ri']['repo_id'] > 0)
$edit = TRUE;
else
$edit = FALSE;
$ui_login = rg_ui_login();
// User is not logged in?
if (!$edit && ($ui_login['uid'] == 0)) {
$ret .= rg_template('user/repo/deny_create.html', $rg, TRUE /*xss*/);
$load_form = FALSE;
break;
}
if ($edit) {
$obj_id = $rg['ri']['repo_id'];
if (rg_rights_allow2($db, 'repo', $obj_id, 'E', '') !== TRUE) {
$ret .= rg_template('user/repo/deny_edit.html',
$rg, TRUE /*xss*/);
$load_form = FALSE;
break;
}
}
if ($rg['doit'] != 1) {
if (!$edit) {
// Defaults
$rg['ri'] = array();
$rg['ri']['exists'] = 0;
$rg['ri']['repo_id'] = '0';
$rg['ri']['master'] = '0';
$rg['ri']['name'] = '';
$rg['ri']['max_commit_size'] = '0';
$rg['ri']['description'] = '';
$rg['ri']['public'] = '1';
$rg['ri']['license'] = '';
$rg['ri']['template'] =
'OS type(s) and version(s)?' . "\n\n"
. 'Application version(s) affected?' . "\n\n"
. 'Steps to reproduce?';
$rg['ri']['main_branch'] = '';
$rg['ri']['hash'] = 'sha1';
}
break;
}
$rg['ri']['repo_id'] = rg_var_uint('repo_id');
$rg['ri']['master'] = rg_var_uint('master');
$rg['ri']['name'] = trim(rg_var_str_nocr('name')); // TODO: filter name!
$rg['ri']['max_commit_size'] = rg_var_uint('max_commit_size');
$rg['ri']['description'] = trim(rg_var_str('description'));
$rg['ri']['public'] = rg_var_bool('public');
$rg['ri']['license'] = trim(rg_var_str('license'));
$rg['ri']['template'] = trim(rg_var_str('template'));
$rg['ri']['main_branch'] = trim(rg_var_str_nocr('main_branch'));
$rg['ri']['hash'] = trim(rg_var_str_nocr('hash'));
rg_repo_cosmetic($db, $rg['ri']);
//rg_log_ml("CHECK: after repo edit: rg[ri]=" . print_r($rg['ri'], TRUE));
if (!rg_valid_referer()) {
$errmsg[] = "invalid referer; try again";
break;
}
if (!rg_token_valid($db, $rg, 'repo_edit_hl', FALSE)) {
// TODO: replace all of these with a template
$errmsg[] = "invalid token; try again.";
break;
}
$r = rg_repo_edit($db, $ui_login, $rg['ri']);
if ($r === FALSE) {
$errmsg[] = rg_repo_error();
break;
}
$rg['ri']['home'] = rg_re_repopage($ui_login, $rg['ri']['name']);
if ($edit) {
$ret .= rg_template("repo/edit_ok.html", $rg, TRUE /*xss*/);
} else {
$ret .= rg_template("repo/create_ok.html", $rg, TRUE /*xss*/);
}
$load_form = FALSE;
break;
}
if ($load_form) {
if ($rg['ri']['master'] > 0) {
$rg['ri']['master_name'] = $rg['ri']['master'];
$_mi = rg_repo_info($db, $rg['ri']['master'], "");
if ($_mi['exists'] == 1)
$rg['ri']['master_name'] = $_mi['name'];
} else {
$rg['ri']['master_name'] = "";
}
$rg['HTML:errmsg'] = rg_template_errmsg($errmsg);
$rg['rg_form_token'] = rg_token_get($db, $rg, 'repo_edit_hl');
$hints = array();
$hints[]['HTML:hint'] = rg_template("hints/repo/create_repo.html", $rg, TRUE /* xss */);
$rg['HTML:repo_edit_hints'] = rg_template_table("hints/list", $hints, $rg);
$ret .= rg_template("repo/add_edit.html", $rg, TRUE /* xss */);
}
rg_log_exit();
return $ret;
}
/*
* High level function for locking a repo
*/
function rg_repo_lock_high_level($db, &$rg)
{
rg_log_enter('repo_lock_high_level');
$ret = "";
$errmsg = array();
$show_form = TRUE;
while (1) {
$obj_id = $rg['ri']['repo_id'];
if (rg_rights_allow2($db, 'repo', $obj_id, 'K', '') !== TRUE) {
$ret .= rg_template("user/repo/deny_lock.html",
$rg, TRUE /*xss*/);
$show_form = FALSE;
break;
}
$ls = rg_repo_lock_status($db, $rg['ri']['repo_id']);
if ($ls['ok'] != 1)
break;
$rg['ri']['next_status'] = 1 - $ls['status'];
$rg['ri']['reason'] = rg_var_str('ri::reason');
$lock = rg_var_int('ri::lock');
rg_log('ZZZ: status=' . $ls['status']);
rg_log('ZZZ: lock=' . $lock);
if ($rg['doit'] != 1)
break;
if (!rg_valid_referer()) {
$errmsg[] = "invalid referer; try again";
break;
}
if (!rg_token_valid($db, $rg, 'repo_lock_hl', FALSE)) {
// TODO: replace all of these with a template
$errmsg[] = "invalid token; try again.";
break;
}
$ui_login = rg_ui_login();
$r = rg_repo_lock($db, $rg['ri']['repo_id'],
$ui_login['uid'], $lock, $rg['ri']['reason']);
if ($r === FALSE) {
$errmsg[] = rg_repo_error();
break;
}
if ($lock == 1)
$t = 'repo/lock_ok.html';
else
$t = 'repo/unlock_ok.html';
$ret .= rg_template($t, $rg, TRUE /*xss*/);
// clean form
$rg['ri']['reason'] = '';
$rg['ri']['next_status'] = 1 - $lock;
break;
}
if ($show_form) {
$rg['HTML:errmsg'] = rg_template_errmsg($errmsg);
$hints = array();
$hints[]['HTML:hint'] = rg_template("hints/repo/lock_repo.html",
$rg, TRUE /*xss*/);
$rg['HTML:repo_lock_hints'] = rg_template_table("hints/list",
$hints, $rg);
$rg['rg_form_token'] = rg_token_get($db, $rg, 'repo_lock_hl');
$ret .= rg_template("repo/lock.html", $rg, TRUE /*xss*/);
}
rg_log_exit();
return $ret;
}
/*
* High level function for 'Repo -> Admin' menu
*/
function rg_repo_admin($db, &$rg, $paras)
{
rg_log("rg_repo_admin paras=" . rg_array2string($paras));
$ret = "";
$_op = empty($paras) ? "edit" : array_shift($paras);
$rg['allow_edit_repo'] = 0;
$rg['allow_lock_repo'] = 0;
$rg['allow_grant_rights'] = 0;
$rg['allow_delete_repo'] = 0;
$obj_id = $rg['ri']['repo_id'];
if (rg_rights_allow2($db, 'repo', $obj_id, 'E', '') === TRUE)
$rg['allow_edit_repo'] = 1;
if (rg_rights_allow2($db, 'repo', $obj_id, 'K', '') === TRUE)
$rg['allow_lock_repo'] = 1;
if (rg_rights_allow2($db, 'repo', $obj_id, 'G', '') === TRUE)
$rg['allow_grant_rights'] = 1;
if (rg_rights_allow2($db, 'repo', $obj_id, 'D', '') === TRUE)
$rg['allow_delete_repo'] = 1;
$rg['menu']['repo'][$_op] = 1;
$rg['HTML:menu_repo_level2'] = rg_template("user/repo/menu.html", $rg, TRUE /*xss*/);
switch ($_op) {
case 'repo_rights':
$ret .= rg_repo_admin_rights($db, $rg, "repo");
break;
case 'refs_rights':
$ret .= rg_repo_admin_rights($db, $rg, "repo_refs");
break;
case 'path_rights':
$ret .= rg_repo_admin_rights($db, $rg, "repo_path");
break;
case 'delete':
$ret .= rg_repo_admin_delete($db, $rg);
break;
case 'lock':
$rg['form_url'] = $rg['ri']['url_repo'] . '/admin/lock';
$ret .= rg_repo_lock_high_level($db, $rg);
break;
default:
$rg['form_url'] = $rg['ri']['url_repo'] . '/admin';
$ret .= rg_repo_edit_high_level($db, $rg);
break;
}
return $ret;
}
/*
* HL function for repo search
*/
function rg_repo_search_high_level($db, $rg, $ui, $url)
{
$q = trim(rg_var_str('q'));
$ret = '';
$errmsg = array();
$rg['q'] = $q;
$rg['HTML:errmsg'] = rg_template_errmsg($errmsg);
$rg['search_url'] = $url;
$ret .= rg_template("repo/search.html", $rg, TRUE /*xss*/);
while ($rg['doit'] == 1) {
$_t = rg_repo_search($db, $ui, $q);
if ($_t === FALSE) {
$errmsg[] = rg_repo_error();
break;
}
$ret .= $_t;
break;
}
return $ret;
}
/*
* Discover top menu helper
* @ui - acts like ui_login
*/
function rg_repo_discover($db, $op, $rg, $ui)
{
rg_log("repo_discover: op=$op");
$ret = '';
if (empty($op))
$op = 'list';
switch ($op) {
case 'search':
$x = rg_repo_search_high_level($db, $rg, $ui, '/op/discover');
break;
default:
$x = rg_repo_list($db, $rg, '', $ui['uid'], 100);
if ($x === FALSE)
$x = rg_template('internal_err.html', $rg, TRUE /*xss*/);
break;
}
$rg['discover_menu_' . $op] = 1;
$ret .= rg_template('repo/discover.html', $rg, TRUE /*xss*/);
$ret .= '<div class="generic_body">' . "\n";
$ret .= $x;
$ret .= '</div>' . "\n";
return $ret;
}
/*
* Show some stats about a repo
*/
function rg_repo_stats($rg)
{
rg_log_enter('repo_start');
while (1) {
if ($rg['ri']['git_dir_done'] == 0) {
$ret = rg_template('repo/err/no_git_dir.html', $rg, TRUE /*xss*/);
break;
}
$path = rg_repo_path_by_id($rg['ri']['uid'], $rg['ri']['repo_id']);
$r = rg_git_repo_is_empty($path);
if ($r == -1) {
$ret = rg_template('internal_err.html', $rg, TRUE /*xss*/);
break;
}
if ($r == 1) {
$ret = rg_template('repo/err/not_init.html', $rg, TRUE /*xss*/);
break;
}
$log = rg_git_log($path, 0 /*max*/, '', '', FALSE /*also_patch*/,
0 /*patch_limit*/);
if ($log === FALSE) {
$ret = rg_template('internal_err.html', $rg, TRUE /*xss*/);
break;
}
$s = rg_git_stats($log);
//rg_log_debug('stats: ' . print_r($s, TRUE));
$rg['ri']['stats'] = $s['global'];
$rg['ri']['HTML:authors'] = rg_template_table('repo/stats/authors',
$s['authors'], $rg);
$ret = rg_template('repo/stats/stats.html', $rg, TRUE /*xss*/);
break;
}
rg_log_exit();
return $ret;
}
/*************************** commit_labels stuff */
/*
* Returns a list of labels attached to a commit
*/
function rg_repo_commit_labels($db, $repo_id, $head)
{
rg_prof_start('repo_commit_labels');
rg_log_enter('repo_commit_labels: repo_id='
. $repo_id . ' head=' . $head);
$ret = array();
$ret['ok'] = 0;
while(1) {
$key = 'repo_commit_labels' . '::' . $repo_id . '::' . $head;
$c = rg_cache_get($key);
if ($c !== FALSE) {
$ret = $c;
//rg_repo_cosmetic($db, $ret);
break;
}
$params = array(
'repo_id' => $repo_id,
'head' => $head
);
$sql = 'SELECT * FROM commit_labels'
. ' WHERE repo_id = @@repo_id@@'
. ' AND head = @@head@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_repo_set_error('cannot query');
break;
}
$rows = rg_sql_num_rows($res);
$ret['list'] = array();
while (($row = rg_sql_fetch_array($res))) {
// Cosmetic
$row['labels'] = rg_unserialize($row['labels']);
$row['itime_nice'] = gmdate('Y-m-d H:i', $row['itime']);
$ret['list'][] = $row;
}
rg_sql_free_result($res);
$ret['ok'] = 1;
rg_cache_set($key, $ret, RG_SOCKET_NO_WAIT);
break;
}
//rg_log_debug('commit_labels: ' . print_r($ret, TRUE));
rg_log_exit();
rg_prof_end('repo_commit_labels');
return $ret;
}
/*
* Show commit labels as nice html
*/
function rg_repo_commit_labels_html($db, $repo_id, $commit)
{
rg_log_enter('repo_commit_labels_html');
$ret = '';
while (1) {
if (empty($commit))
break;
$rg = array();
$r = rg_repo_commit_labels($db, $repo_id, $commit);
//rg_log_debug('r: ' . print_r($r, TRUE));
if ($r['ok'] != 1) {
$ret = rg_template('repo/cl/ierror.html',
$rg, TRUE /*xss*/);
break;
}
if (empty($r['list']))
break;
foreach ($r['list'] as $index => &$i)
$i['labels_nice'] = implode(' ', $i['labels']);
$ret = rg_template_table('repo/cl/list', $r['list'], $rg);
break;
}
rg_log_exit();
return $ret;
}
/*
* API dispatch function
*/
function rg_repo_api($db, $a, $json)
{
rg_prof_start('repo_api');
rg_log_enter('repo_api');
$cmd = $json['cmd'];
$ret = array();
while (1) {
if (isset($json['repo'])) {
$ri = rg_repo_info($db, 0, $a['target_ui']['uid'],
$json['repo']);
} else if (isset($json['repo_id'])) {
$repo_id = intval($json['repo_id']);
$ri = rg_repo_info($db, $repo_id, '', '');
} else {
$ri = FALSE;
}
if (strcmp($cmd, 'repo_list') == 0) {
$params = array('uid' => $a['target_ui']['uid']);
$sql = 'SELECT name FROM repos'
. ' WHERE uid = @@uid@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
$ret['error'] = 'internal error';
break;
}
$ret = array();
while (($row = rg_sql_fetch_array($res))) {
$ret[] = $row['name'];
}
rg_sql_free_result($res);
break;
}
if ($ri === FALSE) {
$ret['error'] = 'no repo_id=/name= specified';
break;
}
if ($ri['exists'] != 1) {
$ret['error'] = 'invalid user or no rights';
break;
}
// From this point below, we have a repo
rg_stats_conns_set('repo_id', $ri['repo_id']);
$obj_id = $ri['repo_id'];
$allow = array();
$allow['A'] = rg_rights_allow2($db, 'repo', $obj_id, 'A', '') === TRUE;
$allow['a'] = rg_rights_allow2($db, 'repo', $obj_id, 'a', '') === TRUE;
if (strcmp($cmd, 'repo_info') == 0) {
if (!$allow['A']) {
$ret['error'] = 'invalid user or no rights';
break;
}
$ret = $ri;
break;
}
if ((strcmp($cmd, 'repo_pr_list') == 0)
|| (strcmp($cmd, 'repo_mr_list') == 0)) {
if (!$allow['A']) {
$ret['error'] = 'invalid user or no rights';
break;
}
$ret['repo_id'] = $ri['repo_id'];
$params = array('repo_id' => $ri['repo_id']);
$sql = 'SELECT * FROM merge_requests'
. ' WHERE repo_id = @@repo_id@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
$ret['error'] = 'internal error';
break;
}
$ret['list'] = array();
while (($row = rg_sql_fetch_array($res))) {
unset($row['repo_id']);
$ret['list'][] = $row;
}
rg_sql_free_result($res);
break;
}
if (strcmp($cmd, 'repo_bug_list') == 0) {
if (!$allow['a']) {
$ret['error'] = 'invalid user or no rights';
break;
}
$ret['repo_id'] = $ri['repo_id'];
$params = array('repo_id' => $ri['repo_id']);
$sql = 'SELECT bug_id FROM bugs'
. ' WHERE repo_id = @@repo_id@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
$ret['error'] = 'internal error';
break;
}
$ret['list'] = array();
while (($row = rg_sql_fetch_array($res))) {
$ret['list'][] = $row['bug_id'];
}
rg_sql_free_result($res);
break;
}
// TODO: for bugs, we do layer violation here!
// Move this into bug.inc.php and register a function?
if (strcmp($cmd, 'repo_bug_info') == 0) {
if (!isset($json['bug_id'])) {
$ret['error'] = 'bug_id= para not specified';
break;
}
$bug_id = intval($json['bug_id']);
if (!$allow['a']) {
$ret['error'] = 'invalid user or no rights';
break;
}
$bi = rg_bug_info($db, $ri['repo_id'], $bug_id);
if ($bi['exists'] != 1) {
$ret['error'] = 'bug does not exists';
break;
}
$ret = array_merge($ret, $bi);
break;
}
$ret['error'] = 'invalid command';
break;
}
rg_log_exit();
rg_prof_end('repo_api');
return $ret;
}
/*
* Helper for SSH/GIT/HTTP(S) fetch/push
* @host - the host name accessed - may be different than the good one
* @cmd: 'git-upload-pack' or 'git-receive-pack'
* @flags: B = bypass rights check (for example, for a global worker)
* TODO: move it to user.inc.php?!
*/
function rg_repo_fetch_push_helper($db, $host, $prefix, $repo_user,
$repo, $cmd, $flags)
{
global $rg_log_sid;
rg_prof_start('repo_fetch_push_helper');
rg_log_enter('repo_fetch_push_helper: host=' . $host
. ' prefix=' . $prefix . ' repo_user=[' . $repo_user . ']'
. ' repo=[' . $repo . '] cmd=[' . $cmd . '] flags=' . $flags);
$ret = array('ok' => 0, 'allow' => 0, 'push_allowed' => 0);
while (1) {
// Extracts command and set needed permissions
if (strncasecmp($cmd, 'git-upload-pack', 15) == 0) {
$cmd = 'git-upload-pack';
$needed_rights = 'F';
$ret['push'] = 0;
} else if (strncasecmp($cmd, 'git-receive-pack', 16) == 0) {
$cmd = 'git-receive-pack';
// We need push or anonymous push
$needed_rights = 'P|H';
$ret['push'] = 1;
} else {
$ret['errmsg'] = 'unknown command';
break;
}
// Validity/security checks
// Load info about the owner
if (rg_user_ok($repo_user) !== TRUE) {
$ret['errmsg'] = 'user is invalid (' . rg_user_error() . ')';
break;
}
$ui_page = rg_user_info($db, 0, $repo_user, '');
if ($ui_page['ok'] != 1) {
$ret['errmsg'] = 'internal problems; try again later, please';
break;
}
if ($ui_page['exists'] != 1) {
$ret['errmsg'] = 'user does not exists';
break;
}
rg_ui_page_set($ui_page);
// TODO: What if the user is deleted?
// Loading info about the repository
if (rg_repo_ok($repo) !== TRUE) {
$ret['errmsg'] = rg_repo_error();
break;
}
$ret['ri'] = rg_repo_info($db, 0, $ui_page['uid'], $repo);
if ($ret['ri']['ok'] != 1) {
$ret['errmsg'] = 'internal problems; try again later, please';
break;
}
if ($ret['ri']['exists'] != 1) {
$ret['errmsg'] = 'repository does not exists';
break;
}
if ($ret['ri']['deleted'] > 0) {
$ret['errmsg'] = 'repository has been deleted';
break;
}
$ui_login = rg_ui_login();
$ls = rg_repo_lock_status($db, $ret['ri']['repo_id']);
if ($ls['ok'] != 1) {
$ret['errmsg'] = 'could not get lock status: ' . rg_repo_error();
break;
}
if (($ls['status'] == 1) && ($ui_login['uid'] != $ls['uid'])) {
$_u = rg_user_info($db, $ls['uid'], '', '');
if ($_u['exists'] == 1)
$_user = $_u['username'];
else
$_user = '?';
$ret['errmsg'] = 'repository has been locked user ' . $_user
. ' at ' . gmdate('Y-m-d H:i', $ls['itime']) . ' UTC;'
. ' reason: ' . $ls['reason'];
break;
}
$repo_path = rg_repo_path_by_id($ui_page['uid'],
$ret['ri']['repo_id']);
$ret['repo_path'] = $repo_path;
// TODO: signal user that the repo moved and provide a hint how to follow
$ret['ok'] = 1;
if (!strstr($flags, 'B')) {
$obj_id = $ret['ri']['repo_id'];
$r = rg_rights_allow2($db, 'repo_refs', $obj_id, $needed_rights, '');
// TODO: 'misc' is not used?
// TODO: what if an error occured? How do we signal this?! $r must have ok key
if ($r !== TRUE) {
$ret['errmsg'] = 'non existing repo or you are not allowed to push';
break;
}
// We need to know if Push (and not anon) is allowed, so we can
// give the user a chance to authenticate.
// TODO: change rg_rights_allow to return what rights are
// allowed and use it.
if ($ret['push'] == 1) {
$r = rg_rights_allow2($db, 'repo_refs', $obj_id, 'P', '');
if ($r === TRUE)
$ret['push_allowed'] = 1;
}
// If we are enrolled, ask for login token
// For push we always ask for it, for fetch only if repo is
// NOT public. And we can ask only if we have a ssh connection.
// For git protocol we cannot because we do not have the
// username/uid of the connecting user.
if (isset($_SERVER['SSH_CONNECTION'])) {
if (($ret['ri']['public'] == 0) || ($ret['push'] == 1)) {
$r = rg_totp_verify_ip($db, $ui_login['uid'],
rg_ip());
if (($r['ok'] !== 1)
|| ($r['enrolled'] && empty($r['ip_list']))) {
$ret['errmsg'] = rg_totp_error();
$ret['ok'] = 0;
break;
}
}
}
}
// TODO: limit per connection
// TODO: limit time and/or cpu
// TODO: limit cpuset
// TODO: limit io
// TODO: put process in a cgroup?
if (($ret['push'] == 1)
&& rg_user_over_limit($db, $ui_page, $max)) {
// TODO: be aware that a push may mean a delete =>
// free space?!
// We should mark the repo over limit when we compute
// the disk space - same problem
$ret['errmsg'] = 'cannot push: user is over limit'
. ' (' . $ui_page['disk_used_mb'] . 'MiB >= '
. $max . 'MiB)';
break;
}
// We are allowed at this point
$ret['allow'] = 1;
// Put in environment all we need
putenv('ROCKETGIT_LOGIN_UID=' . $ui_login['uid']);
putenv('ROCKETGIT_LOGIN_URL=' . rg_re_userpage($ui_login));
putenv('ROCKETGIT_REPO_ID=' . $ret['ri']['repo_id']);
putenv('ROCKETGIT_REPO_PATH=' . $repo_path);
putenv('ROCKETGIT_REPO_NAME=' . $ret['ri']['name']);
putenv('ROCKETGIT_REPO_UID=' . $ret['ri']['uid']);
putenv('ROCKETGIT_REPO_USERNAME=' . $ui_page['username']);
putenv('ROCKETGIT_REPO_CLONE_URL=' . $ret['ri']['clone_url_http']);
putenv('ROCKETGIT_IP=' . rg_ip());
putenv('ROCKETGIT_ITIME=' . microtime(TRUE));
putenv('ROCKETGIT_HOST=' . $host);
putenv('ROCKETGIT_LOG_SID=' . $rg_log_sid);
putenv('ROCKETGIT_DEBUG=' . rg_debug());
if (($ret['push'] == 1) && ($ret['push_allowed'] == 0)) {
rg_log_debug('We need to clone the namespace...');
$namespace = 'rg_' . rg_id(8);
rg_log('namespace is ' . $namespace);
putenv('GIT_NAMESPACE=' . $namespace);
// Prepare refs to avoid the following message:
// 'No refs in common and none specified; doing nothing.
// Perhaps you should specify a branch such as 'master'.'
// TODO: cannot we copy only the pushed branch?!
$dst = $repo_path . '/refs/namespaces/' . $namespace . '/refs';
$r = rg_copy_tree($repo_path . '/refs/heads', $dst . '/heads/', 0700);
if ($r !== TRUE) {
$ret['errmsg'] = 'internal error (cannot copy heads refs)';
break;
}
$r = rg_copy_tree($repo_path . '/refs/tags',
$dst . '/tags/', 0700);
if ($r !== TRUE) {
$ret['errmsg'] =
'internal error (cannot copy tags refs)';
break;
}
}
// TODO: we may want to call here the git-shell cmd and remove
// it from remote.php?
$ret['ok'] = 1;
break;
}
rg_log_exit();
rg_prof_end('repo_fetch_push_helper');
return $ret;
}
// Renders readme files
function rg_repo_show_readme(array $rg, string $ref, string $path)
{
rg_prof_start('repo_show_readme');
rg_log_enter('repo_show_readme: ref=' . $ref);
$ret = '';
do {
$list = rg_git_ls_tree($rg['repo_path'], $ref, $path);
if ($list === FALSE) {
$ret .= rg_template('repo/readme/err.html', $rg, TRUE /*xss*/);
break;
}
rg_log_debug('git_ls_tree: ' . print_r($list, TRUE));
foreach ($list as $i) {
if (!stristr($i['file'], 'readme')
&& !stristr($i['file'], 'read-me')
&& !stristr($i['file'], 'howto'))
continue;
$r = rg_git_content($i['ref']);
rg_log_debug('git_content returned: ' . print_r($r, TRUE));
$rg['readme_file'] = $i['file'];
$r = rg_format_unk_to_html_string($i['file'], $r);
if ($r['ok'] != 1) {
rg_log('error: ' . $r['errmsg']);
$ret .= rg_template('repo/readme/err_file.html', $rg, TRUE /*xss*/);
continue;
}
if (empty($r['doc']))
continue;
$rg['HTML:readme_doc'] = $r['doc'];
$ret .= rg_template('repo/readme/one.html', $rg, TRUE /*xss*/);
}
} while (0);
rg_log_exit();
rg_prof_end('repo_show_readme');
return $ret;
}