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')
173 public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
174 parent::initDisplay($view, $display, $options);
176 // If the default 'json' format is not selected as a format option in the
177 // view display, fallback to the first format available for the default.
178 if (!empty($options['style']['options']['formats']) && !isset($options['style']['options']['formats'][$this->getContentType()])) {
179 $default_format = reset($options['style']['options']['formats']);
180 $this->setContentType($default_format);
183 // Only use the requested content type if it's not 'html'. This allows
184 // still falling back to the default for things like views preview.
185 $request_content_type = $this->view->getRequest()->getRequestFormat();
187 if ($request_content_type !== 'html') {
188 $this->setContentType($request_content_type);
191 $this->setMimeType($this->view->getRequest()->getMimeType($this->getContentType()));
197 public function getType() {
204 public function usesExposed() {
211 public function displaysExposed() {
216 * Sets the request content type.
218 * @param string $mime_type
219 * The response mime type. E.g. 'application/json'.
221 public function setMimeType($mime_type) {
222 $this->mimeType = $mime_type;
226 * Gets the mime type.
228 * This will return any overridden mime type, otherwise returns the mime type
232 * The response mime type. E.g. 'application/json'.
234 public function getMimeType() {
235 return $this->mimeType;
239 * Sets the content type.
241 * @param string $content_type
242 * The content type machine name. E.g. 'json'.
244 public function setContentType($content_type) {
245 $this->contentType = $content_type;
249 * Gets the content type.
252 * The content type machine name. E.g. 'json'.
254 public function getContentType() {
255 return $this->contentType;
259 * Gets the auth options available.
262 * An array to use as value for "#options" in the form element.
264 public function getAuthOptions() {
265 return array_combine($this->authenticationProviderIds, $this->authenticationProviderIds);
271 protected function defineOptions() {
272 $options = parent::defineOptions();
274 // Options for REST authentication.
275 $options['auth'] = ['default' => []];
277 // Set the default style plugin to 'json'.
278 $options['style']['contains']['type']['default'] = 'serializer';
279 $options['row']['contains']['type']['default'] = 'data_entity';
280 $options['defaults']['default']['style'] = FALSE;
281 $options['defaults']['default']['row'] = FALSE;
283 // Remove css/exposed form settings, as they are not used for the data display.
284 unset($options['exposed_form']);
285 unset($options['exposed_block']);
286 unset($options['css_class']);
294 public function optionsSummary(&$categories, &$options) {
295 parent::optionsSummary($categories, $options);
298 $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
300 unset($categories['page'], $categories['exposed']);
301 // Hide some settings, as they aren't useful for pure data output.
302 unset($options['show_admin_links'], $options['analyze-theme']);
304 $categories['path'] = [
305 'title' => $this->t('Path settings'),
306 'column' => 'second',
312 $options['path']['category'] = 'path';
313 $options['path']['title'] = $this->t('Path');
315 'category' => 'path',
316 'title' => $this->t('Authentication'),
317 'value' => views_ui_truncate($auth, 24),
320 // Remove css/exposed form settings, as they are not used for the data
322 unset($options['exposed_form']);
323 unset($options['exposed_block']);
324 unset($options['css_class']);
330 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
331 parent::buildOptionsForm($form, $form_state);
332 if ($form_state->get('section') === 'auth') {
333 $form['#title'] .= $this->t('The supported authentication methods for this view');
335 '#type' => 'checkboxes',
336 '#title' => $this->t('Authentication methods'),
337 '#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.'),
338 '#options' => $this->getAuthOptions(),
339 '#default_value' => $this->getOption('auth'),
347 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
348 parent::submitOptionsForm($form, $form_state);
350 if ($form_state->get('section') == 'auth') {
351 $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
358 public function collectRoutes(RouteCollection $collection) {
359 parent::collectRoutes($collection);
360 $view_id = $this->view->storage->id();
361 $display_id = $this->display['id'];
363 if ($route = $collection->get("view.$view_id.$display_id")) {
364 $style_plugin = $this->getPlugin('style');
366 // REST exports should only respond to GET methods.
367 $route->setMethods(['GET']);
369 $formats = $style_plugin->getFormats();
371 // If there are no configured formats, add all formats that serialization
372 // is known to support.
374 $formats = $this->getFormatOptions();
377 // Format as a string using pipes as a delimiter.
378 $route->setRequirement('_format', implode('|', $formats));
380 // Add authentication to the route if it was set. If no authentication was
381 // set, the default authentication will be used, which is cookie based by
383 $auth = $this->getOption('auth');
385 $route->setOption('_auth', $auth);
391 * Determines whether the view overrides the given route.
393 * @param string $view_path
394 * The path of the view.
395 * @param \Symfony\Component\Routing\Route $view_route
396 * The route of the view.
397 * @param \Symfony\Component\Routing\Route $route
401 * TRUE, when the view should override the given route.
403 protected function overrideApplies($view_path, Route $view_route, Route $route) {
404 $route_formats = explode('|', $route->getRequirement('_format'));
405 $view_route_formats = explode('|', $view_route->getRequirement('_format'));
406 return $this->overrideAppliesPathAndMethod($view_path, $view_route, $route)
407 && (!$route->hasRequirement('_format') || array_intersect($route_formats, $view_route_formats) != []);
413 public static function buildResponse($view_id, $display_id, array $args = []) {
414 $build = static::buildBasicRenderable($view_id, $display_id, $args);
416 // Setup an empty response so headers can be added as needed during views
417 // rendering and processing.
418 $response = new CacheableResponse('', 200);
419 $build['#response'] = $response;
421 /** @var \Drupal\Core\Render\RendererInterface $renderer */
422 $renderer = \Drupal::service('renderer');
424 $output = (string) $renderer->renderRoot($build);
426 $response->setContent($output);
427 $cache_metadata = CacheableMetadata::createFromRenderArray($build);
428 $response->addCacheableDependency($cache_metadata);
430 $response->headers->set('Content-type', $build['#content_type']);
438 public function execute() {
441 return $this->view->render();
447 public function render() {
449 $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
450 return $this->view->style_plugin->render();
453 $this->view->element['#content_type'] = $this->getMimeType();
454 $this->view->element['#cache_properties'][] = '#content_type';
456 // Encode and wrap the output in a pre tag if this is for a live preview.
457 if (!empty($this->view->live_preview)) {
458 $build['#prefix'] = '<pre>';
459 $build['#plain_text'] = $build['#markup'];
460 $build['#suffix'] = '</pre>';
461 unset($build['#markup']);
464 // This display plugin is for returning non-HTML formats. However, we
465 // still invoke the renderer to collect cacheability metadata. Because the
466 // renderer is designed for HTML rendering, it filters #markup for XSS
467 // unless it is already known to be safe, but that filter only works for
468 // HTML. Therefore, we mark the contents as safe to bypass the filter. So
469 // long as we are returning this in a non-HTML response,
470 // this is safe, because an XSS attack only works when executed by an HTML
472 // @todo Decide how to support non-HTML in the render API in
473 // https://www.drupal.org/node/2501313.
474 $build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']);
477 parent::applyDisplayCacheabilityMetadata($build);
485 * The DisplayPluginBase preview method assumes we will be returning a render
486 * array. The data plugin will already return the serialized string.
488 public function preview() {
489 return $this->view->render();
495 public function calculateDependencies() {
496 $dependencies = parent::calculateDependencies();
498 $dependencies += ['module' => []];
499 $dependencies['module'] = array_merge($dependencies['module'], array_filter(array_map(function ($provider) {
500 // During the update path the provider options might be wrong. This can
501 // happen when any update function, like block_update_8300() triggers a
503 return isset($this->authenticationProviderIds[$provider])
504 ? $this->authenticationProviderIds[$provider]
506 }, $this->getOption('auth'))));
508 return $dependencies;
512 * Returns an array of format options.
515 * An array of format options. Both key and value are the same.
517 protected function getFormatOptions() {
518 $formats = array_keys($this->formatProviders);
519 return array_combine($formats, $formats);