2b59af51185dcf06550cc0f84334f576246e25a3
[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, 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    * Constructs a RestExport object.
114    *
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
126    *   The renderer.
127    * @param string[] $authentication_providers
128    *   The authentication providers, keyed by ID.
129    */
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);
132
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,
138     // basic_auth).
139     $this->authenticationProviderIds = array_keys($authentication_providers);
140     // For BC reasons we keep around authenticationProviders as before.
141     $this->authenticationProviders = $authentication_providers;
142   }
143
144   /**
145    * {@inheritdoc}
146    */
147   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
148     return new static(
149       $configuration,
150       $plugin_id,
151       $plugin_definition,
152       $container->get('router.route_provider'),
153       $container->get('state'),
154       $container->get('renderer'),
155       $container->getParameter('authentication_providers')
156
157     );
158   }
159   /**
160    * {@inheritdoc}
161    */
162   public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
163     parent::initDisplay($view, $display, $options);
164
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);
171     }
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']));
177     }
178
179     $this->setMimeType($this->view->getRequest()->getMimeType($this->contentType));
180   }
181
182   /**
183    * {@inheritdoc}
184    */
185   public function getType() {
186     return 'data';
187   }
188
189   /**
190    * {@inheritdoc}
191    */
192   public function usesExposed() {
193     return TRUE;
194   }
195
196   /**
197    * {@inheritdoc}
198    */
199   public function displaysExposed() {
200     return FALSE;
201   }
202
203   /**
204    * Sets the request content type.
205    *
206    * @param string $mime_type
207    *   The response mime type. E.g. 'application/json'.
208    */
209   public function setMimeType($mime_type) {
210     $this->mimeType = $mime_type;
211   }
212
213   /**
214    * Gets the mime type.
215    *
216    * This will return any overridden mime type, otherwise returns the mime type
217    * from the request.
218    *
219    * @return string
220    *   The response mime type. E.g. 'application/json'.
221    */
222   public function getMimeType() {
223     return $this->mimeType;
224   }
225
226   /**
227    * Sets the content type.
228    *
229    * @param string $content_type
230    *   The content type machine name. E.g. 'json'.
231    */
232   public function setContentType($content_type) {
233     $this->contentType = $content_type;
234   }
235
236   /**
237    * Gets the content type.
238    *
239    * @return string
240    *   The content type machine name. E.g. 'json'.
241    */
242   public function getContentType() {
243     return $this->contentType;
244   }
245
246   /**
247    * Gets the auth options available.
248    *
249    * @return string[]
250    *   An array to use as value for "#options" in the form element.
251    */
252   public function getAuthOptions() {
253     return array_combine($this->authenticationProviderIds, $this->authenticationProviderIds);
254   }
255
256   /**
257    * {@inheritdoc}
258    */
259   protected function defineOptions() {
260     $options = parent::defineOptions();
261
262     // Options for REST authentication.
263     $options['auth'] = ['default' => []];
264
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;
270
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']);
275
276     return $options;
277   }
278
279   /**
280    * {@inheritdoc}
281    */
282   public function optionsSummary(&$categories, &$options) {
283     parent::optionsSummary($categories, $options);
284
285     // Authentication.
286     $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
287
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']);
291
292     $categories['path'] = [
293       'title' => $this->t('Path settings'),
294       'column' => 'second',
295       'build' => [
296         '#weight' => -10,
297       ],
298     ];
299
300     $options['path']['category'] = 'path';
301     $options['path']['title'] = $this->t('Path');
302     $options['auth'] = [
303       'category' => 'path',
304       'title' => $this->t('Authentication'),
305       'value' => views_ui_truncate($auth, 24),
306     ];
307
308     // Remove css/exposed form settings, as they are not used for the data
309     // display.
310     unset($options['exposed_form']);
311     unset($options['exposed_block']);
312     unset($options['css_class']);
313   }
314
315   /**
316    * {@inheritdoc}
317    */
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');
322       $form['auth'] = [
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'),
328       ];
329     }
330   }
331
332   /**
333    * {@inheritdoc}
334    */
335   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
336     parent::submitOptionsForm($form, $form_state);
337
338     if ($form_state->get('section') == 'auth') {
339       $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
340     }
341   }
342
343   /**
344    * {@inheritdoc}
345    */
346   public function collectRoutes(RouteCollection $collection) {
347     parent::collectRoutes($collection);
348     $view_id = $this->view->storage->id();
349     $display_id = $this->display['id'];
350
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']);
355
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
361         // anyway.
362         $route->setRequirement('_format', implode('|', $formats + ['html']));
363       }
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
366       // default.
367       $auth = $this->getOption('auth');
368       if (!empty($auth)) {
369         $route->setOption('_auth', $auth);
370       }
371     }
372   }
373
374   /**
375    * Determines whether the view overrides the given route.
376    *
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
382    *   The route itself.
383    *
384    * @return bool
385    *   TRUE, when the view should override the given route.
386    */
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) != []);
392   }
393
394   /**
395    * {@inheritdoc}
396    */
397   public static function buildResponse($view_id, $display_id, array $args = []) {
398     $build = static::buildBasicRenderable($view_id, $display_id, $args);
399
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;
404
405     /** @var \Drupal\Core\Render\RendererInterface $renderer */
406     $renderer = \Drupal::service('renderer');
407
408     $output = (string) $renderer->renderRoot($build);
409
410     $response->setContent($output);
411     $cache_metadata = CacheableMetadata::createFromRenderArray($build);
412     $response->addCacheableDependency($cache_metadata);
413
414     $response->headers->set('Content-type', $build['#content_type']);
415
416     return $response;
417   }
418
419   /**
420    * {@inheritdoc}
421    */
422   public function execute() {
423     parent::execute();
424
425     return $this->view->render();
426   }
427
428   /**
429    * {@inheritdoc}
430    */
431   public function render() {
432     $build = [];
433     $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
434       return $this->view->style_plugin->render();
435     });
436
437     $this->view->element['#content_type'] = $this->getMimeType();
438     $this->view->element['#cache_properties'][] = '#content_type';
439
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']);
446     }
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']);
459     }
460
461     parent::applyDisplayCacheabilityMetadata($build);
462
463     return $build;
464   }
465
466   /**
467    * {@inheritdoc}
468    *
469    * The DisplayPluginBase preview method assumes we will be returning a render
470    * array. The data plugin will already return the serialized string.
471    */
472   public function preview() {
473     return $this->view->render();
474   }
475
476   /**
477    * {@inheritdoc}
478    */
479   public function calculateDependencies() {
480     $dependencies = parent::calculateDependencies();
481
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
486       // view to be saved.
487       return isset($this->authenticationProviderIds[$provider])
488         ? $this->authenticationProviderIds[$provider]
489         : NULL;
490     }, $this->getOption('auth'))));
491
492     return $dependencies;
493   }
494
495 }