/** * Helper function to get the identifier from a property wrapper. * * @param \EntityMetadataWrapper $property_wrapper * The property wrapper to get the ID from. * * @return string * An identifier. */ protected function propertyIdentifier(\EntityMetadataWrapper $property_wrapper) { // The property wrapper is a reference to another entity get the entity // ID. $file_array = $property_wrapper->value(); $identifier = $file_array['fid']; $resource = $this->getResource(); // TODO: Make sure we still want to support fullView. if (!$resource || !$identifier || isset($resource['fullView']) && $resource['fullView'] === FALSE) { return $identifier; } // If there is a resource that we are pointing to, we need to use the id // field that that particular resource has in its configuration. Trying to // load by the entity id in that scenario will lead to a 404. // We'll load the plugin to get the idField configuration. $instance_id = sprintf('%s:%d.%d', $resource['name'], $resource['majorVersion'], $resource['minorVersion']); /* @var ResourceInterface $resource */ $resource = restful()->getResourceManager()->getPluginCopy($instance_id, Request::create('', array(), RequestInterface::METHOD_GET)); $plugin_definition = $resource->getPluginDefinition(); if (empty($plugin_definition['dataProvider']['idField'])) { return $identifier; } try { $file_wrapper = entity_metadata_wrapper('file', $file_array['fid']); return $file_wrapper->{$plugin_definition['dataProvider']['idField']}->value(); } catch (\EntityMetadataWrapperException $e) { return $identifier; } }
/** * This method will parse the url and determine the controller, its method and its parameters if any. * Then we will include the controller file. * Then initialize the controller * then run its method * @return [nothing] */ function handle_request() { $this->controller = $this->uri->url["controller"]; $this->method = restful($this->uri->url["method"]); $this->params = $this->uri->url["params"]; if ($this->debug) { dbug("The controller ", $this->controller); dbug("The method ", $this->method); dbug("The params ", $this->params); } // set file location $file = CONTROLLER_FOLDER . '/' . $this->controller . '.php'; // check if the class file (controller) exist , else throw error if (!file_exists($file)) { header("Content-Type: application/json"); echo json_encode(["status" => "fail", "response" => "The requested API does not exist."]); die(header("HTTP/1.0 404 Oops requested url does not exist or has moved ")); } // include the class file (controller) require_once $file; $controller = $this->controller; // initialize the controller $this->app = new $controller($this->autoload); // if the method exist in this controller if (method_exists($this->app, $this->method)) { $this->app->{$this->method}($this->params); } else { header("Content-Type: application/json"); echo json_encode(["status" => "fail", "response" => "The requested API does not exist."]); header("HTTP/1.0 404 Oops requested url does not exist "); } }
/** * {@inheritdoc} */ public static function create(array $field, RequestInterface $request = NULL) { $request = $request ?: restful()->getRequest(); $resource_field = new static($field, $request); $resource_field->addDefaults(); return $resource_field; }
/** * Returns a random relationship. * * This serves as an example of a use case for the generic relationship. * * @param DataInterpreterInterface $interpreter * The data interpreter. * * @return mixed * The embeddable result. */ public static function randomRelationship(DataInterpreterInterface $interpreter) { /* @var \Drupal\restful\Plugin\resource\ResourceInterface $handler */ $handler = restful()->getResourceManager()->getPlugin('db_query_test:1.0'); // This simbolizes some complex logic that gets a rendered resource. $id = static::complexCalculation(); return $handler->getDataProvider()->view($id); }
/** * {@inheritdoc} */ public static function init(RequestInterface $request, $resource_name, array $version, $resource_path = NULL) { /* @var ResourceInterface $resource */ $instance_id = $resource_name . PluginBase::DERIVATIVE_SEPARATOR . $version[0] . '.' . $version[1]; $resource = restful()->getResourceManager()->getPluginCopy($instance_id, Request::create('', array(), RequestInterface::METHOD_GET)); $plugin_definition = $resource->getPluginDefinition(); $resource->setPath($resource_path); return new static($request, $resource->getFieldDefinitions(), $resource->getAccount(), $resource->getPluginId(), $resource->getPath(), $plugin_definition['dataProvider'], static::getLanguage(), $resource); }
/** * Login a user and return a JSON along with the authentication cookie. * * @return array * Array with the public fields populated. */ public function loginAndRespondWithCookie() { // Login the user. $account = $this->getAccount(); $this->loginUser($account); $user_resource = restful()->getResourceManager()->getPlugin('users:1.0'); // User resource may be disabled. $output = $user_resource ? $user_resource->view($account->uid) : array(); if ($resource_field_collection = reset($output)) { /* @var $resource_field_collection \Drupal\restful\Plugin\resource\Field\ResourceFieldCollectionInterface */ $resource_field_collection->set('X-CSRF-Token', ResourceField::create(array('public_name' => 'X-CSRF-Token', 'callback' => '\\Drupal\\restful\\Plugin\\resource\\LoginCookie__1_0::getCSRFTokenValue'))); } return $output; }
/** * Helper function to get a formatter and apply a method. * * @param string $method * A valid method to call on the FormatterInterface object. * @param array $data * The array of data to process. * @param string $formatter_name * The name of the formatter for the current resource. Leave it NULL to use * the Accept headers. * * @return string * The processed output. */ protected function processData($method, array $data, $formatter_name = NULL) { if ($resource = $this->resource) { $request = $resource->getRequest(); } else { $request = restful()->getRequest(); } $accept = $request->getHeaders()->get('accept')->getValueString(); $formatter = $this->negotiateFormatter($accept, $formatter_name); $output = ResourceManager::executeCallback(array($formatter, $method), array($data, $formatter_name)); // The content type header is modified after the massaging if there is // an error code. Therefore we need to set the content type header after // formatting the output. $content_type = $formatter->getContentTypeHeader(); $response_headers = restful()->getResponse()->getHeaders(); $response_headers->add(HttpHeader::create('Content-Type', $content_type)); return $output; }
/** * Returns the value for the current single field. * * This implementation will also add some metadata to the resource field * object about the entity it is referencing. * * @param \EntityMetadataWrapper $property_wrapper * The property wrapper. Either \EntityDrupalWrapper or \EntityListWrapper. * @param \EntityDrupalWrapper $wrapper * The entity wrapper. * @param object $account * The user account. * * @return mixed * A single value for the field. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\ServerConfigurationException */ protected function singleValue(\EntityMetadataWrapper $property_wrapper, \EntityDrupalWrapper $wrapper, $account) { if ($resource = $this->getResource()) { // TODO: The resource input data in the field definition has changed. // Now it does not need to be keyed by bundle since you don't even need // an entity to use the resource based field. $embedded_identifier = $this->propertyIdentifier($property_wrapper); // Allow embedding entities with ID 0, like the anon user. if (empty($embedded_identifier) && $embedded_identifier !== 0) { return NULL; } if (isset($resource['fullView']) && $resource['fullView'] === FALSE) { return $embedded_identifier; } // We support dot notation for the sparse fieldsets. That means that // clients can specify the fields to show based on the "fields" query // string parameter. $parsed_input = array('fields' => implode(',', $this->nestedDottedChildren('fields')), 'include' => implode(',', $this->nestedDottedChildren('include')), 'filter' => $this->nestedDottedChildren('filter')); $request = Request::create('', array_filter($parsed_input), RequestInterface::METHOD_GET); // Get a plugin (that can be altered with decorators. $embedded_resource = restful()->getResourceManager()->getPluginCopy(sprintf('%s:%d.%d', $resource['name'], $resource['majorVersion'], $resource['minorVersion'])); // Configure the plugin copy with the sub-request and sub-path. $embedded_resource->setPath($embedded_identifier); $embedded_resource->setRequest($request); $embedded_resource->setAccount($account); $metadata = $this->getMetadata($wrapper->getIdentifier()); $metadata = $metadata ?: array(); $metadata[] = $this->buildResourceMetadataItem($property_wrapper); $this->addMetadata($wrapper->getIdentifier(), $metadata); try { // Get the contents to embed in place of the reference ID. /* @var ResourceFieldCollection $embedded_entity */ $embedded_entity = $embedded_resource->getDataProvider()->view($embedded_identifier); } catch (InaccessibleRecordException $e) { // If you don't have access to the embedded entity is like not having // access to the property. return NULL; } catch (UnprocessableEntityException $e) { // If you access a nonexistent embedded entity. return NULL; } // Test if the $embedded_entity meets the filter or not. if (empty($parsed_input['filter'])) { return $embedded_entity; } foreach ($parsed_input['filter'] as $filter) { // Filters only apply if the target is the current field. if (!empty($filter['target']) && $filter['target'] == $this->getPublicName() && !$embedded_entity->evalFilter($filter)) { // This filter is not met. return NULL; } } return $embedded_entity; } if ($this->getFormatter()) { // Get value from field formatter. $value = $this->formatterValue($property_wrapper, $wrapper); } else { // Single value. $value = $this->fieldValue($property_wrapper); } return $value; }
/** * {@inheritdoc} */ public static function create(array $field, RequestInterface $request = NULL) { $request = $request ?: restful()->getRequest(); $resource_field = ResourceField::create($field, $request); $output = new static($field, $request); $output->decorate($resource_field); return $output; }
protected function getHandler() { return restful()->getResourceManager()->getPlugin($this->style->getInstanceId()); }
/** * Parses the provided payload according to a content type. * * @param string $content_type * The contents of the Content-Type header. * * @return array * The parsed body. * * @throws \Drupal\restful\Exception\BadRequestException */ protected static function parseBodyContentType($content_type) { if (!($input_string = file_get_contents('php://input'))) { return NULL; } if ($content_type == 'application/x-www-form-urlencoded') { $body = NULL; parse_str($input_string, $body); return $body; } // Use the Content Type header to negotiate a formatter to parse the body. $formatter = restful()->getFormatterManager()->negotiateFormatter($content_type); return $formatter->parseBody($input_string); }
/** * {@inheritdoc} */ public static function factory(array $fields = array(), RequestInterface $request = NULL) { // TODO: Explore the possibility to change factory methods by using FactoryInterface. return new static($fields, $request ?: restful()->getRequest()); }
/** * Given a field item that contains a cache placeholder render and cache it. * * @param array $field_item * The output to render. * * @param string $includes_path * The includes path encoded in dot notation. * * @return array * The rendered embedded field item. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\InternalServerErrorException */ protected function populateCachePlaceholder(array $field_item, $includes_path) { if (empty($field_item['#cache_placeholder']) || empty($field_item['#resource_id']) || empty($field_item['#resource_plugin'])) { return $field_item; } $embedded_resource = restful()->getResourceManager()->getPluginCopy($field_item['#resource_plugin']); $input = $this->getRequest()->getParsedInput(); $new_input = $input + array('include' => '', 'fields' => ''); // If the field is not supposed to be included, then bail. $old_includes = array_filter(explode(',', $new_input['include'])); if (!in_array($includes_path, $old_includes)) { return $field_item; } $new_input['fields'] = implode(',', $this->unprefixInputOptions(explode(',', $new_input['fields']), $includes_path)); $new_input['include'] = implode(',', $this->unprefixInputOptions($old_includes, $includes_path)); // Create a new request from scratch copying most of the values but the // $query. $embedded_resource->setRequest(Request::create($this->getRequest()->getPath(), array_filter($new_input), $this->getRequest()->getMethod(), $this->getRequest()->getHeaders(), $this->getRequest()->isViaRouter(), $this->getRequest()->getCsrfToken(), $this->getRequest()->getCookies(), $this->getRequest()->getFiles(), $this->getRequest()->getServer(), $this->getRequest()->getParsedBody())); try { $data = $embedded_resource->getDataProvider()->view($field_item['#resource_id']); } catch (InaccessibleRecordException $e) { // Populate it with an empty element. $data = array(); } return array_merge($field_item, $this->extractFieldValues($data, $field_item['#cache_placeholder']['parents'], $field_item['#cache_placeholder']['parent_hashes'])); }
/** * Returns only the allowed fields by filtering out the other ones. * * @param mixed $output * The data structure to filter. * @param bool|string[] $allowed_fields * FALSE to allow all fields. An array of allowed values otherwise. * * @return mixed * The filtered output. */ protected function limitFields($output, $allowed_fields = NULL) { if (!isset($allowed_fields)) { $request = ($resource = $this->getResource()) ? $resource->getRequest() : restful()->getRequest(); $input = $request->getParsedInput(); // Set the field limits to false if there are no limits. $allowed_fields = empty($input['fields']) ? FALSE : explode(',', $input['fields']); } if (!is_array($output)) { // $output is a simple value. return $output; } $result = array(); if (ResourceFieldBase::isArrayNumeric($output)) { foreach ($output as $item) { $result[] = $this->limitFields($item, $allowed_fields); } return $result; } foreach ($output as $field_name => $field_contents) { if ($allowed_fields !== FALSE && !in_array($field_name, $allowed_fields)) { continue; } $result[$field_name] = $this->limitFields($field_contents, $this->unprefixInputOptions($allowed_fields, $field_name)); } return $result; }
/** * {@inheritdoc} */ public function addDefaults() { // Almost all the defaults come are applied by the object's property // defaults. if (!($resource = $this->getResource())) { return; } // Expand array to be verbose. if (!is_array($resource)) { $resource = array('name' => $resource); } // Set default value. $resource += array('fullView' => TRUE); // Set the default value for the version of the referenced resource. if (!isset($resource['majorVersion']) || !isset($resource['minorVersion'])) { list($major_version, $minor_version) = restful()->getResourceManager()->getResourceLastVersion($resource['name']); $resource['majorVersion'] = $major_version; $resource['minorVersion'] = $minor_version; } $this->setResource($resource); }
/** * {@inheritdoc} */ protected function initDataInterpreter($identifier) { $resource_manager = restful()->getResourceManager(); try { $plugin = $resource_manager->getPlugin($identifier); } catch (UnauthorizedException $e) { return NULL; } catch (PluginNotFoundException $e) { throw new NotFoundException('Invalid URL path.'); } // If the plugin is not discoverable throw an access denied exception. $definition = $plugin->getPluginDefinition(); if (empty($definition['discoverable'])) { throw new InaccessibleRecordException(sprintf('The plugin %s is not discoverable.', $plugin->getResourceName())); } return new DataInterpreterPlug($this->getAccount(), new PluginWrapper($plugin)); }
/** * Adds the Allowed-Origin headers. * * @param string $path * The requested path. */ protected function preflight($path) { $plugin_definition = $this->getPluginDefinition(); $header_bag = restful()->getResponse()->getHeaders(); // Populate the Accept header. $accepted_formats = array(); $formatter_manager = restful()->getFormatterManager(); if (empty($plugin_definition['formatter'])) { foreach ($formatter_manager->getPlugins() as $formatter) { /** @var $formatter \Drupal\restful\Plugin\formatter\FormatterInterface */ $header_bag->append(HttpHeader::create('Accept', $formatter->getContentTypeHeader())); } } else { try { $accepted_format = $formatter_manager->getPlugin($plugin_definition['formatter'])->getContentTypeHeader(); $header_bag->add(HttpHeader::create('Accept', $accepted_format)); } catch (PluginNotFoundException $e) { throw new NotImplementedException($e->getMessage()); } } $allowed_origin = empty($plugin_definition['allowOrigin']) ? variable_get('restful_allowed_origin', NULL) : $plugin_definition['allowOrigin']; // Always add the allow origin if configured. if ($allowed_origin) { $header_bag->add(HttpHeader::create('Access-Control-Allow-Origin', check_plain($allowed_origin))); // @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials $accepts_credentials = $allowed_origin == '*' ? 'false' : 'true'; $header_bag->add(HttpHeader::create('Access-Control-Allow-Credentials', $accepts_credentials)); } // Make sure the Access-Control-Allow-Methods is populated. $allowed_methods = array(); foreach ($this->getControllers() as $pattern => $controllers) { // Find the controllers for the provided path. if ($pattern == $path || $pattern && preg_match('/' . $pattern . '/', $path)) { foreach ($controllers as $method => $controller) { if (is_array($controller)) { // If there is a custom access method for this endpoint check it. if (!empty($selected_controller['access callback']) && !ResourceManager::executeCallback(array($this, $selected_controller['access callback']), array($path))) { // There is no access for this method. continue; } } $allowed_methods[] = $method; } $header_bag->add(HttpHeader::create('Access-Control-Allow-Methods', implode(',', $allowed_methods))); break; } } }
/** * Get the (reference) field information for a single item. * * @param ResourceFieldInterface $resource_field * The resource field. * * @throws \Drupal\restful\Exception\BadRequestException * * @return array * An array containing the following keys: * - 'name': Drupal's internal field name. Ex: field_article_related * - 'type': Either a field or a property. * - 'entity_type': The entity type this field points to. Not populated if * the field is not a reference (for instance the destination field used * in the where clause). * - 'bundles': The allowed bundles for this field. Not populated if the * field is not a reference (for instance the destination field used in * the where clause). */ protected function getFieldsFromPublicNameItem(ResourceFieldInterface $resource_field) { $property = $resource_field->getProperty(); $resource = $resource_field->getResource(); $item = array('name' => $property, 'type' => ResourceFieldEntity::propertyIsField($property) ? RelationalFilterInterface::TYPE_FIELD : RelationalFilterInterface::TYPE_PROPERTY, 'entity_type' => NULL, 'bundles' => array()); $item['column'] = $item['type'] == RelationalFilterInterface::TYPE_FIELD ? $resource_field->getColumn() : NULL; $instance_id = sprintf('%s:%d.%d', $resource['name'], $resource['majorVersion'], $resource['minorVersion']); /* @var ResourceEntity $resource */ $resource = restful()->getResourceManager()->getPluginCopy($instance_id, Request::create('', array(), RequestInterface::METHOD_GET)); // Variables for the next iteration. $definitions = $resource->getFieldDefinitions(); $item['entity_type'] = $resource->getEntityType(); $item['bundles'] = $resource->getBundles(); return array($item, $definitions); }
/** * Checks if the current request has reached the rate limit. * * If the user has reached the limit this method will throw an exception. If * not, the hits counter will be updated for subsequent calls. Since the * request can match multiple events, the access is only granted if all events * are cleared. * * @param RequestInterface $request * The request array. * * @throws FloodException if the rate limit has been reached for the * current request. */ public function checkRateLimit(RequestInterface $request) { $now = new \DateTime(); $now->setTimestamp(REQUEST_TIME); // Check all rate limits configured for this handler. foreach ($this->plugins as $instance_id => $plugin) { // If the limit is unlimited then skip everything. /* @var RateLimit $plugin */ $limit = $plugin->getLimit($this->account); $period = $plugin->getPeriod(); if ($limit == static::UNLIMITED_RATE_LIMIT) { // User has unlimited access to the resources. continue; } // If the current request matches the configured event then check if the // limit has been reached. if (!$plugin->isRequestedEvent($request)) { continue; } if (!($rate_limit_entity = $plugin->loadRateLimitEntity($this->account))) { // If there is no entity, then create one. // We don't need to save it since it will be saved upon hit. $rate_limit_entity = entity_create('rate_limit', array('timestamp' => REQUEST_TIME, 'expiration' => $now->add($period)->format('U'), 'hits' => 0, 'event' => $plugin->getPluginId(), 'identifier' => $plugin->generateIdentifier($this->account))); } // When the new rate limit period starts. $new_period = new \DateTime(); $new_period->setTimestamp($rate_limit_entity->expiration); if ($rate_limit_entity->isExpired()) { // If the rate limit has expired renew the timestamps and assume 0 // hits. $rate_limit_entity->timestamp = REQUEST_TIME; $rate_limit_entity->expiration = $now->add($period)->format('U'); $rate_limit_entity->hits = 0; if ($limit == 0) { $exception = new FloodException('Rate limit reached'); $exception->setHeader('Retry-After', $new_period->format(\DateTime::RFC822)); throw $exception; } } else { if ($rate_limit_entity->hits >= $limit) { $exception = new FloodException('Rate limit reached'); $exception->setHeader('Retry-After', $new_period->format(\DateTime::RFC822)); throw $exception; } } // Save a new hit after generating the exception to mitigate DoS attacks. $rate_limit_entity->hit(); // Add the limit headers to the response. $remaining = $limit == static::UNLIMITED_RATE_LIMIT ? 'unlimited' : $limit - ($rate_limit_entity->hits + 1); $response = restful()->getResponse(); $headers = $response->getHeaders(); $headers->append(HttpHeader::create('X-Rate-Limit-Limit', $limit)); $headers->append(HttpHeader::create('X-Rate-Limit-Remaining', $remaining)); $time_remaining = $rate_limit_entity->expiration - REQUEST_TIME; $headers->append(HttpHeader::create('X-Rate-Limit-Reset', $time_remaining)); } }