<?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 . "/prof.inc.php"); $rg_bug_error = ""; function rg_bug_set_error($str) { global $rg_bug_error; $rg_bug_error = $str; } function rg_bug_error() { global $rg_bug_error; return $rg_bug_error; } /* States for a bug */ $rg_bug_states = array( "0" => "Any", "1" => "Open", "2" => "Closed" ); /* * Return the state of a bug, as string */ function rg_bug_state($v) { global $rg_bug_states; if (!isset($rg_bug_states[$v])) return "?"; return $rg_bug_states[$v]; } /* * Returns a select for state */ function rg_bug_state_select($value) { global $rg_bug_states; $ret = ""; $ret .= "<select name=\"state\">\n"; foreach ($rg_bug_states as $key => $name) { $add = ""; if (strcmp($value, $key) == 0) $add = " selected"; $ret .= "\t<option value=\"" . $key . "\"" . $add . ">" . $name . "</option>\n"; } $ret .= "</select>\n"; return $ret; } /* * We want the bug number to be consecutive per repo. * This is why we use a separate table (bugs_max) to track last id. * This function must be inside a transaction. */ function rg_bug_next_id($db, $repo_id) { rg_prof_start("bug_next_id"); rg_log("bug_next_id: repo_id=$repo_id"); $next_bug_id = FALSE; do { $sql = "UPDATE bugs_max SET last_bug_id = last_bug_id + 1" . " WHERE repo_id = $repo_id" . " RETURNING last_bug_id AS next_bug_id"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot get/update max (" . rg_sql_error() . ")"); break; } $rows = rg_sql_num_rows($res); if ($rows == 1) { $row = rg_sql_fetch_array($res); $next_bug_id = $row['next_bug_id']; } rg_sql_free_result($res); if ($rows == 1) break; /* If we are here, it means that we have no entry in bugs_max. */ $res = rg_sql_begin($db); if ($res === FALSE) { rg_bug_set_error("cannot start txn (" . rg_sql_error() . ")"); break; } $sql = "LOCK TABLE bugs_max IN ACCESS EXCLUSIVE MODE"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot lock max table (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); /* * Here, another client may just did the insert and commited * and we obtain the lock. So, we have to check if a insert * took place. if we */ $sql = "SELECT 1 FROM bugs_max WHERE repo_id = $repo_id"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot select 1 from max table (" . rg_sql_error() . ")"); break; } $rows = rg_sql_num_rows($res); rg_sql_free_result($res); if ($rows == 0) { // We were faster, just insert. $sql = "INSERT INTO bugs_max (repo_id, last_bug_id)" . " VALUES ($repo_id, 1)"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot insert into max table (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $next_bug_id = 1; } // commit (will also unlock) $res = rg_sql_commit($db); if ($res === FALSE) { rg_bug_set_error("cannot commit (" . rg_sql_error() . ")"); break; } /* * The other client was faster than us. Just repeat * the whole operation. */ } while ($next_bug_id === FALSE); rg_log("\tDEBUG: next_bug_id=" . $next_bug_id); rg_prof_end("bug_next_id"); return $next_bug_id; } /* * Helper function to populate some fields for a bug */ function rg_bug_cosmetic($db, &$row) { $_ui = rg_user_info($db, $row['uid'], "", ""); if ($_ui['exists'] != 1) $row['owner'] = "?"; else $row['owner'] = $_ui['username']; $row['HTML:body'] = nl2br($row['body']); unset($row['body']); $row['creation'] = gmdate("Y-m-d H:i", $row['itime']); if ($row['utime'] > 0) $row['updated'] = gmdate("Y-m-d H:i", $row['utime']); else $row['updated'] = "-"; $row['assigned_to'] = "-"; if ($row['assigned_uid'] > 0) { $_ui = rg_user_info($db, $row['assigned_uid'], "", ""); if ($_ui['exists'] == 1) $row['assigned_to'] = $_ui['username']; } $row['state'] = rg_bug_state($row['state']); } /* * Return info about a bug */ $rg_bug_info_cache = array(); function rg_bug_info($db, $repo_id, $bug_id) { global $rg_bug_info_cache; rg_prof_start("bug_info"); rg_log("rg_bug_info: repo_id=$repo_id bug_id=$bug_id"); $ret = FALSE; do { $key = $repo_id . "-" . $bug_id; if (isset($rg_bug_info_cache[$key])) { $ret = $rg_bug_info_cache[$key]; break; } $sql = "SELECT * FROM bugs" . " WHERE repo_id = " . $repo_id . " AND bug_id = " . $bug_id; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot list bugs (" . rg_sql_error() . ")"); break; } $ret = array(); $rows = rg_sql_num_rows($res); if ($rows == 1) $ret = rg_sql_fetch_array($res); rg_sql_free_result($res); $ret['exists'] = $rows; if ($ret['exists'] == 0) break; rg_bug_cosmetic($db, $ret); $rg_bug_info_cache[$key] = $ret; } while (0); rg_prof_end("bug_info"); return $ret; } /* * Add a bug */ function rg_bug_add($db, $repo_id, $uid, $data) { rg_prof_start("bug_add"); rg_log("bug_add: repo_id=$repo_id uid=$uid" . " data: " . rg_array2string($data)); // TODO: test if user is allowed to add a bug $e_data = $data; $e_data['title'] = rg_sql_escape($db, $data['title']); $e_data['body'] = rg_sql_escape($db, $data['body']); $e_data['state'] = sprintf("%u", $data['state']); $e_data['labels'] = rg_sql_escape($db, $data['labels']); $itime = time(); $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ""; $ret = FALSE; $rollback = 0; do { if (rg_sql_begin($db) !== TRUE) break; $rollback = 1; $bug_id = rg_bug_next_id($db, $repo_id); if ($bug_id === FALSE) break; $err = rg_bug_label_insert($db, $repo_id, $bug_id, $data['labels']); if ($err !== TRUE) break; $sql = "INSERT INTO bugs (bug_id, itime, utime, repo_id, uid" . ", ip, title, body, state, assigned_uid, deleted)" . " VALUES ($bug_id, $itime, 0, $repo_id, $uid" . ", '$ip', '" . $e_data['title'] . "'" . ", '" . $e_data['body'] . "'" . ", " . $e_data['state'] . ", " . $e_data['assigned_uid'] . ", 0)"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("Cannot insert bug (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); if (rg_sql_commit($db) === FALSE) { rg_bug_set_error("Cannot commit (" . rg_sql_error() . ")"); break; } $ret = $bug_id; $rollback = 0; } while (0); if ($rollback == 1) rg_sql_rollback($db); rg_prof_end("bug_add"); return $ret; } /* * Delete a bug */ function rg_bug_delete($db, $repo_id, $bug_id) { rg_prof_start("bug_delete"); rg_log("bug_delete: $repo_id=$repo_id bug_id=$bug_id"); $ret = FALSE; do { // TODO: Check rights $now = time(); // Only mark it as such, deletion will happen in background $sql = "UPDATE bugs SET deleted = $now" . " WHERE repo_id = $repo_id" . " AND bug_id = $bug_id"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("Cannot delete bug (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; } while (0); rg_prof_end("bug_delete"); return $ret; } /* * Update a bug * TODO: check rights - also for create? */ function rg_bug_update($db, $repo_id, $bug_id, $data) { rg_prof_start("bug_update"); rg_log("bug_update: repo_id=$repo_id bug_id=$bug_id data: " . rg_array2string($data)); $ret = FALSE; do { // First, test if it already exists $bi = rg_bug_info($db, $repoid, $bug_id); if (($bi === FALSE) || ($bi['exists'] != 1)) break; $e_data = $data; $e_data['title'] = rg_sql_escape($db, $data['title']); $e_data['body'] = rg_sql_escape($db, $data['body']); $e_data['state'] = sprintf("%u", $data['state']); // TODO: make a function to sanitize the input to be called from // both update and insert. $utime = time(); $sql = "UPDATE bugs SET utime = $now" . ", title = '" . $e_data['title'] . "'" . ", body = '" . $e_data['body'] . "'" . ", state = " . $e_data['state'] . ", assigned_uid = " . $e_data['assigned_uid'] . " WHERE repo_id = $repo_id" . " AND bug_id = $bug_id"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("Cannot update bug (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; } while (0); rg_prof_end("bug_update"); return $ret; } /* * List bugs */ function rg_bug_list_query($db, $url, $sql) { rg_prof_start("bug_list_query"); rg_log("bug_list_query: url=$url, sql=$sql..."); $ret = FALSE; do { $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot list by query (" . rg_sql_error() . ")"); break; } $d = array(); while (($row = rg_sql_fetch_array($res))) { rg_bug_cosmetic($db, $row); $row['url_bug'] = $url . "/bug/" . $row['bug_id']; $d[] = $row; } rg_sql_free_result($res); $ret = TRUE; } while (0); if ($ret !== FALSE) $ret = rg_template_table("repo/bug/list", $d, array()); rg_prof_end("bug_list_query"); return $ret; } /* * Loads all saved searches */ function rg_bug_search_load_all($db, $repo_id, $uid, $url) { rg_prof_start("bug_search_load_all"); rg_log("bug_search_load_all: repo_id=$repo_id uid=$uid"); $ret = FALSE; do { $sql = "SELECT name FROM bug_search" . " WHERE (repo_id = $repo_id OR repo_id = 0)" . " AND uid = $uid" . " ORDER BY repo_id, name"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot load searches (" . rg_sql_error() . ")"); break; } $data = array(); // Add predefined $data['All'] = "All"; $data['Open'] = "Open"; $data['Closed'] = "Closed"; while (($row = rg_sql_fetch_array($res))) { $name = $row['name']; $data[$name] = $name; } $ret = array(); foreach ($data as $name => $junk) $ret[] = array("HTML:name" => "<a href=" . $url . "/list/" . $name . ">" . $name . "</a>"); rg_sql_free_result($res); } while (0); rg_prof_end("bug_search_load_all"); return $ret; } /* * Loads a saved search */ function rg_bug_search_load($db, $repo_id, $uid, $name) { rg_prof_start("bug_search_load"); rg_log("bug_search_load: repo_id=$repo_id uid=$uid name=$name"); $ret = FALSE; do { $template = array( "name" => $name, "reported_by" => 0, "assigned_to" => 0, "state" => 0, "start" => 0, "end" => 0, "title_string" => "", "body_string" => "", "bugs_per_page" => 25, "global" => 0, "for_all_users" => 1, "standard" => 1 ); // Pre-defined if (strcmp($name, "All") == 0) { $ret = $template; break; } if (strcmp($name, "Open") == 0) { $template['state'] = 1; $ret = $template; break; } if (strcmp($name, "Closed") == 0) { $template['state'] = 2; $ret = $template; break; } $e_name = rg_sql_escape($db, $name); $sql = "SELECT name, data, for_all_users FROM bug_search" . " WHERE (repo_id = $repo_id OR repo_id = 0)" . " AND (uid = $uid OR for_all_users = 1)" . " AND name = '$e_name'" . " ORDER BY name"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot search load (" . rg_sql_error() . ")"); break; } $rows = rg_sql_num_rows($res); if ($rows > 0) { $row = rg_sql_fetch_array($res); $_data = @unserialize($row['data']); if ($_data === FALSE) { rg_bug_set_error("cannot unserialize search data"); break; } $ret = $_data; $ret['name'] = $row['name']; //TODO: escape?! $ret['for_all_users'] = $row['for_all_users']; $ret['standard'] = 0; } else { $ret = array(); } rg_sql_free_result($res); } while (0); rg_prof_end("bug_search_load"); return $ret; } /* * Saves the search filtering * TODO: name should not be inside data? */ function rg_bug_search_save($db, $repo_id, $uid, $q) { rg_prof_start("bug_search_save"); rg_log("rg_bug_search_save: repo_id=$repo_id uid=$uid" . " q=" . rg_array2string($q)); $ret = FALSE; do { $name = $q['name']; unset($q['name']); // Test if is already present $old = rg_bug_search_load($db, $repo_id, $uid, $name); if ($old === FALSE) break; $s = serialize($q); $e_data = rg_sql_escape($db, $s); $e_name = rg_sql_escape($db, $name); // Global? if (isset($q['global']) && ($q['global'] == 0)) $e_repo_id = $repo_id; else $e_repo_id = 0; if (isset($q['for_all_users']) && ($q['for_all_users'] == 1)) $e_for_all_users = 1; else $e_for_all_users = 0; // We will not overwrite somebody else's search if (empty($old) || ($old['uid'] != $uid)) { $sql = "INSERT INTO bug_search (repo_id, uid, name" . ", data, for_all_users)" . " VALUES ($e_repo_id, $uid, '$e_name'" . ", '$e_data', $e_for_all_users)"; } else { $sql = "UPDATE bug_search" . " SET data = '$e_data'" . ", for_all_users = $e_for_all_users" . " WHERE repo_id = $e_repo_id" . " AND uid = $uid" . " AND name = '$e_name'"; } $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot save search (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; } while (0); rg_prof_end("bug_search_save"); return $ret; } /* * Search for bugs */ function rg_bug_search($db, $url, $repo_id, $uid, $q) { rg_prof_start("bug_search"); rg_log("bug_search: url=$url repo_id=$repo_id uid=$uid" . " q=" . rg_array2string($q)); $add = array(); $limit = 25; $ret = FALSE; do { // reported_by if (!empty($q['reported_by'])) { $_ui = rg_user_info($db, "", $q['reported_by'], "", ""); if ($_ui['exists'] != 1) { rg_bug_set_error("cannot lookup user (reported_by)"); break; } $add[] = "uid = " . $_ui['uid']; } // assigned to if (!empty($q['assigned_to'])) { $_ui = rg_user_info($db, "", $q['assigned_to'], "", ""); if ($_ui['exists'] != 1) { rg_bug_set_error("cannot lookup user (assigned_to)"); break; } $add[] = "AND assigned_uid = " . $_ui['uid']; } // state if (isset($q['state']) && ($q['state'] > 0)) $add[] = "AND state = " . $q['state']; // start if (!empty($q['start'])) { $ts = rg_date2ts($q['start'], 0, 0, 0); if ($ts === FALSE) { rg_bug_set_error("invalid start date format"); break; } $add[] = "AND itime >= $ts"; } // end if (!empty($q['end'])) { $ts = rg_date2ts_last_second($q['end']); if ($ts === FALSE) { rg_bug_set_error("invalid end date format"); break; } $add[] = "AND itime <= $ts"; } // title_string if (!empty($q['title_string'])) { $_t = rg_sql_escape($db, $q['title_string']); $add[] = "AND title ILIKE '%" . $_t . "%'"; } // body_string if (!empty($q['body_string'])) { $_t = rg_sql_escape($db, $q['body_string']); $add[] = "AND body ILIKE '%$" . $_t . "%'"; } // bugs_per_page if (!empty($q['bugs_per_page'])) $limit = $q['bugs_per_page']; // Only if we have a name and the user is logged in if (!empty($q['name']) && ($uid > 0)) { $r = rg_bug_search_save($db, $repo_id, $uid, $q); if ($r === FALSE) break; } $sql = "SELECT * FROM bugs" . " WHERE repo_id = $repo_id" . " AND deleted = 0" . " " . implode(" ", $add) . " ORDER BY itime" . " LIMIT $limit"; // TODO: order $ret = TRUE; } while (0); if ($ret !== FALSE) $ret = rg_bug_list_query($db, $url, $sql); rg_prof_end("bug_search"); return $ret; } /* * Delete a saved search * TODO: Check rights! */ function rg_bug_search_remove($db, $repo_id, $uid, $name) { rg_prof_start("bug_search_delete"); rg_log("bug_search_remove: repo_id=$repo_id uid=$uid name=$name"); $ret = FALSE; do { $e_name = rg_sql_escape($db, $name); $sql = "DELETE FROM bug_search" . " WHERE repo_id = $repo_id" . " AND uid = $uid" . " AND name = '$e_name'"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("cannot remove search (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; } while (0); rg_prof_end("bug_search_delete"); return $ret; } /*** NOTES ***/ /* * Add a note for a bug */ function rg_bug_note_add($db, $repo_id, $bug_id, $uid, $data) { rg_prof_start("bug_note_add"); rg_log("bug_note_add: repo_id=$repo_id bug_id=$bug_id" . " data: " . rg_array2string($data)); $ret = FALSE; do { // TODO: test if user is allowed to add a note $e_data = $data; $e_data['note'] = rg_sql_escape($db, $data['note']); $itime = time(); $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : "?"; $sql = "INSERT INTO bug_notes (repo_id, bug_id, itime, uid, ip" . ", note)" . " VALUES ($repo_id, $bug_id, $itime, $uid, '$ip'" . ", '" . $e_data['note'] . "')"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("Cannot insert bug note (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; } while (0); rg_prof_end("bug_note_add"); return $ret; } /* * List notes for a bug */ function rg_bug_note_list($db, $repo_id, $bug_id, $offset) { rg_prof_start("bug_note_list"); rg_log("bug_note_list: repo_id=$repo_id bug_id=$bug_id"); $ret = FALSE; do { // TODO: test if user is allowed to see a note $sql = "SELECT * FROM bug_notes" . " WHERE repo_id = $repo_id" . " AND bug_id = $bug_id" . " ORDER BY itime" . " LIMIT 20 OFFSET $offset"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("Cannot select bug notes (" . rg_sql_error() . ")"); break; } $ret = array(); while (($row = rg_sql_fetch_array($res))) { $row['creation'] = gmdate("Y-m-d H:i", $row['itime']); $_ui = rg_user_info($db, $row['uid'], "", ""); if ($_ui['exists'] == 1) $row['owner'] = $_ui['username']; else $row['owner'] = "?"; $row['HTML:note'] = nl2br($row['note']); unset($row['note']); $ret[] = $row; } rg_sql_free_result($res); } while (0); rg_prof_end("bug_note_list"); return $ret; } /*** LABELS ***/ /* * Build an array from a string of labels */ function rg_bug_label_string2array($s) { $ret = array(); $s = preg_replace('/[\r\n\t ]/u', ',', $s); rg_log("DEBUG: s=[$s]"); $list = explode(",", $s); rg_log("DEBUG: list: " . rg_array2string($list)); if (empty($list)) return array(); foreach ($list as $label) { if (empty($label)) continue; $ret[] = $label; } return $ret; } /* * Builds a string of labels, from an array */ function rg_bug_label_array2string($a) { return implode(",", $a); } /* * Returns labels that are in the first set but not in the second */ function rg_bug_label_diff($a, $b) { $ret = array(); if (empty($a)) return array(); foreach ($a as $label) if (!isset($b[$label])) $ret[] = $label; return $ret; } /* * Get all labels for a project */ function rg_bug_label_get($db, $repo_id, $bug_id) { rg_prof_start("bug_label_get"); rg_log("bug_label_get: repo_id=$repo_id bug_id=$bug_id"); $ret = FALSE; do { $sql = "SELECT DISTINCT label FROM bug_labels" . " WHERE repo_id = $repo_id" . " AND bug_id = $bug_id" . " ORDER BY label"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("Cannot select labels (" . rg_sql_error() . ")"); break; } $ret = array(); while (($row = rg_sql_fetch_array($res))) $ret[] = $row['label']; rg_sql_free_result($res); } while(0); rg_prof_end("bug_label_get"); return $ret; } /* * Inserts labels */ function rg_bug_label_insert($db, $repo_id, $bug_id, $labels) { rg_prof_start("bug_label_insert"); rg_log("bug_label_insert: repo_id=$repo_id bug_id=$bug_id labels=$labels"); $ret = FALSE; do { $labels = rg_bug_label_string2array($labels); rg_log("DEBUG: labels: " . rg_array2string($labels)); if (empty($labels)) { $ret = TRUE; break; } $existing = rg_bug_label_get($db, $repo_id, $bug_id); rg_log("DEBUG: existing: " . rg_array2string($existing)); if ($existing === FALSE) break; $diff = rg_bug_label_diff($labels, $existing); rg_log("DEBUG: diff: " . rg_array2string($diff)); if (empty($diff)) { $ret = TRUE; break; } $list = array(); foreach ($diff as $label) { $e_label = rg_sql_escape($db, $label); $list[] = "($repo_id, $bug_id, '$e_label')"; } $sql = "INSERT INTO bug_labels (repo_id, bug_id, label)" . " VALUES " . implode(",", $list); $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_bug_set_error("Cannot insert labels (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; } while (0); rg_prof_end("bug_label_insert"); return $ret; } /* * Returns labels as HTML */ function rg_bug_label_html($db, $repo_id, $bug_id) { rg_prof_start("bug_label_html"); $labels = rg_bug_label_get($db, $repo_id, $bug_id); $a = array(); if (!empty($labels)) { foreach ($labels as $label) $a[] = array("HTML:label" => $label); } $ret = rg_template_table("repo/bug/list_labels", $a, array()); rg_prof_end("bug_label_html"); return $ret; } ?>