<?php require_once($INC . "/util.inc.php"); require_once($INC . "/log.inc.php"); require_once($INC . "/sql.inc.php"); require_once($INC . "/user.inc.php"); require_once($INC . "/git.inc.php"); require_once($INC . "/rights.inc.php"); require_once($INC . "/prof.inc.php"); require_once($INC . "/mail.inc.php"); require_once($INC . "/events.inc.php"); require_once($INC . "/webhooks.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', 'K' => 'Lock repo', 'r' => 'Reopen bugs', 'T' => 'Delete artifacts', 't' => 'Access 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() . 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); } /* * 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['ip'] = ""; $a['can_be_deleted'] = 0; $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; } /* * Returns TRUE if the login user has @rights rights */ function rg_repo_has_rights($db, $rg, $rights) { $x = array(); $x['obj_id'] = $rg['ri']['repo_id']; $x['type'] = 'repo'; $x['owner'] = $rg['ri']['uid']; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = $rights; $x['ip'] = $rg['ip']; $x['misc'] = ''; return rg_rights_allow($db, $x); } /* * 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 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"; $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', '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']['uid'], $e['ri']['repo_id']); $id_path_rel = rg_repo_path_by_id_rel($e['ui']['uid'], $e['ri']['repo_id']); $new_path = rg_repo_path_by_name($e['ui']['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']['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); 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"; // 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)); if (!isset($event['ui']['uid'])) $event['ui']['uid'] = 0; $ret = FALSE; while (1) { $now = time(); $params = array("now" => $now, "repo_id" => $event['ri']['repo_id'], "uid" => $event['ui']['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? // 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 * @max_seconds: limit the search to less than max_seconds in the past */ function rg_repo_history_load($db, $repo_id, $category, $number, $max_seconds) { rg_prof_start("repo_history_load"); rg_log_enter("repo_history_load: repo_id=$repo_id, category=$category" . ", number=$number max_seconds=$max_seconds"); $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 ($max_seconds > 0) $time_sql = " AND itime >= " . ($now - $max_seconds); $sql = "SELECT * 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) { rg_prof_start("repo_delete"); rg_log_enter("repo_delete: uid=" . $ui['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', 'prio' => 50, 'ui' => $ui, '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['uid'] . '::' . $ri['name'], RG_SOCKET_NO_WAIT); rg_cache_unset('user' . '::' . $ui['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 * @login_ui - 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, $login_ui, &$new) { rg_prof_start("repo_edit"); rg_log_enter("repo_edit: login_uid=" . $login_ui['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, $login_ui['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'], $login_ui['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, $login_ui['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'] = $login_ui['uid']; if (!isset($new['main_branch'])) $new['main_branch'] = ''; 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)" . " VALUES (@@uid@@, @@master@@, @@name@@" . ", @@itime@@, @@max_commit_size@@" . ", @@description@@, 0, @@public@@" . ", @@license@@, @@template@@, @@main_branch@@)" . " 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@@" . " 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-" . $login_ui['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, 'prio' => 50, 'notification' => $notification, 'ui' => $login_ui, 'history_category' => $hcat, 'history_message' => $hmess); $event = rg_array_merge($event, 'ri_old', $ri); $new['url'] = rg_base_url() . rg_re_repopage($login_ui, $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::" . $login_ui['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, $login_ui) { 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, $login_ui); } /* * List repos of page user 'ui'. * @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 . " login_uid=" . $rg['login_ui']['uid']); $params = array( "uid" => $uid, "login_uid" => $rg['login_ui']['uid']); if ($uid > 0) $add = ' AND uid = @@uid@@'; else $add = ' AND uid != @@login_uid@@'; // TODO: also admin must be able to see them? if (($rg['login_ui']['uid'] == 0) || ($rg['login_ui']['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, $rg['login_ui']); } /* * Search in all repositories owned by 'login_ui' or public * We need to exclude private repositories. */ function rg_repo_search($db, $login_ui, $q) { rg_prof_start("repo_search"); rg_log("repo_search: q=[$q]"); $admin = 0; if (isset($login_ui['admin']) && ($login_ui['admin'] == 1)) $admin = 1; $params = array("q" => "%" . $q . "%", "uid" => $login_ui['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, $login_ui); 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_ml('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_ml('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' => array()); $h['ri']['repo_id'] = $repo_id; $h['ui']['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 1 AS value, itime FROM repos' . ' WHERE itime >= @@start@@ AND itime <= @@end@@'; break; case 'create_bug': $q = 'SELECT 1 AS value, itime FROM bugs' . ' WHERE itime >= @@start@@ AND itime <= @@end@@'; break; case 'history': $q = 'SELECT 1 AS value, 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"); /* 'repo' is correct here, we test for granting rights on repo */ $x = array(); $x['obj_id'] = $rg['ri']['repo_id']; $x['type'] = 'repo'; $x['owner'] = $rg['ri']['uid']; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = 'G'; $x['ip'] = $rg['ip']; $x['misc'] = ""; if (rg_rights_allow($db, $x) !== TRUE) return rg_template("user/repo/rights/deny.html", $rg, TRUE /* xss */); $ret = ""; $a = array(); $a['type'] = $type; $a['right_id'] = rg_var_uint("right_id"); $a['edit_id'] = rg_var_uint("edit_id"); $a['username'] = trim(rg_var_str("username")); $a['rights'] = rg_rights_a2s(rg_var_str("rights")); $a['misc'] = trim(rg_var_str("misc")); $a['ip'] = trim(rg_var_str("ip")); $a['prio'] = rg_var_uint("prio"); $a['description'] = trim(rg_var_str("description")); //rg_log_ml("CHECK: a(POST)=" . print_r($a, TRUE)); $errmsg = array(); $list_errmsg = array(); $load_defaults = 1; $delete = rg_var_bool("delete"); while ($delete == 1) { if (!rg_valid_referer()) { $errmsg[] = "invalid referer; try again"; break; } if (!rg_token_valid($db, $rg, 'repo_admin_rights', FALSE)) { $errmsg[] = "invalid token; try again"; break; } $list = rg_var_uint("rights_delete_ids"); if (empty($list)) { $list_errmsg[] = "please select at least one item"; break; } $my_list = array(); foreach ($list as $k => $junk) $my_list[] = $k; $r = rg_rights_delete_list($db, $type, $rg['ri']['repo_id'], $my_list); if ($r !== TRUE) { $list_errmsg[] = "cannot delete rights: " . rg_rights_error(); break; } $ret .= rg_template("user/repo/rights/delete_ok.html", $rg, TRUE /* xss */); break; } // edit while ($a['edit_id'] > 0) { $owner = $rg['ri']['uid']; $r = rg_rights_get($db, $rg['ri']['repo_id'], $type, $owner, -1, $a['edit_id']); if ($r['ok'] != 1) { $list_errmsg[] = "cannot load rights: " . rg_rights_error(); break; } if (empty($r['list'])) { $list_errmsg[] = "right not found"; break; } // Only one right is returned when we edit by right_id $a = $r['list'][0]; $load_defaults = 0; break; } $grant = rg_var_bool("grant"); while ($grant == 1) { $load_defaults = 0; if (!rg_valid_referer()) { $errmsg[] = "invalid referer; try again"; break; } if (!rg_token_valid($db, $rg, 'repo_admin_rights', FALSE)) { $errmsg[] = "invalid token; try again"; break; } if (strcmp($a['username'], '*') == 0) { $a['uid'] = 0; } else { $_ui = rg_user_info($db, 0, $a['username'], ""); if ($_ui['exists'] == 1) { $a['uid'] = $_ui['uid']; } else { $errmsg[] = rg_user_error(); break; } } $a['obj_id'] = $rg['ri']['repo_id']; $a['who'] = $rg['login_ui']['uid']; $r = rg_rights_set($db, $type, $a); if ($r !== TRUE) { $errmsg[] = rg_rights_error(); break; } $ret .= rg_template("user/repo/rights/grant_ok.html", $rg, TRUE /* xss */); $load_defaults = 1; break; } if ($load_defaults == 1) { $rg['right_id'] = 0; $rg['username'] = ""; $rg['rights'] = rg_rights_default($type); $rg['description'] = ""; $rg['misc'] = ""; $rg['ip'] = ""; $rg['prio'] = 100; } else { $rg = rg_array_merge($rg, '', $a); } $rg['rg_form_token'] = rg_token_get($db, $rg, 'repo_admin_rights'); $rg['HTML:errmsg'] = rg_template_errmsg($errmsg); $rg['HTML:list_errmsg'] = rg_template_errmsg($list_errmsg); $rg['HTML:rights_checkboxes'] = rg_rights_checkboxes($type, "rights", $rg['rights']); // list rights $r = rg_rights_get($db, $rg['ri']['repo_id'], $type, $rg['ri']['uid'], -1, 0); if ($r['ok'] != 1) $ret .= rg_warning("Cannot load rights. Try later."); else $ret .= rg_template_table("user/repo/rights/list_" . $type, $r['list'], $rg); $ret .= rg_template("user/repo/rights/form_" . $type . ".html", $rg, TRUE /* xss */); // hints $hints = array(); $hints[]['HTML:hint'] = rg_template("hints/repo/edit_rights.html", $rg, TRUE /* xss */); $hints[]['HTML:hint'] = rg_template("hints/repo/edit_" . $type . "_rights.html", $rg, TRUE /* xss */); $ret .= rg_template_table("hints/list", $hints, $rg); return $ret; } /* * High level function for repo deletion */ function rg_repo_admin_delete($db, $rg) { $ret = ""; $x = array(); $x['obj_id'] = $rg['ri']['repo_id']; $x['type'] = 'repo'; $x['owner'] = $rg['ri']['uid']; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = 'D'; $x['ip'] = $rg['ip']; $x['misc'] = ""; if (rg_rights_allow($db, $x) !== 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)); $r = rg_repo_delete($db, $rg['ri']['repo_id'], $rg['login_ui']); 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("rg_repo_edit_high_level"); $ret = ""; $errmsg = array(); $load_form = TRUE; while (1) { if ($rg['ri']['repo_id'] > 0) $edit = TRUE; else $edit = FALSE; // User is not logged in? if (!$edit && ($rg['login_ui']['uid'] == 0)) { $ret .= rg_template("user/repo/deny_create.html", $rg, TRUE /* xss */); $load_form = FALSE; break; } if ($edit) { $x = array(); $x['obj_id'] = $rg['ri']['repo_id']; $x['type'] = 'repo'; $x['owner'] = $rg['ri']['uid']; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = 'E'; // E = create/edit $x['ip'] = $rg['ip']; $x['misc'] = ""; if (rg_rights_allow($db, $x) !== 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'] = ''; } 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_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, $rg['login_ui'], $rg['ri']); if ($r === FALSE) { $errmsg[] = rg_repo_error(); break; } $rg['ri']['home'] = rg_re_repopage($rg['login_ui'], $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("rg_repo_lock_high_level"); $ret = ""; $errmsg = array(); $show_form = TRUE; while (1) { $x = array(); $x['obj_id'] = $rg['ri']['repo_id']; $x['type'] = 'repo'; $x['owner'] = $rg['ri']['uid']; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = 'K'; $x['ip'] = $rg['ip']; $x['misc'] = ''; if (rg_rights_allow($db, $x) !== 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; } $r = rg_repo_lock($db, $rg['ri']['repo_id'], $rg['login_ui']['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; $x = array(); $x['obj_id'] = $rg['ri']['repo_id']; $x['type'] = 'repo'; $x['owner'] = $rg['ri']['uid']; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['ip'] = $rg['ip']; $x['misc'] = ''; $x['needed_rights'] = 'E'; if (rg_rights_allow($db, $x) === TRUE) $rg['allow_edit_repo'] = 1; $x['needed_rights'] = 'K'; if (rg_rights_allow($db, $x) === TRUE) $rg['allow_lock_repo'] = 1; $x['needed_rights'] = 'G'; if (rg_rights_allow($db, $x) === TRUE) $rg['allow_grant_rights'] = 1; $x['needed_rights'] = 'D'; if (rg_rights_allow($db, $x) === 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 login_ui */ 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_ml('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_ml('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_ml('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']); $x = array(); $x['obj_id'] = $ri['repo_id']; $x['type'] = 'repo'; $x['owner'] = $ri['uid']; $x['uid'] = $a['login_ui']['uid']; $x['username'] = $a['login_ui']['username']; $x['needed_rights'] = 'A'; // A = Access repo; E = create/edit $x['ip'] = $a['ip']; $x['misc'] = ''; if (strcmp($cmd, 'repo_info') == 0) { if (rg_rights_allow($db, $x) !== TRUE) { $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 (rg_rights_allow($db, $x) !== TRUE) { $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) { $y = $x; $y['needed_rights'] = 'a'; // a = Access bug repo if (rg_rights_allow($db, $y) !== TRUE) { $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']); $y = $x; $y['needed_rights'] = 'a'; // a = Access bug repo if (rg_rights_allow($db, $y) !== TRUE) { $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 * @ip - from where the connections come * @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, $ip, $login_ui, $prefix, $user, $repo, $cmd, $flags) { global $rg_log_sid; rg_prof_start('repo_fetch_push_helper'); rg_log_enter('repo_fetch_push_helper: host=' . $host . ' ip=' . $ip . ' prefix=' . $prefix . ' user=[' . $user . ']' . ' repo=[' . $repo . '] cmd=[' . $cmd . '] flags=' . $flags); $ret = array('ok' => 0, 'allow' => 0, 'push_allowed' => 0); while (1) { // Extracts command and computes 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($user) !== TRUE) { $ret['errmsg'] = 'user is invalid (' . rg_user_error() . ')'; break; } $ret['owner_ui'] = rg_user_info($db, 0, $user, ''); if ($ret['owner_ui']['ok'] != 1) { $ret['errmsg'] = 'internal problems; try again later, please'; break; } if ($ret['owner_ui']['exists'] != 1) { $ret['errmsg'] = 'user does not exists'; break; } // 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, $ret['owner_ui']['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; } $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) && ($login_ui['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($ret['owner_ui']['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')) { $x = array(); $x['obj_id'] = $ret['ri']['repo_id']; $x['type'] = 'repo_refs'; $x['owner'] = $ret['ri']['uid']; $x['uid'] = $login_ui['uid']; $x['username'] = $login_ui['uid'] == 0 ? '' : $login_ui['username']; $x['needed_rights'] = $needed_rights; $x['ip'] = $ip; $x['misc'] = ''; $r = rg_rights_allow($db, $x); // 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) { $x['needed_rights'] = 'P'; $r = rg_rights_allow($db, $x); 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, $login_ui['uid'], $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, $ret['owner_ui'], $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' . ' (' . $ret['owner_ui']['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=' . $login_ui['uid']); putenv('ROCKETGIT_LOGIN_URL=' . rg_re_userpage($login_ui)); 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_CLONE_URL=' . $ret['ri']['clone_url_http']); putenv('ROCKETGIT_IP=' . $ip); putenv('ROCKETGIT_ITIME=' . microtime(TRUE)); putenv('ROCKETGIT_HOST=' . $host); putenv('ROCKETGIT_LOG_SID=' . $rg_log_sid); 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; }