diff --git a/core/modules/rest/rest.module b/core/modules/rest/rest.module index c0c6be3..123a553 100644 --- a/core/modules/rest/rest.module +++ b/core/modules/rest/rest.module @@ -5,6 +5,7 @@ * RESTful web services module. */ +use Drupal\Component\Utility\String; use Drupal\Core\Routing\RouteMatchInterface; /** @@ -40,6 +41,37 @@ function rest_help($route_name, RouteMatchInterface $route_match) { $output .= '
' . t('REST support for content items of the Node module is enabled by default, and support for other types of content entities can be enabled. To enable support, you can use a process based on configuration editing or the contributed Rest UI module.', array('!config' => 'https://drupal.org/documentation/modules/rest', '!restui' => 'https://drupal.org/project/restui')) . '
'; $output .= '
' . t('You will also need to grant anonymous users permission to perform each of the REST operations you want to be available, and set up authentication properly to authorize web requests.') . '
'; $output .= ''; + $output .= '

' . t('API Docs') . '

'; + $output .= '

' . t('The API documentation is viewable on') . ' /docs/rest/api

'; return $output; } } + + +/** + * Implements hook_theme(). + */ +function rest_theme() { + return array( + 'rest_documentation' => array( + 'variables' => array('field_description' => NULL, 'methods' => array()), + 'template' => 'rest-documentation', + ), + 'rest_documentation_section' => array( + 'variables' => array('method' => NULL, 'headers' => NULL, 'body' => NULL), + 'template' => 'rest-documentation-section', + ), + 'rest_documentation_endpoints' => array( + 'variables' => array('endpoints' => NULL), + 'template' => 'rest-documentation-endpoints', + ), + 'rest_documentation_type' => array( + 'variables' => array('required' => NULL, 'optional' => NULL), + 'template' => 'rest-documentation-type', + ) + ); +} + +function template_preprocess_rest_documentation($variables) { + $variables['field_description'] = String::checkPlain($variables['field_description']); +} diff --git a/core/modules/rest/rest.routing.yml b/core/modules/rest/rest.routing.yml index 843dee9..da1f7a4 100644 --- a/core/modules/rest/rest.routing.yml +++ b/core/modules/rest/rest.routing.yml @@ -4,3 +4,9 @@ rest.csrftoken: _controller: '\Drupal\rest\RequestHandler::csrfToken' requirements: _access: 'TRUE' +rest.docs: + path: '/docs/rest/api' + defaults: + _controller: 'Drupal\rest\Controller::docRoot' + requirements: + _access: 'TRUE' diff --git a/core/modules/rest/src/Controller.php b/core/modules/rest/src/Controller.php new file mode 100644 index 0000000..f86acbb --- /dev/null +++ b/core/modules/rest/src/Controller.php @@ -0,0 +1,173 @@ +get('resources') ?: array(); + + $all_bundles = $this->getRestBundles(); + + $items = array(); + foreach ($config as $id => $config) { + $item = array(); + foreach ($config as $method => $settings) { + $supported_formats = join(', ', $settings['supported_formats']); + $supported_auth = join(", ", $settings['supported_auth']); + $item[] = t("Method %method using formats '%supported_formats' with authentication methods '%supported_auth'", array( + '%method' => $method, + '%supported_auth' => $supported_auth, + '%supported_formats' => $supported_formats + )); + } + + list(, $entity) = explode(":", $id); + $bundles = array_keys($all_bundles[$entity]); + $doc_refs = array(); + foreach ($bundles as $bundle) { + $doc_refs[] = l($bundle, '/docs/rest/api/types/' . $entity . '/' . $bundle); + } + $items[] = array( + '#theme' => 'item_list', + '#title' => t("Resource %entity has documentation for !doc-urls", array( + '%entity' => $entity, + '!doc-urls' => join(', ', $doc_refs) + )), + '#items' => $item, + ); + } + + $result = array( + '#theme' => 'item_list', + '#title' => t('Rest resources'), + '#items' => $items, + ); + + return $result; + } + + /** + * List all fields for given entity type and bundle. + * + * @param $entity_type + * @param $bundle + * @return array + */ + public function type($entity_type, $bundle) { + $required = array(); + $optional = array(); + + $fields = $this->getEntityManager()->getFieldDefinitions($entity_type, $bundle); + + /** @var \Drupal\Core\Field\FieldDefinitionInterface $field */ + foreach ($fields as $id => $field) { + $item = l($field->getName(), "rest/relation/$entity_type/$bundle/$id"); + + if ($field->isRequired()) { + $required[] = $item; + } + else { + $optional[] = $item; + } + } + + $requiredItems = array( + '#theme' => 'item_list', + '#title' => "Required fields", + '#items' => $required, + ); + $optionalItems = array( + '#theme' => 'item_list', + '#title' => "Optional fields", + '#items' => $optional, + ); + + $render = array( + '#theme' => 'item_list', + '#title' => 'Fields for ' . $entity_type . ' / ' . $bundle, + '#items' => array($requiredItems, $optionalItems), + ); + return $render; + } + + /** + * Generate field page. + * + * @param $entity_type + * @param $bundle + * @param $field_name + * @return mixed + */ + public function relation($entity_type, $bundle, $field_name) { + // TODO fix for drupal_set_title + // drupal_set_title($field_name); + + /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields */ + $fields = $this->getEntityManager()->getFieldDefinitions($entity_type, $bundle); + + /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ + $field_definition = $fields[$field_name]; + + // TODO: SA what information is disclosed? + // All settings are exposed + // TODO: how to check for field permissions? + + $rows = array(); + $rows[] = array("Type", $field_definition->getType()); + $rows[] = array("Description", $field_definition->getDescription()); + foreach ($field_definition->getSettings() as $key => $value) { + $rows[] = array("Settings - $key", print_r($value, TRUE)); + } + + $result = array( + '#theme' => 'table', + '#title' => t("Rest resources for !entity_type / !bundle / !field_name", array( + '!entity_type' => l($entity_type, ''), + '!bundle' => l($bundle, '/docs/rest/api/types/' . $entity_type . '/' . $bundle), + '!field_name' => $field_name, + )), + '#rows' => $rows, + ); + return $result; + } + + protected function getRestBundles() { + // TODO use DIC + $bundles = \Drupal::entityManager()->getAllBundleInfo(); + + $config = \Drupal::config('rest.settings')->get('resources'); + + // TODO: Change this to only expose info for REST enabled entity types. + // aka filter out all ConfigEntities too. + + return $bundles; + } + + protected function getEntityManager() { + // TODO use DIC + return \Drupal::entityManager(); + } + +} diff --git a/core/modules/rest/src/Routing/ResourceRoutes.php b/core/modules/rest/src/Routing/ResourceRoutes.php index 5605048..a0b0ea9 100644 --- a/core/modules/rest/src/Routing/ResourceRoutes.php +++ b/core/modules/rest/src/Routing/ResourceRoutes.php @@ -8,17 +8,17 @@ namespace Drupal\rest\Routing; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Routing\RouteBuildEvent; use Drupal\Core\Routing\RouteSubscriberBase; +use Drupal\Core\Routing\RoutingEvents; use Drupal\rest\Plugin\Type\ResourcePluginManager; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; /** * Subscriber for REST-style routes. */ -class ResourceRoutes extends RouteSubscriberBase{ +class ResourceRoutes extends RouteSubscriberBase { /** * The plugin manager for REST plugins. @@ -66,7 +66,7 @@ protected function alterRoutes(RouteCollection $collection) { $method = $route->getRequirement('_method'); // Only expose routes where the method is enabled in the configuration. if ($method && isset($enabled_methods[$method])) { - $route->setRequirement('_access_rest_csrf', 'TRUE'); + $route->setRequirement('_access_rest_csrf', 'TRUE'); // Check that authentication providers are defined. if (empty($enabled_methods[$method]['supported_auth']) || !is_array($enabled_methods[$method]['supported_auth'])) { @@ -97,4 +97,86 @@ protected function alterRoutes(RouteCollection $collection) { } } + /** + * Generate relational routes. + * + * TODO: why do we generate routes for relations? + * + * @param RouteBuildEvent $event + */ + public function relationRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); + + // TODO: why filter out the non relation ones? Ie field_image is exposed through _links to ie HAL browser. + $link_field_types = array( + 'entity_reference', + 'taxonomy_term_reference', + ); + $fieldMap = \Drupal::entityManager()->getFieldMap(); + + foreach ($fieldMap as $entity_type => $fields) { + foreach ($fields as $field_name => $field) { + // FIXME + //if (in_array($field['type'], $link_field_types)) { + foreach ($field['bundles'] as $bundle) { + // TODO why use relation in link. This should be in */api/* path + $route = new Route("/rest/relation/$entity_type/$bundle/$field_name", array( + '_content' => 'Drupal\rest\Controller::relation', + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'field_name' => $field_name, + ), array( + '_method' => 'GET', + '_access' => 'TRUE', + )); + $collection->add("rest.relation.$entity_type.$bundle.$field_name", $route); + } + //} + } + } + } + + /** + * Add all supported EntityTypes to the routes. + * + * @param RouteBuildEvent $event + */ + public function typeRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); + + foreach ($this->getRestBundles() as $entity_type => $bundles) { + foreach ($bundles as $bundle_name => $bundle) { + $route = new Route("/docs/rest/api/types/$entity_type/$bundle_name", array( + '_content' => 'Drupal\rest\Controller::type', + 'entity_type' => $entity_type, + 'bundle' => $bundle_name, + ), array( + '_method' => 'GET', + '_access' => 'TRUE', + )); + $collection->add("rest.type.$entity_type.$bundle_name", $route); + } + } + } + + protected function getRestBundles() { + $bundles = \Drupal::entityManager()->getAllBundleInfo(); + + // TODO: Change this to only expose info for REST enabled entity types. + // TODO: filter out all ConfigEntities + + return $bundles; + } + + /** + * Implements EventSubscriberInterface::getSubscribedEvents(). + */ + static function getSubscribedEvents() { + $events = parent::getSubscribedEvents(); + + $events[RoutingEvents::DYNAMIC][] = array('relationRoutes'); + $events[RoutingEvents::DYNAMIC][] = array('typeRoutes'); + return $events; + } + }