Pull merge.
[yaffs-website] / web / core / modules / views / src / Plugin / views / display / PathPluginBase.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\display;
4
5 use Drupal\Component\Utility\UrlHelper;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\Core\Language\LanguageInterface;
8 use Drupal\Core\Routing\UrlGeneratorTrait;
9 use Drupal\Core\State\StateInterface;
10 use Drupal\Core\Routing\RouteCompiler;
11 use Drupal\Core\Routing\RouteProviderInterface;
12 use Drupal\Core\Url;
13 use Drupal\views\Views;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
15 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
16 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
17 use Symfony\Component\Routing\Route;
18 use Symfony\Component\Routing\RouteCollection;
19
20 /**
21  * The base display plugin for path/callbacks. This is used for pages and feeds.
22  *
23  * @see \Drupal\views\EventSubscriber\RouteSubscriber
24  */
25 abstract class PathPluginBase extends DisplayPluginBase implements DisplayRouterInterface, DisplayMenuInterface {
26
27   use UrlGeneratorTrait;
28
29   /**
30    * The route provider.
31    *
32    * @var \Drupal\Core\Routing\RouteProviderInterface
33    */
34   protected $routeProvider;
35
36   /**
37    * The state key value store.
38    *
39    * @var \Drupal\Core\State\StateInterface
40    */
41   protected $state;
42
43   /**
44    * Constructs a PathPluginBase object.
45    *
46    * @param array $configuration
47    *   A configuration array containing information about the plugin instance.
48    * @param string $plugin_id
49    *   The plugin_id for the plugin instance.
50    * @param mixed $plugin_definition
51    *   The plugin implementation definition.
52    * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
53    *   The route provider.
54    * @param \Drupal\Core\State\StateInterface $state
55    *   The state key value store.
56    */
57   public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state) {
58     parent::__construct($configuration, $plugin_id, $plugin_definition);
59
60     $this->routeProvider = $route_provider;
61     $this->state = $state;
62   }
63
64   /**
65    * {@inheritdoc}
66    */
67   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
68     return new static(
69       $configuration,
70       $plugin_id,
71       $plugin_definition,
72       $container->get('router.route_provider'),
73       $container->get('state')
74     );
75   }
76
77   /**
78    * {@inheritdoc}
79    */
80   public function hasPath() {
81     return TRUE;
82   }
83
84   /**
85    * {@inheritdoc}
86    */
87   public function getPath() {
88     $bits = explode('/', $this->getOption('path'));
89     if ($this->isDefaultTabPath()) {
90       array_pop($bits);
91     }
92     return implode('/', $bits);
93   }
94
95   /**
96    * Determines if this display's path is a default tab.
97    *
98    * @return bool
99    *   TRUE if the display path is for a default tab, FALSE otherwise.
100    */
101   protected function isDefaultTabPath() {
102     $menu = $this->getOption('menu');
103     $tab_options = $this->getOption('tab_options');
104     return $menu['type'] == 'default tab' && !empty($tab_options['type']) && $tab_options['type'] != 'none';
105   }
106
107   /**
108    * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase:defineOptions().
109    */
110   protected function defineOptions() {
111     $options = parent::defineOptions();
112     $options['path'] = ['default' => ''];
113     $options['route_name'] = ['default' => ''];
114
115     return $options;
116   }
117
118   /**
119    * Generates a route entry for a given view and display.
120    *
121    * @param string $view_id
122    *   The ID of the view.
123    * @param string $display_id
124    *   The current display ID.
125    *
126    * @return \Symfony\Component\Routing\Route
127    *   The route for the view.
128    */
129   protected function getRoute($view_id, $display_id) {
130     $defaults = [
131       '_controller' => 'Drupal\views\Routing\ViewPageController::handle',
132       '_title' => $this->view->getTitle(),
133       'view_id' => $view_id,
134       'display_id' => $display_id,
135       '_view_display_show_admin_links' => $this->getOption('show_admin_links'),
136     ];
137
138     // @todo How do we apply argument validation?
139     $bits = explode('/', $this->getOption('path'));
140     // @todo Figure out validation/argument loading.
141     // Replace % with %views_arg for menu autoloading and add to the
142     // page arguments so the argument actually comes through.
143     $arg_counter = 0;
144
145     $argument_ids = array_keys((array) $this->getOption('arguments'));
146     $total_arguments = count($argument_ids);
147
148     $argument_map = [];
149
150     // Replace arguments in the views UI (defined via %) with parameters in
151     // routes (defined via {}). As a name for the parameter use arg_$key, so
152     // it can be pulled in the views controller from the request.
153     foreach ($bits as $pos => $bit) {
154       if ($bit == '%') {
155         // Generate the name of the parameter using the key of the argument
156         // handler.
157         $arg_id = 'arg_' . $arg_counter++;
158         $bits[$pos] = '{' . $arg_id . '}';
159         $argument_map[$arg_id] = $arg_id;
160       }
161       elseif (strpos($bit, '%') === 0) {
162         // Use the name defined in the path.
163         $parameter_name = substr($bit, 1);
164         $arg_id = 'arg_' . $arg_counter++;
165         $argument_map[$arg_id] = $parameter_name;
166         $bits[$pos] = '{' . $parameter_name . '}';
167       }
168     }
169
170     // Add missing arguments not defined in the path, but added as handler.
171     while (($total_arguments - $arg_counter) > 0) {
172       $arg_id = 'arg_' . $arg_counter++;
173       $bit = '{' . $arg_id . '}';
174       // In contrast to the previous loop add the defaults here, as % was not
175       // specified, which means the argument is optional.
176       $defaults[$arg_id] = NULL;
177       $argument_map[$arg_id] = $arg_id;
178       $bits[] = $bit;
179     }
180
181     // If this is to be a default tab, create the route for the parent path.
182     if ($this->isDefaultTabPath()) {
183       $bit = array_pop($bits);
184       if (empty($bits)) {
185         $bits[] = $bit;
186       }
187     }
188
189     $route_path = '/' . implode('/', $bits);
190
191     $route = new Route($route_path, $defaults);
192
193     // Add access check parameters to the route.
194     $access_plugin = $this->getPlugin('access');
195     if (!isset($access_plugin)) {
196       // @todo Do we want to support a default plugin in getPlugin itself?
197       $access_plugin = Views::pluginManager('access')->createInstance('none');
198     }
199     $access_plugin->alterRouteDefinition($route);
200
201     // Set the argument map, in order to support named parameters.
202     $route->setOption('_view_argument_map', $argument_map);
203     $route->setOption('_view_display_plugin_id', $this->getPluginId());
204     $route->setOption('_view_display_plugin_class', get_called_class());
205     $route->setOption('_view_display_show_admin_links', $this->getOption('show_admin_links'));
206
207     // Store whether the view will return a response.
208     $route->setOption('returns_response', !empty($this->getPluginDefinition()['returns_response']));
209
210     return $route;
211   }
212
213   /**
214    * {@inheritdoc}
215    */
216   public function collectRoutes(RouteCollection $collection) {
217     $view_id = $this->view->storage->id();
218     $display_id = $this->display['id'];
219
220     $route = $this->getRoute($view_id, $display_id);
221
222     if (!($route_name = $this->getOption('route_name'))) {
223       $route_name = "view.$view_id.$display_id";
224     }
225     $collection->add($route_name, $route);
226     return ["$view_id.$display_id" => $route_name];
227   }
228
229   /**
230    * Determines whether the view overrides the given route.
231    *
232    * @param string $view_path
233    *   The path of the view.
234    * @param \Symfony\Component\Routing\Route $view_route
235    *   The route of the view.
236    * @param \Symfony\Component\Routing\Route $route
237    *   The route itself.
238    *
239    * @return bool
240    *   TRUE, when the view should override the given route.
241    */
242   protected function overrideApplies($view_path, Route $view_route, Route $route) {
243     return $this->overrideAppliesPathAndMethod($view_path, $view_route, $route)
244       && (!$route->hasRequirement('_format') || $route->getRequirement('_format') === 'html');
245   }
246
247   /**
248    * Determines whether a override for the path and method should happen.
249    *
250    * @param string $view_path
251    *   The path of the view.
252    * @param \Symfony\Component\Routing\Route $view_route
253    *   The route of the view.
254    * @param \Symfony\Component\Routing\Route $route
255    *   The route itself.
256    *
257    * @return bool
258    *   TRUE, when the view should override the given route.
259    */
260   protected function overrideAppliesPathAndMethod($view_path, Route $view_route, Route $route) {
261     // Find all paths which match the path of the current display..
262     $route_path = RouteCompiler::getPathWithoutDefaults($route);
263     $route_path = RouteCompiler::getPatternOutline($route_path);
264
265     // Ensure that we don't override a route which is already controlled by
266     // views.
267     return !$route->hasDefault('view_id')
268     && ('/' . $view_path == $route_path)
269     // Also ensure that we don't override for example REST routes.
270     && (!$route->getMethods() || in_array('GET', $route->getMethods()));
271   }
272
273   /**
274    * {@inheritdoc}
275    */
276   public function alterRoutes(RouteCollection $collection) {
277     $view_route_names = [];
278     $view_path = $this->getPath();
279     $view_id = $this->view->storage->id();
280     $display_id = $this->display['id'];
281     $view_route = $this->getRoute($view_id, $display_id);
282
283     foreach ($collection->all() as $name => $route) {
284       if ($this->overrideApplies($view_path, $view_route, $route)) {
285         $parameters = $route->compile()->getPathVariables();
286
287         // @todo Figure out whether we need to merge some settings (like
288         // requirements).
289
290         // Replace the existing route with a new one based on views.
291         $original_route = $collection->get($name);
292         $collection->remove($name);
293
294         $path = $view_route->getPath();
295         // Replace the path with the original parameter names and add a mapping.
296         $argument_map = [];
297         // We assume that the numeric ids of the parameters match the one from
298         // the view argument handlers.
299         foreach ($parameters as $position => $parameter_name) {
300           $path = str_replace('{arg_' . $position . '}', '{' . $parameter_name . '}', $path);
301           $argument_map['arg_' . $position] = $parameter_name;
302         }
303         // Copy the original options from the route, so for example we ensure
304         // that parameter conversion options is carried over.
305         $view_route->setOptions($view_route->getOptions() + $original_route->getOptions());
306
307         if ($original_route->hasDefault('_title_callback')) {
308           $view_route->setDefault('_title_callback', $original_route->getDefault('_title_callback'));
309         }
310
311         // Set the corrected path and the mapping to the route object.
312         $view_route->setOption('_view_argument_map', $argument_map);
313         $view_route->setPath($path);
314
315         $collection->add($name, $view_route);
316         $view_route_names[$view_id . '.' . $display_id] = $name;
317       }
318     }
319
320     return $view_route_names;
321   }
322
323   /**
324    * {@inheritdoc}
325    */
326   public function getMenuLinks() {
327     $links = [];
328
329     // Replace % with the link to our standard views argument loader
330     // views_arg_load -- which lives in views.module.
331
332     $bits = explode('/', $this->getOption('path'));
333
334     // Replace % with %views_arg for menu autoloading and add to the
335     // page arguments so the argument actually comes through.
336     foreach ($bits as $pos => $bit) {
337       if ($bit == '%') {
338         // If a view requires any arguments we cannot create a static menu link.
339         return [];
340       }
341     }
342
343     $path = implode('/', $bits);
344     $view_id = $this->view->storage->id();
345     $display_id = $this->display['id'];
346     $view_id_display = "{$view_id}.{$display_id}";
347     $menu_link_id = 'views.' . str_replace('/', '.', $view_id_display);
348
349     if ($path) {
350       $menu = $this->getOption('menu');
351       if (!empty($menu['type']) && $menu['type'] == 'normal') {
352         $links[$menu_link_id] = [];
353         // Some views might override existing paths, so we have to set the route
354         // name based upon the altering.
355         $links[$menu_link_id] = [
356           'route_name' => $this->getRouteName(),
357           // Identify URL embedded arguments and correlate them to a handler.
358           'load arguments'  => [$this->view->storage->id(), $this->display['id'], '%index'],
359           'id' => $menu_link_id,
360         ];
361         $links[$menu_link_id]['title'] = $menu['title'];
362         $links[$menu_link_id]['description'] = $menu['description'];
363         $links[$menu_link_id]['parent'] = $menu['parent'];
364         $links[$menu_link_id]['enabled'] = $menu['enabled'];
365         $links[$menu_link_id]['expanded'] = $menu['expanded'];
366
367         if (isset($menu['weight'])) {
368           $links[$menu_link_id]['weight'] = intval($menu['weight']);
369         }
370
371         // Insert item into the proper menu.
372         $links[$menu_link_id]['menu_name'] = $menu['menu_name'];
373         // Keep track of where we came from.
374         $links[$menu_link_id]['metadata'] = [
375           'view_id' => $view_id,
376           'display_id' => $display_id,
377         ];
378       }
379     }
380
381     return $links;
382   }
383
384   /**
385    * {@inheritdoc}
386    */
387   public function execute() {
388     // Prior to this being called, the $view should already be set to this
389     // display, and arguments should be set on the view.
390     $this->view->build();
391
392     if (!empty($this->view->build_info['fail'])) {
393       throw new NotFoundHttpException();
394     }
395
396     if (!empty($this->view->build_info['denied'])) {
397       throw new AccessDeniedHttpException();
398     }
399   }
400
401   /**
402    * {@inheritdoc}
403    */
404   public function optionsSummary(&$categories, &$options) {
405     parent::optionsSummary($categories, $options);
406
407     $categories['page'] = [
408       'title' => $this->t('Page settings'),
409       'column' => 'second',
410       'build' => [
411         '#weight' => -10,
412       ],
413     ];
414
415     $path = strip_tags($this->getOption('path'));
416
417     if (empty($path)) {
418       $path = $this->t('No path is set');
419     }
420     else {
421       $path = '/' . $path;
422     }
423
424     $options['path'] = [
425       'category' => 'page',
426       'title' => $this->t('Path'),
427       'value' => views_ui_truncate($path, 24),
428     ];
429   }
430
431   /**
432    * {@inheritdoc}
433    */
434   public function buildOptionsForm(&$form, FormStateInterface $form_state) {
435     parent::buildOptionsForm($form, $form_state);
436
437     switch ($form_state->get('section')) {
438       case 'path':
439         $form['#title'] .= $this->t('The menu path or URL of this view');
440         $form['path'] = [
441           '#type' => 'textfield',
442           '#title' => $this->t('Path'),
443           '#description' => $this->t('This view will be displayed by visiting this path on your site. You may use "%" in your URL to represent values that will be used for contextual filters: For example, "node/%/feed". If needed you can even specify named route parameters like taxonomy/term/%taxonomy_term'),
444           '#default_value' => $this->getOption('path'),
445           '#field_prefix' => '<span dir="ltr">' . $this->url('<none>', [], ['absolute' => TRUE]),
446           '#field_suffix' => '</span>&lrm;',
447           '#attributes' => ['dir' => LanguageInterface::DIRECTION_LTR],
448           // Account for the leading backslash.
449           '#maxlength' => 254,
450         ];
451         break;
452     }
453   }
454
455   /**
456    * {@inheritdoc}
457    */
458   public function validateOptionsForm(&$form, FormStateInterface $form_state) {
459     parent::validateOptionsForm($form, $form_state);
460
461     if ($form_state->get('section') == 'path') {
462       $errors = $this->validatePath($form_state->getValue('path'));
463       foreach ($errors as $error) {
464         $form_state->setError($form['path'], $error);
465       }
466
467       // Automatically remove '/' and trailing whitespace from path.
468       $form_state->setValue('path', trim($form_state->getValue('path'), '/ '));
469     }
470   }
471
472   /**
473    * {@inheritdoc}
474    */
475   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
476     parent::submitOptionsForm($form, $form_state);
477
478     if ($form_state->get('section') == 'path') {
479       $this->setOption('path', $form_state->getValue('path'));
480     }
481   }
482
483   /**
484    * Validates the path of the display.
485    *
486    * @param string $path
487    *   The path to validate.
488    *
489    * @return array
490    *   A list of error strings.
491    */
492   protected function validatePath($path) {
493     $errors = [];
494     if (strpos($path, '%') === 0) {
495       $errors[] = $this->t('"%" may not be used for the first segment of a path.');
496     }
497
498     $parsed_url = UrlHelper::parse($path);
499     if (empty($parsed_url['path'])) {
500       $errors[] = $this->t('Path is empty.');
501     }
502
503     if (!empty($parsed_url['query'])) {
504       $errors[] = $this->t('No query allowed.');
505     }
506
507     if (!parse_url('internal:/' . $path)) {
508       $errors[] = $this->t('Invalid path. Valid characters are alphanumerics as well as "-", ".", "_" and "~".');
509     }
510
511     $path_sections = explode('/', $path);
512     // Symfony routing does not allow to use numeric placeholders.
513     // @see \Symfony\Component\Routing\RouteCompiler
514     $numeric_placeholders = array_filter($path_sections, function ($section) {
515       return (preg_match('/^%(.*)/', $section, $matches)
516         && is_numeric($matches[1]));
517     });
518     if (!empty($numeric_placeholders)) {
519       $errors[] = $this->t("Numeric placeholders may not be used. Please use plain placeholders (%).");
520     }
521     return $errors;
522   }
523
524   /**
525    * {@inheritdoc}
526    */
527   public function validate() {
528     $errors = parent::validate();
529
530     $errors += $this->validatePath($this->getOption('path'));
531
532     return $errors;
533   }
534
535   /**
536    * {@inheritdoc}
537    */
538   public function getUrlInfo() {
539     return Url::fromRoute($this->getRouteName());
540   }
541
542   /**
543    * {@inheritdoc}
544    */
545   public function getRouteName() {
546     $view_id = $this->view->storage->id();
547     $display_id = $this->display['id'];
548     $view_route_key = "$view_id.$display_id";
549
550     // Check for overridden route names.
551     $view_route_names = $this->getAlteredRouteNames();
552
553     return (isset($view_route_names[$view_route_key]) ? $view_route_names[$view_route_key] : "view.$view_route_key");
554   }
555
556   /**
557    * {@inheritdoc}
558    */
559   public function getAlteredRouteNames() {
560     return $this->state->get('views.view_route_names') ?: [];
561   }
562
563   /**
564    * {@inheritdoc}
565    */
566   public function remove() {
567     $menu_links = $this->getMenuLinks();
568     /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
569     $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
570     foreach ($menu_links as $menu_link_id => $menu_link) {
571       $menu_link_manager->removeDefinition("views_view:$menu_link_id");
572     }
573   }
574
575 }