<?php require_once($INC . "/util.inc.php"); require_once($INC . "/log.inc.php"); require_once($INC . "/sql.inc.php"); require_once($INC . "/prof.inc.php"); $rg_wh_error = ""; function rg_wh_set_error($str) { global $rg_wh_error; $rg_wh_error = $str; rg_log($str); } function rg_wh_error() { global $rg_wh_error; return $rg_wh_error; } // Here plugins will store the functions $rg_wh_plugins = array(); $rg_wh_flags = array( 'D' => 'Hook disabled' ); $rg_wh_events = array( 'C' => 'Create repository', 'P' => 'Push', 'B' => 'Create branch' ); /* * Generates event list as html */ function rg_wh_check_events($events) { global $rg_wh_events; if (empty($rg_wh_events)) return ''; $ret = '<fieldset>'; $ret .= '<legend>Select trigger events</legend>'; $br = ''; foreach ($rg_wh_events as $id => $name) { $add = ''; if (strchr($events, $id)) $add = ' checked="checked"'; $ret .= $br . '<input type="checkbox" name="wh::idata::events[' . $id . ']"' . ' id="events-' . $id . '"' . $add . ' />' . "\n" . '<label for="events-' . $id . '">' . $name . '</label>'; $br = '<br />' . "\n"; } $ret .= '</fieldset>' . "\n"; return $ret; } /* * Generates an events list as text */ function rg_wh_events($events) { global $rg_wh_events; $a = array(); foreach ($rg_wh_events as $id => $name) { if (strchr($events, $id)) $a[] = $name; } return implode(', ', $a); } /* * Generates flags list */ function rg_wh_check_flags($all_flags, $flags) { global $rg_wh_flags; $ret = ''; $br = ''; $list = array_merge($rg_wh_flags, $all_flags); foreach ($list as $id => $name) { $add = ''; if (strchr($flags, $id)) $add = ' checked="checked"'; $ret .= $br . '<input type="checkbox" name="wh::flags[' . $id . ']"' . ' id="flags-' . $id . '"' . $add . ' />' . "\n" . '<label for="flags-' . $id . '">' . $name . '</label>'; $br = '<br />' . "\n"; } return $ret; } /* * Generates a flags list as text */ function rg_wh_flags($all_flags, $flags) { global $rg_wh_flags; $a = array(); $list = array_merge($rg_wh_flags, $all_flags); foreach ($list as $id => $name) { if (strchr($flags, $id)) $a[] = $name; } return implode(', ', $a); } /* * Some cosmetics applied to a webhook */ function rg_wh_cosmetic(&$list) { global $rg_wh_plugins; rg_log('DEBUG: rg_wh_cosmetic'); foreach ($list as $id => &$row) { $type = $row['htype']; if (isset($row['itime'])) $row['itime_nice'] = gmdate('Y-m-d H:i', $row['itime']); $row['HTML:flags_text'] = rg_wh_flags($rg_wh_plugins[$type]['flags'], $row['flags']); if (isset($row['description'])) { if (empty($row['description'])) $row['HTML:description_nice'] = 'n/a'; else $row['HTML:description_nice'] = nl2br(rg_xss_safe($row['description'])); } if (isset($row['last_output'])) { if (empty($row['last_output'])) $row['HTML:last_output_nice'] = 'n/a'; else $row['HTML:last_output_nice'] = nl2br(rg_xss_safe($row['last_output'])); } if ($rg_wh_plugins[$type]['have_events'] == 1) { if (isset($row['idata']['events'])) $row['idata']['events_text'] = rg_wh_events($row['idata']['events']); } $row['idata']['HTML:secrets_list'] = rg_template_table('user/settings/wh/secrets_show', $row['idata']['secrets'], array()); rg_wh_call_callback($type, $row['hsubtype'], 'cosmetic_pre', $xret_junk, $row, $junk); rg_wh_call_callback($type, $row['hsubtype'], 'cosmetic_post', $xret_junk, $row, $junk); } unset($row); } /* * Set last_output field of a webhook */ function rg_wh_set_last_output($db, $uid, $id, $output) { rg_prof_start('wh_set_last_output'); rg_log_enter('wh_set_last_output id=' . $id); $ret = FALSE; while (1) { $params = array('id' => $id, 'last_output' => $output); $sql = 'UPDATE webhooks' . ' SET last_output = @@last_output@@' . ' WHERE id = @@id@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_wh_set_error('cannot insert/update data'); break; } rg_sql_free_result($res); $key = 'wh' . '::' . $uid . '::' . 'list' . '::' . $id . '::' . 'last_output'; rg_cache_set($key, $output, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); rg_prof_end('wh_set_last_output'); return $ret; } /* * Sorting the webhooks list by itime DESC */ function rg_wh_sort_helper($a, $b) { if ($a['itime'] > $b['itime']) return -1; if ($a['itime'] == $b['itime']) return 0; return 1; } /* * Returns a list of webhooks associated with a user */ function rg_wh_list($db, $uid) { rg_prof_start('wh_list'); rg_log_enter('wh_list'); $ret = array('ok' => 0, 'list' => array()); while (1) { $key = 'wh' . '::' . $uid; $r = rg_cache_get($key); if (($r !== FALSE) && isset($r['LIST_LOADED'])) { $ret['list'] = $r['list']; //rg_log_ml('list: ' . print_r($ret['list'], TRUE)); $ret['ok'] = 1; break; } $params = array('uid' => $uid); $sql = 'SELECT * FROM webhooks' . ' WHERE uid = @@uid@@' . ' ORDER BY itime DESC'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_wh_set_error('cannot load data'); break; } while (($row = rg_sql_fetch_array($res))) { //rg_log_ml('DEBUG: wh_list: row: ' . print_r($row, TRUE)); $id = $row['id']; if (!empty($row['idata'])) { $row['idata'] = rg_unserialize($row['idata']); if ($row['idata'] === FALSE) { // TODO: we try to continue $row['idata'] = array(); } } else { $row['idata'] = array(); } // We used to not populate hsubtype in the past if (empty($row['hsubtype'])) { if (isset($row['idata']['url']) && stristr($row['idata']['url'], 'slack.com')) $row['hsubtype'] = 'slack'; else $row['hsubtype'] = 'generic'; } $ret['list'][$id] = $row; } rg_sql_free_result($res); $a = array('LIST_LOADED' => 1, 'list' => $ret['list']); rg_cache_merge($key, $a, RG_SOCKET_NO_WAIT); $ret['ok'] = 1; break; } uasort($ret['list'], 'rg_wh_sort_helper'); rg_log_exit(); rg_prof_end('wh_list'); return $ret; } /* * Adds/edits a webhook */ function rg_wh_add($db, $uid, $data) { rg_prof_start('wh_add'); rg_log_enter('wh_add'); $ret = array('ok' => 0); while (1) { $data['uid'] = $uid; $data['itime'] = time(); $params = $data; $params['idata'] = rg_serialize($params['idata']); if ($data['id'] == 0) { $data['last_output'] = ''; $sql = 'INSERT INTO webhooks (uid, itime' . ', htype, hsubtype, flags, repo, refname' . ', add_ip, description, idata)' . ' VALUES (@@uid@@, @@itime@@' . ', @@htype@@, @@hsubtype@@, @@flags@@, @@repo@@' . ', @@refname@@' . ', @@add_ip@@' . ', @@description@@, @@idata@@)' . ' RETURNING id'; } else { $sql = 'UPDATE webhooks' . ' SET description = @@description@@' . ', flags = @@flags@@' . ', repo = @@repo@@' . ', refname = @@refname@@' . ', idata = @@idata@@' . ' WHERE uid = @@uid@@' . ' AND id = @@id@@'; } $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_wh_set_error('cannot insert/update data'); break; } if ($data['id'] == 0) $row = rg_sql_fetch_array($res); rg_sql_free_result($res); if ($data['id'] == 0) $data['id'] = $row['id']; $key = 'wh' . '::' . $uid . '::' . 'list' . '::' . $data['id']; rg_cache_merge($key, $data, RG_SOCKET_NO_WAIT); $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('wh_add'); return $ret; } /* * Removes a list of webhooks */ function rg_wh_remove($db, $uid, $list) { rg_prof_start('wh_remove'); rg_log_enter('wh_remove'); $ret = array('ok' => 0); while (1) { if (empty($list)) { rg_wh_set_error('you did not select anything'); break; } $my_list = array(); foreach ($list as $id => $junk) $my_list[] = sprintf("%u", $id); $params = array('uid' => $uid); $sql_list = implode(', ', $my_list); $sql = 'DELETE FROM webhooks' . ' WHERE uid = @@uid@@' . ' AND id IN (' . $sql_list . ')'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_wh_set_error('cannot remove webhooks'); break; } rg_sql_free_result($res); foreach ($my_list as $junk => $id) { $key = 'wh' . '::' . $uid . '::' . 'list' . '::' . $id; rg_cache_unset($key, RG_SOCKET_NO_WAIT); } $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('wh_remove'); return $ret; } /* * Extract vars from request */ function rg_wh_fill_vars(&$rg) { global $rg_wh_plugins; $ret = FALSE; while (1) { $type = $rg['wh']['htype']; $rg['wh']['repo'] = rg_var_str('wh::repo'); $rg['wh']['flags'] = rg_var_a2s('wh::flags'); $rg['wh']['refname'] = rg_var_str('wh::refname'); $rg['wh']['itime'] = time(); $rg['wh']['add_ip'] = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; $rg['wh']['description'] = trim(rg_var_str('wh::description')); $rg['wh']['idata'] = array(); // secrets $rg['wh']['idata']['secrets'] = array(); for ($i = 0; $i < 5; $i++) { $rg['wh']['idata']['secrets'][$i]['name'] = rg_var_str('wh::idata::secrets::' . $i . '::name'); $rg['wh']['idata']['secrets'][$i]['value'] = rg_var_str('wh::idata::secrets::' . $i . '::value'); } $rg['wh']['idata']['events'] = rg_var_a2s('wh::idata::events'); // TODO // TODO: move this! if (!isset($rg_wh_plugins[$type])) { $errmsg[] = rg_template('user/settings/wh/invalid_htype', $rg, TRUE /*xss*/); break; } rg_wh_call_callback($type, $rg['wh']['hsubtype'], 'fill_vars', $xret_junk, $rg, $junk); $ret = TRUE; break; } return $ret; } /* * Validate parameters */ function rg_wh_validate_vars($rg, &$errmsg) { global $rg_wh_flags; global $rg_wh_plugins; rg_log_ml('wh_validate_vars: wh: ' . print_r($rg['wh'], TRUE)); $ret = FALSE; while (1) { $type = $rg['wh']['htype']; // Move this? if (!isset($rg_wh_plugins[$type])) { $errmsg[] = rg_template('user/settings/wh/invalid_htype.txt', $rg, TRUE /*xss*/); break; } $all_ok = TRUE; $list = array_merge($rg_wh_flags, $rg_wh_plugins[$type]['flags']); $len = strlen($rg['wh']['flags']); for ($i = 0; $i < $len; $i++) { $f = $rg['wh']['flags'][$i]; if (!isset($list[$f])) { $all_ok = FALSE; $errmsg[] = rg_template('user/settings/wh/inv_flag.txt', $rg, TRUE /*xss*/); break; } } if (!$all_ok) break; // Some hooks do not need events, for example CodeDeploy if ($rg_wh_plugins[$type]['have_events'] == 1) { if (empty($rg['wh']['idata']['events'])) { $errmsg[] = rg_template('user/settings/wh/inv_events.txt', $rg, TRUE /*xss*/); break; } } $r = rg_wh_call_callback($type, $rg['wh']['hsubtype'], 'validate_vars', $ret, $rg, $errmsg); if (($r === TRUE) && ($ret === FALSE)) break; $ret = TRUE; break; } return $ret; } /* * Generic add_form function */ function rg_wh_add_form($db, &$rg) { global $rg_wh_plugins; rg_log('DEBUG: wh_add_form'); while (1) { $type = $rg['wh']['htype']; if (!isset($rg_wh_plugins[$type])) break; $rg['HTML:check_flags'] = rg_wh_check_flags($rg_wh_plugins[$type]['flags'], $rg['wh']['flags']); if ($rg_wh_plugins[$type]['have_events'] == 1) $rg['HTML:check_events'] = rg_wh_check_events($rg['wh']['idata']['events']); $rg['HTML:secrets'] = rg_template_table('user/settings/wh/secrets', $rg['wh']['idata']['secrets'], $rg); rg_wh_call_callback($type, $rg['wh']['hsubtype'], 'add_form_pre', $xret_junk, $db, $rg); rg_wh_call_callback($type, $rg['wh']['hsubtype'], 'add_form_post', $xret_junk, $db, $rg); break; } rg_log_ml('DEBUG: AFTER wh_add_form: wh[idata]: ' . print_r($rg['wh']['idata'], TRUE)); } /* * Generic default_paras function */ function rg_wh_default_paras(&$rg) { if (!isset($rg['wh']['id'])) $rg['wh']['id'] = 0; if (!isset($rg['wh']['description'])) $rg['wh']['description'] = ''; if (!isset($rg['wh']['flags'])) $rg['wh']['flags'] = ''; if (!isset($rg['wh']['repo'])) $rg['wh']['repo'] = ''; if (!isset($rg['wh']['refname'])) $rg['wh']['refname'] = ''; if (!isset($rg['wh']['idata'])) $rg['wh']['idata'] = array(); if (!isset($rg['wh']['idata']['secrets'])) $rg['wh']['idata']['secrets'] = array(); for ($i = 0; $i < 5; $i++) if (!isset($rg['wh']['idata']['secrets'][$i])) $rg['wh']['idata']['secrets'][$i] = array('name' => '', 'value' => ''); if (!isset($rg['wh']['idata']['events'])) $rg['wh']['idata']['events'] = ''; if (!isset($rg['wh']['idata']['url_example'])) $rg['wh']['idata']['url_example'] = ''; } /* * Generic hints add function */ function rg_wh_fill_hints(&$rg, &$hints) { while (1) { $hints[]['HTML:hint'] = rg_template('user/settings/wh/hints_tags.html', $rg, TRUE /*xss*/); rg_wh_call_callback($rg['wh']['htype'], $rg['wh']['hsubtype'], 'fill_hints', $xret_junk, $rg, $hints); break; } } /* * Returns a HTML list with possible htypes */ function rg_wh_htypes($rg) { global $rg_wh_plugins; foreach ($rg_wh_plugins as $htype => &$pi) { $pi['HTML:hsubtype_list'] = ''; $add = ''; foreach ($pi['subtypes'] as $info) { $a = array( 'htype' => $htype, 'hsubtype' => $info['subtype'], 'name' => $info['name'] ); $pi['HTML:hsubtype_list'] .= $add . rg_template('user/settings/wh/subtype.html', $a, TRUE /*xss*/); $add = ' | '; } } unset($pi); asort($rg_wh_plugins); return rg_template_table('user/settings/wh/plugins_list', $rg_wh_plugins, $rg); } /* * Returns TRUE if htype is valid */ function rg_wh_valid($htype) { global $rg_wh_plugins; return isset($rg_wh_plugins[$htype]); } /* * Function used to replace ##tag## */ function rg_wh_replace_tags(&$ev) { //rg_log_ml('wh_replace_tags: ev=' . print_r($ev, TRUE)); $branch = isset($ev['refname']) ? rg_repo_ref_nice($ev['refname']) : ''; //rg_log('DEBUG: branch=' . $branch); $repo_name = isset($ev['ri']['name']) ? $ev['ri']['name'] : ''; $new_rev = isset($ev['new_rev']) ? $ev['new_rev'] : ''; if (isset($ev['wh']['idata']['content_type']) && (strcasecmp($ev['wh']['idata']['content_type'], 'application/json') == 0)) $ev['wh']['idata']['custom_body'] = @json_decode($ev['wh']['idata']['custom_body'], TRUE); $keys = array('##repo_url##', '##commit_url##', '##branch##', '##repo##', '##hook_id##', '##commit##', '##date##', '##time##', '##ip##', '##timestamp##'); $values = array($ev['ri']['url'], $ev['ri']['url'] . '/source/log/commit/' . $new_rev, $branch, $repo_name, $ev['wh']['id'], $new_rev, gmdate('Y-m-d', $ev['itime']), gmdate('H:i:s', $ev['itime']), $ev['ip'], $ev['itime']); foreach ($ev['wh']['idata'] as $var => $value) $ev['wh']['idata'][$var] = rg_str_replace($keys, $values, $value, 0); if (isset($ev['wh']['idata']['content_type']) && (strcasecmp($ev['wh']['idata']['content_type'], 'application/json') == 0)) $ev['wh']['idata']['custom_body'] = @json_encode($ev['wh']['idata']['custom_body']); //rg_log_ml('after: ' . print_r($ev['wh']['idata'], TRUE)); } /* * Used to filter hooks by repo name */ function rg_wh_repo_match($pattern, $repo_name) { rg_log('wh_repo_match pattern=[' . $pattern . ']' . ' repo_name=[' . $repo_name . ']'); $r = preg_match('`' . $pattern . '`uD', $repo_name); if ($r === FALSE) { rg_internal_error('preg_match failed: pattern=[' . $pattern . '] repo_name=' . $repo_name); return FALSE; } return $r === 1; } /* * Register a new plugin */ function rg_wh_register_type($type, $info) { global $rg_wh_plugins; if (!isset($cb['subtypes'])) $cb['subtypes'] = array(); $rg_wh_plugins[$type] = $info; } /* * Allow subtype to register with a main htype */ function rg_wh_register_subtype($type, $subtype, $name, $callbacks) { global $rg_wh_plugins; $rg_wh_plugins[$type]['subtypes'][] = array( 'subtype' => $subtype, 'name' => $name, 'cb' => $callbacks ); } /* * Returns the information for a subtype by name */ function rg_wh_find_subtype($type, $subtype) { global $rg_wh_plugins; foreach ($rg_wh_plugins[$type]['subtypes'] as $st) { if (strcmp($st['subtype'], $subtype) == 0) return $st; } return FALSE; } /* * Call plugin and subtype callbacks * Returns TRUE if callback wes called, else TRUE */ function rg_wh_call_callback($type, $subtype, $cb, &$xret, &$para1, &$para2) { global $rg_wh_plugins; rg_log_enter('wh_call_callback: type=' . $type . ' subtype=' . $subtype . ' cb=' . $cb); $ret = FALSE; while (1) { if (!isset($rg_wh_plugins[$type])) break; $pt = $rg_wh_plugins[$type]; //rg_log_ml('DEBUG: pt: ' . print_r($pt, TRUE)); if (isset($pt['cb'][$cb])) { rg_log('DEBUG: calling type callback ' . $cb); $xret = $pt['cb'][$cb]($para1, $para2); $ret = TRUE; } else { rg_log('DEBUG: type cb [' . $cb . '] not defined!'); } $si = rg_wh_find_subtype($type, $subtype); //rg_log_ml('DEBUG: si: ' . print_r($si, TRUE)); if ($si === FALSE) break; if (!isset($si['cb'][$cb])) { rg_log('DEBUG: subtype cb ' . $cb . ' is not defined!'); break; } rg_log('DEBUG: calling subtype callback ' . $cb); $xret = $si['cb'][$cb]($para1, $para2); $ret = TRUE; break; } rg_log_exit(); return $ret; } /* * Validate a custom_body of type JSON */ function rg_wh_validate_json($rg, &$errmsg) { $ret = FALSE; while (1) { $r = @json_decode($rg['wh']['idata']['custom_body'], TRUE); if ($r === NULL) { $errmsg[] = 'cannot validate json: ' . json_last_error_msg(); break; } $ret = TRUE; break; } return $ret; }