[#32] merged webhook2api (dev_32)
This commit is contained in:
commit
8aeaa3120b
|
|
@ -111,7 +111,7 @@ $rest_allowed_actions = array(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
'123.45.678.1' => array(
|
'123.45.67.8' => array(
|
||||||
'Contact' => array(
|
'Contact' => array(
|
||||||
'getsingle' => array(
|
'getsingle' => array(
|
||||||
'first_name' => 'string',
|
'first_name' => 'string',
|
||||||
|
|
@ -124,3 +124,35 @@ $rest_allowed_actions = array(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/****************************************************************
|
||||||
|
** WebHook2API CONFIGURATIONS **
|
||||||
|
****************************************************************/
|
||||||
|
# remove if you don't want this feature or rename to $webhook2api to activate
|
||||||
|
$_webhook2api = [
|
||||||
|
"configurations" => [
|
||||||
|
"default" => [
|
||||||
|
"name" => "Example",
|
||||||
|
"ip_sources" => ['172.10.0.1/24', '192.168.1.1/24'], // only accept source ID from the given range
|
||||||
|
"data_sources" => ["POST/json", "REQUEST"], // POST/json json-decodes the post data, REQUEST is PHP's $_REQUEST array
|
||||||
|
"sentinel" => [["type", "equal:customer.created"]], // only execute if all of these are true
|
||||||
|
"entity" => "Contact",
|
||||||
|
"action" => "create",
|
||||||
|
"api_key" => "api key",
|
||||||
|
"parameter_mapping" => [
|
||||||
|
[["data", "object", "metadata", "salutation"], ["prefix_id"]],
|
||||||
|
[["data", "object", "metadata", "first_name"], ["first_name"]],
|
||||||
|
[["data", "object", "metadata", "last_name"], ["last_name"]],
|
||||||
|
[["data", "object", "metadata", "street"], ["street_address"]],
|
||||||
|
[["data", "object", "metadata", "zip_code"], ["postal_code"]],
|
||||||
|
[["data", "object", "metadata", "city"], ["city"]],
|
||||||
|
[["data", "object", "metadata", "country"], ["country_id"]],
|
||||||
|
[["data", "object", "metadata", "telephone"], ["phone"]],
|
||||||
|
[["data", "object", "metadata", "birthday"], ["birth_date"]],
|
||||||
|
[["data", "object", "metadata", "email"], ["email"]]
|
||||||
|
],
|
||||||
|
"parameter_sanitation" => [],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
<?php
|
||||||
|
/*--------------------------------------------------------+
|
||||||
|
| SYSTOPIA CiviProxy |
|
||||||
|
| a simple proxy solution for external access to CiviCRM |
|
||||||
|
| Copyright (C) 2019 SYSTOPIA |
|
||||||
|
| Author: B. Endres (endres -at- systopia.de) |
|
||||||
|
| http://www.systopia.de/ |
|
||||||
|
+---------------------------------------------------------*/
|
||||||
|
|
||||||
|
require_once "config.php";
|
||||||
|
require_once "proxy.php";
|
||||||
|
|
||||||
|
// first check if webhooks are enabled
|
||||||
|
if (empty($webhook2api)) civiproxy_http_error("Feature disabled", 405);
|
||||||
|
|
||||||
|
// basic check
|
||||||
|
if (!civiproxy_security_check('webhook2api')) {
|
||||||
|
civiproxy_http_error("Access denied", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the right configuration
|
||||||
|
if (!empty($_REQUEST['id']) && isset($webhook2api['configurations'][$_REQUEST['id']])) {
|
||||||
|
// we found the if in the configurations
|
||||||
|
$configurations = [$webhook2api['configurations'][$_REQUEST['id']]];
|
||||||
|
} elseif (empty($_REQUEST['id']) && isset($webhook2api['configurations']['default'])) {
|
||||||
|
// this is teh default configuration
|
||||||
|
$configurations = [$webhook2api['configurations']['default']];
|
||||||
|
} else {
|
||||||
|
// use all of them (first one matching is executed)
|
||||||
|
$configurations = $webhook2api['configurations'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// read some input
|
||||||
|
$post_input = @file_get_contents('php://input');
|
||||||
|
error_log("DEBUG: " . json_encode($post_input));
|
||||||
|
|
||||||
|
// MAIN: iterate through all (eligible) configurations
|
||||||
|
$last_error = ["No handler found", 501];
|
||||||
|
foreach ($configurations as $configuration) {
|
||||||
|
$last_error = webhook2api_processConfiguration($configuration, $post_input);
|
||||||
|
if ($last_error == NULL) {
|
||||||
|
// success!
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally - if there was only errors, return the last one
|
||||||
|
if ($last_error) {
|
||||||
|
civiproxy_http_error($last_error[0], $last_error[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the given configuration. If it matches and executes,
|
||||||
|
* it returns NULL, otherwise
|
||||||
|
*
|
||||||
|
* @param $configuration array configuration/specification
|
||||||
|
* @return null|array [status_code, error message]
|
||||||
|
*/
|
||||||
|
function webhook2api_processConfiguration($configuration, $post_input) {
|
||||||
|
// check the IP/range restrictions
|
||||||
|
if (!empty($configuration['ip_sources']) && is_array($configuration['ip_sources'])) {
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||||||
|
$access_granted = FALSE;
|
||||||
|
foreach ($configuration['ip_sources'] as $netmask) {
|
||||||
|
// copied from https://secure.php.net/manual/de/ref.network.php
|
||||||
|
list ($net, $mask) = explode("/", $netmask);
|
||||||
|
$ip_net = ip2long ($net);
|
||||||
|
$ip_mask = ~((1 << (32 - $mask)) - 1);
|
||||||
|
$ip_ip = ip2long ($ip);
|
||||||
|
$ip_ip_net = $ip_ip & $ip_mask;
|
||||||
|
if ($ip_ip_net == $ip_net) {
|
||||||
|
$access_granted = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$access_granted) {
|
||||||
|
// this configuration is not eligible
|
||||||
|
return ["Access denied", 403];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gather source data
|
||||||
|
$data = [];
|
||||||
|
if (!empty($configuration['data_sources']) && is_array($configuration['data_sources'])) {
|
||||||
|
error_log(json_encode($configuration));
|
||||||
|
foreach ($configuration['data_sources'] as $data_source) {
|
||||||
|
switch ($data_source) {
|
||||||
|
case 'POST/json': # JSON data in POST field
|
||||||
|
$more_data = json_decode($post_input, TRUE);
|
||||||
|
error_log(json_encode($more_data));
|
||||||
|
$data = array_merge_recursive($data, $more_data);
|
||||||
|
break;
|
||||||
|
case 'REQUEST': # simple request parameters
|
||||||
|
$data = array_merge_recursive($data, $_REQUEST);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
civiproxy_log("Webhook2API[{$configuration['name']}]: unknown source '{$data_source}' in configuration. Ignored.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default return code if everything goes according to plan
|
||||||
|
$http_code = 200;
|
||||||
|
// check if we have a json_array and react accordingly
|
||||||
|
if (isset($data[0]) && is_array($data[0])) {
|
||||||
|
foreach ($data as $d) {
|
||||||
|
$result = webhook2api_callCiviApi($configuration, $d);
|
||||||
|
if(isset($result['internal_error'])) {
|
||||||
|
// internal communication Error occured. Aborting process
|
||||||
|
civiproxy_log("Webhook2API[{$configuration['name']}]: internal error occured: " . json_encode($result['internal_error']));
|
||||||
|
return $result['internal_error'];
|
||||||
|
}
|
||||||
|
if (!empty($result['values']['http_code'])) {
|
||||||
|
$http_code = $result['values']['http_code'];
|
||||||
|
} else {
|
||||||
|
$http_code = 403;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$result = webhook2api_callCiviApi($configuration, $data);
|
||||||
|
if(isset($result['internal_error'])) {
|
||||||
|
// internal communication Error occured. Aborting process
|
||||||
|
civiproxy_log("Webhook2API[{$configuration['name']}]: internal error occured: " . json_encode($result['internal_error']));
|
||||||
|
return $result['internal_error'];
|
||||||
|
}
|
||||||
|
if (!empty($result['values']['http_code'])) {
|
||||||
|
$http_code = $result['values']['http_code'];
|
||||||
|
} else {
|
||||||
|
$http_code = 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($http_code != '200') {
|
||||||
|
// we received and parsed the webhook event successfully, but an error occured with civicrm:
|
||||||
|
civiproxy_log("Webhook2API[{$configuration['name']}]: Internal CiviCRM Error. Error Code: {$http_code}. Full Message: " . json_encode($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// process result
|
||||||
|
if (!empty($configuration['response_mapping']) && is_array($configuration['response_mapping'])) {
|
||||||
|
// TODO: implement
|
||||||
|
//error_log("Webhook2API.response_mapping: not implemented!");
|
||||||
|
http_response_code('200');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// default behaviour:
|
||||||
|
http_response_code('200');
|
||||||
|
}
|
||||||
|
// all done
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse Configuration and given data set, apply it and send it to civicrm.
|
||||||
|
* Returns an internal error if communication to civicrm isn't successful
|
||||||
|
*
|
||||||
|
* @param $configuration
|
||||||
|
* @param $data
|
||||||
|
*
|
||||||
|
* @return array|mixed|void
|
||||||
|
* @throws \CiviCRM_API3_Exception
|
||||||
|
*/
|
||||||
|
function webhook2api_callCiviApi($configuration, $data) {
|
||||||
|
// evaluate sentinels
|
||||||
|
if (!empty($configuration['sentinel']) && is_array($configuration['sentinel'])) {
|
||||||
|
foreach ($configuration['sentinel'] as $sentinel) {
|
||||||
|
list($value_source, $check) = $sentinel;
|
||||||
|
$value = webhook2api_getValue($data, $value_source);
|
||||||
|
if (substr($check, 0, 6) == "equal:") {
|
||||||
|
// check if terms a equal
|
||||||
|
if (substr($check, 6) != $value) {
|
||||||
|
return ["internal_error" => "Access denied", 403];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "Error";
|
||||||
|
// unknown instruction
|
||||||
|
// //error_log("Webhook2API[{$configuration['name']}]: don't understad sentinel '{$check}'. Ignored.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile API query
|
||||||
|
$params = [];
|
||||||
|
if (!empty($configuration['parameter_mapping']) && is_array($configuration['parameter_mapping'])) {
|
||||||
|
foreach ($configuration['parameter_mapping'] as $mapping) {
|
||||||
|
$source_path = $mapping[0];
|
||||||
|
$target_path = $mapping[1];
|
||||||
|
$modifiers = isset($mapping[2]) ? $mapping[2] : [];
|
||||||
|
|
||||||
|
// get value
|
||||||
|
$value = webhook2api_getValue($data, $source_path);
|
||||||
|
|
||||||
|
// run modifiers
|
||||||
|
foreach ($modifiers as $modifier) {
|
||||||
|
// TODO: implement
|
||||||
|
//error_log("Webhook2API.modifiers: not implemented!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// set to target
|
||||||
|
webhook2api_setValue($params, $target_path, $value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$params = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitise data
|
||||||
|
if (!empty($configuration['parameter_sanitation']) && is_array($configuration['parameter_sanitation'])) {
|
||||||
|
// TODO: implement
|
||||||
|
//error_log("Webhook2API.sanitation: not implemented!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// send to target REST API
|
||||||
|
if (empty($configuration['entity']) || empty($configuration['action'])) {
|
||||||
|
//error_log("Webhook2API[{$configuration['name']}]: Missing entity/action.");
|
||||||
|
return ["internal_error" => "Configuration error", 403];
|
||||||
|
}
|
||||||
|
if (empty($configuration['api_key'])) {
|
||||||
|
//error_log("Webhook2API[{$configuration['name']}]: Missing api_key.");
|
||||||
|
return ["internal_error" => "Configuration error", 403];
|
||||||
|
}
|
||||||
|
$params['api_key'] = $configuration['api_key'];
|
||||||
|
|
||||||
|
// run API call
|
||||||
|
return civicrm_api3($configuration['entity'], $configuration['action'], $params);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value from a multidimensional array,
|
||||||
|
* specified by the path
|
||||||
|
*
|
||||||
|
* @param $data array multidimensional data array
|
||||||
|
* @param $path array|string path description
|
||||||
|
* @return mixed value
|
||||||
|
*/
|
||||||
|
function webhook2api_getValue($data, $path) {
|
||||||
|
if (is_string($path)) {
|
||||||
|
if (isset($data[$path])) {
|
||||||
|
return $data[$path];
|
||||||
|
} else {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
} elseif (is_array($path)) {
|
||||||
|
if (count($path) == 0) {
|
||||||
|
return NULL;
|
||||||
|
} elseif (count($path) == 1) {
|
||||||
|
return webhook2api_getValue($data, $path[0]);
|
||||||
|
} else {
|
||||||
|
$path_element = array_shift($path);
|
||||||
|
$sub_data = webhook2api_getValue($data, $path_element);
|
||||||
|
if (is_array($sub_data)) {
|
||||||
|
return webhook2api_getValue($sub_data, $path);
|
||||||
|
} else {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value from a multidimensional array as specified by the path
|
||||||
|
*
|
||||||
|
* @param $data array the data
|
||||||
|
* @param $target_path array destination
|
||||||
|
* @param $value mixed value
|
||||||
|
*/
|
||||||
|
function webhook2api_setValue(&$data, $target_path, $value) {
|
||||||
|
if (is_array($target_path)) {
|
||||||
|
if (count($target_path) == 0) {
|
||||||
|
civiproxy_log("Webhook2API.setValue: Empty target path!");
|
||||||
|
return;
|
||||||
|
|
||||||
|
} elseif (count($target_path) == 1) {
|
||||||
|
// last element -> set value
|
||||||
|
$data[$target_path[0]] = $value;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// not last element
|
||||||
|
$element = array_shift($target_path);
|
||||||
|
if (!isset($data[$element])) {
|
||||||
|
$data[$element] = [];
|
||||||
|
}
|
||||||
|
if (is_array($data[$element])) {
|
||||||
|
webhook2api_setValue($data[$element], $target_path, $value);
|
||||||
|
} else {
|
||||||
|
civiproxy_log("Webhook2API.setValue: path node is not an array!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} elseif (is_string($target_path)) {
|
||||||
|
webhook2api_setValue($data, [$target_path], $value);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
civiproxy_log("Webhook2API.setValue: path neither string nor array!");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue