-
Notifications
You must be signed in to change notification settings - Fork 1
/
extension.driver.php
370 lines (335 loc) · 11 KB
/
extension.driver.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
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
<?php
require_once TOOLKIT.'/class.sectionmanager.php';
require_once TOOLKIT.'/class.gateway.php';
/**
* @package extensions/webhooks
*/
/**
* A simple Symphony extension that allows developers to assocate WebHooks with content publishing
* events. These hooks are assigned to a content section and then to a specific event delegate for
* the content associated within this section: PUT, POST, DELETE. If a matching event occurs within
* an assigned content section, this extension will send a push notification to the specified URL
* with the event type and all information associated with the content entry.
*/
class Extension_WebHooks extends Extension {
/**
* Instantiates this class and assigns values to properties.
*
* @access public
* @param array $args
* Any arguments passed via URL query string with be relayed through here.
* @return NULL
*/
public function __construct(array $args){
parent::__construct($args);
}
/**
* Method that provides metadata relevant to this extension.
*
* @access public
* @param none
* @return array
* Extension metadata.
*/
public function about() {
return array(
'name' => 'WebHooks',
'version' => '0.0.1',
'release-date' => '2011-09-01',
'author' => array(
'name' => 'Wilhelm Murdoch',
'website' => 'http://thedrunkenepic.com/',
'email' => '[email protected]'
)
);
}
/**
* Installs this extension by adding the appropriate tables to the database.
*
* @access public
* @param none
* @return boolean
* TRUE if successful, FALSE if failed.
*/
public function install() {
return Symphony::Database()->import("
DROP TABLE IF EXISTS `sym_extensions_webhooks`;
CREATE TABLE `sym_extensions_webhooks` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`label` varchar(64) DEFAULT NULL,
`section_id` int(11) DEFAULT NULL,
`verb` enum('POST','PUT','DELETE') DEFAULT NULL,
`callback` varchar(255) DEFAULT NULL,
`is_active` tinyint(1) DEFAULT 1,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
");
}
/**
* Uninstalls this extension by removing the appropriate tables from the database.
*
* @access public
* @param none
* @return boolean
* TRUE if successful, FALSE if failed.
*/
public function uninstall() {
return Symphony::Database()->import("
DROP TABLE IF EXISTS `sym_extensions_webhooks`
");
}
/**
* Adds a navigation entry at the specified position to the administrative control panel.
*
* @access public
* @param none
* @return array
* Menu entries and their associated locations, positions within existing menus.
*/
public function fetchNavigation() {
return array(
array(
'location' => __('System'),
'name' => __('WebHooks'),
'link' => '/hooks/'
)
);
}
/**
* Utility method for grabbing the base URL for the WebHook management area.
*
* @access public
* @param none
* @return string
* Base URL for WebHook management.
*/
public static function baseURL(){
return SYMPHONY_URL . '/extension/webhooks/hooks';
}
/**
* The delegates, and associated callbacks, used by this extension. This extension
* essentially watches for edited, removed and created content from ALL sections.
*
* @access public
* @param none
* @return array
* Array containing a list of delegates and their associated callbacks for this extensiono.
*/
public function getSubscribedDelegates() {
return array(
array(
'page' => '/publish/new/',
'delegate' => 'EntryPostCreate',
'callback' => '__pushNotification'
),
array(
'page' => '/publish/edit/',
'delegate' => 'EntryPostEdit',
'callback' => '__pushNotification'
),
array(
'page' => '/publish/',
'delegate' => 'Delete',
'callback' => '__pushNotification'
)
);
}
/**
* Responsible for sending push notifications for all active WebHooks.
*
* @access public
* @param array $context
* The current Symphony context.
* @return NULL
* @uses Gateway
* @uses Entry
* @uses Log
* @uses Section
*/
public function __pushNotification(array $context) {
/**
* Determine the proper HTTP method verb based on the given Symphony delegate:
*/
switch($context['delegate']) {
case 'EntryPostEdit':
$verb = 'PUT';
break;
case 'Delete':
$verb = 'DELETE';
break;
case 'EntryPostCreate':
default:
$verb = 'POST';
}
/**
* POST, PUT, DELETE action has been intercepted.
*
* @delegate WebHookInit
* @param string $context
* '/publish/'
* @param Section $Section
* @param Entry $Entry
* @param string $verb
*/
Symphony::ExtensionManager()->notifyMembers('WebHookInit', '/publish/', array('section' => $Section, 'entry' => $Entry, 'verb' => $verb));
/**
* Grab all active WebHooks so we can begin cycling through them;
*/
$webHooks = Symphony::Database()->fetch('
SELECT
`id`,
`label`,
`section_id`,
`verb`,
`callback`,
`is_active`
FROM `sym_extensions_webhooks`
WHERE `is_active` = TRUE
');
/**
* Obviously, we don't want to go any farther if we don't have any active
* WebHooks to iterate through:
*/
if(false == count($webHooks))
return;
/**
* Declare a few variables we'll be using throughout the rest of the process:
*
* $section: an instance of class `Section` containing the current entry's associated Symphony section `class.section.php`
* $entry: an instance of class `Entry` containing the current Symphony entry `class.entry.php`
* $Gateway: an instance of class `Gateway`, Symphony's HTTP request utility `class.gateway.php`
* $Log: an instance of class `Log`, Symphony's logging utility. We use this to track any issues we might come accross
*/
$pageCallback = Administration::instance()->getPageCallback();
$section = $this->__getSectionByHandle($pageCallback['context']['section_handle']);
if($verb != 'DELETE') {
$entry = $context['entry']->getData();
}
$Gateway = new Gateway;
$Gateway->init();
$Gateway->setopt('HTTPHEADER', array('Content-Type:' => 'application/json'));
$Gateway->setopt('TIMEOUT', __NOTIFICATION_TIMEOUT);
$Gateway->setopt('POST', TRUE);
$Log = new Log(__NOTIFICATION_LOG);
/**
* Begin iterating through our active WebHooks. Only send push notifications using WebHooks that contain
* a `section_id` and `verb` that corresponds with the current entry:
*/
foreach($webHooks as $webHook) {
if($section['id'] == $webHook['section_id'] && $webHook['verb'] == $verb) {
/**
* Being the notification process by setting the appropriate request options:
*
* URL: This is the destination of our notification
* POSTFIELDS: This contains the body of our notification: verb: $webHook['verb'], callback: $webHook['callback'], body: JSON-encoded string representing our entry
*/
$Gateway->setopt('URL', $webHook['callback']);
$Gateway->setopt('POSTFIELDS', ($verb == 'DELETE' ? json_encode($section) : $this->__compilePayload($context['section'], $context['entry'], $webHook)));
//echo "<pre>";
//var_dump($this->__compilePayload($context['section'], $context['entry'], $webHook));
//echo "</pre>";
//die();
/**
* Obviously, we don't want to continue if something goes wrong. So, let's log this error
* and move on to the next active WebHook:
*
* @todo Probably best to use exception handling for this stuff; possibly create a WebHook exception class.
*/
if(false === $response = $Gateway->exec()) {
$Log->pushToLog(
sprintf(
'Notification Failed[cURL Error]: section: %d, entry: %d, verb: %d, url: %d',
$section['id'],
($verb == 'DELETE' ? $context['entry_id'][0] : $context['entry']->get('id')),
$verb,
$webHook['callback']
), E_ERROR, true
);
continue;
} else {
/**
* We need to make sure we have a valid response from our URL. At the moment, we consider only responses with the
* HTTP response code of 200 OK.
*
* @todo Allow for more extensive error checking and better HTTP response support.
*/
$responseInfo = $Gateway->getInfoLast();
if($responseInfo['http_code'] != 200) {
$Log->pushToLog(
sprintf(
'Notification Failed[Response Code: %s]: section: %d, entry: %d, verb: %s, url: %s',
$responseInfo['http_code'],
$section['id'],
($verb == 'DELETE' ? $context['entry_id'][0] : $context['entry']->get('id')),
$verb,
$webHook['callback']
), E_ERROR, true
);
continue;
}
}
}
}
}
/**
* Responsible for compiling the body of the notification payload.
*
* @access private
* @param object $Section
* An instance of class `Section` representing the current entry's associated Symphony section.
* @param object $Entry
* An instance of class `Entry` representing the current Symphony entry
* @param array $webHook
* The current active WebHook we are preparing the notification payload for.
* @return array
* Contains the current payload that will be passed along the notification in the POST body of the request.
*/
private function __compilePayload(Section $Section, Entry $Entry, array $webHook) {
$body = array();
$data = $Entry->getData();
$entry = $Entry->get();
foreach($Section->fetchFieldsSchema() as $field) {
$field['value'] = $data[$field['id']];
$body[] = $field;
}
$return = array(
'verb' => $webHook['verb'],
'callback' => $webHook['callback'],
'entry' => json_encode($entry),
'body' => json_encode(array_values($body))
);
/**
* Notification body has been created.
*
* @delegate WebHookBodyCompile
* @param string $context
* '/publish/'
* @param Section $Section
* @param Entry $Entry
* @param array $webHook
* @param array $return
*/
Symphony::ExtensionManager()->notifyMembers('WebHookBodyCompile', '/publish/', array('section' => $Section, 'entry' => $Entry, 'webhook' => &$webHook, 'return' => &$return));
return $return;
}
/**
* Returns a section record from the given section handle value.
*
* @access private
* @param string $sectionHandle
* The handle of the section to search for.
* @return array
* Associative array containing the database record of the matching section.
*/
private function __getSectionByHandle($sectionHandle) {
return current(Symphony::Database()->fetch("SELECT `id` FROM `sym_sections` WHERE `handle` = '{$sectionHandle}'"));
}
}
/**
* Absolute file path to the WebHooks log file (Must be writable!):
*/
define_safe('__NOTIFICATION_LOG', EXTENSIONS.'/webhooks/logs/main');
/**
* Request timeout, in seconds, for push notifications.
*/
define_safe('__NOTIFICATION_TIMEOUT', 15);