-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathrules_web_remote.inc
506 lines (441 loc) · 14.9 KB
/
rules_web_remote.inc
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
<?php
// $Id$
/**
* @file Rules Remote Sites - Include file.
*/
/**
* Menu callback for notifying us of a remote event.
*/
function rules_web_remote_notify($remote) {
if (($json = file_get_contents('php://input')) && $data = @drupal_json_decode($json)) {
// Check message token.
if (!rules_web_remote_valid_message($remote, $data)) {
drupal_add_http_header('Status', '403 Forbidden');
return;
}
$events = $remote->events();
if (!empty($data['event_name']) && !empty($data['event_data']) && isset($events[$data['event_name']])) {
$name = 'rules_web_' . $remote->name . '_' . $data['event_name'];
if ($event_set = rules_get_cache('event_' . $name)) {
// Make sure passed arguments are complete and invoke event.
$event_args = $event_set->argumentInfo();
if (($args = array_intersect_key((array)$data['event_data'], $event_args)) && count($args) == count($event_args)) {
// Make sure data passed for entities is an object.
$info = entity_get_info();
foreach ($args as $name => $value) {
if (isset($info[$event_args[$name]['type']]) && is_array($value)) {
$args[$name] = (object)$value;
}
}
$event_set->execute($args);
$t_args = array('%event' => $data['event_name'], '%name' => $remote->label, '!log' => RulesLog::logger()->render());
watchdog('rules web remote', 'Event %event of remote %name triggered rule evaluation: !log', $t_args, WATCHDOG_NOTICE);
}
}
else {
// We don't have any configured rules, so unsubscribe.
$remote->unsubscribe($data['event_name']);
}
}
elseif (isset($data['handshake'])) {
// Accept handshakes to verify that this is a valid callback.
return TRUE;
}
else {
drupal_add_http_header('Status', '404 Not Found');
}
}
else {
drupal_add_http_header('Status', '415 Unsupported Media Type');
}
}
/**
* Checks whether the provided message is a valid event notification by checking
* the token.
*/
function rules_web_remote_valid_message($remote, $data) {
if (isset($data['time']) && isset($data['token'])) {
// We don't accept too old messages.
if (($time = (int)$data['time']) + 3600 > time()) {
return $data['token'] === md5($time . md5($remote->token));
}
}
return FALSE;
}
/**
* Class representing remote sites.
*/
class RulesWebRemote extends EntityDBExtendable {
public $settings = array();
public $name;
public $label;
public $url;
public $token;
protected $endpoint = NULL;
protected $info = array();
protected $subscriptions = array();
function __construct($values = array(), $entity_type = 'rules_web_remote') {
parent::__construct($values, $entity_type);
// Make sure there is a token for communicating with the remote.
if (!isset($this->token)) {
$this->token = drupal_get_token(time());
}
}
/**
* Determines access to the remote.
*/
public function access($account = NULL) {
if (method_exists($this->endpoint(), 'access')) {
return $this->endpoint()->access($account);
}
return user_access('interact with remote ' . $this->name, $account);
}
/**
* Returns the associated remote endpoint object.
*
* @return RulesWebRemoteEndpointInterface
*/
public function endpoint() {
if (!isset($this->endpoint)) {
$types = rules_web_remote_get_types();
$this->endpoint = new $types[$this->type]['class']($this);
}
return $this->endpoint;
}
/**
* Loads the entity of the given type and id. In case of errors a
* RulesException is thrown.
*/
public function load($type, $id) {
try {
return $this->endpoint()->load($type, $id);
}
catch (Exception $e) {
$args = array('%name' => $this->name, '%message' => $e->getMessage(), '%type' => $type, '%id' => print_r($id,1));
throw new RulesException('Unable to load %id of type %type from the remote site %name. Error message: %message', $args, NULL, RulesLog::WARN);
}
}
/**
* Returns info about events of the remote site.
*/
public function events() {
if (empty($this->info)) {
$this->refreshInfo();
}
return !empty($this->info['events']) ? $this->info['events'] : array();
}
/**
* Returns info about the entities of the remote site.
*/
public function entities() {
if (empty($this->info)) {
$this->refreshInfo();
}
return !empty($this->info['entities']) ? $this->info['entities'] : array();
}
/**
* Returns info about the data types of the remote site, thus types being not
* entities.
*/
public function dataTypes() {
if (empty($this->info)) {
$this->refreshInfo();
}
return !empty($this->info['dataTypes']) ? $this->info['dataTypes'] : array();
}
/**
* Returns info about the actions of the remote site.
*/
public function actions() {
if (empty($this->info)) {
$this->refreshInfo();
}
return !empty($this->info['actions']) ? $this->info['actions'] : array();
}
/**
* Returns info about the conditions of the remote site.
*/
public function conditions() {
if (empty($this->info)) {
$this->refreshInfo();
}
return !empty($this->info['conditions']) ? $this->info['conditions'] : array();
}
protected function refreshInfo() {
try {
$this->info['entities'] = $this->endpoint()->entities();
$this->info['dataTypes'] = $this->endpoint()->dataTypes();
$this->info['actions'] = $this->endpoint()->actions();
$this->info['conditions'] = $this->endpoint()->conditions();
$this->info['events'] = $this->endpoint()->events();
$this->storeInfo();
}
catch (Exception $e) {
$args = array('%name' => $this->name, '%message' => $e->getMessage());
watchdog('rules remote', 'Error getting definitions from the remote site %name. Error message: %message.', $args, WATCHDOG_ALERT);
}
}
public function clearCache() {
$this->info = array();
}
public function isSubscribedTo($event) {
return isset($this->subscriptions[$event]);
}
/**
* Subscribes to the given event, so we get event notitifcations whenever the
* event occurs.
*/
public function subscribe($event) {
// Make sure the info is retrieved and thus the token is stored, so the
// token won't change in future. $this->events() does that for us.
if (($events = $this->events()) && isset($events[$event])) {
try {
$this->endpoint()->subscribe($event);
$this->subscriptions[$event] = TRUE;
db_merge('rules_web_remote_subscriptions')
->key(array(
'name' => $this->name,
'event' => $event,
))
->execute();
}
catch (Exception $e) {
$args = array('%event' => $event, '%name' => $this->name, '%message' => $e->getMessage());
watchdog('rules remote', 'Error subscribing to event %event of remote site %name. Error message: %message.', $args, WATCHDOG_ERROR);
}
}
}
/**
* Unsubscribes from the given event.
*/
public function unsubscribe($event) {
try {
$this->endpoint()->unsubscribe($event);
unset($this->subscriptions[$event]);
db_delete('rules_web_remote_subscriptions')
->condition('name', $this->name)
->condition('event', $event)
->execute();
}
catch (Exception $e) {
$args = array('%event' => $event, '%name' => $this->name, '%message' => $e->getMessage());
watchdog('rules remote', 'Error unsubscribing from event %event of remote site %name. Error message: %message.', $args, WATCHDOG_ERROR);
}
}
/**
* Stores the token and remote metadata independent from the remote site
* object itself, so a remote site may live in code via the default hook but
* still store this information.
*/
protected function storeInfo() {
db_merge('rules_web_remote_info')
->key(array('name' => $this->name))
->fields(array(
'info' => serialize($this->info),
'token' => $this->token,
))
->execute();
}
public function save() {
parent::save();
$this->storeInfo();
}
/**
* Loads the stored info for the given remote site objects.
*/
public static function attachLoad($remotes) {
$result = db_select('rules_web_remote_info', 'r')
->fields('r')
->condition('name', array(array_keys($remotes)))
->execute();
foreach ($result as $record) {
$remotes[$record->name]->info = unserialize($record->info);
$remotes[$record->name]->token = $record->token;
}
// Load subscribed events.
$result = db_select('rules_web_remote_subscriptions', 'r')
->fields('r')
->condition('name', array(array_keys($remotes)))
->execute();
foreach ($result as $record) {
$remotes[$record->name]->subscriptions[$record->event] = TRUE;
}
}
}
/**
* Implements hook_rules_web_remote_load().
*/
function rules_web_remote_rules_web_remote_load($remotes) {
RulesWebRemote::attachLoad($remotes);
}
/**
* Implements hook_default_rules_web_remote_alter().
*/
function rules_web_remote_default_rules_web_remote_alter($remotes) {
RulesWebRemote::attachLoad($remotes);
}
/**
* Interface for remote endpoints. In case of any errors the implementing
* classes should throw exceptions.
*/
interface RulesWebRemoteEndpointInterface {
public function __construct(RulesWebRemote $remote);
/**
* Load remote data.
*/
public function load($type, $id);
/**
* An array of definitions for the provided events.
*/
public function events();
/**
* Subscribe to a remote event.
*/
public function subscribe($event);
/**
* Unsubscribe from a remote event.
*/
public function unsubscribe($event);
/**
* An array of info about entity types used by the provided
* events/conditions/actions.
*/
public function entities();
/**
* An array of info about data types used by the provided events/conditions/
* actions being not entities.
*/
public function dataTypes();
/**
* An array of definitions for the provided actions.
*/
public function actions();
/**
* An array of definitions for the provided conditions.
*/
public function conditions();
/**
* Allows altering the configuration form of remote site definitions, such
* that the form can include endpoint type specific configuration settings.
*/
public function formAlter(&$form, &$form_state);
}
/**
* A remote endpoint types for rules web hooks.
*/
class RulesWebRemoteEndpointWebHooks implements RulesWebRemoteEndpointInterface {
/**
* @var RulesWebRemote
*/
protected $remote, $url;
/**
* @var RestClient
*/
protected $client;
public function __construct(RulesWebRemote $remote, $base_path = 'rules_web') {
$this->remote = $remote;
$this->url = $remote->url . '/' . $base_path . '/';
}
public function client() {
if (!isset($this->client)) {
$this->client = new RestClient(NULL, new RestClientBaseFormatter(RestClientBaseFormatter::FORMAT_JSON));
// Pass through additional curl options.
if (!empty($this->remote->settings['curl options'])) {
$this->client->curlOpts = $this->remote->settings['curl options'];
}
$this->client->curlOpts += variable_get('rules_web_custom_curl_options', array());
}
return $this->client;
}
public function load($type, $id) {
if (valid_url($url = $this->url . "$type/$id.json", TRUE)) {
return (object)$this->client()->get($url);
}
}
public function events() {
return $this->client()->get($this->url . "rules_web_hook.json");
}
public function subscribe($event) {
if (valid_url($url = $this->url . "rules_web_hook/$event/subscribe.json", TRUE)) {
$this->client()->post($url, array(
'url' => url('rules_web/rules_web_remote/' . $this->remote->name . '/notify', array('absolute' => TRUE)),
'http_auth' => rules_web_remote_get_http_auth(),
'token' => $this->remote->token,
));
}
}
public function unsubscribe($event) {
if (valid_url($url = $this->url . "rules_web_hook/$event/unsubscribe.json", TRUE)) {
$this->client()->post($url, array(
'url' => url('rules_web/rules_web_remote/' . $this->remote->name . '/notify', array('absolute' => TRUE)),
'token' => $this->remote->token,
));
}
}
public function entities() {
return $this->client()->get($this->url . 'entity_metadata.json');
}
public function actions() { }
public function conditions() { }
public function dataTypes() { }
public function formAlter(&$form, &$form_state) { }
}
/**
* A controller for loading remote data.
*/
class RulesWebRemoteEntityController extends DrupalDefaultEntityController {
protected $remote;
function __construct($entityType) {
parent::__construct($entityType);
$this->remote = rules_web_remote_load($this->entityInfo['rules web remote']['remote']);
}
/**
* Override load to fetch the data from the remote site. For now we don't
* support using $conditions or revisions.
*/
public function load($ids = array(), $conditions = array()) {
$entities = array();
// Create a new variable which is either a prepared version of the $ids
// array for later comparison with the entity cache, or FALSE if no $ids
// were passed. The $ids array is reduced as items are loaded from cache,
// and we need to know if it's empty for this reason to avoid querying the
// database when all requested entities are loaded from cache.
$passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
// Try to load entities from the static cache, if the entity type supports
// static caching.
if ($this->cache) {
$entities += $this->cacheGet($ids, $conditions);
// If any entities were loaded, remove them from the ids still to load.
if ($passed_ids) {
$ids = array_keys(array_diff_key($passed_ids, $entities));
}
}
// Load any remaining entities from the remote site.
if ($ids === FALSE || $ids) {
foreach ($ids as $id) {
$queried_entities[$id] = $this->remote->load($this->entityInfo['rules web remote']['type'], $id);
}
}
// Pass all entities loaded through $this->attachLoad(),
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
if (!empty($queried_entities)) {
$this->attachLoad($queried_entities);
$entities += $queried_entities;
}
if ($this->cache && !empty($queried_entities)) {
$this->cacheSet($queried_entities);
}
// Ensure that the returned array is ordered the same as the original
// $ids array if this was passed in and remove any invalid ids.
if ($passed_ids) {
// Remove any invalid ids from the array.
$passed_ids = array_intersect_key($passed_ids, $entities);
foreach ($entities as $entity) {
$passed_ids[$entity->{$this->idKey}] = $entity;
}
$entities = $passed_ids;
}
return $entities;
}
}