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.
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 * Constructs a RestExport object.
115 * @param array $configuration
116 * A configuration array containing information about the plugin instance.
117 * @param string $plugin_id
118 * The plugin_id for the plugin instance.
119 * @param mixed $plugin_definition
120 * The plugin implementation definition.
121 * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
122 * The route provider.
123 * @param \Drupal\Core\State\StateInterface $state
124 * The state key value store.
125 * @param \Drupal\Core\Render\RendererInterface $renderer
127 * @param string[] $authentication_providers
128 * The authentication providers, keyed by ID.
130 public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer, array $authentication_providers) {
131 parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
133 $this->renderer = $renderer;
134 // $authentication_providers as defined in
135 // \Drupal\Core\DependencyInjection\Compiler\AuthenticationProviderPass
136 // and as such it is an array, with authentication providers (cookie,
137 // basic_auth) as keys and modules providing those as values (user,
139 $this->authenticationProviderIds = array_keys($authentication_providers);
140 // For BC reasons we keep around authenticationProviders as before.
141 $this->authenticationProviders = $authentication_providers;
147 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
152 $container->get('router.route_provider'),
153 $container->get('state'),
154 $container->get('renderer'),
155 $container->getParameter('authentication_providers')
162 public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
163 parent::initDisplay($view, $display, $options);
165 $request_content_type = $this->view->getRequest()->getRequestFormat();
166 // Only use the requested content type if it's not 'html'. If it is then
167 // default to 'json' to aid debugging.
168 // @todo Remove the need for this when we have better content negotiation.
169 if ($request_content_type != 'html') {
170 $this->setContentType($request_content_type);
172 // If the requested content type is 'html' and the default 'json' is not
173 // selected as a format option in the view display, fallback to the first
174 // format in the array.
175 elseif (!empty($options['style']['options']['formats']) && !isset($options['style']['options']['formats'][$this->getContentType()])) {
176 $this->setContentType(reset($options['style']['options']['formats']));
179 $this->setMimeType($this->view->getRequest()->getMimeType($this->contentType));
185 public function getType() {
192 public function usesExposed() {
199 public function displaysExposed() {
204 * Sets the request content type.
206 * @param string $mime_type
207 * The response mime type. E.g. 'application/json'.
209 public function setMimeType($mime_type) {
210 $this->mimeType = $mime_type;
214 * Gets the mime type.
216 * This will return any overridden mime type, otherwise returns the mime type
220 * The response mime type. E.g. 'application/json'.
222 public function getMimeType() {
223 return $this->mimeType;
227 * Sets the content type.
229 * @param string $content_type
230 * The content type machine name. E.g. 'json'.
232 public function setContentType($content_type) {
233 $this->contentType = $content_type;
237 * Gets the content type.
240 * The content type machine name. E.g. 'json'.
242 public function getContentType() {
243 return $this->contentType;
247 * Gets the auth options available.
250 * An array to use as value for "#options" in the form element.
252 public function getAuthOptions() {
253 return array_combine($this->authenticationProviderIds, $this->authenticationProviderIds);
259 protected function defineOptions() {
260 $options = parent::defineOptions();
262 // Options for REST authentication.
263 $options['auth'] = ['default' => []];
265 // Set the default style plugin to 'json'.
266 $options['style']['contains']['type']['default'] = 'serializer';
267 $options['row']['contains']['type']['default'] = 'data_entity';
268 $options['defaults']['default']['style'] = FALSE;
269 $options['defaults']['default']['row'] = FALSE;
271 // Remove css/exposed form settings, as they are not used for the data display.
272 unset($options['exposed_form']);
273 unset($options['exposed_block']);
274 unset($options['css_class']);
282 public function optionsSummary(&$categories, &$options) {
283 parent::optionsSummary($categories, $options);
286 $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
288 unset($categories['page'], $categories['exposed']);
289 // Hide some settings, as they aren't useful for pure data output.
290 unset($options['show_admin_links'], $options['analyze-theme']);
292 $categories['path'] = [
293 'title' => $this->t('Path settings'),
294 'column' => 'second',
300 $options['path']['category'] = 'path';
301 $options['path']['title'] = $this->t('Path');
303 'category' => 'path',
304 'title' => $this->t('Authentication'),
305 'value' => views_ui_truncate($auth, 24),
308 // Remove css/exposed form settings, as they are not used for the data
310 unset($options['exposed_form']);
311 unset($options['exposed_block']);
312 unset($options['css_class']);
318 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
319 parent::buildOptionsForm($form, $form_state);
320 if ($form_state->get('section') === 'auth') {
321 $form['#title'] .= $this->t('The supported authentication methods for this view');
323 '#type' => 'checkboxes',
324 '#title' => $this->t('Authentication methods'),
325 '#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.'),
326 '#options' => $this->getAuthOptions(),
327 '#default_value' => $this->getOption('auth'),
335 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
336 parent::submitOptionsForm($form, $form_state);
338 if ($form_state->get('section') == 'auth') {
339 $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
346 public function collectRoutes(RouteCollection $collection) {
347 parent::collectRoutes($collection);
348 $view_id = $this->view->storage->id();
349 $display_id = $this->display['id'];
351 if ($route = $collection->get("view.$view_id.$display_id")) {
352 $style_plugin = $this->getPlugin('style');
353 // REST exports should only respond to get methods.
354 $route->setMethods(['GET']);
356 // Format as a string using pipes as a delimiter.
357 if ($formats = $style_plugin->getFormats()) {
358 // Allow a REST Export View to be returned with an HTML-only accept
359 // format. That allows browsers or other non-compliant systems to access
360 // the view, as it is unlikely to have a conflicting HTML representation
362 $route->setRequirement('_format', implode('|', $formats + ['html']));
364 // Add authentication to the route if it was set. If no authentication was
365 // set, the default authentication will be used, which is cookie based by
367 $auth = $this->getOption('auth');
369 $route->setOption('_auth', $auth);
375 * Determines whether the view overrides the given route.
377 * @param string $view_path
378 * The path of the view.
379 * @param \Symfony\Component\Routing\Route $view_route
380 * The route of the view.
381 * @param \Symfony\Component\Routing\Route $route
385 * TRUE, when the view should override the given route.
387 protected function overrideApplies($view_path, Route $view_route, Route $route) {
388 $route_formats = explode('|', $route->getRequirement('_format'));
389 $view_route_formats = explode('|', $view_route->getRequirement('_format'));
390 return $this->overrideAppliesPathAndMethod($view_path, $view_route, $route)
391 && (!$route->hasRequirement('_format') || array_intersect($route_formats, $view_route_formats) != []);
397 public static function buildResponse($view_id, $display_id, array $args = []) {
398 $build = static::buildBasicRenderable($view_id, $display_id, $args);
400 // Setup an empty response so headers can be added as needed during views
401 // rendering and processing.
402 $response = new CacheableResponse('', 200);
403 $build['#response'] = $response;
405 /** @var \Drupal\Core\Render\RendererInterface $renderer */
406 $renderer = \Drupal::service('renderer');
408 $output = (string) $renderer->renderRoot($build);
410 $response->setContent($output);
411 $cache_metadata = CacheableMetadata::createFromRenderArray($build);
412 $response->addCacheableDependency($cache_metadata);
414 $response->headers->set('Content-type', $build['#content_type']);
422 public function execute() {
425 return $this->view->render();
431 public function render() {
433 $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
434 return $this->view->style_plugin->render();
437 $this->view->element['#content_type'] = $this->getMimeType();
438 $this->view->element['#cache_properties'][] = '#content_type';
440 // Encode and wrap the output in a pre tag if this is for a live preview.
441 if (!empty($this->view->live_preview)) {
442 $build['#prefix'] = '<pre>';
443 $build['#plain_text'] = $build['#markup'];
444 $build['#suffix'] = '</pre>';
445 unset($build['#markup']);
447 elseif ($this->view->getRequest()->getFormat($this->view->element['#content_type']) !== 'html') {
448 // This display plugin is primarily for returning non-HTML formats.
449 // However, we still invoke the renderer to collect cacheability metadata.
450 // Because the renderer is designed for HTML rendering, it filters
451 // #markup for XSS unless it is already known to be safe, but that filter
452 // only works for HTML. Therefore, we mark the contents as safe to bypass
453 // the filter. So long as we are returning this in a non-HTML response
454 // (checked above), this is safe, because an XSS attack only works when
455 // executed by an HTML agent.
456 // @todo Decide how to support non-HTML in the render API in
457 // https://www.drupal.org/node/2501313.
458 $build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']);
461 parent::applyDisplayCacheabilityMetadata($build);
469 * The DisplayPluginBase preview method assumes we will be returning a render
470 * array. The data plugin will already return the serialized string.
472 public function preview() {
473 return $this->view->render();
479 public function calculateDependencies() {
480 $dependencies = parent::calculateDependencies();
482 $dependencies += ['module' => []];
483 $dependencies['module'] = array_merge($dependencies['module'], array_filter(array_map(function ($provider) {
484 // During the update path the provider options might be wrong. This can
485 // happen when any update function, like block_update_8300() triggers a
487 return isset($this->authenticationProviderIds[$provider])
488 ? $this->authenticationProviderIds[$provider]
490 }, $this->getOption('auth'))));
492 return $dependencies;