-
Notifications
You must be signed in to change notification settings - Fork 173
/
restful.module
589 lines (537 loc) · 19.6 KB
/
restful.module
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
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
<?php
/**
* @file
* Turn Drupal to a RESTful server, following best practices.
*/
include_once __DIR__ . '/restful.entity.inc';
include_once __DIR__ . '/restful.cache.inc';
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Plugin\PluginBase;
use Drupal\restful\Exception\RestfulException;
use Drupal\restful\Http\HttpHeader;
use Drupal\restful\Http\RequestInterface;
use Drupal\restful\Http\Response;
use Drupal\restful\Http\ResponseInterface;
use Drupal\restful\Plugin\resource\Decorators\CacheDecoratedResource;
use Drupal\restful\Plugin\resource\Decorators\RateLimitDecoratedResource;
use Drupal\restful\Plugin\resource\ResourceInterface;
use Drupal\restful\RestfulManager;
/**
* Implements hook_menu().
*/
function restful_menu() {
$base_path = variable_get('restful_hook_menu_base_path', 'api');
$items = array();
$plugins = restful()
->getResourceManager()
->getPlugins();
foreach ($plugins->getIterator() as $plugin) {
if (!$plugin instanceof ResourceInterface) {
// If the plugin is disabled $plugin gets set to NULL. If that is the case
// do not set any menu values based on it.
continue;
}
$plugin_definition = $plugin->getPluginDefinition();
if (!$plugin_definition['hookMenu']) {
// Plugin explicitly declared no hook menu should be created automatically
// for it.
continue;
}
$item = array(
'title' => $plugin_definition['name'],
'access callback' => RestfulManager::FRONT_CONTROLLER_ACCESS_CALLBACK,
'access arguments' => array($plugin_definition['resource']),
'page callback' => RestfulManager::FRONT_CONTROLLER_CALLBACK,
'page arguments' => array($plugin_definition['resource']),
'delivery callback' => 'restful_delivery',
'type' => MENU_CALLBACK,
);
// If there is no specific menu item allow the different version variations.
if (!isset($plugin_definition['menuItem'])) {
// Add the version string to the arguments.
$item['access arguments'][] = 1;
$item['page arguments'][] = 1;
// Ex: api/v1.2/articles
$items[$base_path . '/v' . $plugin_definition['majorVersion'] . '.' . $plugin_definition['minorVersion'] . '/' . $plugin_definition['resource']] = $item;
// Ex: api/v1/articles will use the latest minor version.
$items[$base_path . '/v' . $plugin_definition['majorVersion'] . '/' . $plugin_definition['resource']] = $item;
// Ex: api/articles will use the header or the latest version.
// Do not add the version string to the arguments.
$item['access arguments'] = $item['page arguments'] = array(1);
$items[$base_path . '/' . $plugin_definition['resource']] = $item;
}
else {
$path = implode('/', array($base_path, $plugin_definition['menuItem']));
// Remove trailing slashes that can lead to 404 errors.
$path = rtrim($path, '/');
$items[$path] = $item;
}
}
// Make sure the Login endpoint has the correct access callback.
if (!empty($items[$base_path . '/login'])) {
$items[$base_path . '/login']['access callback'] = 'user_is_anonymous';
}
// Add administration page.
$items['admin/config/services/restful'] = array(
'title' => 'RESTful',
'description' => 'Administer the RESTful module.',
'page callback' => 'drupal_get_form',
'page arguments' => array('restful_admin_settings'),
'access arguments' => array('administer restful'),
'file' => 'restful.admin.inc',
);
$items['admin/config/services/restful/restful'] = $items['admin/config/services/restful'];
$items['admin/config/services/restful/restful']['type'] = MENU_DEFAULT_LOCAL_TASK;
// Add cache administration page.
$items['admin/config/services/restful/cache'] = array(
'title' => 'Cache',
'description' => 'Administer the RESTful module cache system.',
'page callback' => 'drupal_get_form',
'page arguments' => array('restful_admin_cache_settings'),
'access arguments' => array('administer restful'),
'file' => 'restful.cache.inc',
'type' => MENU_LOCAL_TASK,
'weight' => 2,
);
return $items;
}
/**
* Implements hook_permission().
*/
function restful_permission() {
return array(
'administer restful' => array(
'title' => t('Administer the RESTful module'),
'description' => t('Access the administration pages for the RESTful module.'),
),
'administer restful resources' => array(
'title' => t('Administer the resources'),
'description' => t('Perform operations on the resources.'),
),
'restful clear render caches' => array(
'title' => t('Clear RESTful render caches'),
'description' => t('Clear the render caches and their correspoding cache fragments.'),
),
);
}
/**
* Implements hook_help().
*/
function restful_help($path, $arg) {
switch ($path) {
case 'admin/config/services/restful':
case 'admin/help#restful':
$message = t('This module is managed in GitHub. Please make sure to read the files in the !link folder for more help.', array(
'!link' => l(t('Docs'), 'https://github.com/RESTful-Drupal/restful/tree/7.x-2.x/docs'),
));
return '<p>' . $message . '</p>';
case 'admin/config/services/restful/cache':
$message = t('The RESTful module contains several layers of caching for enhanced performance: (1) page cache (aka URL level caching) for anonymous users. This cache is extremely fast, but not very flexible. (2) The render cache can be configured for each resource and allows you to serve cached versions of your records (even to authenticated users!). The render cache also contains smart invalidation, which means that you do not need to have a TTL based cache system. Instead the caches are evicted when automatically when necessary.');
return '<p>' . $message . '</p>';
}
}
/**
* Get the RestfulManager.
*
* Calling restful() from anywhere in the code will give you access to the
* RestfulManager. That in turn will provide you access to all the elements
* involved.
*
* @return RestfulManager
* The manager.
*/
function restful() {
static $manager;
if (!isset($manager)) {
$manager = RestfulManager::createFromGlobals();
}
return $manager;
}
/**
* Access callback; Determine access for an API call.
*
* @param string $resource_name
* The name of the resource (e.g. "articles").
*
* @param string $version_string
* The version array.
*
* @return bool
* TRUE if user is allowed to access resource.
*/
function restful_menu_access_callback($resource_name, $version_string = NULL) {
$resource_manager = restful()->getResourceManager();
if (!empty($version_string) && preg_match('/v[0-9]+(\.[0-9]+)?/', $version_string)) {
$version_string = substr($version_string, 1);
$parsed_versions = explode('.', $version_string);
if (count($parsed_versions) == 2) {
// If there is only the major we need to get the version from the request,
// to get the latest version within the major version.
$versions = $parsed_versions;
}
}
if (empty($versions) && !$versions = $resource_manager->getVersionFromRequest()) {
// No version could be found.
return FALSE;
}
try {
$instance_id = $resource_name . PluginBase::DERIVATIVE_SEPARATOR . implode('.', $versions);
$resource = $resource_manager->getPlugin($instance_id, restful()->getRequest());
if (!$resource) {
// Throw a PluginNotFoundException exception instead of a denied access.
throw new PluginNotFoundException($instance_id);
}
return $resource->access();
}
catch (RestfulException $e) {
// We can get here if the request method is not valid or if no resource can
// be negotiated.
$response = restful()->getResponse();
$output = _restful_build_http_api_error($e, $response);
$response->setStatusCode($e->getCode());
$response->setContent(drupal_json_encode($output));
$response->send();
exit();
}
catch (PluginNotFoundException $e) {
restful_delivery(MENU_NOT_FOUND);
exit();
}
}
/**
* Page callback; Return the response for an API call.
*
* @param string $resource_name
* The name of the resource (e.g. "articles").
* @param string $version
* The version, prefixed with v (e.g. v1, v2.2).
*
* @throws \Drupal\restful\Exception\ServiceUnavailableException
*
* @return string
* JSON output with the result of the API call.
*
* @see http://tools.ietf.org/html/draft-nottingham-http-problem-06
*/
function restful_menu_process_callback($resource_name, $version = NULL) {
$path = func_get_args();
array_shift($path);
if (preg_match('/^v\d+(\.\d+)?$/', $version)) {
array_shift($path);
}
$resource_manager = restful()->getResourceManager();
list($major_version, $minor_version) = $resource_manager->getVersionFromRequest();
$request = restful()->getRequest();
$request->setViaRouter(TRUE);
$resource = $resource_manager->getPlugin($resource_name . PluginBase::DERIVATIVE_SEPARATOR . $major_version . '.' . $minor_version, $request);
$response_headers = restful()
->getResponse()
->getHeaders();
$version_array = $resource->getVersion();
$version_string = 'v' . $version_array['major'] . '.' . $version_array['minor'];
$response_headers->add(HttpHeader::create('X-API-Version', $version_string));
// Vary the response with the presence of the X-API-Version or Accept headers.
$vary = $request
->getHeaders()
->get('Vary')
->getValueString() ?: '';
$additional_variations = array($vary, 'Accept');
if ($x_api_version = $request
->getHeaders()
->get('X-API-Version')
->getValueString()) {
$additional_variations[] = 'X-API-Version';
}
if ($additional_variations) {
$response_headers->append(HttpHeader::create('Vary', implode(',', $additional_variations)));
}
// Always add the allow origin if configured.
$plugin_definition = $resource->getPluginDefinition();
if (!empty($plugin_definition['allowOrigin'])) {
$response_headers->append(HttpHeader::create('Access-Control-Allow-Origin', $plugin_definition['allowOrigin']));
}
try {
$resource->setPath(implode('/', $path));
$result = $resource->process();
}
catch (RestfulException $e) {
$result = _restful_build_http_api_error($e);
}
catch (Exception $e) {
$result = array(
'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1',
'title' => $e->getMessage(),
'status' => 500,
);
}
// If the user was switched during the execution thread, then switch it back.
$resource->switchUserBack();
return $result;
}
/**
* Returns data in JSON format.
*
* We do not use drupal_json_output(), in order to maintain the "Content-Type"
* header.
*
* @param mixed $var
* (optional) If set, the variable will be converted to JSON and output.
*
* @see restful_menu_process_callback()
*/
function restful_delivery($var = NULL) {
if (!isset($var)) {
return;
}
$response = _restful_delivery_fill_reponse($var, restful()->getResponse());
$response->send();
}
/**
* Gets the response object to be send.
*
* @param mixed $var
* If set, the variable will be converted to JSON and output.
* @param ResponseInterface $response
* If set, this response will be used to add the response data.
*
* @return ResponseInterface
* A prepared response for sending.
*/
function _restful_delivery_fill_reponse($var, ResponseInterface $response = NULL) {
$request = restful()->getRequest();
$response = $response ? $response : new Response();
if (!empty($var['status'])) {
$response->setStatusCode($var['status']);
}
if (is_int($var)) {
_restful_get_data_from_menu_status($var);
if (!empty($var['status'])) {
$response->setStatusCode($var['status']);
}
try {
// Adhere to the API Problem draft proposal.
$formatter_id = variable_get('restful_default_output_formatter', 'json');
// Get the data in the default output format.
$data = restful()
->getFormatterManager()
->negotiateFormatter(NULL, $formatter_id)
->format($var);
$response->setContent($data);
$response->prepare($request);
return $response;
}
catch (RestfulException $e) {
// If there is an exception during delivery, just JSON encode this.
$output = _restful_build_http_api_error($e, $response);
$response->setStatusCode($e->getCode());
$response->setContent(drupal_json_encode($output));
return $response;
}
}
try {
// Get the formatter for the current resource.
$resource = restful()
->getResourceManager()
->negotiate();
// Get a new formatter manager.
$formatter_manager = restful()
->getFormatterManager();
$formatter_manager->setResource($resource);
$plugin_definition = $resource->getPluginDefinition();
if ($request->getMethod() == RequestInterface::METHOD_OPTIONS) {
// There is no guarantee that other formatters can process the
// auto-discovery output correctly.
$formatter_name = 'json';
}
else {
$formatter_name = isset($plugin_definition['formatter']) ? $plugin_definition['formatter'] : NULL;
}
$output = $formatter_manager->format($var, $formatter_name);
$response->setContent($output);
}
catch (RestfulException $e) {
// Handle if the formatter does not exist.
$output = _restful_build_http_api_error($e, $response);
$response->setStatusCode($e->getCode());
$response->setContent(drupal_json_encode($output));
return $response;
}
$response->prepare($request);
return $response;
}
/**
* Convert a menu status response to a valid JSON.
*
* @param int $var
* The integer value of the menu status, passed by reference.
*/
function _restful_get_data_from_menu_status(&$var) {
switch ($var) {
case MENU_NOT_FOUND:
$class_name = '\Drupal\restful\Exception\NotFoundException';
$message = 'Invalid URL path.';
break;
case MENU_ACCESS_DENIED:
$class_name = '\Drupal\restful\Exception\ForbiddenException';
$message = 'Access denied.';
break;
case MENU_SITE_OFFLINE:
$class_name = '\Drupal\restful\Exception\ServiceUnavailableException';
$message = 'Site is offline.';
break;
default:
$class_name = '\Drupal\restful\Exception\RestfulException';
$message = 'Unknown exception';
}
$var = _restful_build_http_api_error(new $class_name($message));
}
/**
* Helper function to build the structured array for the error output.
*
* @param RestfulException $exception
* The exception.
* @param ResponseInterface $response
* The response object to alter.
*
* @return array
* The structured output.
*/
function _restful_build_http_api_error(RestfulException $exception, ResponseInterface $response = NULL) {
$response = $response ?: restful()->getResponse();
// Adhere to the API Problem draft proposal.
$exception->setHeader('Content-Type', 'application/problem+json; charset=utf-8');
$result = array(
'type' => $exception->getType(),
'title' => $exception->getMessage(),
'status' => $exception->getCode(),
'detail' => $exception->getDescription(),
);
if ($instance = $exception->getInstance()) {
$result['instance'] = $instance;
}
if ($errors = $exception->getFieldErrors()) {
$result['errors'] = $errors;
}
$headers = $response->getHeaders();
foreach ($exception->getHeaders() as $header_name => $header_value) {
$headers->add(HttpHeader::create($header_name, $header_value));
}
drupal_page_is_cacheable(FALSE);
// Add a log entry with the error / warning.
if ($exception->getCode() < 500) {
// Even though it's an exception, it's in fact not a server error - it
// might be just access denied, or a bad request, so we just want to log
// it, but without marking it as an actual exception.
watchdog('restful', $exception->getMessage());
}
else {
watchdog_exception('restful', $exception);
}
return $result;
}
/**
* Implements hook_page_delivery_callback_alter().
*
* Hijack api/* to be under RESTful. We make sure that any call to api/* pages
* that isn't valid, will still return with a well formatted error, instead of
* a 404 HTML page.
*/
function restful_page_delivery_callback_alter(&$callback) {
if (!variable_get('restful_hijack_api_pages', TRUE)) {
return;
}
$base_path = variable_get('restful_hook_menu_base_path', 'api');
if (strpos($_GET['q'], $base_path . '/') !== 0 && $_GET['q'] != $base_path) {
// Page doesn't start with the base path (e.g. "api" or "api/").
return;
}
if (menu_get_item()) {
// Path is valid (i.e. not 404).
return;
}
$callback = 'restful_deliver_menu_not_found';
}
/**
* Delivers a not found (404) error.
*/
function restful_deliver_menu_not_found($page_callback_result) {
restful_delivery(MENU_NOT_FOUND);
}
/**
* Implements hook_cron().
*/
function restful_cron() {
\Drupal\restful\RateLimit\RateLimitManager::deleteExpired();
}
/**
* Page callback: returns a session token for the currently active user.
*/
function restful_csrf_session_token() {
return array('X-CSRF-Token' => drupal_get_token(\Drupal\restful\Plugin\authentication\Authentication::TOKEN_VALUE));
}
/**
* Element validate \DateTime format function.
*/
function restful_date_time_format_element_validate($element, &$form_state) {
$value = $element['#value'];
try {
new \DateInterval($value);
}
catch (\Exception $e) {
form_error($element, t('%name must be compatible with the !link.', array(
'%name' => $element['#title'],
'!link' => l(t('\DateInterval format'), 'http://php.net/manual/en/class.dateinterval.php'),
)));
}
}
/**
* Implements hook_restful_resource_alter().
*
* Decorate an existing resource with other services (e.g. rate limit and render
* cache).
*/
function restful_restful_resource_alter(ResourceInterface &$resource) {
// Disable any plugin in the disabled plugins variable.
$disabled_plugins = array(
// Disable the Files Upload resource based on the settings variable.
'files_upload:1.0' => (bool) !variable_get('restful_file_upload', FALSE),
// Disable the Users resources based on the settings variable.
'users:1.0' => (bool) !variable_get('restful_enable_users_resource', TRUE),
// Disable the Login Cookie resources based on the settings variable.
'login_cookie:1.0' => (bool) !variable_get('restful_enable_user_login_resource', TRUE),
// Disable the Discovery resource based on the settings variable.
'discovery:1.0' => (bool) !variable_get('restful_enable_discovery_resource', TRUE),
) + variable_get('restful_disabled_plugins', array());
if (!empty($disabled_plugins[$resource->getResourceName()])) {
$resource->disable();
}
elseif (
isset($disabled_plugins[$resource->getResourceName()]) &&
$disabled_plugins[$resource->getResourceName()] === FALSE &&
!$resource->isEnabled()
) {
$resource->enable();
}
$plugin_definition = $resource->getPluginDefinition();
// If render cache is enabled for the current resource, or there is no render
// cache information for the resource but render cache is enabled globally,
// then decorate the resource with cache capabilities.
if (
!empty($plugin_definition['renderCache']['render']) ||
(!isset($plugin_definition['renderCache']['render']) && variable_get('restful_render_cache', FALSE))
) {
$resource = new CacheDecoratedResource($resource);
}
// Check for the rate limit configuration.
if (!empty($plugin_definition['rateLimit']) || variable_get('restful_global_rate_limit', 0)) {
$resource = new RateLimitDecoratedResource($resource);
}
// Disable the discovery endpoint if it's disabled.
if (
$resource->getResourceMachineName() == 'discovery' &&
!variable_get('restful_enable_discovery_resource', TRUE) &&
$resource->isEnabled()
) {
$resource->disable();
}
}