<?php $INC = isset($INC) ? $INC : dirname(__FILE__); require_once($INC . "/util.inc.php"); require_once($INC . "/log.inc.php"); require_once($INC . "/sql.inc.php"); require_once($INC . "/sess.inc.php"); require_once($INC . "/rights.inc.php"); require_once($INC . "/events.inc.php"); require_once($INC . "/cache.inc.php"); require_once($INC . "/mail.inc.php"); require_once($INC . "/plan.inc.php"); require_once($INC . "/totp.inc.php"); require_once($INC . '/stats.inc.php'); $rg_user_rights = array( "C" => "Create repository", "A" => "Create user", "E" => "Edit user", "R" => "Remove user", "S" => "Suspend user", "G" => "Grant rights", "M" => "Give admin rights" ); /* We do not have defaults rights and no compare function for misc */ rg_rights_register("user", $rg_user_rights, "", FALSE, FALSE); $rg_user_error = ""; function rg_user_set_error($str) { global $rg_user_error; $rg_user_error = $str; rg_log($str); } function rg_user_error() { global $rg_user_error; return $rg_user_error; } /* * Event functions */ $rg_user_functions = array( 2000 => "rg_user_event_new", 'user_event_new' => 'rg_user_event_new', 2001 => "rg_user_event_login", 'user_event_login' => "rg_user_event_login", 2002 => "rg_user_event_notify_user", 2005 => "rg_user_event_rename", 2006 => "rg_user_link_by_name", 'ask-email-confirmation' => 'rg_user_event_ask_email_confirmation', // new new style 'user_event_new' => 'rg_user_event_new', 'user_event_login' => 'rg_user_event_login', 'user_event_notify_user' => 'rg_user_event_notify_user', 'user_event_rename' => 'rg_user_event_rename', 'user_link_by_name' => 'rg_user_link_by_name' ); rg_event_register_functions($rg_user_functions); /* * Ask for e-mail confirmation */ function rg_user_event_ask_email_confirmation($db, $ev) { rg_log('rg_user_event_ask_email_confirmation: ev=' . rg_array2string($ev)); if (!isset($ev['ui']['email'])) { $ev['ui'] = rg_user_info($db, $ev['ui']['uid'], '', ''); if ($ev['ui']['exists'] != 1) return FALSE; } $ev['ui']['ignore_confirmed'] = 1; rg_mail_template('mail/user/econf', $ev); return array(); } /* * Event for adding a new user */ function rg_user_event_new($db, $event) { $ret = array(); //rg_log_ml('DEBUG: user_event_new: event: ' . print_r($event, TRUE)); $event['op'] = 'new'; // create link by name $ret[] = array_merge($event, array('category' => 'user_link_by_name', 'prio' => 500)); // notify user $ret[] = array_merge($event, array('category' => 'user_event_notify_user', 'prio' => 200)); return $ret; } /* * Event for login */ function rg_user_event_login($db, $event) { $ret = FALSE; while (1) { $r = rg_user_set_last_seen($db, $event['ui']['uid'], $event['itime'], $event['ip']); if ($r !== TRUE) break; $ret = array(); break; } return $ret; } /* * Notify user: welcome */ function rg_user_event_notify_user($db, $event) { rg_log("user_event_notify_user: event=" . rg_array2string($event)); if (strcmp($event['op'], "rename") == 0) { $r = rg_mail_template("mail/user/rename", $event); } else { $r = rg_mail_template("mail/user/welcome", $event); } // TODO: we may want to return here an error? return array(); } /* * Event for renaming an user */ function rg_user_event_rename($db, $event) { $ret = array(); $event['op'] = "rename"; // notify user $ret[] = array_merge($event, array('category' => 'user_event_notify_user', 'prio' => 100)); // TODO: notify watchers return $ret; } /* * Empty user data */ function rg_user_empty() { return array( 'ok' => 0, 'uid' => 0, 'is_admin' => 0, 'rights' => '', 'homepage' => '', 'username' => '', 'exists' => 0, 'organization' => 0, 'confirmed' => 0 ); } /* * Cosmetic function for 'user' info */ function rg_user_cosmetic(&$row) { if (isset($row['itime'])) $row['itime_nice'] = gmdate('Y-m-d', $row['itime']); $row['email_md5'] = md5(strtolower(trim($row['email']))); $row['homepage'] = rg_re_userpage($row); $row['HTML:gravatar'] = '<img class="gravatar" src="https://www.gravatar.com/avatar/' . $row['email_md5'] . '?s=64&r=g" alt="avatar" width="64" height="64" />'; } /* * Event for creating a link by name to the uid */ function rg_user_link_by_name($db, $event) { rg_log("user_link_by_name: event=" . rg_array2string($event)); $by_id = rg_user_path_by_id($event['ui']['uid']); if (!is_dir($by_id) && (mkdir($by_id, 0700, TRUE) === FALSE)) { rg_user_set_error("cannot mkdir by_id=$by_id (" . rg_php_err() . ")"); return FALSE; } $by_name = rg_user_path_by_name($event['ui']['username']); $by_name_parent = dirname($by_name); if (!is_dir($by_name_parent) && (mkdir($by_name_parent, 0700, TRUE) === FALSE)) { rg_user_set_error("cannot mkdir by_name_parent=$by_name_parent (" . rg_php_err() . ")"); return FALSE; } $by_id_rel = rg_user_path_by_id_rel($event['ui']['uid']); if (is_link($by_name)) unlink($by_name); if (symlink($by_id_rel, $by_name) === FALSE) { rg_user_set_error("cannot symlink $by_id_rel <- $by_name (" . rg_php_err() . ")!"); return FALSE; } return array(); } /* * Returns the relative path to user home based on uid */ function rg_user_path_by_id_rel($uid) { $xuid = sprintf("%08X", $uid); $uid_path = array(); for ($i = 0; $i < 8; $i += 2) $uid_path[] = $xuid[$i] . $xuid[$i + 1]; $uid_path = implode("/", $uid_path); return "../../../../by_id/" . $uid_path . "/" . $xuid; } /* * Returns the path to user home based on uid */ function rg_user_path_by_id($uid) { global $rg_repos; $xuid = sprintf("%08X", $uid); $uid_path = array(); for ($i = 0; $i < 8; $i += 2) $uid_path[] = $xuid[$i] . $xuid[$i + 1]; $uid_path = implode("/", $uid_path); return $rg_repos . "/by_id/" . $uid_path . "/" . $xuid; } /* * Returns the path to user home based on name */ function rg_user_path_by_name($name) { global $rg_repos; $x = $name . "__"; return $rg_repos . "/by_name/" . $x[0] . "/" . $x[1] . "/" . $x[2] . "/" . $name; } /* * Returns the URL to a user home based on $ui */ function rg_user_url($ui) { if ($ui['organization'] == 0) $prefix = "/user"; else $prefix = ""; return $prefix . "/" . $ui['username']; } /* * Computes password hash */ function rg_user_pass($salt, $pass) { return sha512($salt . "===" . $pass); } /* * Validates a password */ function rg_user_pass_ok($pass) { if (mb_strlen($pass) < 5) { rg_user_set_error("password is too short (less than 5 chars)"); return FALSE; } return TRUE; } /* * Returns true if the user is ok */ function rg_user_ok($user) { global $rg_user_allow; global $rg_user_min_len; global $rg_user_max_len; if (strcmp(substr($user, 0, 1), '-') == 0) { rg_user_set_error('invalid user name' . ' (cannot start with minus)'); return FALSE; } if (strpos($user, ':') !== FALSE) { rg_user_set_error('invalid user name' . ' (cannot contain :)'); return FALSE; } if (rg_chars_allow($user, $rg_user_allow, $invalid) !== 1) { rg_user_set_error("invalid user name (invalid chars: '$invalid')"); return FALSE; } $len = mb_strlen($user); if ($len < $rg_user_min_len) { rg_user_set_error("user name too short (shorter than $rg_user_min_len)"); return FALSE; } if ($len > $rg_user_max_len) { rg_user_set_error("user name too long (longer than $rg_user_max_len)"); return FALSE; } return TRUE; } /* * Lookup in db the old names, so we can redirect the user to the new name */ function rg_user_lookup_by_old_name($db, $old_name) { rg_prof_start("user_lookup_by_old_name"); rg_log_enter("user_lookup_by_old_name: old_name=$old_name"); $ret = FALSE; while (1) { $x = rg_cache_get("old_name::" . $old_name); if ($x !== FALSE) { $ret = $x; break; } $params = array("old_name" => $old_name); $sql = "SELECT uid FROM users_renames" . " WHERE old_name = @@old_name@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_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['uid']; rg_cache_set("old_name::" . $old_name, $ret, RG_SOCKET_NO_WAIT); break; } rg_log_exit(); rg_prof_end("user_lookup_by_old_name"); return $ret; } /* * Add a rename to the database */ function rg_user_insert_rename($db, $uid, $old_name) { rg_prof_start("user_insert_rename"); rg_log_enter("user_insert_rename: uid=$uid old_name=$old_name"); $ret = FALSE; while(1) { // Check if already exists in the database $r = rg_user_lookup_by_old_name($db, $old_name); if ($r === FALSE) break; $params = array("uid" => $uid, "old_name" => $old_name, "now" => time()); if ($r > 0) { $sql = "UPDATE users_renames" . " SET uid = @@uid@@" . " WHERE old_name = @@old_name@@"; } else { $sql = "INSERT INTO users_renames (uid, old_name" . ", itime)" . " VALUES (@@uid@@, @@old_name@@, @@now@@)"; } $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot link with old_name in db" . " (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); rg_cache_set("old_name::" . $old_name, $uid, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("user_insert_rename"); return $ret; } /* * Rename a user * We keep the old name around, so the clients do not break. */ function rg_user_rename($db, $ui, $new_name) { rg_prof_start("user_rename"); rg_log_enter("user_rename: from=[" . $ui['username'] . "]" . " to=[" . $new_name . "]..."); $user_path = rg_user_path_by_id($ui['uid']); $old_path = rg_user_path_by_id_rel($ui['uid']); $new_path = rg_user_path_by_name($new_name); rg_log("old_path=$old_path new_path=$new_path"); $ret = FALSE; while (1) { $do_link = TRUE; // 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($old_path, $v) == 0) { // Link already done $do_link = FALSE; } else { // 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; } } } if ($do_link === TRUE) { // Now, the new name is free, do the link $r = symlink($old_path, $new_path); if ($r !== TRUE) { rg_internal_error("Cannot symlink $old_path -> $new_path (" . rg_php_err() . ")!"); break; } } // TOOD: transaction? $r = rg_user_insert_rename($db, $ui['uid'], $new_name); if ($r !== TRUE) break; // TODO: Check if all parameters are used. $event = array( 'category' => 'user_event_rename', 'prio' => 50, 'ui' => $ui, 'rename_from' => $ui['username'], 'rename_to' => $new_name ); $r = rg_event_add($db, $event); if ($r !== TRUE) { rg_user_set_error("cannot add event" . " (" . rg_event_error() . ")"); break; } rg_event_signal_daemon("", 0); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("user_rename"); return $ret; } /* * Helper for rg_user_edit */ function rg_user_edit_no_check($db, $d) { rg_prof_start('user_edit_no_check'); rg_log_enter('user_edit_no_check: d: ' . rg_array2string($d)); $ret = array('ok' => 0, 'already_exists' => 0); while (1) { $add = $d['uid'] == 0; $update_pass = $add || !empty($d['pass']); if ($update_pass) { if (strcmp($d['pass'], $d['pass2']) != 0) { $ret['errmsg'] = 'passwords are not the same'; break; } // We do not need it anymore unset($d['pass2']); if (rg_user_pass_ok($d['pass']) !== TRUE) { $ret['errmsg'] = rg_user_error(); break; } $d['salt'] = rg_id(40); $d['pass'] = rg_user_pass($d['salt'], $d['pass']); } else { // No need to keep them in memory unset($d['pass']); unset($d['pass2']); } if ($add) { $d['itime'] = time(); if (!isset($d['suspended'])) $d['suspended'] = 0; $d['deleted'] = 0; $d['last_seen'] = 0; $d['disk_used_mb'] = 0; $sql = 'INSERT INTO users (username, realname, salt' . ', pass, email, itime' . ', is_admin, rights, session_time' . ', confirmed, confirm_token, plan_id' . ', suspended, last_seen, disk_used_mb' . ', deleted, last_ip)' . ' VALUES (@@username@@, @@realname@@, @@salt@@' . ', @@pass@@, @@email@@, @@itime@@' . ', @@is_admin@@, @@rights@@, @@session_time@@' . ', @@confirmed@@, @@confirm_token@@, @@plan_id@@' . ', @@suspended@@, @@last_seen@@' . ', @@disk_used_mb@@, @@deleted@@' . ', @@last_ip@@)' . ' RETURNING uid'; $ignore = array(RG_SQL_UNIQUE_VIOLATION); } else { // edit $salt_pass_add = ""; if ($update_pass) $salt_pass_add = ', pass = @@pass@@' . ', salt = @@salt@@'; $sql = 'UPDATE users' . ' SET username = @@username@@' . ', realname = @@realname@@' . ', email = @@email@@' . ', is_admin = @@is_admin@@' . ', rights = @@rights@@' . ', session_time = @@session_time@@' . ', plan_id = @@plan_id@@' . ', confirmed = @@confirmed@@' . ', confirm_token = @@confirm_token@@' . $salt_pass_add . ' WHERE uid = @@uid@@'; $ignore = array(); } $res = rg_sql_query_params_ignore($db, $sql, $d, $ignore, $ignore_kicked); if ($res === FALSE) { if (!$ignore_kicked) { $ret['errmsg'] = 'cannot insert/update user info'; break; } rg_log('DEBUG: username already present'); $ret['already_exists'] = 1; break; } if ($add) { $row = rg_sql_fetch_array($res); $d['uid'] = $row['uid']; } rg_sql_free_result($res); if ($add) { rg_cache_set('user' . '::' . $d['uid'] . '::' . 'info', $d, RG_SOCKET_NO_WAIT); $d2 = $d; // we need to be able to send the welcome mail $d2['ignore_confirmed'] = 1; $event = array( 'category' => 'user_event_new', 'prio' => 50, 'ui' => $d2, 'base_url' => rg_base_url() ); $r = rg_event_add($db, $event); if ($r === FALSE) { $ret['errmsg'] = 'cannot add event'; break; } rg_event_signal_daemon('', 0); } else { // edit rg_cache_merge('user' . '::' . $d['uid'] . '::' . 'info', $d, RG_SOCKET_NO_WAIT); if (isset($d['ask_for_email_confirmation']) && ($d['ask_for_email_confirmation'] == 1)) { $r = rg_user_ask_for_email_confirmation($db, $d['uid']); if ($r === FALSE) { $ret['errmsg'] = 'cannot add event'; break; } } } // TODO: should we cache here the user_by_uid and user_by_name $ret['ui'] = $d; $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('user_edit_no_check'); return $ret; } /* * Add/edit a user * If uid > 0 - edit, else, add */ function rg_user_edit($db, $d) { rg_prof_start("user_edit"); rg_log_enter("user_edit: d: " . rg_array2string($d)); $ret = FALSE; while (1) { if (rg_user_ok($d['username']) !== TRUE) break; // TODO: check rights // TODO - check if user is allowed to give passed rights if (isset($d['ask_for_email_confirmation']) && ($d['ask_for_email_confirmation'] == 1)) $d['confirmed'] = 0; $r = rg_user_edit_no_check($db, $d); if ($r['ok'] !== 1) { rg_user_set_error($r['errmsg']); break; } $ret = $r['ui']['uid']; break; } rg_log_exit(); rg_prof_end("user_edit"); return $ret; } /* * Delete a user * @uid - the uid of the user to be removed * This function only marks the user to be removed. */ function rg_user_remove($db, $rg, $uid) { rg_prof_start('user_remove'); rg_log_enter('user_remove: uid=$uid'); $ret = FALSE; while (1) { $x = array(); $x['obj_id'] = $uid; $x['type'] = 'user'; $x['owner'] = $uid; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = 'R'; $x['ip'] = $rg['ip']; $x['misc'] = ''; if (rg_rights_allow($db, $x) !== TRUE) break; $now = time(); $params = array( 'now' => $now, 'uid' => $uid ); $sql = 'UPDATE users SET deleted = @@now@@' . ' WHERE uid = @@uid@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error('cannot remove user'); break; } rg_sql_free_result($res); // update cache rg_cache_set('user' . '::' . $uid . '::' . 'info' . '::' . 'deleted', $now, RG_SOCKET_NO_WAIT); // invalidate session rg_sess_destroy($db, $rg['sid'], $rg['login_ui']); $ret = TRUE; break; } rg_log_exit(); rg_prof_end('user_remove'); return $ret; } /* * Returns info about a user (by uid, user or e-mail) */ function rg_user_info($db, $uid, $user, $email) { rg_prof_start('user_info'); $ret = rg_user_empty(); while (1) { $params = array("uid" => $uid, "user" => $user, "email" => $email); if ($uid > 0) { // We will get all info about the user not only 'info' // to populate the cache with all info. $c = rg_cache_get('user' . '::' . $uid); if (($c !== FALSE) && (isset($c['info'])) && (isset($c['info']['itime']))) { $ret = $c['info']; $ret['ok'] = 1; $ret['exists'] = 1; rg_user_cosmetic($ret); break; } $sql = "SELECT * FROM users WHERE uid = @@uid@@"; } else if (!empty($user)) { if (rg_user_ok($user) !== TRUE) { // cannot exists $ret['ok'] = 1; break; } $c = rg_cache_get("username_to_uid::" . $user); if ($c !== FALSE) { $uid = $c; continue; } $sql = "SELECT * FROM users WHERE username = @@user@@"; } else if (!empty($email)) { $c = rg_cache_get("email_to_uid::" . $email); if ($c !== FALSE) { $uid = $c; continue; } $sql = "SELECT * FROM users WHERE email = @@email@@"; } else { $ret['ok'] = 1; break; } $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot get info (" . rg_sql_error() . ")"); break; } $ret['ok'] = 1; $rows = rg_sql_num_rows($res); if ($rows > 0) $row = rg_sql_fetch_array($res); rg_sql_free_result($res); if ($rows == 0) { rg_log("user_info: user [$uid/$user/$email] not found"); rg_user_set_error("user not found"); break; } $ret = array_merge($ret, $row); rg_cache_set('user' . '::' . $ret['uid'] . '::' . 'info', $ret, RG_SOCKET_NO_WAIT); rg_cache_set('username_to_uid::' . $ret['username'], $ret['uid'], RG_SOCKET_NO_WAIT); rg_cache_set('email_to_uid::' . $ret['email'], $ret['uid'], RG_SOCKET_NO_WAIT); rg_user_cosmetic($ret); $ret['exists'] = 1; break; } rg_prof_end("user_info"); return $ret; } /* * Update last_seen field */ function rg_user_set_last_seen($db, $uid, $ts, $ip) { rg_log_enter("user_set_last_seen: uid=$uid ts=$ts"); $ret = FALSE; while (1) { $params = array("last_seen" => $ts, "last_ip" => $ip, "uid" => $uid); $sql = 'UPDATE users SET last_seen = @@last_seen@@' . ', last_ip = @@last_ip@@' . ' WHERE uid = @@uid@@' . ' AND last_seen != @@last_seen@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot update last seen (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); rg_cache_merge('user' . '::' . $uid . '::' . 'info', $params, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); return $ret; } /* * Loads ui based on sid, if possible */ function rg_user_login_by_sid($db, &$rg) { rg_prof_start("user_login_by_sid"); rg_log_enter("user_login_by_sid: sid=" . $rg['sid']); // Make sure it is not passed by client $rg['login_ui'] = rg_user_empty(); $ret = FALSE; while (1) { if (empty($rg['sid'])) break; // Is a pre login sesison? if (strncmp($rg['sid'], "X", 1) == 0) break; $sess = rg_sess_valid($db, $rg['sid']); if ($sess == FALSE) break; $uid = $sess['uid']; $rg['login_ui'] = rg_user_info($db, $uid, "", ""); if ($rg['login_ui']['exists'] != 1) { rg_log("Uid $uid does not exists (" . rg_user_error() . ")!"); rg_user_set_error("invalid uid"); break; } rg_sess_update($db, $sess); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("user_login_by_sid"); return $ret; } /* * Test if a password is valid */ function rg_user_pass_valid($db, $uid, $pass) { rg_log_enter("user_pass_valid: uid=$uid"); $ret = FALSE; while (1) { if (empty($pass)) { rg_user_set_error("password is empty"); break; } $ui = rg_user_info($db, $uid, "", ""); if ($ui['exists'] != 1) { rg_user_set_error("user does not exists"); break; } $pass_hash = rg_user_pass($ui['salt'], $pass); if (strcmp($pass_hash, $ui['pass']) != 0) { rg_user_set_error("password is not ok"); break; } rg_log("Pass is valid."); $ret = TRUE; break; } rg_log_exit(); return TRUE; } /* * Set session cookie */ function rg_user_set_session_cookie($db, $uid, $sess_time, $lock_ip, $https, $domain) { rg_log_enter("user_set_session_cookie: uid=$uid domain=$domain"); if ($https) { $secure = TRUE; $cookie_name = 'sids'; } else { $secure = FALSE; $cookie_name = 'sidu'; } $sid = rg_id(40); if ($uid > 0) rg_sess_add($db, $uid, $sid, $sess_time, $lock_ip); else $sid = "X" . $sid; rg_log('DEBUG: setting cookie \'' . $cookie_name . '\' to ' . $sid); setcookie($cookie_name, $sid, 0, '/', $domain, $secure, TRUE /*httponly*/); rg_log_exit(); return $sid; } /* * Auto login the user */ function rg_user_auto_login($db, $uid, $lock_ip, $https, $domain, &$ui) { rg_prof_start("user_auto_login"); rg_log_enter("user_auto_login: uid=$uid lock_ip=$lock_ip"); $ret = FALSE; while (1) { $ui = rg_user_info($db, $uid, "", ""); if ($ui['ok'] != 1) break; if ($ui['exists'] != 1) { rg_log("user does not exists"); break; } rg_user_set_session_cookie($db, $uid, $ui['session_time'], $lock_ip, $https, $domain); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("user_auto_login"); return $ret; } /* * Helper for rg_user_login_by_user_pass for db */ function rg_user_login_by_user_pass_db($db, $user, $pass, $ip, $lock_ip, $https, $domain, &$ui) { global $rg_account_email_confirm; rg_prof_start('user_login_by_user_pass_db'); rg_log_enter('user_login_by_user_pass_db: user=' . $user . ' lock_ip=' . $lock_ip . ' https=' . $https . ' domain=' . $domain); $ui = rg_user_empty(); $ret = FALSE; while (1) { $ui0 = rg_user_info($db, 0, $user, ""); if ($ui0['ok'] != 1) break; if ($ui0['exists'] != 1) { rg_user_set_error('invalid user, pass or login token'); rg_log('user doesn\'t exists'); break; } if ($rg_account_email_confirm && ($ui0['confirmed'] == 0)) { rg_user_set_error("invalid user, pass or login token"); rg_log("account is not confirmed"); break; } $pass_hash = rg_user_pass($ui0['salt'], $pass); if (strcmp($pass_hash, $ui0['pass']) != 0) { rg_user_set_error("invalid user, pass or login token"); rg_log("pass mismatch db:" . $ui0['pass'] . " computed=$pass_hash"); break; } $ui = $ui0; $ret = TRUE; break; } rg_log_exit(); rg_prof_end('user_login_by_user_pass_db'); return $ret; } /* * Authorize a user */ function rg_user_login_by_user_pass_helper($db, $user, $pass, $login_token, $ip, $lock_ip, $https, $domain, &$ui) { global $rg_login_functions; // TODO: what about $ui - for ldap will not have some elements. // Should we fake them? rg_prof_start('user_login_by_user_pass_helper'); rg_log_enter('user_login_by_user_pass_helper: user=' . $user . ' login_token=' . $login_token . ' lock_ip=' . $lock_ip . ' https=' . $https . ' domain=' . $domain); $ui = rg_user_empty(); $ret = array(); $ret['ok'] = 0; while (1) { if (empty($user) || empty($pass)) { rg_log("user or pass are empty"); $ret['errmsg'] = 'invalid user, pass or login token'; break; } while (1) { $r = rg_user_login_by_user_pass_db($db, $user, $pass, $ip, $lock_ip, $https, $domain, $ui); if ($r === TRUE) break; $ret['errmsg'] = rg_user_error(); // Try external authentication (LDAP etc.) foreach ($rg_login_functions as $funcs) { $r = $funcs['login']($db, $user, $pass, $ui); if ($r['ok'] === 1) { unset($ret['errmsg']); $post = $r['post']; break; } } if (isset($ret['errmsg'])) break; // We found a good user/pass combination rg_log_ml('DEBUG: post: ' . print_r($post, TRUE)); rg_log_ml('DEBUG: ui returned by login callback: ' . print_r($ui, TRUE)); $check_for_changes = TRUE; if ($ui['uid'] == 0) { // User found, but not in 'users' table. // We update the table and call the callback // to set uid into external auth table. rg_log('DEBUG: user not in \'users\' table (ui[uid] 0), add it'); $ui['last_ip'] = $ip; $r = rg_user_edit_no_check($db, $ui); if ($r['ok'] !== 1) { if ($r['already_exists'] == 0) { $ret['errmsg'] = $r['errmsg']; break; } rg_log('DEBUG: user is already present' . '; lookup by username'); $ui = rg_user_info($db, 0, $ui['username'], ''); if ($ui['ok'] !== 1) { $ret['errmsg'] = rg_user_error(); break; } } else { $ui['uid'] = $r['ui']['uid']; $check_for_changes = FALSE; } // This will update external auth cache uid. if (isset($funcs['post_login'])) { $r = $funcs['post_login']($db, $ui['uid'], $post); // We will not return an error here if ($r['ok'] !== 1) rg_log('post_login function' . ' returned error: ' . $r['errmsg']); } } else { // User found, but some fields may have old // values and we may need to update 'users'. rg_log('DEBUG: user exists in db (ui[uid]!=0), load info'); $ui = rg_user_info($db, $ui['uid'], '', ''); if ($ui['ok'] !== 1) { $ret['errmsg'] = rg_user_error(); break; } } // Some fields could change $do_update = FALSE; while (1) { if (!$check_for_changes) break; if (strcmp($ui['username'], $post['username']) != 0) { rg_log('DEBUG: different username: ' . $ui['username'] . ' != ' . $post['username']); $ui['username'] = $post['username']; $do_update = TRUE; break; } if (strcmp($ui['email'], $post['mail']) != 0) { rg_log('DEBUG: different mail'); $ui['email'] = $post['mail']; $do_update = TRUE; break; } if (strcmp($ui['realname'], $post['realname']) != 0) { rg_log('DEBUG: different realname'); $ui['realname'] = $post['realname']; $do_update = TRUE; break; } if (strcmp($ui['pass'], $post['password']) != 0) { rg_log('DEBUG: different passwords'); $ui['pass'] = $post['password']; $ui['pass2'] = $post['password']; $do_update = TRUE; break; } // TODO: expiration? // TODO: plan_id will change when we will edit // the ldap server info. Same with is_admin. // Same with 'prio'. break; } if ($do_update) { rg_log('DEBUG: desync between sql and ldap'); // If we do not have pass2, we do not want // to change the password. if (!isset($ui['pass2'])) unset($ui['pass']); $r = rg_user_edit_no_check($db, $ui); if ($r['ok'] !== 1) { $ret['errmsg'] = $r['errmsg']; break; } } break; } if (isset($ret['errmsg'])) break; if ($ui['deleted'] > 0) { $ret['errmsg'] = 'invalid user, pass or login token'; rg_log('account is deleted'); break; } if ($ui['suspended'] > 0) { $ret['errmsg'] = 'invalid user, pass or login token'; rg_log('account is suspended'); break; } $vi = rg_totp_verify_any($db, $ui['uid'], $login_token); if ($vi['ok'] != 1) { $ret['errmsg'] = 'login token error: ' . rg_totp_error(); break; } //rg_log_ml('DEBUG: vi: ' . print_r($vi, TRUE)); if (($vi['enrolled'] == 1) && ($vi['token_valid'] != 1)) { // It may be possible that the login_token to be appended // to the pass. $_list = array(); $_found = FALSE; foreach ($_list as $_lt) { $vi = rg_totp_verify_any($db, $ui['uid'], $_lt); if ($vi['ok'] == 1) { $_found = TRUE; break; } } if (!$_found) { $ret['errmsg'] = 'invalid user, pass or login token'; rg_log('invalid token'); break; } } $event = array( 'category' => 'user_event_login', 'prio' => 100, 'ui' => $ui, 'itime' => time()); $r = rg_event_add($db, $event); if ($r !== TRUE) { $ret['errmsg'] = 'internal error; try again later'; break; } rg_event_signal_daemon('', 0); rg_user_auto_login($db, $ui['uid'], $lock_ip, $https, $domain, $ui); $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('user_login_by_user_pass_helper'); return $ret; } function rg_user_login_by_user_pass($db, $user, $pass, $login_token, $ip, $lock_ip, $https, $domain, &$ui) { rg_prof_start('user_login_by_user_pass'); rg_log_enter('rg_user_login_by_user_pass'); while (1) { $ret = rg_user_login_by_user_pass_helper($db, $user, $pass, $login_token, $ip, $lock_ip, $https, $domain, $ui); if ($ret['ok'] == 1) break; if (!empty($login_token)) break; // Password may contain a login_token $lt = substr($pass, -6); $pass2 = substr($pass, 0, -6); $ret = rg_user_login_by_user_pass_helper($db, $user, $pass2, $lt, $ip, $lock_ip, $https, $domain, $ui); if ($ret['ok'] == 1) break; // Password may contain a scratch code $lt = substr($pass, -8); $pass2 = substr($pass, 0, -8); $ret = rg_user_login_by_user_pass_helper($db, $user, $pass2, $lt, $ip, $lock_ip, $https, $domain, $ui); break; } rg_log_exit(); rg_prof_end('user_login_by_user_pass'); return $ret; } /* * Suspend an account * 1=suspend, 0=unsuspend */ function rg_user_suspend($db, $rg, $uid, $op) { rg_log_enter("user_suspend: uid=$uid, op=$op"); $ret = FALSE; while (1) { $x = array(); $x['obj_id'] = $uid; $x['type'] = 'user'; $x['owner'] = $uid; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = 'S'; $x['ip'] = $rg['ip']; $x['misc'] = ""; if (rg_rights_allow($db, $x) !== TRUE) break; $now = time(); if ($op == 1) $v = $now; else $v = 0; $params = array("suspeneded" => $v, "uid" => $uid); $sql = "UPDATE users SET suspended = @@suspended@@" . " WHERE uid = @@uid@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot suspend (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); // update cache // TODO: what if we cannot update? rg_cache_set('user' . '::' . $uid . '::' . 'info' . '::' . 'suspended', $v, RG_SOCKET_NO_WAIT); break; } rg_log_exit(); return $ret; } /* * Make/remove admin * @op: 1=make, 0=remove */ function rg_user_make_admin($db, $rg, $uid, $op) { rg_prof_start("user_make_admin"); rg_log_enter("user_make_admin: uid=$uid, op=$op"); $ret = FALSE; while (1) { $x = array(); $x['obj_id'] = $uid; $x['type'] = 'user'; $x['owner'] = $uid; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = 'M'; $x['ip'] = $rg['ip']; $x['misc'] = ""; if (rg_rights_allow($db, $x) !== TRUE) break; $params = array("op" => $op, "uid" => $uid); $sql = "UPDATE users SET is_admin = @@op@@" . " WHERE uid = @@uid@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot make admin (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); rg_cache_set('user' . '::' . $uid . '::' . 'info' . '::' . 'is_admin', 1, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("user_make_admin"); return $ret; } /* * List users * TODO: switch to templates. */ function rg_user_list($db) { rg_prof_start("user_list"); rg_log_enter("user_list"); $ret = FALSE; while (1) { $sql = 'SELECT * FROM users' . ' WHERE deleted = 0' . ' ORDER BY username'; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_user_set_error("cannot get info (" . rg_sql_error() . ")!"); break; } $ret = ""; $ret .= "<table>\n"; $ret .= "<tr>\n"; $ret .= " <th>User name</th>\n"; $ret .= " <th>Name</th>\n"; $ret .= " <th>E-mail</th>\n"; $ret .= " <th>Admin?</th>\n"; $ret .= " <th>Creation date (UTC)</th>\n"; $ret .= " <th>Plan</th>\n"; $ret .= " <th>Suspended?</th>\n"; $ret .= " <th>Confirmed?</th>\n"; $ret .= " <th>Session time</th>\n"; $ret .= " <th>Last seen (UTC)</th>\n"; $ret .= " <th>Last IP</th>\n"; $ret .= " <th>Rights</th>\n"; $ret .= " <th>Disk (MiB)</th>\n"; $ret .= " <th>Git (MiB)</th>\n"; $ret .= " <th>Artifacts (MiB)</th>\n"; $ret .= " <th>Operations</th>\n"; $ret .= "</tr>\n"; while (($row = rg_sql_fetch_array($res))) { $ret .= "<tr>\n"; $ret .= " <td>" . $row['username'] . "</td>\n"; $ret .= " <td>" . $row['realname'] . "</td>\n"; $ret .= " <td>" . $row['email'] . "</td>\n"; $ret .= " <td>" . ($row['is_admin'] == 1 ? "Yes" : "No") . "</td>\n"; $ret .= " <td>" . gmdate("Y-m-d", $row['itime']) . "</td>\n"; $pi = rg_plan_info($db, $row['plan_id']); $plan = "Unlimited"; if ($pi['exists'] == 1) $plan = $pi['name']; $ret .= " <td>" . $plan . "</td>\n"; $ret .= " <td>" . ($row['suspended'] == 0 ? "No" : "Yes") . "</th>\n"; $ret .= " <td>" . ($row['confirmed'] == 0 ? "No" : gmdate("Y-m-d", $row['confirmed'])) . "</th>\n"; $ret .= " <td>" . $row['session_time'] . "s</td>\n"; $v = $row['last_seen'] == 0 ? "-" : gmdate("Y-m-d", $row['last_seen']); $ret .= " <td>" . $v . "</td>\n"; $ret .= " <td>" . $row['last_ip'] . "</td>\n"; $v = implode(", ", rg_rights_text("user", $row['rights'])); $ret .= " <td>" . $v . "</td>\n"; $ret .= " <td>" . $row['disk_used_mb'] . "</td>\n"; $ret .= " <td>" . $row['git_mb'] . "</td>\n"; $ret .= " <td>" . $row['artifacts_mb'] . "</td>\n"; // operations $url = "/op/admin/users"; $url2 = $row['username']; $ret .= " <td>"; // edit $ret .= "[<a href=\"$url/edit/$url2\">Edit</a>]"; // suspend $v = "suspend"; $t = "Suspend"; if ($row['suspended'] > 0) { $t = "Unsuspend"; $v = "unsuspend"; } $ret .= "[<a href=\"$url/$v/$url2\">$t</a>]"; // admin $v = "make_admin"; $t = "Make admin"; if ($row['is_admin'] == 1) { $t = "Remove admin"; $v = "remove_admin"; } $ret .= "[<a href=\"$url/$v/$url2\">$t</a>]"; // remove if ($row['suspended'] > 0) $ret .= "[<a href=\"$url/remove/$url2\">Remove!</a>]"; $ret .= " </td>"; $ret .= "</tr>\n"; } $ret .= "</table>\n"; rg_sql_free_result($res); break; } rg_log_exit(); rg_prof_end("user_list"); return $ret; } /* * Returns uid by token, if not expired */ function rg_user_forgot_pass_uid($db, $token) { rg_prof_start("user_forgot_pass_uid"); rg_log_enter("user_forgot_pass_uid: token=$token"); if (strlen($token) > 20) $token = substr($token, 0, 20); $ret = array(); $ret['ok'] = 0; $ret['uid'] = 0; while (1) { $now = time(); $params = array("token" => $token, "now" => $now); $sql = "SELECT uid FROM forgot_pass" . " WHERE token = @@token@@" . " AND expire > @@now@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error('cannot lookup token'); break; } $ret['ok'] = 1; $rows = rg_sql_num_rows($res); if ($rows > 0) $row = rg_sql_fetch_array($res); rg_sql_free_result($res); if ($rows == 0) { rg_user_set_error('invalid token'); break; } $ret['uid'] = $row['uid']; break; } rg_log_exit(); rg_prof_end("user_forgot_pass_uid"); return $ret; } /* * Reset password function (send mail) - helper */ function rg_user_forgot_pass_mail_prepare($db, $email) { rg_prof_start('user_forgot_pass_mail_prepare'); rg_log_enter("user_forgot_pass_mail_prepare: email=$email"); $ret = array(); $ret['ok'] = 0; $ret['exists'] = 0; $expire = time() + 24 * 3600; $token = rg_id(20); while (1) { $r = rg_user_info($db, 0, "", $email); if ($r['ok'] == 0) { rg_log("Internal error."); break; } if ($r['exists'] == 0) { rg_log("User does not exists."); $ret['ok'] = 1; break; } $uid = $r['uid']; // store token in database $params = array("token" => $token, "uid" => $uid, "expire" => $expire); $sql = "INSERT INTO forgot_pass (token, uid, expire)" . " VALUES (@@token@@, @@uid@@, @@expire@@)"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot query (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret['ok'] = 1; $ret['exists'] = 1; $ret['token'] = $token; break; } rg_log("DEBUG: user_forgot_pass_mail_prepare: ret=" . rg_array2string($ret)); rg_log_exit(); rg_prof_end('user_forgot_pass_mail_prepare'); return $ret; } /* * Reset password function (send mail) */ function rg_user_forgot_pass_mail($db, $rg, $email) { global $rg_admin_name; rg_prof_start('user_forgot_pass_mail'); rg_log_enter("user_forgot_pass_mail: email=$email"); $ret = array(); $ret['ok'] = 0; $ret['exists'] = 0; while (1) { $r = rg_user_forgot_pass_mail_prepare($db, $email); if ($r['exists'] != 1) { rg_log("User does not exists."); $ret = $r; break; } $ret['exists'] = 1; $rg['ui'] = $rg['login_ui']; $rg['ui']['email'] = $email; $rg['ui']['ignore_confirmed'] = 1; $rg['forgot_token'] = $r['token']; $r = rg_mail_template('mail/user/forgot/recover', $rg); if ($r === FALSE) { rg_user_set_error(rg_mail_error()); break; } $ret['ok'] = 1; break; } //rg_log("DEBUG: user_forgot_pass_mail: ret=" . rg_array2string($ret) . "."); rg_log_exit(); rg_prof_end('user_forgot_pass_mail'); return $ret; } /* * After reseting the pass, we have to destroy all 'reset pass' requests */ function rg_user_forgot_pass_destroy($db, $uid) { rg_prof_start('user_forgot_pass_destroy'); rg_log_enter("user_forgot_pass_destroy: uid=$uid"); $ret = FALSE; while (1) { $params = array("uid" => $uid); $sql = "DELETE FROM forgot_pass WHERE uid = @@uid@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot query (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; break; } rg_log_exit(); rg_prof_end('user_forgot_pass_destroy'); return $ret; } /* * Change the password of a user */ function rg_user_set_pass($db, $uid, $pass) { rg_prof_start("user_set_pass"); rg_log_enter("user_set_pass: uid=$uid"); $ret = FALSE; while (1) { $salt = rg_id(40); $pass = rg_user_pass($salt, $pass); $params = array("salt" => $salt, "pass" => $pass, "uid" => $uid); $sql = "UPDATE users SET" . " salt = @@salt@@" . ", pass = @@pass@@" . " WHERE uid = @@uid@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot update pass (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); unset($params['uid']); rg_cache_merge('user' . '::' . $uid . '::' . 'info', $params, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("user_set_pass"); return $ret; } /* * Confirm account creation */ function rg_user_confirm($db, $token) { rg_prof_start("user_confirm"); rg_log_enter("user_confirm: token=$token"); $now = time(); $token = preg_replace("/[^A-Za-z0-9]/", '', $token); $ret = FALSE; while (1) { if (empty($token)) { rg_user_set_error("invalid token"); break; } $params = array("token" => $token); $sql = 'SELECT uid, confirmed FROM users' . ' WHERE confirm_token = @@token@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error('cannot search for token'); break; } $rows = rg_sql_num_rows($res); if ($rows == 1) $row = rg_sql_fetch_array($res); rg_sql_free_result($res); if ($rows != 1) { rg_user_set_error("cannot find token"); break; } $uid = $row['uid']; if ($row['confirmed'] == 0) { // "< 2" because we mark with "1" if "no need to confirm" $params = array("confirmed" => $now, "uid" => $uid); $sql = 'UPDATE users SET confirmed = @@confirmed@@' . ', confirm_token = \'\'' . ' WHERE uid = @@uid@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error('cannot set confirmed'); break; } rg_sql_free_result($res); $_a = array('confirmed' => $now, 'confirm_token' => ''); rg_cache_merge('user' . '::' . $uid . '::' . 'info', $_a, RG_SOCKET_NO_WAIT); } $ret = $uid; break; } rg_log_exit(); rg_prof_end("user_confirm"); return $ret; } /* * Add a suggestion to database */ function rg_user_suggestion($db, $uid, $suggestion) { rg_prof_start("user_suggestion"); rg_log_enter("user_suggestion: uid=$uid suggestion=$suggestion"); $ret = FALSE; while (1) { $params = array('uid' => $uid, 'itime' => time(), 'sug' => $suggestion); $sql = "INSERT INTO suggestions (uid, itime, suggestion)" . " VALUES (@@uid@@, @@itime@@, @@sug@@)"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_user_set_error("cannot add suggestion (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("user_suggestion"); return $ret; } /* * Checks if a user is over limit. * Returns maximum allowed in @max. */ function rg_user_over_limit($db, $ui, &$max) { $pi = rg_plan_info($db, $ui['plan_id']); if ($pi['exists'] != 1) { rg_internal_error("Invalid plan."); return FALSE; } $max = $pi['disk_mb']; if ($max == 0) return FALSE; if ($ui['disk_used_mb'] >= $max) return TRUE; return FALSE; } /* * Always returns a nice text representation of the uid, even if invalid. */ function rg_user_nice($db, $uid) { if ($uid == 0) return 'anonymous'; $ui = rg_user_info($db, $uid, '', ''); if ($ui['exists'] == 1) return $ui['username']; return 'n/a'; } /* * Loads data for graphs * @unit - interval on which a sum is made */ function rg_user_data($db, $type, $start, $end, $unit, $mode) { $params = array('start' => $start, 'end' => $end); switch ($type) { case 'create_account': $q = 'SELECT 1 AS value, itime FROM users' . ' WHERE itime >= @@start@@ AND itime <= @@end@@' . ' AND is_admin = 0'; break; case 'disk': $q = 'SELECT disk_used_mb AS value, itime FROM users' . ' WHERE itime >= @@start@@ AND itime <= @@end@@' . ' AND is_admin = 0'; break; default: rg_internal_error('invalid type'); return FALSE; } $ret = rg_graph_query($db, $start, $end, $unit, $mode, $q, $params, ''); if ($ret === FALSE) rg_user_set_error(rg_graph_error()); return $ret; } /* * High level functions */ /* * High-level function for editing a user */ function rg_user_edit_high_level($db, &$rg) { global $rg_session_time; rg_log("user_edit_high_level"); rg_log_ml("user_edit_high_level:rg:" . print_r($rg, TRUE)); $ret = ""; if (($rg['target_ui']['uid'] == 0) && ($rg['rg_account_allow_creation'] != 1)) { $ret .= rg_template("user/create_na.html", $rg, TRUE /* xss */); return $ret; } $owner = $rg['target_ui']['uid']; if ($owner > 0) { $x = array(); $x['obj_id'] = $rg['target_ui']['uid']; $x['type'] = 'user'; $x['owner'] = $owner; $x['uid'] = $rg['login_ui']['uid']; $x['username'] = $rg['login_ui']['username']; $x['needed_rights'] = 'E'; $x['ip'] = $rg['ip']; $x['misc'] = ""; if (rg_rights_allow($db, $x) !== TRUE) { $ret .= rg_template("access_denied.html", $rg, TRUE /* xss */); return $ret; } } if ($rg['target_ui']['uid'] > 0) $rg['create_mode'] = 0; else $rg['create_mode'] = 1; if ($rg['doit'] == 0) { if ($rg['target_ui']['uid'] > 0) { // TODO: check also access rights? $ui = $rg['target_ui']; } else { // Defaults $ui = array(); $ui['uid'] = 0; $ui['username'] = ""; $ui['realname'] = ""; $ui['email'] = ""; $ui['pass'] = ""; $ui['pass2'] = ""; $ui['is_admin'] = "0"; $ui['rights'] = rg_rights_checkboxes("user", "rights", "C"); // TODO $ui['plan_id'] = 0; $ui['session_time'] = $rg_session_time; $ui['tos'] = 1; } } $v = microtime(TRUE); $gen1 = sprintf("%u", $v); $gen2 = sprintf("%03u", ($v - intval($v)) * 1000); $errmsg = array(); $load_form = TRUE; while (1) { if ($rg['doit'] == 0) break; $ui = array(); $ui['uid'] = $rg['target_ui']['uid']; $ui['username'] = trim(rg_var_str_nocr("username")); $ui['realname'] = trim(rg_var_str_nocr("realname")); $ui['email'] = rg_var_email('email'); $ui['pass'] = rg_var_str("pass"); $ui['pass2'] = rg_var_str("pass2"); $ui['is_admin'] = rg_var_bool("is_admin"); $ui['rights'] = "C"; // TODO $ui['plan_id'] = rg_var_uint("plan_id"); $ui['session_time'] = rg_var_uint("session_time"); $ui['confirmed'] = 0; if ($rg['no_tos'] == 1) $ui['tos'] = 1; else $ui['tos'] = rg_var_uint('tos'); $ui['last_ip'] = $rg['ip']; $ui['confirm_token'] = rg_id(20); if ($rg['target_ui']['uid'] > 0) { if (strcasecmp($rg['target_ui']['email'], $ui['email']) != 0) { $ui['ask_for_email_confirmation'] = 1; } else { // reuse $ui['confirm_token'] = $rg['target_ui']['confirm_token']; } } // We try to prevent bots to create accounts if ($rg['target_ui']['uid'] == 0) { $gen = rg_var_str('gen'); if (empty($gen)) { $diff = 0; } else { $xgen1 = substr($gen, 3); $xgen2 = substr($gen, 0, 3); $diff = ($gen1 - $xgen1) * 1000 + $gen2 - $xgen2; } if ($diff < 2000) { rg_log('Bot tried to create account in ' . $diff . 'ms'); $errmsg[] = 'invalid token; try again'; break; } } if ($ui['tos'] != 1) { $errmsg[] = rg_template('user/tos_deny.html', $rg, TRUE /*xss*/); break; } if (!rg_valid_referer()) { $errmsg[] = "invalid referer; try again"; break; } if (!rg_token_valid($db, $rg, 'user_edit_hl', FALSE)) { $errmsg[] = "invalid token; try again"; break; } if (($rg['login_ui']['is_admin'] != 1) && ($ui['is_admin'] != 0)) { $errmsg[] = "you are not admin, you cannot give admin rights"; break; } if ($ui['uid'] == 0) { $_ui = rg_user_info($db, 0, $ui['username'], ''); if ($_ui['ok'] != 1) { $errmsg[] = rg_user_error(); break; } if ($_ui['exists'] == 1) { $errmsg[] = "user already exists"; break; } } // Security warning - a user can pass "pass" var to me avoiding // being asked for old pass if ($ui['uid'] > 0) { if (!empty($ui['pass'])) rg_security_violation_no_exit("User tried to" . " change pass using 'edit info' page."); $ui['pass'] = ''; } $r = rg_user_edit($db, $ui); if ($r === FALSE) { $errmsg[] = rg_user_error(); break; } // TODO: should we just redirect to login page? // TODO: or to user page if there is no need to confirm the account? if ($ui['uid'] == 0) $ret = rg_template("user/create_ok.html", $rg, TRUE /* xss */); else $ret = rg_template("user/edit_ok.html", $rg, TRUE /* xss */); $load_form = FALSE; break; } if ($load_form) { $rg = array_merge($rg, $ui); $rg['HTML:select_plan'] = rg_plan_select($db, 'plan_id', $ui['plan_id']); $rg['HTML:checkbox_rights'] = rg_rights_checkboxes("user", "rights", $ui['rights']); $rg['HTML:errmsg'] = rg_template_errmsg($errmsg); $rg['rg_form_token'] = rg_token_get($db, $rg, 'user_edit_hl'); $rg['gen'] = $gen2 . $gen1; $ret .= rg_template("user/add_edit.html", $rg, TRUE /*xss*/); } return $ret; } /* * API dispatch function */ function rg_user_api($db, $a, $json) { rg_prof_start('user_api'); rg_log_enter('user_api'); $cmd = $json['cmd']; $ret = array(); while (1) { if ($a['target_ui']['exists'] != 1) { $ret['error'] = 'invalid user or no rights'; break; } // TODO: allow also a master user to access user info if (($a['login_ui']['is_admin'] != 1) && ($a['target_ui']['uid'] != $a['login_ui']['uid'])) { $ret['error'] = 'invalid user or no rights'; rg_log('user has no rights'); break; } if (strcmp($cmd, 'user_info') == 0) { $ret = $a['target_ui']; $list = array('pass', 'salt', 'confirm_token'); foreach ($list as $k) unset($ret[$k]); break; } if (strcmp($cmd, 'user_ssh_keys_list') == 0) { $ret['list'] = rg_keys_list($db, $a['target_ui']); break; } if (strcmp($cmd, 'user_rights_list') == 0) { $params = array('who' => $a['target_ui']['uid']); $sql = 'SELECT * FROM rights' . ' WHERE who = @@who@@'; $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))) { rg_rights_cosmetic($db, $row); $ret['list'][] = $row; } rg_sql_free_result($res); break; } if (strcmp($cmd, 'user_wh_list') == 0) { $r = rg_wh_list($db, $a['target_ui']['uid']); if ($r['ok'] !== 1) { $ret['error'] = rg_wh_error(); break; } rg_wh_cosmetic($r['list']); // list is indexed by hook_id, remove this $ret['list'] = array(); foreach ($r['list'] as $index => $d) $ret['list'][] = $d; break; } $ret['error'] = 'invalid command'; break; } rg_log_exit(); rg_prof_end('user_api'); return $ret; } /* * Add an ask-for-email-confirmation event to queue */ function rg_user_ask_for_email_confirmation($db, $uid) { $ui = rg_user_info($db, $uid, '', ''); if ($ui['exists'] != 1) return FALSE; $ev = array( 'category' => 'ask-email-confirmation', 'prio' => 200, 'ui' => $ui, 'base_url' => rg_base_url() ); $r = rg_event_add($db, $ev); if ($r !== TRUE) return FALSE; rg_event_signal_daemon('', 0); return TRUE; } /* * Transforms a list of uids into a list with full user info */ function rg_user_list_to_full_info($db, $list) { $ret = array(); foreach ($list as $index => $uid) { $ret['ui'] = rg_user_info($db, $uid, '', ''); if ($ret['ui']['ok'] != 1) return FALSE; if ($ret['ui']['exists'] != 1) continue; } return $ret; } /* * This function deals with incoming (compressed) input. * Please note that if the webserver was configured without an input filter * (like apache's SetInputFilter DEFLATE), this function will * take care of it. * TE is alway empty and CL>0 using the current nginx configuration. * content_encoding may be gzip or empty. * Returns the uncompressed stream. */ function rg_process_input($content_length, $content_encoding, &$err) { $te = isset($_SERVER['HTTP_TRANSFER_ENCODING']) ? $_SERVER['HTTP_TRANSFER_ENCODING'] : ''; rg_log_enter('DEBUG: process_input: content_length=' . $content_length . ' content_encoding=' . $content_encoding . ' te=' . $te); $ret = FALSE; while (1) { $max_nice = ini_get('post_max_size'); $max = rg_mega2bytes($max_nice); $max_nice = rg_1024($max); if ($content_length > $max) { $err = 'Your push size (' . rg_1024($content_length) . ')' . ' is bigger than the max allowed' . ' (' . $max_nice . ').' . "\n" . 'You may want to ask the admin to raise' . ' the PHP limit (post_max_size).'; rg_internal_error($err); break; } // TODO: I have working requests with cl > 0 and te = 'chunked' if ($content_length == 0) { if (strcasecmp($te, 'chunked') != 0) { $err = 'No Content-Length header and' . ' Transfer-Encoding is not' . ' "chunked". Please let us know.'; rg_internal_error($err); break; } if (!empty($te)) { $err = 'Unknown Transfer-Encoding: ' . $te; rg_internal_error($err); break; } // TODO: Sadly, we are not able to do the streaming! // It is not available to php-fpm! rg_git_info_pack("\x02", 'We could not process' . ' the data (chunked).'); $err = 'You are trying to push chunked encoding data' . ' but php-fpm does not support it.' . "\n" . ' Try to run' . ' \'git config http.postBuffer 200000000\'.' . "\n" . ' The value must be bigger than what you' . ' want to push.'; rg_internal_error($err); break; } else { // Content-Length > 0 } if (strcmp($content_encoding, 'gzip') == 0) { // ok, we are dealing with it } else if (!empty($content_encoding)) { $err = 'Unknown Content-Encoding: ' . $content_encoding; rg_internal_error($err); break; } $ret = TRUE; break; } rg_log_exit(); return $ret; } /* * Helper for rg_user_http_git */ function rg_user_http_git_cb_input($index, &$a, $stream) { rg_log_enter('rg_user_http_git_cb_input'); switch ($stream) { case 1: // stdout echo $a['in_buf']; $a['in_buf'] = ''; break; case 2: // stderr $a['cb_input_stderr_func']($a['err_buf']); $a['err_buf'] = ''; break; } rg_log_exit(); } /* * Deals with push/fetch by HTTP(S) */ function rg_user_http_git($db, $rg, $paras) { global $rg_log_sid; rg_prof_start('user_http_git'); rg_log_enter('user_http_git'); if (strcasecmp($rg['ct'], 'application/x-git-upload-pack-request') == 0) $service = 'git-upload-pack'; else if (strcasecmp($rg['ct'], 'application/x-git-receive-pack-request') == 0) $service = 'git-receive-pack'; else $service = isset($_REQUEST['service']) ? $_REQUEST['service'] : ''; rg_log('DEBUG: service=' . $service); $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'; $ret = FALSE; while (1) { if (empty($service)) break; $ret = TRUE; rg_stats_conns_set('cmd', $service); set_time_limit(3600); rg_log_ml('paras: ' . print_r($paras, TRUE)); if (count($paras) < 2) { //TODO } $prefix = array_shift($paras); if (strcmp($prefix, 'user') != 0) { $user = $prefix; $prefix = ''; } else { $user = array_shift($paras); } $repo = array_shift($paras); $file = implode('/', $paras); rg_log('DEBUG: file=' . $file); header('Expires: Fri, 01 Jan 1980 00:00:00 GMT'); header('Pragma: no-cache'); header('Cache-Control: no-cache, max-age=0, must-revalidate'); header('Content-Type: text/plain'); // If user is valid, retry auth. // If user is not valid, consider anonymous. $authd = array('ok' => 0); $u = ''; $no_user_provided = TRUE; $auth_ui = rg_user_empty(); while (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { $u = $_SERVER['PHP_AUTH_USER']; $p = $_SERVER['PHP_AUTH_PW']; rg_log('DEBUG: HTTP auth: u=' . $u); $no_user_provided = FALSE; if (strcasecmp($u, 'guest') == 0) $u = ''; if (empty($u)) break; $auth_ui = rg_user_info($db, 0, $u, ''); if ($auth_ui['ok'] !== 1) { rg_log('DEBUG: set errror (500): ' . rg_user_error()); header('X-Rocketgit-Error: ' . rg_user_error()); header($protocol . ' 500 Internal server error'); break; } $authd = rg_user_login_by_user_pass($db, $u, $p, '' /*login_token*/, $rg['ip'], TRUE /*lock IP*/, $rg['https'], $rg['hostname'], $rg['login_ui']); break; } $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; // TODO: not clear if passing here login_ui is correct $helper_flags = ''; $r = rg_repo_fetch_push_helper($db, $host, $rg['ip'], $rg['login_ui'], $prefix, $user, $repo, $service, $helper_flags); rg_log_ml('DEBUG: repo_fetch_push_helper returns: ' . print_r($r, TRUE)); if ($r['ok'] !== 1) { rg_log('DEBUG: set errror: ' . $r['errmsg']); header('X-Rocketgit-Error: ' . $r['errmsg']); if (isset($r['owner_ui'])) { if ($r['owner_ui']['ok'] != 1) { header($protocol . ' 500 Internal server error'); break; } if ($r['owner_ui']['exists'] != 1) { header($protocol . ' 404 Not found'); break; } } if (isset($r['ri'])) { if ($r['ri']['ok'] != 1) { header($protocol . ' 500 Internal server error'); break; } if (($r['ri']['exists'] != 1) || ($r['ri']['deleted'] > 0)) { header($protocol . ' 404 Not found'); break; } } header($protocol . ' 500 Internal server error'); break; } $repo_path = $r['repo_path']; // for 'conns' stats rg_stats_conns_set('uid', $r['owner_ui']['uid']); rg_stats_conns_set('repo_id', $r['ri']['repo_id']); // push_allowed is only about the rights, not about auth/login_tokens rg_log('DEBUG: push=' . $r['push'] . ' push_allowed=' . $r['push_allowed'] . ' no_user_provided=' . ($no_user_provided ? 'yes' : 'no') . ' authd=' . ($authd['ok'] === 1 ? 'yes' : 'no') . ' exists=' . $auth_ui['exists']); $send_401 = FALSE; while (1) { if ($r['allow'] !== 1) { rg_log('DEBUG: allow != 1 => 401'); // Connecting user has no rights to push, not even anon. // The user may be authed at this point, but may try another // user/pass combination. $send_401 = TRUE; break; } if ($r['push'] !== 1) { rg_log('DEBUG: it is a fetch'); break; } if ($no_user_provided) { rg_log('DEBUG: no user provided'); $send_401 = TRUE; break; } if (($authd['ok'] !== 1) && ($auth_ui['exists'] == 1)) { rg_log('DEBUG: user exists but not authenticated'); $send_401 = TRUE; break; } break; } if ($send_401) { rg_log('DEBUG: sending 401'); header($protocol . ' 401 Unauthorized status'); header('WWW-Authenticate: Basic' . ' realm="Use user \'guest\' if you have no account"'); echo 'RocketGit: Info: == Welcome to RocketGit! ==' . "\n"; echo 'RocketGit: Info: you are connecting from IP ' . $rg['ip'] . ' by http(s).' . "\n"; echo 'RocketGit: Info: date/time: ' . gmdate('Y-m-d H:i:s') . ' (UTC), debug id ' . $rg_log_sid . '.' . "\n"; echo 'RocketGit: Info: Use user \'guest\' with any' . ' password if you want to push anonymously.' . "\n"; echo 'RocketGit: Info: Append the login token or the' . ' scratch code to the password if you are' . ' enrolled into 2fa.' . "\n"; break; } $content_length = isset($_SERVER['CONTENT_LENGTH']) ? intval($_SERVER['CONTENT_LENGTH']) : 0; $content_encoding = isset($_SERVER['HTTP_CONTENT_ENCODING']) ? $_SERVER['HTTP_CONTENT_ENCODING'] : ''; rg_log('DEBUG: cl=' . $content_length . ' ce=' . $content_encoding . '.'); $input_fd = @fopen('php://input', 'r'); if ($input_fd === FALSE) { $err = 'cannot open php://input: '. rg_php_err(); rg_internal_error($err); break; } $cmd = array( 'cmds' => array( 'cmd1' => array( 'cb_output' => 'rg_exec2_helper_read_fd', 'cb_output_fd' => $input_fd, //TODO 'out_buf_done' => 0, 'cb_input' => 'rg_user_http_git_cb_input' ) ) ); if (strcasecmp($content_encoding, 'gzip') == 0) $cmd['cmds']['cmd1']['out_buf_helper'] = 'rg_exec2_helper_gzip_in'; if (strcmp($file, 'info/refs') == 0) { rg_log('DEBUG: info/refs'); // TODO: we should allow this only if the connecting // user has fetch rights! header('Content-Type: application/x-' . $service . '-advertisement'); echo rg_git_pack('# service=' . $service . "\n"); echo rg_git_flush(); $cmd['cmds']['cmd1']['cmd'] = '/usr/libexec/git-core/' . $service . ' --stateless-rpc --advertise-refs' . ' ' . escapeshellarg($repo_path); $cmd['cmds']['cmd1']['cb_input_stderr_func'] = 'rg_git_band_2'; $e = rg_exec2($cmd); //rg_log_ml('XDEBUG: e: ' . print_r($e, TRUE)); if (!isset($e['code'])) exit(1); if ($e['ok'] != 1) { $err = 'error executing command: ' . $e['errmsg']; rg_internal_error($err); break; } rg_log('Done!'); break; } if (strcasecmp($rg['ct'], 'application/x-git-upload-pack-request') == 0) { rg_log('DEBUG: git-upload-pack...'); header('Content-Type: application/x-git-upload-pack-result'); // TODO: seems I cannot do this here: // remote receive-pack expects ACK/NAK first: // fatal: git fetch_pack: expected ACK/NAK, got '... // Should I send NAK first? /* rg_git_info_pack("\x02", '== Welcome to RocketGit! =='); rg_git_info_pack("\x02", 'you are connecting from IP ' . $rg['ip'] . ' by http(s).'); rg_git_info_pack("\x02", 'date/time: ' . gmdate('Y-m-d H:i:s') . ' (UTC), debug id ' . $rg_log_sid . '.'); // If user does not connect to the correct URL, correct them if (!empty($host) && (strcasecmp($host, $rg['hostname_port']) != 0)) rg_git_info_pack("\x02", 'Please use ' . $rg['hostname_port'] . ' instead of ' . $host . '.'); putenv('ROCKETGIT_SHOW_INFO=0'); */ $r = rg_process_input($content_length, $content_encoding, $err); if ($r === FALSE) break; $cmd['cmds']['cmd1']['cmd'] = '/usr/libexec/git-core/git-upload-pack' . ' --stateless-rpc' . ' ' . escapeshellarg($repo_path); $cmd['cmds']['cmd1']['cb_input_stderr_func'] = 'rg_git_band_2'; $e = rg_exec2($cmd); if ($e['ok'] != 1) { $err = 'error executing command: ' . $e['errmsg']; rg_internal_error($err); break; } rg_log('Done!'); } else if (strcasecmp($rg['ct'], 'application/x-git-receive-pack-request') == 0) { rg_log('DEBUG: git-receive-pack...'); header('Content-Type: application/x-git-receive-pack-result'); rg_git_info_pack("\x02", '== Welcome to RocketGit! =='); rg_git_info_pack("\x02", 'you are connecting from IP ' . $rg['ip'] . ' by http(s).'); rg_git_info_pack("\x02", 'date/time: ' . gmdate('Y-m-d H:i:s') . ' (UTC), debug id ' . $rg_log_sid . '.'); // If user does not connect to the correct URL, correct them if (!empty($host) && (strcasecmp($host, $rg['hostname_port']) != 0)) rg_git_info_pack("\x02", 'Please use ' . $rg['hostname_port'] . ' instead of ' . $host . '.'); putenv('ROCKETGIT_SHOW_INFO=0'); $r = rg_process_input($content_length, $content_encoding, $err); if ($r === FALSE) { // We have to send '200'. // Else, we cannot send the hints header($protocol . ' 200 Service unavailable'); rg_git_info_pack("\x02", $err); echo rg_git_flush(); break; } $cmd['cmds']['cmd1']['cmd'] = '/usr/libexec/git-core/git-receive-pack' . ' --stateless-rpc' . ' ' . escapeshellarg($repo_path); $cmd['cmds']['cmd1']['cb_input_stderr_func'] = 'rg_echo'; $e = rg_exec2($cmd); if ($e['ok'] != 1) { $err = 'error executing command: ' . $e['errmsg']; rg_internal_error($err); break; } rg_log('Done!'); } else { rg_log('Unknown service!'); // TODO: send some errors, also above } break; } rg_log_exit(); rg_prof_end('user_http_git'); return $ret; } /* * Delete account high level function */ function rg_user_delete_account_high_level($db, $rg, $paras) { $ret = ''; $are_you_sure = rg_var_uint('are_you_sure'); $errmsg = array(); $show_form = TRUE; while (1) { if ($rg['doit'] != 1) break; if ($are_you_sure == 0) { $ret .= rg_template('user/settings/delete/no.html', $rg, TRUE/*xss*/); $show_form = FALSE; break; } if (!rg_valid_referer()) { $errmsg[] = 'invalid referer; try again'; break; } if (!rg_token_valid($db, $rg, 'delete_account', FALSE)) { $errmsg[] = 'invalid token; try again'; break; } $r = rg_user_remove($db, $rg, $rg['login_ui']['uid']); if ($r !== TRUE) { $errmsg[] = rg_user_error(); break; } $ret .= rg_template('user/settings/delete/done.html', $rg, TRUE/*xss*/); $show_form = FALSE; break; } if ($show_form) { // hints $hints = array(); $hints[]['HTML:hint'] = rg_template('hints/user/delete_account.html', $rg, TRUE /*xss*/); $rg['HTML:hints'] = rg_template_table('hints/list', $hints, $rg); $rg['HTML:errmsg'] = rg_template_errmsg($errmsg); $rg['rg_form_token'] = rg_token_get($db, $rg, 'delete_account'); $ret .= rg_template('user/settings/delete/sure.html', $rg, TRUE/*xss*/); } return $ret; }