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, keyed by ID.
94 protected $authenticationProviders;
97 * Constructs a RestExport object.
99 * @param array $configuration
100 * A configuration array containing information about the plugin instance.
101 * @param string $plugin_id
102 * The plugin_id for the plugin instance.
103 * @param mixed $plugin_definition
104 * The plugin implementation definition.
105 * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
106 * The route provider.
107 * @param \Drupal\Core\State\StateInterface $state
108 * The state key value store.
109 * @param \Drupal\Core\Render\RendererInterface $renderer
111 * @param string[] $authentication_providers
112 * The authentication providers, keyed by ID.
114 public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer, array $authentication_providers) {
115 parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
117 $this->renderer = $renderer;
118 $this->authenticationProviders = $authentication_providers;
124 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
129 $container->get('router.route_provider'),
130 $container->get('state'),
131 $container->get('renderer'),
132 $container->getParameter('authentication_providers')
139 public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
140 parent::initDisplay($view, $display, $options);
142 $request_content_type = $this->view->getRequest()->getRequestFormat();
143 // Only use the requested content type if it's not 'html'. If it is then
144 // default to 'json' to aid debugging.
145 // @todo Remove the need for this when we have better content negotiation.
146 if ($request_content_type != 'html') {
147 $this->setContentType($request_content_type);
149 // If the requested content type is 'html' and the default 'json' is not
150 // selected as a format option in the view display, fallback to the first
151 // format in the array.
152 elseif (!empty($options['style']['options']['formats']) && !isset($options['style']['options']['formats'][$this->getContentType()])) {
153 $this->setContentType(reset($options['style']['options']['formats']));
156 $this->setMimeType($this->view->getRequest()->getMimeType($this->contentType));
162 public function getType() {
169 public function usesExposed() {
176 public function displaysExposed() {
181 * Sets the request content type.
183 * @param string $mime_type
184 * The response mime type. E.g. 'application/json'.
186 public function setMimeType($mime_type) {
187 $this->mimeType = $mime_type;
191 * Gets the mime type.
193 * This will return any overridden mime type, otherwise returns the mime type
197 * The response mime type. E.g. 'application/json'.
199 public function getMimeType() {
200 return $this->mimeType;
204 * Sets the content type.
206 * @param string $content_type
207 * The content type machine name. E.g. 'json'.
209 public function setContentType($content_type) {
210 $this->contentType = $content_type;
214 * Gets the content type.
217 * The content type machine name. E.g. 'json'.
219 public function getContentType() {
220 return $this->contentType;
224 * Gets the auth options available.
227 * An array to use as value for "#options" in the form element.
229 public function getAuthOptions() {
230 return array_combine($this->authenticationProviders, $this->authenticationProviders);
236 protected function defineOptions() {
237 $options = parent::defineOptions();
239 // Options for REST authentication.
240 $options['auth'] = ['default' => []];
242 // Set the default style plugin to 'json'.
243 $options['style']['contains']['type']['default'] = 'serializer';
244 $options['row']['contains']['type']['default'] = 'data_entity';
245 $options['defaults']['default']['style'] = FALSE;
246 $options['defaults']['default']['row'] = FALSE;
248 // Remove css/exposed form settings, as they are not used for the data display.
249 unset($options['exposed_form']);
250 unset($options['exposed_block']);
251 unset($options['css_class']);
259 public function optionsSummary(&$categories, &$options) {
260 parent::optionsSummary($categories, $options);
263 $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
265 unset($categories['page'], $categories['exposed']);
266 // Hide some settings, as they aren't useful for pure data output.
267 unset($options['show_admin_links'], $options['analyze-theme']);
269 $categories['path'] = [
270 'title' => $this->t('Path settings'),
271 'column' => 'second',
277 $options['path']['category'] = 'path';
278 $options['path']['title'] = $this->t('Path');
280 'category' => 'path',
281 'title' => $this->t('Authentication'),
282 'value' => views_ui_truncate($auth, 24),
285 // Remove css/exposed form settings, as they are not used for the data
287 unset($options['exposed_form']);
288 unset($options['exposed_block']);
289 unset($options['css_class']);
295 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
296 parent::buildOptionsForm($form, $form_state);
297 if ($form_state->get('section') === 'auth') {
298 $form['#title'] .= $this->t('The supported authentication methods for this view');
300 '#type' => 'checkboxes',
301 '#title' => $this->t('Authentication methods'),
302 '#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.'),
303 '#options' => $this->getAuthOptions(),
304 '#default_value' => $this->getOption('auth'),
312 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
313 parent::submitOptionsForm($form, $form_state);
315 if ($form_state->get('section') == 'auth') {
316 $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
323 public function collectRoutes(RouteCollection $collection) {
324 parent::collectRoutes($collection);
325 $view_id = $this->view->storage->id();
326 $display_id = $this->display['id'];
328 if ($route = $collection->get("view.$view_id.$display_id")) {
329 $style_plugin = $this->getPlugin('style');
330 // REST exports should only respond to get methods.
331 $route->setMethods(['GET']);
333 // Format as a string using pipes as a delimiter.
334 if ($formats = $style_plugin->getFormats()) {
335 // Allow a REST Export View to be returned with an HTML-only accept
336 // format. That allows browsers or other non-compliant systems to access
337 // the view, as it is unlikely to have a conflicting HTML representation
339 $route->setRequirement('_format', implode('|', $formats + ['html']));
341 // Add authentication to the route if it was set. If no authentication was
342 // set, the default authentication will be used, which is cookie based by
344 $auth = $this->getOption('auth');
346 $route->setOption('_auth', $auth);
352 * Determines whether the view overrides the given route.
354 * @param string $view_path
355 * The path of the view.
356 * @param \Symfony\Component\Routing\Route $view_route
357 * The route of the view.
358 * @param \Symfony\Component\Routing\Route $route
362 * TRUE, when the view should override the given route.
364 protected function overrideApplies($view_path, Route $view_route, Route $route) {
365 $route_formats = explode('|', $route->getRequirement('_format'));
366 $view_route_formats = explode('|', $view_route->getRequirement('_format'));
367 return $this->overrideAppliesPathAndMethod($view_path, $view_route, $route)
368 && (!$route->hasRequirement('_format') || array_intersect($route_formats, $view_route_formats) != []);
374 public static function buildResponse($view_id, $display_id, array $args = []) {
375 $build = static::buildBasicRenderable($view_id, $display_id, $args);
377 // Setup an empty response so headers can be added as needed during views
378 // rendering and processing.
379 $response = new CacheableResponse('', 200);
380 $build['#response'] = $response;
382 /** @var \Drupal\Core\Render\RendererInterface $renderer */
383 $renderer = \Drupal::service('renderer');
385 $output = (string) $renderer->renderRoot($build);
387 $response->setContent($output);
388 $cache_metadata = CacheableMetadata::createFromRenderArray($build);
389 $response->addCacheableDependency($cache_metadata);
391 $response->headers->set('Content-type', $build['#content_type']);
399 public function execute() {
402 return $this->view->render();
408 public function render() {
410 $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function() {
411 return $this->view->style_plugin->render();
414 $this->view->element['#content_type'] = $this->getMimeType();
415 $this->view->element['#cache_properties'][] = '#content_type';
417 // Encode and wrap the output in a pre tag if this is for a live preview.
418 if (!empty($this->view->live_preview)) {
419 $build['#prefix'] = '<pre>';
420 $build['#plain_text'] = $build['#markup'];
421 $build['#suffix'] = '</pre>';
422 unset($build['#markup']);
424 elseif ($this->view->getRequest()->getFormat($this->view->element['#content_type']) !== 'html') {
425 // This display plugin is primarily for returning non-HTML formats.
426 // However, we still invoke the renderer to collect cacheability metadata.
427 // Because the renderer is designed for HTML rendering, it filters
428 // #markup for XSS unless it is already known to be safe, but that filter
429 // only works for HTML. Therefore, we mark the contents as safe to bypass
430 // the filter. So long as we are returning this in a non-HTML response
431 // (checked above), this is safe, because an XSS attack only works when
432 // executed by an HTML agent.
433 // @todo Decide how to support non-HTML in the render API in
434 // https://www.drupal.org/node/2501313.
435 $build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']);
438 parent::applyDisplayCachablityMetadata($build);
446 * The DisplayPluginBase preview method assumes we will be returning a render
447 * array. The data plugin will already return the serialized string.
449 public function preview() {
450 return $this->view->render();
456 public function calculateDependencies() {
457 $dependencies = parent::calculateDependencies();
459 $dependencies += ['module' => []];
460 $modules = array_map(function ($authentication_provider) {
461 return $this->authenticationProviders[$authentication_provider];
462 }, $this->getOption('auth'));
463 $dependencies['module'] = array_merge($dependencies['module'], $modules);
465 return $dependencies;