15abaa6fdfe13f2cac90c222b1006ccd097f7afa
[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;
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, keyed by ID.
91    *
92    * @var string[]
93    */
94   protected $authenticationProviders;
95
96   /**
97    * Constructs a RestExport object.
98    *
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
110    *   The renderer.
111    * @param string[] $authentication_providers
112    *   The authentication providers, keyed by ID.
113    */
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);
116
117     $this->renderer = $renderer;
118     $this->authenticationProviders = $authentication_providers;
119   }
120
121   /**
122    * {@inheritdoc}
123    */
124   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
125     return new static(
126       $configuration,
127       $plugin_id,
128       $plugin_definition,
129       $container->get('router.route_provider'),
130       $container->get('state'),
131       $container->get('renderer'),
132       $container->getParameter('authentication_providers')
133
134     );
135   }
136   /**
137    * {@inheritdoc}
138    */
139   public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
140     parent::initDisplay($view, $display, $options);
141
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);
148     }
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']));
154     }
155
156     $this->setMimeType($this->view->getRequest()->getMimeType($this->contentType));
157   }
158
159   /**
160    * {@inheritdoc}
161    */
162   public function getType() {
163     return 'data';
164   }
165
166   /**
167    * {@inheritdoc}
168    */
169   public function usesExposed() {
170     return TRUE;
171   }
172
173   /**
174    * {@inheritdoc}
175    */
176   public function displaysExposed() {
177     return FALSE;
178   }
179
180   /**
181    * Sets the request content type.
182    *
183    * @param string $mime_type
184    *   The response mime type. E.g. 'application/json'.
185    */
186   public function setMimeType($mime_type) {
187     $this->mimeType = $mime_type;
188   }
189
190   /**
191    * Gets the mime type.
192    *
193    * This will return any overridden mime type, otherwise returns the mime type
194    * from the request.
195    *
196    * @return string
197    *   The response mime type. E.g. 'application/json'.
198    */
199   public function getMimeType() {
200     return $this->mimeType;
201   }
202
203   /**
204    * Sets the content type.
205    *
206    * @param string $content_type
207    *   The content type machine name. E.g. 'json'.
208    */
209   public function setContentType($content_type) {
210     $this->contentType = $content_type;
211   }
212
213   /**
214    * Gets the content type.
215    *
216    * @return string
217    *   The content type machine name. E.g. 'json'.
218    */
219   public function getContentType() {
220     return $this->contentType;
221   }
222
223   /**
224    * Gets the auth options available.
225    *
226    * @return string[]
227    *   An array to use as value for "#options" in the form element.
228    */
229   public function getAuthOptions() {
230     return array_combine($this->authenticationProviders, $this->authenticationProviders);
231   }
232
233   /**
234    * {@inheritdoc}
235    */
236   protected function defineOptions() {
237     $options = parent::defineOptions();
238
239     // Options for REST authentication.
240     $options['auth'] = ['default' => []];
241
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;
247
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']);
252
253     return $options;
254   }
255
256   /**
257    * {@inheritdoc}
258    */
259   public function optionsSummary(&$categories, &$options) {
260     parent::optionsSummary($categories, $options);
261
262     // Authentication.
263     $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
264
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']);
268
269     $categories['path'] = [
270       'title' => $this->t('Path settings'),
271       'column' => 'second',
272       'build' => [
273         '#weight' => -10,
274       ],
275     ];
276
277     $options['path']['category'] = 'path';
278     $options['path']['title'] = $this->t('Path');
279     $options['auth'] = [
280       'category' => 'path',
281       'title' => $this->t('Authentication'),
282       'value' => views_ui_truncate($auth, 24),
283     ];
284
285     // Remove css/exposed form settings, as they are not used for the data
286     // display.
287     unset($options['exposed_form']);
288     unset($options['exposed_block']);
289     unset($options['css_class']);
290   }
291
292   /**
293    * {@inheritdoc}
294    */
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');
299       $form['auth'] = [
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'),
305       ];
306     }
307   }
308
309   /**
310    * {@inheritdoc}
311    */
312   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
313     parent::submitOptionsForm($form, $form_state);
314
315     if ($form_state->get('section') == 'auth') {
316       $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
317     }
318   }
319
320   /**
321    * {@inheritdoc}
322    */
323   public function collectRoutes(RouteCollection $collection) {
324     parent::collectRoutes($collection);
325     $view_id = $this->view->storage->id();
326     $display_id = $this->display['id'];
327
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']);
332
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
338         // anyway.
339         $route->setRequirement('_format', implode('|', $formats + ['html']));
340       }
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
343       // default.
344       $auth = $this->getOption('auth');
345       if (!empty($auth)) {
346         $route->setOption('_auth', $auth);
347       }
348     }
349   }
350
351   /**
352    * Determines whether the view overrides the given route.
353    *
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
359    *   The route itself.
360    *
361    * @return bool
362    *   TRUE, when the view should override the given route.
363    */
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) != []);
369   }
370
371   /**
372    * {@inheritdoc}
373    */
374   public static function buildResponse($view_id, $display_id, array $args = []) {
375     $build = static::buildBasicRenderable($view_id, $display_id, $args);
376
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;
381
382     /** @var \Drupal\Core\Render\RendererInterface $renderer */
383     $renderer = \Drupal::service('renderer');
384
385     $output = (string) $renderer->renderRoot($build);
386
387     $response->setContent($output);
388     $cache_metadata = CacheableMetadata::createFromRenderArray($build);
389     $response->addCacheableDependency($cache_metadata);
390
391     $response->headers->set('Content-type', $build['#content_type']);
392
393     return $response;
394   }
395
396   /**
397    * {@inheritdoc}
398    */
399   public function execute() {
400     parent::execute();
401
402     return $this->view->render();
403   }
404
405   /**
406    * {@inheritdoc}
407    */
408   public function render() {
409     $build = [];
410     $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function() {
411       return $this->view->style_plugin->render();
412     });
413
414     $this->view->element['#content_type'] = $this->getMimeType();
415     $this->view->element['#cache_properties'][] = '#content_type';
416
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']);
423     }
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']);
436     }
437
438     parent::applyDisplayCachablityMetadata($build);
439
440     return $build;
441   }
442
443   /**
444    * {@inheritdoc}
445    *
446    * The DisplayPluginBase preview method assumes we will be returning a render
447    * array. The data plugin will already return the serialized string.
448    */
449   public function preview() {
450     return $this->view->render();
451   }
452
453   /**
454    * {@inheritdoc}
455    */
456   public function calculateDependencies() {
457     $dependencies = parent::calculateDependencies();
458
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);
464
465     return $dependencies;
466   }
467
468 }