52daee05f040fea0df626f50d9c292f00be32669
[yaffs-website] / web / core / modules / rest / src / Plugin / views / display / RestExport.php
1 <?php
2
3 namespace Drupal\rest\Plugin\views\display;
4
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;
19
20 /**
21  * The plugin that handles Data response callbacks for REST resources.
22  *
23  * @ingroup views_display_plugins
24  *
25  * @ViewsDisplay(
26  *   id = "rest_export",
27  *   title = @Translation("REST export"),
28  *   help = @Translation("Create a REST export resource."),
29  *   uses_route = TRUE,
30  *   admin = @Translation("REST export"),
31  *   returns_response = TRUE
32  * )
33  */
34 class RestExport extends PathPluginBase implements ResponseDisplayPluginInterface {
35
36   /**
37    * {@inheritdoc}
38    */
39   protected $usesAJAX = FALSE;
40
41   /**
42    * {@inheritdoc}
43    */
44   protected $usesPager = FALSE;
45
46   /**
47    * {@inheritdoc}
48    */
49   protected $usesMore = FALSE;
50
51   /**
52    * {@inheritdoc}
53    */
54   protected $usesAreas = FALSE;
55
56   /**
57    * {@inheritdoc}
58    */
59   protected $usesOptions = FALSE;
60
61   /**
62    * Overrides the content type of the data response, if needed.
63    *
64    * @var string
65    */
66   protected $contentType = 'json';
67
68   /**
69    * The mime type for the response.
70    *
71    * @var string
72    */
73   protected $mimeType = 'application/json';
74
75   /**
76    * The renderer.
77    *
78    * @var \Drupal\Core\Render\RendererInterface
79    */
80   protected $renderer;
81
82   /**
83    * The collector of authentication providers.
84    *
85    * @var \Drupal\Core\Authentication\AuthenticationCollectorInterface
86    */
87   protected $authenticationCollector;
88
89   /**
90    * The authentication providers, like 'cookie' and 'basic_auth'.
91    *
92    * @var string[]
93    */
94   protected $authenticationProviderIds;
95
96   /**
97    * The authentication providers' modules, keyed by provider ID.
98    *
99    * Authentication providers like 'cookie' and 'basic_auth' are the array
100    * keys. The array values are the module names, e.g.:
101    * @code
102    *   ['cookie' => 'user', 'basic_auth' => 'basic_auth']
103    * @endcode
104    *
105    * @deprecated as of 8.4.x, will be removed in before Drupal 9.0.0, see
106    *   https://www.drupal.org/node/2825204.
107    *
108    * @var string[]
109    */
110   protected $authenticationProviders;
111
112   /**
113    * The serialization format providers, keyed by format.
114    *
115    * @var string[]
116    */
117   protected $formatProviders;
118
119   /**
120    * Constructs a RestExport object.
121    *
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
133    *   The 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.
138    */
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);
141
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,
147     // basic_auth).
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;
152   }
153
154   /**
155    * {@inheritdoc}
156    */
157   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
158     return new static(
159       $configuration,
160       $plugin_id,
161       $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')
167     );
168   }
169   /**
170    * {@inheritdoc}
171    */
172   public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
173     parent::initDisplay($view, $display, $options);
174
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);
180     }
181
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();
185
186     if ($request_content_type !== 'html') {
187       $this->setContentType($request_content_type);
188     }
189
190     $this->setMimeType($this->view->getRequest()->getMimeType($this->getContentType()));
191   }
192
193   /**
194    * {@inheritdoc}
195    */
196   public function getType() {
197     return 'data';
198   }
199
200   /**
201    * {@inheritdoc}
202    */
203   public function usesExposed() {
204     return TRUE;
205   }
206
207   /**
208    * {@inheritdoc}
209    */
210   public function displaysExposed() {
211     return FALSE;
212   }
213
214   /**
215    * Sets the request content type.
216    *
217    * @param string $mime_type
218    *   The response mime type. E.g. 'application/json'.
219    */
220   public function setMimeType($mime_type) {
221     $this->mimeType = $mime_type;
222   }
223
224   /**
225    * Gets the mime type.
226    *
227    * This will return any overridden mime type, otherwise returns the mime type
228    * from the request.
229    *
230    * @return string
231    *   The response mime type. E.g. 'application/json'.
232    */
233   public function getMimeType() {
234     return $this->mimeType;
235   }
236
237   /**
238    * Sets the content type.
239    *
240    * @param string $content_type
241    *   The content type machine name. E.g. 'json'.
242    */
243   public function setContentType($content_type) {
244     $this->contentType = $content_type;
245   }
246
247   /**
248    * Gets the content type.
249    *
250    * @return string
251    *   The content type machine name. E.g. 'json'.
252    */
253   public function getContentType() {
254     return $this->contentType;
255   }
256
257   /**
258    * Gets the auth options available.
259    *
260    * @return string[]
261    *   An array to use as value for "#options" in the form element.
262    */
263   public function getAuthOptions() {
264     return array_combine($this->authenticationProviderIds, $this->authenticationProviderIds);
265   }
266
267   /**
268    * {@inheritdoc}
269    */
270   protected function defineOptions() {
271     $options = parent::defineOptions();
272
273     // Options for REST authentication.
274     $options['auth'] = ['default' => []];
275
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;
281
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']);
286
287     return $options;
288   }
289
290   /**
291    * {@inheritdoc}
292    */
293   public function optionsSummary(&$categories, &$options) {
294     parent::optionsSummary($categories, $options);
295
296     // Authentication.
297     $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
298
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']);
302
303     $categories['path'] = [
304       'title' => $this->t('Path settings'),
305       'column' => 'second',
306       'build' => [
307         '#weight' => -10,
308       ],
309     ];
310
311     $options['path']['category'] = 'path';
312     $options['path']['title'] = $this->t('Path');
313     $options['auth'] = [
314       'category' => 'path',
315       'title' => $this->t('Authentication'),
316       'value' => views_ui_truncate($auth, 24),
317     ];
318
319     // Remove css/exposed form settings, as they are not used for the data
320     // display.
321     unset($options['exposed_form']);
322     unset($options['exposed_block']);
323     unset($options['css_class']);
324   }
325
326   /**
327    * {@inheritdoc}
328    */
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');
333       $form['auth'] = [
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'),
339       ];
340     }
341   }
342
343   /**
344    * {@inheritdoc}
345    */
346   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
347     parent::submitOptionsForm($form, $form_state);
348
349     if ($form_state->get('section') == 'auth') {
350       $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
351     }
352   }
353
354   /**
355    * {@inheritdoc}
356    */
357   public function collectRoutes(RouteCollection $collection) {
358     parent::collectRoutes($collection);
359     $view_id = $this->view->storage->id();
360     $display_id = $this->display['id'];
361
362     if ($route = $collection->get("view.$view_id.$display_id")) {
363       $style_plugin = $this->getPlugin('style');
364
365       // REST exports should only respond to GET methods.
366       $route->setMethods(['GET']);
367
368       $formats = $style_plugin->getFormats();
369
370       // If there are no configured formats, add all formats that serialization
371       // is known to support.
372       if (!$formats) {
373         $formats = $this->getFormatOptions();
374       }
375
376       // Format as a string using pipes as a delimiter.
377       $route->setRequirement('_format', implode('|', $formats));
378
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
381       // default.
382       $auth = $this->getOption('auth');
383       if (!empty($auth)) {
384         $route->setOption('_auth', $auth);
385       }
386     }
387   }
388
389   /**
390    * Determines whether the view overrides the given route.
391    *
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
397    *   The route itself.
398    *
399    * @return bool
400    *   TRUE, when the view should override the given route.
401    */
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) != []);
407   }
408
409   /**
410    * {@inheritdoc}
411    */
412   public static function buildResponse($view_id, $display_id, array $args = []) {
413     $build = static::buildBasicRenderable($view_id, $display_id, $args);
414
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;
419
420     /** @var \Drupal\Core\Render\RendererInterface $renderer */
421     $renderer = \Drupal::service('renderer');
422
423     $output = (string) $renderer->renderRoot($build);
424
425     $response->setContent($output);
426     $cache_metadata = CacheableMetadata::createFromRenderArray($build);
427     $response->addCacheableDependency($cache_metadata);
428
429     $response->headers->set('Content-type', $build['#content_type']);
430
431     return $response;
432   }
433
434   /**
435    * {@inheritdoc}
436    */
437   public function execute() {
438     parent::execute();
439
440     return $this->view->render();
441   }
442
443   /**
444    * {@inheritdoc}
445    */
446   public function render() {
447     $build = [];
448     $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
449       return $this->view->style_plugin->render();
450     });
451
452     $this->view->element['#content_type'] = $this->getMimeType();
453     $this->view->element['#cache_properties'][] = '#content_type';
454
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']);
461     }
462     else {
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
470       // agent.
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']);
474     }
475
476     parent::applyDisplayCacheabilityMetadata($build);
477
478     return $build;
479   }
480
481   /**
482    * {@inheritdoc}
483    *
484    * The DisplayPluginBase preview method assumes we will be returning a render
485    * array. The data plugin will already return the serialized string.
486    */
487   public function preview() {
488     return $this->view->render();
489   }
490
491   /**
492    * {@inheritdoc}
493    */
494   public function calculateDependencies() {
495     $dependencies = parent::calculateDependencies();
496
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
501       // view to be saved.
502       return isset($this->authenticationProviderIds[$provider])
503         ? $this->authenticationProviderIds[$provider]
504         : NULL;
505     }, $this->getOption('auth'))));
506
507     return $dependencies;
508   }
509
510   /**
511    * Returns an array of format options.
512    *
513    * @return string[]
514    *   An array of format options. Both key and value are the same.
515    */
516   protected function getFormatOptions() {
517     $formats = array_keys($this->formatProviders);
518     return array_combine($formats, $formats);
519   }
520
521 }