Updated Drupal to 8.6. This goes with the following updates because it's possible...
[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   /**
171    * {@inheritdoc}
172    */
173   public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
174     parent::initDisplay($view, $display, $options);
175
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);
181     }
182
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();
186
187     if ($request_content_type !== 'html') {
188       $this->setContentType($request_content_type);
189     }
190
191     $this->setMimeType($this->view->getRequest()->getMimeType($this->getContentType()));
192   }
193
194   /**
195    * {@inheritdoc}
196    */
197   public function getType() {
198     return 'data';
199   }
200
201   /**
202    * {@inheritdoc}
203    */
204   public function usesExposed() {
205     return TRUE;
206   }
207
208   /**
209    * {@inheritdoc}
210    */
211   public function displaysExposed() {
212     return FALSE;
213   }
214
215   /**
216    * Sets the request content type.
217    *
218    * @param string $mime_type
219    *   The response mime type. E.g. 'application/json'.
220    */
221   public function setMimeType($mime_type) {
222     $this->mimeType = $mime_type;
223   }
224
225   /**
226    * Gets the mime type.
227    *
228    * This will return any overridden mime type, otherwise returns the mime type
229    * from the request.
230    *
231    * @return string
232    *   The response mime type. E.g. 'application/json'.
233    */
234   public function getMimeType() {
235     return $this->mimeType;
236   }
237
238   /**
239    * Sets the content type.
240    *
241    * @param string $content_type
242    *   The content type machine name. E.g. 'json'.
243    */
244   public function setContentType($content_type) {
245     $this->contentType = $content_type;
246   }
247
248   /**
249    * Gets the content type.
250    *
251    * @return string
252    *   The content type machine name. E.g. 'json'.
253    */
254   public function getContentType() {
255     return $this->contentType;
256   }
257
258   /**
259    * Gets the auth options available.
260    *
261    * @return string[]
262    *   An array to use as value for "#options" in the form element.
263    */
264   public function getAuthOptions() {
265     return array_combine($this->authenticationProviderIds, $this->authenticationProviderIds);
266   }
267
268   /**
269    * {@inheritdoc}
270    */
271   protected function defineOptions() {
272     $options = parent::defineOptions();
273
274     // Options for REST authentication.
275     $options['auth'] = ['default' => []];
276
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;
282
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']);
287
288     return $options;
289   }
290
291   /**
292    * {@inheritdoc}
293    */
294   public function optionsSummary(&$categories, &$options) {
295     parent::optionsSummary($categories, $options);
296
297     // Authentication.
298     $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
299
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']);
303
304     $categories['path'] = [
305       'title' => $this->t('Path settings'),
306       'column' => 'second',
307       'build' => [
308         '#weight' => -10,
309       ],
310     ];
311
312     $options['path']['category'] = 'path';
313     $options['path']['title'] = $this->t('Path');
314     $options['auth'] = [
315       'category' => 'path',
316       'title' => $this->t('Authentication'),
317       'value' => views_ui_truncate($auth, 24),
318     ];
319
320     // Remove css/exposed form settings, as they are not used for the data
321     // display.
322     unset($options['exposed_form']);
323     unset($options['exposed_block']);
324     unset($options['css_class']);
325   }
326
327   /**
328    * {@inheritdoc}
329    */
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');
334       $form['auth'] = [
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'),
340       ];
341     }
342   }
343
344   /**
345    * {@inheritdoc}
346    */
347   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
348     parent::submitOptionsForm($form, $form_state);
349
350     if ($form_state->get('section') == 'auth') {
351       $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
352     }
353   }
354
355   /**
356    * {@inheritdoc}
357    */
358   public function collectRoutes(RouteCollection $collection) {
359     parent::collectRoutes($collection);
360     $view_id = $this->view->storage->id();
361     $display_id = $this->display['id'];
362
363     if ($route = $collection->get("view.$view_id.$display_id")) {
364       $style_plugin = $this->getPlugin('style');
365
366       // REST exports should only respond to GET methods.
367       $route->setMethods(['GET']);
368
369       $formats = $style_plugin->getFormats();
370
371       // If there are no configured formats, add all formats that serialization
372       // is known to support.
373       if (!$formats) {
374         $formats = $this->getFormatOptions();
375       }
376
377       // Format as a string using pipes as a delimiter.
378       $route->setRequirement('_format', implode('|', $formats));
379
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
382       // default.
383       $auth = $this->getOption('auth');
384       if (!empty($auth)) {
385         $route->setOption('_auth', $auth);
386       }
387     }
388   }
389
390   /**
391    * Determines whether the view overrides the given route.
392    *
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
398    *   The route itself.
399    *
400    * @return bool
401    *   TRUE, when the view should override the given route.
402    */
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) != []);
408   }
409
410   /**
411    * {@inheritdoc}
412    */
413   public static function buildResponse($view_id, $display_id, array $args = []) {
414     $build = static::buildBasicRenderable($view_id, $display_id, $args);
415
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;
420
421     /** @var \Drupal\Core\Render\RendererInterface $renderer */
422     $renderer = \Drupal::service('renderer');
423
424     $output = (string) $renderer->renderRoot($build);
425
426     $response->setContent($output);
427     $cache_metadata = CacheableMetadata::createFromRenderArray($build);
428     $response->addCacheableDependency($cache_metadata);
429
430     $response->headers->set('Content-type', $build['#content_type']);
431
432     return $response;
433   }
434
435   /**
436    * {@inheritdoc}
437    */
438   public function execute() {
439     parent::execute();
440
441     return $this->view->render();
442   }
443
444   /**
445    * {@inheritdoc}
446    */
447   public function render() {
448     $build = [];
449     $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
450       return $this->view->style_plugin->render();
451     });
452
453     $this->view->element['#content_type'] = $this->getMimeType();
454     $this->view->element['#cache_properties'][] = '#content_type';
455
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']);
462     }
463     else {
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
471       // agent.
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']);
475     }
476
477     parent::applyDisplayCacheabilityMetadata($build);
478
479     return $build;
480   }
481
482   /**
483    * {@inheritdoc}
484    *
485    * The DisplayPluginBase preview method assumes we will be returning a render
486    * array. The data plugin will already return the serialized string.
487    */
488   public function preview() {
489     return $this->view->render();
490   }
491
492   /**
493    * {@inheritdoc}
494    */
495   public function calculateDependencies() {
496     $dependencies = parent::calculateDependencies();
497
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
502       // view to be saved.
503       return isset($this->authenticationProviderIds[$provider])
504         ? $this->authenticationProviderIds[$provider]
505         : NULL;
506     }, $this->getOption('auth'))));
507
508     return $dependencies;
509   }
510
511   /**
512    * Returns an array of format options.
513    *
514    * @return string[]
515    *   An array of format options. Both key and value are the same.
516    */
517   protected function getFormatOptions() {
518     $formats = array_keys($this->formatProviders);
519     return array_combine($formats, $formats);
520   }
521
522 }