-
Notifications
You must be signed in to change notification settings - Fork 18
/
webhook2api.php
298 lines (266 loc) · 9.56 KB
/
webhook2api.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
<?php
/*--------------------------------------------------------+
| SYSTOPIA CiviProxy |
| a simple proxy solution for external access to CiviCRM |
| Copyright (C) 2019-2021 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');
// 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'])) {
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);
$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 occurred. Aborting process
civiproxy_log("Webhook2API[{$configuration['name']}]: internal error occurred: " . 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 occurred. Aborting process
civiproxy_log("Webhook2API[{$configuration['name']}]: internal error occurred: " . json_encode($result['internal_error']));
return $result['internal_error'];
}
if (!empty($result['values']['http_code'])) {
$http_code = $result['values']['http_code'];
} elseif ($result['is_error'] != 0) {
$http_code = 403;
}
}
if ($http_code != '200') {
// we received and parsed the webhook event successfully, but an error occurred 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
http_response_code('200');
civiproxy_log("Webhook successful, response mapped.");
} else {
// default behaviour:
http_response_code('200');
civiproxy_log("Webhook successful.");
}
// 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
civiproxy_log("Webhook2API[{$configuration['name']}]: don't understand 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
civiproxy_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
civiproxy_log("Webhook2API.sanitation: not implemented!");
}
// send to target REST API
if (empty($configuration['entity']) || empty($configuration['action'])) {
civiproxy_log("Webhook2API[{$configuration['name']}]: Missing entity/action.");
return ["internal_error" => "Configuration error", 403];
}
if (empty($configuration['api_key'])) {
civiproxy_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!");
}
}