3 namespace Drupal\rest\Plugin\views\display;
5 use Drupal\Core\Cache\CacheableMetadata;
6 use Drupal\Core\Cache\CacheableResponse;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Render\RenderContext;
9 use Drupal\Core\Render\RendererInterface;
10 use Drupal\Core\Routing\RouteProviderInterface;
11 use Drupal\Core\State\StateInterface;
12 use Drupal\views\Plugin\views\display\ResponseDisplayPluginInterface;
13 use Drupal\views\Render\ViewsRenderPipelineMarkup;
14 use Drupal\views\ViewExecutable;
15 use Drupal\views\Plugin\views\display\PathPluginBase;
16 use Symfony\Component\DependencyInjection\ContainerInterface;
17 use Symfony\Component\Routing\Route;
18 use Symfony\Component\Routing\RouteCollection;
21 * The plugin that handles Data response callbacks for REST resources.
23 * @ingroup views_display_plugins
27 * title = @Translation("REST export"),
28 * help = @Translation("Create a REST export resource."),
30 * admin = @Translation("REST export"),
31 * returns_response = TRUE
34 class RestExport extends PathPluginBase implements ResponseDisplayPluginInterface {
39 protected $usesAJAX = FALSE;
44 protected $usesPager = FALSE;
49 protected $usesMore = FALSE;
54 protected $usesAreas = FALSE;
59 protected $usesOptions = FALSE;
62 * Overrides the content type of the data response, if needed.
66 protected $contentType = 'json';
69 * The mime type for the response.
73 protected $mimeType = 'application/json';
78 * @var \Drupal\Core\Render\RendererInterface
83 * The collector of authentication providers.
85 * @var \Drupal\Core\Authentication\AuthenticationCollectorInterface
87 protected $authenticationCollector;
90 * The authentication providers, like 'cookie' and 'basic_auth'.
94 protected $authenticationProviderIds;
97 * The authentication providers' modules, keyed by provider ID.
99 * Authentication providers like 'cookie' and 'basic_auth' are the array
100 * keys. The array values are the module names, e.g.:
102 * ['cookie' => 'user', 'basic_auth' => 'basic_auth']
105 * @deprecated as of 8.4.x, will be removed in before Drupal 9.0.0, see
106 * https://www.drupal.org/node/2825204.
110 protected $authenticationProviders;
113 * The serialization format providers, keyed by format.
117 protected $formatProviders;
120 * Constructs a RestExport object.
122 * @param array $configuration
123 * A configuration array containing information about the plugin instance.
124 * @param string $plugin_id
125 * The plugin_id for the plugin instance.
126 * @param mixed $plugin_definition
127 * The plugin implementation definition.
128 * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
129 * The route provider.
130 * @param \Drupal\Core\State\StateInterface $state
131 * The state key value store.
132 * @param \Drupal\Core\Render\RendererInterface $renderer
134 * @param string[] $authentication_providers
135 * The authentication providers, keyed by ID.
136 * @param string[] $serializer_format_providers
137 * The serialization format providers, keyed by format.
139 public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer, array $authentication_providers, array $serializer_format_providers) {
140 parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
142 $this->renderer = $renderer;
143 // $authentication_providers as defined in
144 // \Drupal\Core\DependencyInjection\Compiler\AuthenticationProviderPass
145 // and as such it is an array, with authentication providers (cookie,
146 // basic_auth) as keys and modules providing those as values (user,
148 $this->authenticationProviderIds = array_keys($authentication_providers);
149 // For BC reasons we keep around authenticationProviders as before.
150 $this->authenticationProviders = $authentication_providers;
151 $this->formatProviders = $serializer_format_providers;
157 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
162 $container->get('router.route_provider'),
163 $container->get('state'),
164 $container->get('renderer'),
165 $container->getParameter('authentication_providers'),
166 $container->getParameter('serializer.format_providers')
172 public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
173 parent::initDisplay($view, $display, $options);
175 // If the default 'json' format is not selected as a format option in the
176 // view display, fallback to the first format available for the default.
177 if (!empty($options['style']['options']['formats']) && !isset($options['style']['options']['formats'][$this->getContentType()])) {
178 $default_format = reset($options['style']['options']['formats']);
179 $this->setContentType($default_format);
182 // Only use the requested content type if it's not 'html'. This allows
183 // still falling back to the default for things like views preview.
184 $request_content_type = $this->view->getRequest()->getRequestFormat();
186 if ($request_content_type !== 'html') {
187 $this->setContentType($request_content_type);
190 $this->setMimeType($this->view->getRequest()->getMimeType($this->getContentType()));
196 public function getType() {
203 public function usesExposed() {
210 public function displaysExposed() {
215 * Sets the request content type.
217 * @param string $mime_type
218 * The response mime type. E.g. 'application/json'.
220 public function setMimeType($mime_type) {
221 $this->mimeType = $mime_type;
225 * Gets the mime type.
227 * This will return any overridden mime type, otherwise returns the mime type
231 * The response mime type. E.g. 'application/json'.
233 public function getMimeType() {
234 return $this->mimeType;
238 * Sets the content type.
240 * @param string $content_type
241 * The content type machine name. E.g. 'json'.
243 public function setContentType($content_type) {
244 $this->contentType = $content_type;
248 * Gets the content type.
251 * The content type machine name. E.g. 'json'.
253 public function getContentType() {
254 return $this->contentType;
258 * Gets the auth options available.
261 * An array to use as value for "#options" in the form element.
263 public function getAuthOptions() {
264 return array_combine($this->authenticationProviderIds, $this->authenticationProviderIds);
270 protected function defineOptions() {
271 $options = parent::defineOptions();
273 // Options for REST authentication.
274 $options['auth'] = ['default' => []];
276 // Set the default style plugin to 'json'.
277 $options['style']['contains']['type']['default'] = 'serializer';
278 $options['row']['contains']['type']['default'] = 'data_entity';
279 $options['defaults']['default']['style'] = FALSE;
280 $options['defaults']['default']['row'] = FALSE;
282 // Remove css/exposed form settings, as they are not used for the data display.
283 unset($options['exposed_form']);
284 unset($options['exposed_block']);
285 unset($options['css_class']);
293 public function optionsSummary(&$categories, &$options) {
294 parent::optionsSummary($categories, $options);
297 $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
299 unset($categories['page'], $categories['exposed']);
300 // Hide some settings, as they aren't useful for pure data output.
301 unset($options['show_admin_links'], $options['analyze-theme']);
303 $categories['path'] = [
304 'title' => $this->t('Path settings'),
305 'column' => 'second',
311 $options['path']['category'] = 'path';
312 $options['path']['title'] = $this->t('Path');
314 'category' => 'path',
315 'title' => $this->t('Authentication'),
316 'value' => views_ui_truncate($auth, 24),
319 // Remove css/exposed form settings, as they are not used for the data
321 unset($options['exposed_form']);
322 unset($options['exposed_block']);
323 unset($options['css_class']);
329 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
330 parent::buildOptionsForm($form, $form_state);
331 if ($form_state->get('section') === 'auth') {
332 $form['#title'] .= $this->t('The supported authentication methods for this view');
334 '#type' => 'checkboxes',
335 '#title' => $this->t('Authentication methods'),
336 '#description' => $this->t('These are the supported authentication providers for this view. When this view is requested, the client will be forced to authenticate with one of the selected providers. Make sure you set the appropriate requirements at the <em>Access</em> section since the Authentication System will fallback to the anonymous user if it fails to authenticate. For example: require Access: Role | Authenticated User.'),
337 '#options' => $this->getAuthOptions(),
338 '#default_value' => $this->getOption('auth'),
346 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
347 parent::submitOptionsForm($form, $form_state);
349 if ($form_state->get('section') == 'auth') {
350 $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
357 public function collectRoutes(RouteCollection $collection) {
358 parent::collectRoutes($collection);
359 $view_id = $this->view->storage->id();
360 $display_id = $this->display['id'];
362 if ($route = $collection->get("view.$view_id.$display_id")) {
363 $style_plugin = $this->getPlugin('style');
365 // REST exports should only respond to GET methods.
366 $route->setMethods(['GET']);
368 $formats = $style_plugin->getFormats();
370 // If there are no configured formats, add all formats that serialization
371 // is known to support.
373 $formats = $this->getFormatOptions();
376 // Format as a string using pipes as a delimiter.
377 $route->setRequirement('_format', implode('|', $formats));
379 // Add authentication to the route if it was set. If no authentication was
380 // set, the default authentication will be used, which is cookie based by
382 $auth = $this->getOption('auth');
384 $route->setOption('_auth', $auth);
390 * Determines whether the view overrides the given route.
392 * @param string $view_path
393 * The path of the view.
394 * @param \Symfony\Component\Routing\Route $view_route
395 * The route of the view.
396 * @param \Symfony\Component\Routing\Route $route
400 * TRUE, when the view should override the given route.
402 protected function overrideApplies($view_path, Route $view_route, Route $route) {
403 $route_formats = explode('|', $route->getRequirement('_format'));
404 $view_route_formats = explode('|', $view_route->getRequirement('_format'));
405 return $this->overrideAppliesPathAndMethod($view_path, $view_route, $route)
406 && (!$route->hasRequirement('_format') || array_intersect($route_formats, $view_route_formats) != []);
412 public static function buildResponse($view_id, $display_id, array $args = []) {
413 $build = static::buildBasicRenderable($view_id, $display_id, $args);
415 // Setup an empty response so headers can be added as needed during views
416 // rendering and processing.
417 $response = new CacheableResponse('', 200);
418 $build['#response'] = $response;
420 /** @var \Drupal\Core\Render\RendererInterface $renderer */
421 $renderer = \Drupal::service('renderer');
423 $output = (string) $renderer->renderRoot($build);
425 $response->setContent($output);
426 $cache_metadata = CacheableMetadata::createFromRenderArray($build);
427 $response->addCacheableDependency($cache_metadata);
429 $response->headers->set('Content-type', $build['#content_type']);
437 public function execute() {
440 return $this->view->render();
446 public function render() {
448 $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
449 return $this->view->style_plugin->render();
452 $this->view->element['#content_type'] = $this->getMimeType();
453 $this->view->element['#cache_properties'][] = '#content_type';
455 // Encode and wrap the output in a pre tag if this is for a live preview.
456 if (!empty($this->view->live_preview)) {
457 $build['#prefix'] = '<pre>';
458 $build['#plain_text'] = $build['#markup'];
459 $build['#suffix'] = '</pre>';
460 unset($build['#markup']);
463 // This display plugin is for returning non-HTML formats. However, we
464 // still invoke the renderer to collect cacheability metadata. Because the
465 // renderer is designed for HTML rendering, it filters #markup for XSS
466 // unless it is already known to be safe, but that filter only works for
467 // HTML. Therefore, we mark the contents as safe to bypass the filter. So
468 // long as we are returning this in a non-HTML response,
469 // this is safe, because an XSS attack only works when executed by an HTML
471 // @todo Decide how to support non-HTML in the render API in
472 // https://www.drupal.org/node/2501313.
473 $build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']);
476 parent::applyDisplayCacheabilityMetadata($build);
484 * The DisplayPluginBase preview method assumes we will be returning a render
485 * array. The data plugin will already return the serialized string.
487 public function preview() {
488 return $this->view->render();
494 public function calculateDependencies() {
495 $dependencies = parent::calculateDependencies();
497 $dependencies += ['module' => []];
498 $dependencies['module'] = array_merge($dependencies['module'], array_filter(array_map(function ($provider) {
499 // During the update path the provider options might be wrong. This can
500 // happen when any update function, like block_update_8300() triggers a
502 return isset($this->authenticationProviderIds[$provider])
503 ? $this->authenticationProviderIds[$provider]
505 }, $this->getOption('auth'))));
507 return $dependencies;
511 * Returns an array of format options.
514 * An array of format options. Both key and value are the same.
516 protected function getFormatOptions() {
517 $formats = array_keys($this->formatProviders);
518 return array_combine($formats, $formats);