1<?php
2
3namespace Drupal\views\Plugin\views\argument;
4
5use Drupal\Component\Plugin\DependentPluginInterface;
6use Drupal\Component\Utility\Html;
7use Drupal\Component\Utility\NestedArray;
8use Drupal\Core\Cache\Cache;
9use Drupal\Core\Cache\CacheableDependencyInterface;
10use Drupal\Core\Form\FormStateInterface;
11use Drupal\Core\Render\Element;
12use Drupal\views\Plugin\views\display\DisplayPluginBase;
13use Drupal\views\ViewExecutable;
14use Drupal\views\Plugin\views\HandlerBase;
15use Drupal\views\Views;
16
17/**
18 * @defgroup views_argument_handlers Views argument handlers
19 * @{
20 * Handler plugins for Views contextual filters.
21 *
22 * Handler plugins help build the view query object. Views argument handlers
23 * are for contextual filtering.
24 *
25 * Views argument handlers extend
26 * \Drupal\views\Plugin\views\argument\ArgumentPluginBase. They must be
27 * annotated with \Drupal\views\Annotation\ViewsArgument annotation, and they
28 * must be in namespace directory Plugin\views\argument.
29 *
30 * @ingroup views_plugins
31 * @see plugin_api
32 */
33
34/**
35 * Base class for argument (contextual filter) handler plugins.
36 *
37 * The basic argument works for very simple arguments such as nid and uid
38 *
39 * Definition terms for this handler:
40 * - name field: The field to use for the name to use in the summary, which is
41 *               the displayed output. For example, for the node: nid argument,
42 *               the argument itself is the nid, but node.title is displayed.
43 * - name table: The table to use for the name, should it not be in the same
44 *               table as the argument.
45 * - empty field name: For arguments that can have no value, such as taxonomy
46 *                     which can have "no term", this is the string which
47 *                     will be displayed for this lack of value. Be sure to use
48 *                     $this->t().
49 * - validate type: A little used string to allow an argument to restrict
50 *                  which validator is available to just one. Use the
51 *                  validator ID. This probably should not be used at all,
52 *                  and may disappear or change.
53 * - numeric: If set to TRUE this field is numeric and will use %d instead of
54 *            %s in queries.
55 */
56abstract class ArgumentPluginBase extends HandlerBase implements CacheableDependencyInterface {
57
58  public $validator = NULL;
59  public $argument = NULL;
60  public $value = NULL;
61
62  /**
63   * The table to use for the name, should it not be in the same table as the argument.
64   * @var string
65   */
66  public $name_table;
67
68  /**
69   * The field to use for the name to use in the summary, which is
70   * the displayed output. For example, for the node: nid argument,
71   * the argument itself is the nid, but node.title is displayed.
72   * @var string
73   */
74  public $name_field;
75
76  /**
77   * Overrides Drupal\views\Plugin\views\HandlerBase:init().
78   */
79  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
80    parent::init($view, $display, $options);
81
82    if (!empty($this->definition['name field'])) {
83      $this->name_field = $this->definition['name field'];
84    }
85    if (!empty($this->definition['name table'])) {
86      $this->name_table = $this->definition['name table'];
87    }
88  }
89
90  public function isException($arg = NULL) {
91    if (!isset($arg)) {
92      $arg = isset($this->argument) ? $this->argument : NULL;
93    }
94    return !empty($this->options['exception']['value']) && $this->options['exception']['value'] === $arg;
95  }
96
97  public function exceptionTitle() {
98    // If title overriding is off for the exception, return the normal title.
99    if (empty($this->options['exception']['title_enable'])) {
100      return $this->getTitle();
101    }
102    return $this->options['exception']['title'];
103  }
104
105  /**
106   * Determine if the argument needs a style plugin.
107   *
108   * @return bool
109   */
110  public function needsStylePlugin() {
111    $info = $this->defaultActions($this->options['default_action']);
112    $validate_info = $this->defaultActions($this->options['validate']['fail']);
113    return !empty($info['style plugin']) || !empty($validate_info['style plugin']);
114  }
115
116  protected function defineOptions() {
117    $options = parent::defineOptions();
118
119    $options['default_action'] = ['default' => 'ignore'];
120    $options['exception'] = [
121      'contains' => [
122        'value' => ['default' => 'all'],
123        'title_enable' => ['default' => FALSE],
124        'title' => ['default' => 'All'],
125      ],
126    ];
127    $options['title_enable'] = ['default' => FALSE];
128    $options['title'] = ['default' => ''];
129    $options['default_argument_type'] = ['default' => 'fixed'];
130    $options['default_argument_options'] = ['default' => []];
131    $options['default_argument_skip_url'] = ['default' => FALSE];
132    $options['summary_options'] = ['default' => []];
133    $options['summary'] = [
134      'contains' => [
135        'sort_order' => ['default' => 'asc'],
136        'number_of_records' => ['default' => 0],
137        'format' => ['default' => 'default_summary'],
138      ],
139    ];
140    $options['specify_validation'] = ['default' => FALSE];
141    $options['validate'] = [
142      'contains' => [
143        'type' => ['default' => 'none'],
144        'fail' => ['default' => 'not found'],
145      ],
146    ];
147    $options['validate_options'] = ['default' => []];
148
149    return $options;
150  }
151
152  /**
153   * {@inheritdoc}
154   */
155  public static function trustedCallbacks() {
156    $callbacks = parent::trustedCallbacks();
157    $callbacks[] = 'preRenderMoveArgumentOptions';
158    return $callbacks;
159  }
160
161  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
162    parent::buildOptionsForm($form, $form_state);
163
164    $argument_text = $this->view->display_handler->getArgumentText();
165
166    $form['#pre_render'][] = [static::class, 'preRenderMoveArgumentOptions'];
167
168    $form['description'] = [
169      '#markup' => $argument_text['description'],
170      '#theme_wrappers' => ['container'],
171      '#attributes' => ['class' => ['description']],
172    ];
173
174    $form['no_argument'] = [
175      '#type' => 'details',
176      '#title' => $argument_text['filter value not present'],
177      '#open' => TRUE,
178    ];
179    // Everything in the details is floated, so the last element needs to
180    // clear those floats.
181    $form['no_argument']['clearfix'] = [
182      '#weight' => 1000,
183      '#markup' => '<div class="clearfix"></div>',
184    ];
185    $form['default_action'] = [
186      '#title' => $this->t('Default actions'),
187      '#title_display' => 'invisible',
188      '#type' => 'radios',
189      '#process' => [[$this, 'processContainerRadios']],
190      '#default_value' => $this->options['default_action'],
191      '#fieldset' => 'no_argument',
192    ];
193
194    $form['exception'] = [
195      '#type' => 'details',
196      '#title' => $this->t('Exceptions'),
197      '#fieldset' => 'no_argument',
198    ];
199    $form['exception']['value'] = [
200      '#type' => 'textfield',
201      '#title' => $this->t('Exception value'),
202      '#size' => 20,
203      '#default_value' => $this->options['exception']['value'],
204      '#description' => $this->t('If this value is received, the filter will be ignored; i.e, "all values"'),
205    ];
206    $form['exception']['title_enable'] = [
207      '#type' => 'checkbox',
208      '#title' => $this->t('Override title'),
209      '#default_value' => $this->options['exception']['title_enable'],
210    ];
211    $form['exception']['title'] = [
212      '#type' => 'textfield',
213      '#title' => $this->t('Override title'),
214      '#title_display' => 'invisible',
215      '#size' => 20,
216      '#default_value' => $this->options['exception']['title'],
217      '#description' => $this->t('Override the view and other argument titles. You may use Twig syntax in this field as well as the "arguments" and "raw_arguments" arrays.'),
218      '#states' => [
219        'visible' => [
220          ':input[name="options[exception][title_enable]"]' => ['checked' => TRUE],
221        ],
222      ],
223    ];
224
225    $options = [];
226    $defaults = $this->defaultActions();
227    $validate_options = [];
228    foreach ($defaults as $id => $info) {
229      $options[$id] = $info['title'];
230      if (empty($info['default only'])) {
231        $validate_options[$id] = $info['title'];
232      }
233      if (!empty($info['form method'])) {
234        $this->{$info['form method']}($form, $form_state);
235      }
236    }
237    $form['default_action']['#options'] = $options;
238
239    $form['argument_present'] = [
240      '#type' => 'details',
241      '#title' => $argument_text['filter value present'],
242      '#open' => TRUE,
243    ];
244    $form['title_enable'] = [
245      '#type' => 'checkbox',
246      '#title' => $this->t('Override title'),
247      '#default_value' => $this->options['title_enable'],
248      '#fieldset' => 'argument_present',
249    ];
250    $form['title'] = [
251      '#type' => 'textfield',
252      '#title' => $this->t('Provide title'),
253      '#title_display' => 'invisible',
254      '#default_value' => $this->options['title'],
255      '#description' => $this->t('Override the view and other argument titles. You may use Twig syntax in this field.'),
256      '#states' => [
257        'visible' => [
258          ':input[name="options[title_enable]"]' => ['checked' => TRUE],
259        ],
260      ],
261      '#fieldset' => 'argument_present',
262    ];
263
264    $output = $this->getTokenHelp();
265    $form['token_help'] = [
266      '#type' => 'details',
267      '#title' => $this->t('Replacement patterns'),
268      '#value' => $output,
269      '#states' => [
270        'visible' => [
271          [
272            ':input[name="options[title_enable]"]' => ['checked' => TRUE],
273          ],
274          [
275            ':input[name="options[exception][title_enable]"]' => ['checked' => TRUE],
276          ],
277        ],
278      ],
279    ];
280
281    $form['specify_validation'] = [
282      '#type' => 'checkbox',
283      '#title' => $this->t('Specify validation criteria'),
284      '#default_value' => $this->options['specify_validation'],
285      '#fieldset' => 'argument_present',
286    ];
287
288    $form['validate'] = [
289      '#type' => 'container',
290      '#fieldset' => 'argument_present',
291    ];
292    // Validator options include derivatives with :. These are sanitized for js
293    // and reverted on submission.
294    $form['validate']['type'] = [
295      '#type' => 'select',
296      '#title' => $this->t('Validator'),
297      '#default_value' => static::encodeValidatorId($this->options['validate']['type']),
298      '#states' => [
299        'visible' => [
300          ':input[name="options[specify_validation]"]' => ['checked' => TRUE],
301        ],
302      ],
303    ];
304
305    $plugins = Views::pluginManager('argument_validator')->getDefinitions();
306    foreach ($plugins as $id => $info) {
307      if (!empty($info['no_ui'])) {
308        continue;
309      }
310
311      $valid = TRUE;
312      if (!empty($info['type'])) {
313        $valid = FALSE;
314        if (empty($this->definition['validate type'])) {
315          continue;
316        }
317        foreach ((array) $info['type'] as $type) {
318          if ($type == $this->definition['validate type']) {
319            $valid = TRUE;
320            break;
321          }
322        }
323      }
324
325      // If we decide this validator is ok, add it to the list.
326      if ($valid) {
327        $plugin = $this->getPlugin('argument_validator', $id);
328        if ($plugin) {
329          if ($plugin->access() || $this->options['validate']['type'] == $id) {
330            // Sanitize ID for js.
331            $sanitized_id = static::encodeValidatorId($id);
332            $form['validate']['options'][$sanitized_id] = [
333              '#prefix' => '<div id="edit-options-validate-options-' . $sanitized_id . '-wrapper">',
334              '#suffix' => '</div>',
335              '#type' => 'item',
336              // Even if the plugin has no options add the key to the form_state.
337              // trick it into checking input to make #process run.
338              '#input' => TRUE,
339              '#states' => [
340                'visible' => [
341                  ':input[name="options[specify_validation]"]' => ['checked' => TRUE],
342                  ':input[name="options[validate][type]"]' => ['value' => $sanitized_id],
343                ],
344              ],
345              '#id' => 'edit-options-validate-options-' . $sanitized_id,
346              '#default_value' => [],
347            ];
348            $plugin->buildOptionsForm($form['validate']['options'][$sanitized_id], $form_state);
349            $validate_types[$sanitized_id] = $info['title'];
350          }
351        }
352      }
353    }
354
355    asort($validate_types);
356    $form['validate']['type']['#options'] = $validate_types;
357
358    $form['validate']['fail'] = [
359      '#type' => 'select',
360      '#title' => $this->t('Action to take if filter value does not validate'),
361      '#default_value' => $this->options['validate']['fail'],
362      '#options' => $validate_options,
363      '#states' => [
364        'visible' => [
365          ':input[name="options[specify_validation]"]' => ['checked' => TRUE],
366        ],
367      ],
368      '#fieldset' => 'argument_present',
369    ];
370  }
371
372  /**
373   * Provide token help information for the argument.
374   *
375   * @return array
376   *   A render array.
377   */
378  protected function getTokenHelp() {
379    $output = [];
380
381    foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
382      /** @var \Drupal\views\Plugin\views\argument\ArgumentPluginBase $handler */
383      $options[(string) t('Arguments')]["{{ arguments.$arg }}"] = $this->t('@argument title', ['@argument' => $handler->adminLabel()]);
384      $options[(string) t('Arguments')]["{{ raw_arguments.$arg }}"] = $this->t('@argument input', ['@argument' => $handler->adminLabel()]);
385    }
386
387    // We have some options, so make a list.
388    if (!empty($options)) {
389      $output[] = [
390        '#markup' => '<p>' . $this->t("The following replacement tokens are available for this argument.") . '</p>',
391      ];
392      foreach (array_keys($options) as $type) {
393        if (!empty($options[$type])) {
394          $items = [];
395          foreach ($options[$type] as $key => $value) {
396            $items[] = $key . ' == ' . $value;
397          }
398          $item_list = [
399            '#theme' => 'item_list',
400            '#items' => $items,
401          ];
402          $output[] = $item_list;
403        }
404      }
405    }
406
407    return $output;
408  }
409
410  public function validateOptionsForm(&$form, FormStateInterface $form_state) {
411    $option_values = &$form_state->getValue('options');
412    if (empty($option_values)) {
413      return;
414    }
415
416    // Let the plugins do validation.
417    $default_id = $option_values['default_argument_type'];
418    $plugin = $this->getPlugin('argument_default', $default_id);
419    if ($plugin) {
420      $plugin->validateOptionsForm($form['argument_default'][$default_id], $form_state, $option_values['argument_default'][$default_id]);
421    }
422
423    // summary plugin
424    $summary_id = $option_values['summary']['format'];
425    $plugin = $this->getPlugin('style', $summary_id);
426    if ($plugin) {
427      $plugin->validateOptionsForm($form['summary']['options'][$summary_id], $form_state, $option_values['summary']['options'][$summary_id]);
428    }
429
430    $sanitized_id = $option_values['validate']['type'];
431    // Correct ID for js sanitized version.
432    $validate_id = static::decodeValidatorId($sanitized_id);
433    $plugin = $this->getPlugin('argument_validator', $validate_id);
434    if ($plugin) {
435      $plugin->validateOptionsForm($form['validate']['options'][$default_id], $form_state, $option_values['validate']['options'][$sanitized_id]);
436    }
437
438  }
439
440  public function submitOptionsForm(&$form, FormStateInterface $form_state) {
441    $option_values = &$form_state->getValue('options');
442    if (empty($option_values)) {
443      return;
444    }
445
446    // Let the plugins make submit modifications if necessary.
447    $default_id = $option_values['default_argument_type'];
448    $plugin = $this->getPlugin('argument_default', $default_id);
449    if ($plugin) {
450      $options = &$option_values['argument_default'][$default_id];
451      $plugin->submitOptionsForm($form['argument_default'][$default_id], $form_state, $options);
452      // Copy the now submitted options to their final resting place so they get saved.
453      $option_values['default_argument_options'] = $options;
454    }
455
456    // summary plugin
457    $summary_id = $option_values['summary']['format'];
458    $plugin = $this->getPlugin('style', $summary_id);
459    if ($plugin) {
460      $options = &$option_values['summary']['options'][$summary_id];
461      $plugin->submitOptionsForm($form['summary']['options'][$summary_id], $form_state, $options);
462      // Copy the now submitted options to their final resting place so they get saved.
463      $option_values['summary_options'] = $options;
464    }
465
466    // If the 'Specify validation criteria' checkbox is not checked, reset the
467    // validation options.
468    if (empty($option_values['specify_validation'])) {
469      $option_values['validate']['type'] = 'none';
470      // We need to keep the empty array of options for the 'None' plugin as
471      // it will be needed later.
472      $option_values['validate']['options'] = ['none' => []];
473      $option_values['validate']['fail'] = 'not found';
474    }
475
476    $sanitized_id = $option_values['validate']['type'];
477    // Correct ID for js sanitized version.
478    $option_values['validate']['type'] = $validate_id = static::decodeValidatorId($sanitized_id);
479    $plugin = $this->getPlugin('argument_validator', $validate_id);
480    if ($plugin) {
481      $options = &$option_values['validate']['options'][$sanitized_id];
482      $plugin->submitOptionsForm($form['validate']['options'][$sanitized_id], $form_state, $options);
483      // Copy the now submitted options to their final resting place so they get saved.
484      $option_values['validate_options'] = $options;
485    }
486
487    // Clear out the content of title if it's not enabled.
488    if (empty($option_values['title_enable'])) {
489      $option_values['title'] = '';
490    }
491  }
492
493  /**
494   * Provide a list of default behaviors for this argument if the argument
495   * is not present.
496   *
497   * Override this method to provide additional (or fewer) default behaviors.
498   */
499  protected function defaultActions($which = NULL) {
500    $defaults = [
501      'ignore' => [
502        'title' => $this->t('Display all results for the specified field'),
503        'method' => 'defaultIgnore',
504      ],
505      'default' => [
506        'title' => $this->t('Provide default value'),
507        'method' => 'defaultDefault',
508        'form method' => 'defaultArgumentForm',
509        'has default argument' => TRUE,
510        // This can only be used for missing argument, not validation failure.
511        'default only' => TRUE,
512      ],
513      'not found' => [
514        'title' => $this->t('Hide view'),
515        'method' => 'defaultNotFound',
516        // This is a hard fail condition.
517        'hard fail' => TRUE,
518      ],
519      'summary' => [
520        'title' => $this->t('Display a summary'),
521        'method' => 'defaultSummary',
522        'form method' => 'defaultSummaryForm',
523        'style plugin' => TRUE,
524      ],
525      'empty' => [
526        'title' => $this->t('Display contents of "No results found"'),
527        'method' => 'defaultEmpty',
528      ],
529      'access denied' => [
530        'title' => $this->t('Display "Access Denied"'),
531        'method' => 'defaultAccessDenied',
532      ],
533    ];
534
535    if ($this->view->display_handler->hasPath()) {
536      $defaults['not found']['title'] = $this->t('Show "Page not found"');
537    }
538
539    if ($which) {
540      if (!empty($defaults[$which])) {
541        return $defaults[$which];
542      }
543    }
544    else {
545      return $defaults;
546    }
547  }
548
549  /**
550   * Provide a form for selecting the default argument when the
551   * default action is set to provide default argument.
552   */
553  public function defaultArgumentForm(&$form, FormStateInterface $form_state) {
554    $plugins = Views::pluginManager('argument_default')->getDefinitions();
555    $options = [];
556
557    $form['default_argument_skip_url'] = [
558      '#type' => 'checkbox',
559      '#title' => $this->t('Skip default argument for view URL'),
560      '#default_value' => $this->options['default_argument_skip_url'],
561      '#description' => $this->t('Select whether to include this default argument when constructing the URL for this view. Skipping default arguments is useful e.g. in the case of feeds.'),
562    ];
563
564    $form['default_argument_type'] = [
565      '#prefix' => '<div id="edit-options-default-argument-type-wrapper">',
566      '#suffix' => '</div>',
567      '#type' => 'select',
568      '#id' => 'edit-options-default-argument-type',
569      '#title' => $this->t('Type'),
570      '#default_value' => $this->options['default_argument_type'],
571      '#states' => [
572        'visible' => [
573          ':input[name="options[default_action]"]' => ['value' => 'default'],
574        ],
575      ],
576      // Views custom key, moves this element to the appropriate container
577      // under the radio button.
578      '#argument_option' => 'default',
579    ];
580
581    foreach ($plugins as $id => $info) {
582      if (!empty($info['no_ui'])) {
583        continue;
584      }
585      $plugin = $this->getPlugin('argument_default', $id);
586      if ($plugin) {
587        if ($plugin->access() || $this->options['default_argument_type'] == $id) {
588          $form['argument_default']['#argument_option'] = 'default';
589          $form['argument_default'][$id] = [
590            '#prefix' => '<div id="edit-options-argument-default-options-' . $id . '-wrapper">',
591            '#suffix' => '</div>',
592            '#id' => 'edit-options-argument-default-options-' . $id,
593            '#type' => 'item',
594            // Even if the plugin has no options add the key to the form_state.
595            '#input' => TRUE,
596            '#states' => [
597              'visible' => [
598                ':input[name="options[default_action]"]' => ['value' => 'default'],
599                ':input[name="options[default_argument_type]"]' => ['value' => $id],
600              ],
601            ],
602            '#default_value' => [],
603          ];
604          $options[$id] = $info['title'];
605          $plugin->buildOptionsForm($form['argument_default'][$id], $form_state);
606        }
607      }
608    }
609
610    asort($options);
611    $form['default_argument_type']['#options'] = $options;
612  }
613
614  /**
615   * Provide a form for selecting further summary options when the
616   * default action is set to display one.
617   */
618  public function defaultSummaryForm(&$form, FormStateInterface $form_state) {
619    $style_plugins = Views::pluginManager('style')->getDefinitions();
620    $summary_plugins = [];
621    $format_options = [];
622    foreach ($style_plugins as $key => $plugin) {
623      if (isset($plugin['display_types']) && in_array('summary', $plugin['display_types'])) {
624        $summary_plugins[$key] = $plugin;
625        $format_options[$key] = $plugin['title'];
626      }
627    }
628
629    $form['summary'] = [
630      // Views custom key, moves this element to the appropriate container
631      // under the radio button.
632      '#argument_option' => 'summary',
633    ];
634    $form['summary']['sort_order'] = [
635      '#type' => 'radios',
636      '#title' => $this->t('Sort order'),
637      '#options' => ['asc' => $this->t('Ascending'), 'desc' => $this->t('Descending')],
638      '#default_value' => $this->options['summary']['sort_order'],
639      '#states' => [
640        'visible' => [
641          ':input[name="options[default_action]"]' => ['value' => 'summary'],
642        ],
643      ],
644    ];
645    $form['summary']['number_of_records'] = [
646      '#type' => 'radios',
647      '#title' => $this->t('Sort by'),
648      '#default_value' => $this->options['summary']['number_of_records'],
649      '#options' => [
650        0 => $this->getSortName(),
651        1 => $this->t('Number of records'),
652      ],
653      '#states' => [
654        'visible' => [
655          ':input[name="options[default_action]"]' => ['value' => 'summary'],
656        ],
657      ],
658    ];
659
660    $form['summary']['format'] = [
661      '#type' => 'radios',
662      '#title' => $this->t('Format'),
663      '#options' => $format_options,
664      '#default_value' => $this->options['summary']['format'],
665      '#states' => [
666        'visible' => [
667          ':input[name="options[default_action]"]' => ['value' => 'summary'],
668        ],
669      ],
670    ];
671
672    foreach ($summary_plugins as $id => $info) {
673      $plugin = $this->getPlugin('style', $id);
674      if (!$plugin->usesOptions()) {
675        continue;
676      }
677      if ($plugin) {
678        $form['summary']['options'][$id] = [
679          '#prefix' => '<div id="edit-options-summary-options-' . $id . '-wrapper">',
680          '#suffix' => '</div>',
681          '#id' => 'edit-options-summary-options-' . $id,
682          '#type' => 'item',
683          // Trick it into checking input to make #process run.
684          '#input' => TRUE,
685          '#states' => [
686            'visible' => [
687              ':input[name="options[default_action]"]' => ['value' => 'summary'],
688              ':input[name="options[summary][format]"]' => ['value' => $id],
689            ],
690          ],
691          '#default_value' => [],
692        ];
693        $options[$id] = $info['title'];
694        $plugin->buildOptionsForm($form['summary']['options'][$id], $form_state);
695      }
696    }
697  }
698
699  /**
700   * Handle the default action, which means our argument wasn't present.
701   *
702   * Override this method only with extreme care.
703   *
704   * @return
705   *   A boolean value; if TRUE, continue building this view. If FALSE,
706   *   building the view will be aborted here.
707   */
708  public function defaultAction($info = NULL) {
709    if (!isset($info)) {
710      $info = $this->defaultActions($this->options['default_action']);
711    }
712
713    if (!$info) {
714      return FALSE;
715    }
716
717    if (!empty($info['method args'])) {
718      return call_user_func_array([&$this, $info['method']], $info['method args']);
719    }
720    else {
721      return $this->{$info['method']}();
722    }
723  }
724
725  /**
726   * How to act if validation fails.
727   */
728  public function validateFail() {
729    $info = $this->defaultActions($this->options['validate']['fail']);
730    return $this->defaultAction($info);
731  }
732
733  /**
734   * Default action: ignore.
735   *
736   * If an argument was expected and was not given, in this case, simply
737   * ignore the argument entirely.
738   */
739  public function defaultIgnore() {
740    return TRUE;
741  }
742
743  /**
744   * Default action: not found.
745   *
746   * If an argument was expected and was not given, in this case, report
747   * the view as 'not found' or hide it.
748   */
749  protected function defaultNotFound() {
750    // Set a failure condition and let the display manager handle it.
751    $this->view->build_info['fail'] = TRUE;
752    return FALSE;
753  }
754
755  /**
756   * Default action: access denied.
757   *
758   * If an argument was expected and was not given, in this case, report
759   * the view as 'access denied'.
760   */
761  public function defaultAccessDenied() {
762    $this->view->build_info['denied'] = TRUE;
763    return FALSE;
764  }
765
766  /**
767   * Default action: empty.
768   *
769   * If an argument was expected and was not given, in this case, display
770   * the view's empty text
771   */
772  public function defaultEmpty() {
773    // We return with no query; this will force the empty text.
774    $this->view->built = TRUE;
775    $this->view->executed = TRUE;
776    $this->view->result = [];
777    return FALSE;
778  }
779
780  /**
781   * This just returns true. The view argument builder will know where
782   * to find the argument from.
783   */
784  protected function defaultDefault() {
785    return TRUE;
786  }
787
788  /**
789   * Determine if the argument is set to provide a default argument.
790   */
791  public function hasDefaultArgument() {
792    $info = $this->defaultActions($this->options['default_action']);
793    return !empty($info['has default argument']);
794  }
795
796  /**
797   * Get a default argument, if available.
798   */
799  public function getDefaultArgument() {
800    $plugin = $this->getPlugin('argument_default');
801    if ($plugin) {
802      return $plugin->getArgument();
803    }
804  }
805
806  /**
807   * Process the summary arguments for display.
808   *
809   * For example, the validation plugin may want to alter an argument for use in
810   * the URL.
811   */
812  public function processSummaryArguments(&$args) {
813    if ($this->options['validate']['type'] != 'none') {
814      if (isset($this->validator) || $this->validator = $this->getPlugin('argument_validator')) {
815        $this->validator->processSummaryArguments($args);
816      }
817    }
818  }
819
820  /**
821   * Default action: summary.
822   *
823   * If an argument was expected and was not given, in this case, display
824   * a summary query.
825   */
826  protected function defaultSummary() {
827    $this->view->build_info['summary'] = TRUE;
828    $this->view->build_info['summary_level'] = $this->options['id'];
829
830    // Change the display style to the summary style for this
831    // argument.
832    $this->view->style_plugin = Views::pluginManager("style")->createInstance($this->options['summary']['format']);
833    $this->view->style_plugin->init($this->view, $this->displayHandler, $this->options['summary_options']);
834
835    // Clear out the normal primary field and whatever else may have
836    // been added and let the summary do the work.
837    $this->query->clearFields();
838    $this->summaryQuery();
839
840    $by = $this->options['summary']['number_of_records'] ? 'num_records' : NULL;
841    $this->summarySort($this->options['summary']['sort_order'], $by);
842
843    // Summaries have their own sorting and fields, so tell the View not
844    // to build these.
845    $this->view->build_sort = FALSE;
846    return TRUE;
847  }
848
849  /**
850   * Build the info for the summary query.
851   *
852   * This must:
853   * - addGroupBy: group on this field in order to create summaries.
854   * - addField: add a 'num_nodes' field for the count. Usually it will
855   *   be a count on $view->base_field
856   * - setCountField: Reset the count field so we get the right paging.
857   *
858   * @return
859   *   The alias used to get the number of records (count) for this entry.
860   */
861  protected function summaryQuery() {
862    $this->ensureMyTable();
863    // Add the field.
864    $this->base_alias = $this->query->addField($this->tableAlias, $this->realField);
865
866    $this->summaryNameField();
867    return $this->summaryBasics();
868  }
869
870  /**
871   * Add the name field, which is the field displayed in summary queries.
872   * This is often used when the argument is numeric.
873   */
874  protected function summaryNameField() {
875    // Add the 'name' field. For example, if this is a uid argument, the
876    // name field would be 'name' (i.e, the username).
877
878    if (isset($this->name_table)) {
879      // if the alias is different then we're probably added, not ensured,
880      // so look up the join and add it instead.
881      if ($this->tableAlias != $this->name_table) {
882        $j = HandlerBase::getTableJoin($this->name_table, $this->table);
883        if ($j) {
884          $join = clone $j;
885          $join->leftTable = $this->tableAlias;
886          $this->name_table_alias = $this->query->addTable($this->name_table, $this->relationship, $join);
887        }
888      }
889      else {
890        $this->name_table_alias = $this->query->ensureTable($this->name_table, $this->relationship);
891      }
892    }
893    else {
894      $this->name_table_alias = $this->tableAlias;
895    }
896
897    if (isset($this->name_field)) {
898      $this->name_alias = $this->query->addField($this->name_table_alias, $this->name_field);
899    }
900    else {
901      $this->name_alias = $this->base_alias;
902    }
903  }
904
905  /**
906   * Some basic summary behavior that doesn't need to be repeated as much as
907   * code that goes into summaryQuery()
908   */
909  public function summaryBasics($count_field = TRUE) {
910    // Add the number of nodes counter
911    $distinct = ($this->view->display_handler->getOption('distinct') && empty($this->query->no_distinct));
912
913    $count_alias = $this->query->addField($this->view->storage->get('base_table'), $this->view->storage->get('base_field'), 'num_records', ['count' => TRUE, 'distinct' => $distinct]);
914    $this->query->addGroupBy($this->name_alias);
915
916    if ($count_field) {
917      $this->query->setCountField($this->tableAlias, $this->realField);
918    }
919
920    $this->count_alias = $count_alias;
921  }
922
923  /**
924   * Sorts the summary based upon the user's selection. The base variant of
925   * this is usually adequate.
926   *
927   * @param $order
928   *   The order selected in the UI.
929   * @param string|null $by
930   *   (optional) This parameter sets the direction for which to order.
931   *   Defaults to NULL.
932   */
933  public function summarySort($order, $by = NULL) {
934    $this->query->addOrderBy(NULL, NULL, $order, (!empty($by) ? $by : $this->name_alias));
935  }
936
937  /**
938   * Provide the argument to use to link from the summary to the next level;
939   * this will be called once per row of a summary, and used as part of
940   * $view->getUrl().
941   *
942   * @param $data
943   *   The query results for the row.
944   */
945  public function summaryArgument($data) {
946    return $data->{$this->base_alias};
947  }
948
949  /**
950   * Provides the name to use for the summary. By default this is just
951   * the name field.
952   *
953   * @param $data
954   *   The query results for the row.
955   */
956  public function summaryName($data) {
957    $value = $data->{$this->name_alias};
958    if (empty($value) && !empty($this->definition['empty field name'])) {
959      $value = $this->definition['empty field name'];
960    }
961    return $value;
962  }
963
964  /**
965   * Set up the query for this argument.
966   *
967   * The argument sent may be found at $this->argument.
968   */
969  public function query($group_by = FALSE) {
970    $this->ensureMyTable();
971    $this->query->addWhere(0, "$this->tableAlias.$this->realField", $this->argument);
972  }
973
974  /**
975   * Get the title this argument will assign the view, given the argument.
976   *
977   * This usually needs to be overridden to provide a proper title.
978   */
979  public function title() {
980    return $this->argument;
981  }
982
983  /**
984   * Called by the view object to get the title. This may be set by a
985   * validator so we don't necessarily call through to title().
986   */
987  public function getTitle() {
988    if (isset($this->validated_title)) {
989      return $this->validated_title;
990    }
991    else {
992      return $this->title();
993    }
994  }
995
996  /**
997   * Validate that this argument works. By default, all arguments are valid.
998   */
999  public function validateArgument($arg) {
1000    // By using % in URLs, arguments could be validated twice; this eases
1001    // that pain.
1002    if (isset($this->argument_validated)) {
1003      return $this->argument_validated;
1004    }
1005
1006    if ($this->isException($arg)) {
1007      return $this->argument_validated = TRUE;
1008    }
1009
1010    $plugin = $this->getPlugin('argument_validator');
1011    return $this->argument_validated = $plugin->validateArgument($arg);
1012  }
1013
1014  /**
1015   * Called by the menu system to validate an argument.
1016   *
1017   * This checks to see if this is a 'soft fail', which means that if the
1018   * argument fails to validate, but there is an action to take anyway,
1019   * then validation cannot actually fail.
1020   */
1021  public function validateMenuArgument($arg) {
1022    $validate_info = $this->defaultActions($this->options['validate']['fail']);
1023    if (empty($validate_info['hard fail'])) {
1024      return TRUE;
1025    }
1026
1027    $rc = $this->validateArgument($arg);
1028
1029    // If the validator has changed the validate fail condition to a
1030    // soft fail, deal with that:
1031    $validate_info = $this->defaultActions($this->options['validate']['fail']);
1032    if (empty($validate_info['hard fail'])) {
1033      return TRUE;
1034    }
1035
1036    return $rc;
1037  }
1038
1039  /**
1040   * Set the input for this argument.
1041   *
1042   * @return TRUE if it successfully validates; FALSE if it does not.
1043   */
1044  public function setArgument($arg) {
1045    $this->argument = $arg;
1046    return $this->validateArgument($arg);
1047  }
1048
1049  /**
1050   * Get the value of this argument.
1051   */
1052  public function getValue() {
1053    // If we already processed this argument, we're done.
1054    if (isset($this->argument)) {
1055      return $this->argument;
1056    }
1057
1058    // Otherwise, we have to pretend to process ourselves to find the value.
1059    $value = NULL;
1060    // Find the position of this argument within the view.
1061    $position = 0;
1062    foreach ($this->view->argument as $id => $argument) {
1063      if ($id == $this->options['id']) {
1064        break;
1065      }
1066      $position++;
1067    }
1068
1069    $arg = isset($this->view->args[$position]) ? $this->view->args[$position] : NULL;
1070    $this->position = $position;
1071
1072    // Clone ourselves so that we don't break things when we're really
1073    // processing the arguments.
1074    $argument = clone $this;
1075    if (!isset($arg) && $argument->hasDefaultArgument()) {
1076      $arg = $argument->getDefaultArgument();
1077
1078      // remember that this argument was computed, not passed on the URL.
1079      $this->is_default = TRUE;
1080    }
1081    // Set the argument, which will also validate that the argument can be set.
1082    if ($argument->setArgument($arg)) {
1083      $value = $argument->argument;
1084    }
1085    unset($argument);
1086    return $value;
1087  }
1088
1089  /**
1090   * Get the display or row plugin, if it exists.
1091   */
1092  public function getPlugin($type = 'argument_default', $name = NULL) {
1093    $options = [];
1094    switch ($type) {
1095      case 'argument_default':
1096        if (!isset($this->options['default_argument_type'])) {
1097          return;
1098        }
1099        $plugin_name = $this->options['default_argument_type'];
1100        $options_name = 'default_argument_options';
1101        break;
1102
1103      case 'argument_validator':
1104        if (!isset($this->options['validate']['type'])) {
1105          return;
1106        }
1107        $plugin_name = $this->options['validate']['type'];
1108        $options_name = 'validate_options';
1109        break;
1110
1111      case 'style':
1112        if (!isset($this->options['summary']['format'])) {
1113          return;
1114        }
1115        $plugin_name = $this->options['summary']['format'];
1116        $options_name = 'summary_options';
1117    }
1118
1119    if (!$name) {
1120      $name = $plugin_name;
1121    }
1122
1123    // we only fetch the options if we're fetching the plugin actually
1124    // in use.
1125    if ($name == $plugin_name) {
1126      $options = isset($this->options[$options_name]) ? $this->options[$options_name] : [];
1127    }
1128
1129    $plugin = Views::pluginManager($type)->createInstance($name);
1130    if ($plugin) {
1131      $plugin->init($this->view, $this->displayHandler, $options);
1132
1133      if ($type !== 'style') {
1134        // It's an argument_default/argument_validate plugin, so set the argument.
1135        $plugin->setArgument($this);
1136      }
1137      return $plugin;
1138    }
1139  }
1140
1141  /**
1142   * Return a description of how the argument would normally be sorted.
1143   *
1144   * Subclasses should override this to specify what the default sort order of
1145   * their argument is (e.g. alphabetical, numeric, date).
1146   */
1147  public function getSortName() {
1148    return $this->t('Default sort', [], ['context' => 'Sort order']);
1149  }
1150
1151  /**
1152   * Custom form radios process function.
1153   *
1154   * Roll out a single radios element to a list of radios, using the options
1155   * array as index. While doing that, create a container element underneath
1156   * each option, which contains the settings related to that option.
1157   *
1158   * @see \Drupal\Core\Render\Element\Radios::processRadios()
1159   */
1160  public static function processContainerRadios($element) {
1161    if (count($element['#options']) > 0) {
1162      foreach ($element['#options'] as $key => $choice) {
1163        $element += [$key => []];
1164        // Generate the parents as the autogenerator does, so we will have a
1165        // unique id for each radio button.
1166        $parents_for_id = array_merge($element['#parents'], [$key]);
1167
1168        $element[$key] += [
1169          '#type' => 'radio',
1170          '#title' => $choice,
1171          // The key is sanitized in drupal_attributes() during output from the
1172          // theme function.
1173          '#return_value' => $key,
1174          '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : NULL,
1175          '#attributes' => $element['#attributes'],
1176          '#parents' => $element['#parents'],
1177          '#id' => Html::getUniqueId('edit-' . implode('-', $parents_for_id)),
1178          '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
1179        ];
1180        $element[$key . '_options'] = [
1181          '#type' => 'container',
1182          '#attributes' => ['class' => ['views-admin-dependent']],
1183        ];
1184      }
1185    }
1186    return $element;
1187  }
1188
1189  /**
1190   * Moves argument options into their place.
1191   *
1192   * When configuring the default argument behavior, almost each of the radio
1193   * buttons has its own fieldset shown below it when the radio button is
1194   * clicked. That fieldset is created through a custom form process callback.
1195   * Each element that has #argument_option defined and pointing to a default
1196   * behavior gets moved to the appropriate fieldset.
1197   * So if #argument_option is specified as 'default', the element is moved
1198   * to the 'default_options' fieldset.
1199   */
1200  public static function preRenderMoveArgumentOptions($form) {
1201    foreach (Element::children($form) as $key) {
1202      $element = $form[$key];
1203      if (!empty($element['#argument_option'])) {
1204        $container_name = $element['#argument_option'] . '_options';
1205        if (isset($form['no_argument']['default_action'][$container_name])) {
1206          $form['no_argument']['default_action'][$container_name][$key] = $element;
1207        }
1208        // Remove the original element this duplicates.
1209        unset($form[$key]);
1210      }
1211    }
1212
1213    return $form;
1214  }
1215
1216  /**
1217   * Sanitize validator options including derivatives with : for js.
1218   *
1219   * Reason and alternative: https://www.drupal.org/node/2035345.
1220   *
1221   * @param string $id
1222   *   The identifier to be sanitized.
1223   *
1224   * @return string
1225   *   The sanitized identifier.
1226   *
1227   * @see decodeValidatorId()
1228   */
1229  public static function encodeValidatorId($id) {
1230    return str_replace(':', '---', $id);
1231  }
1232
1233  /**
1234   * Revert sanitized validator options.
1235   *
1236   * @param string $id
1237   *   The sanitized identifier to be reverted.
1238   *
1239   * @return string
1240   *   The original identifier.
1241   */
1242  public static function decodeValidatorId($id) {
1243    return str_replace('---', ':', $id);
1244  }
1245
1246  /**
1247   * Splits an argument into value and operator properties on this instance.
1248   *
1249   * @param bool $force_int
1250   *   Enforce that values should be numeric.
1251   */
1252  protected function unpackArgumentValue($force_int = FALSE) {
1253    $break = static::breakString($this->argument, $force_int);
1254    $this->value = $break->value;
1255    $this->operator = $break->operator;
1256  }
1257
1258  /**
1259   * {@inheritdoc}
1260   */
1261  public function getCacheMaxAge() {
1262    $max_age = Cache::PERMANENT;
1263
1264    // Asks all subplugins (argument defaults, argument validator and styles).
1265    if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheableDependencyInterface) {
1266      $max_age = Cache::mergeMaxAges($max_age, $plugin->getCacheMaxAge());
1267    }
1268
1269    if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheableDependencyInterface) {
1270      $max_age = Cache::mergeMaxAges($max_age, $plugin->getCacheMaxAge());
1271    }
1272
1273    // Summaries use style plugins.
1274    if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheableDependencyInterface) {
1275      $max_age = Cache::mergeMaxAges($max_age, $plugin->getCacheMaxAge());
1276    }
1277
1278    return $max_age;
1279  }
1280
1281  /**
1282   * {@inheritdoc}
1283   */
1284  public function getCacheContexts() {
1285    $contexts = [];
1286    // By definition arguments depends on the URL.
1287    // @todo Once contexts are properly injected into block views we could pull
1288    //   the information from there.
1289    $contexts[] = 'url';
1290
1291    // Asks all subplugins (argument defaults, argument validator and styles).
1292    if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheableDependencyInterface) {
1293      $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts());
1294    }
1295
1296    if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheableDependencyInterface) {
1297      $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts());
1298    }
1299
1300    if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheableDependencyInterface) {
1301      $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts());
1302    }
1303
1304    return $contexts;
1305  }
1306
1307  /**
1308   * {@inheritdoc}
1309   */
1310  public function getCacheTags() {
1311    $tags = [];
1312
1313    // Asks all subplugins (argument defaults, argument validator and styles).
1314    if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheableDependencyInterface) {
1315      $tags = Cache::mergeTags($tags, $plugin->getCacheTags());
1316    }
1317
1318    if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheableDependencyInterface) {
1319      $tags = Cache::mergeTags($tags, $plugin->getCacheTags());
1320    }
1321
1322    if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheableDependencyInterface) {
1323      $tags = Cache::mergeTags($tags, $plugin->getCacheTags());
1324    }
1325
1326    return $tags;
1327  }
1328
1329  /**
1330   * {@inheritdoc}
1331   */
1332  public function calculateDependencies() {
1333    $dependencies = [];
1334    if (($argument_default = $this->getPlugin('argument_default')) && $argument_default instanceof DependentPluginInterface) {
1335      $dependencies = NestedArray::mergeDeep($dependencies, $argument_default->calculateDependencies());
1336    }
1337    if (($argument_validator = $this->getPlugin('argument_validator')) && $argument_validator instanceof DependentPluginInterface) {
1338      $dependencies = NestedArray::mergeDeep($dependencies, $argument_validator->calculateDependencies());
1339    }
1340    if (($style = $this->getPlugin('style')) && $style instanceof DependentPluginInterface) {
1341      $dependencies = NestedArray::mergeDeep($dependencies, $style->calculateDependencies());
1342    }
1343
1344    return $dependencies;
1345  }
1346
1347  /**
1348   * Returns a context definition for this argument.
1349   *
1350   * @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface|null
1351   *   A context definition that represents the argument or NULL if that is
1352   *   not possible.
1353   */
1354  public function getContextDefinition() {
1355    return $this->getPlugin('argument_validator')->getContextDefinition();
1356  }
1357
1358}
1359
1360/**
1361 * @}
1362 */
1363