1<?php
2
3namespace Drupal\views\Entity;
4
5use Drupal\Component\Utility\NestedArray;
6use Drupal\Core\Cache\Cache;
7use Drupal\Core\Config\Entity\ConfigEntityBase;
8use Drupal\Core\Entity\ContentEntityTypeInterface;
9use Drupal\Core\Entity\EntityStorageInterface;
10use Drupal\Core\Entity\FieldableEntityInterface;
11use Drupal\Core\Language\LanguageInterface;
12use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
13use Drupal\views\Views;
14use Drupal\views\ViewEntityInterface;
15
16/**
17 * Defines a View configuration entity class.
18 *
19 * @ConfigEntityType(
20 *   id = "view",
21 *   label = @Translation("View", context = "View entity type"),
22 *   label_collection = @Translation("Views", context = "View entity type"),
23 *   label_singular = @Translation("view", context = "View entity type"),
24 *   label_plural = @Translation("views", context = "View entity type"),
25 *   label_count = @PluralTranslation(
26 *     singular = "@count view",
27 *     plural = "@count views",
28 *     context = "View entity type",
29 *   ),
30 *   admin_permission = "administer views",
31 *   entity_keys = {
32 *     "id" = "id",
33 *     "label" = "label",
34 *     "status" = "status"
35 *   },
36 *   config_export = {
37 *     "id",
38 *     "label",
39 *     "module",
40 *     "description",
41 *     "tag",
42 *     "base_table",
43 *     "base_field",
44 *     "display",
45 *   }
46 * )
47 */
48class View extends ConfigEntityBase implements ViewEntityInterface {
49
50  /**
51   * The name of the base table this view will use.
52   *
53   * @var string
54   */
55  protected $base_table = 'node';
56
57  /**
58   * The unique ID of the view.
59   *
60   * @var string
61   */
62  protected $id = NULL;
63
64  /**
65   * The label of the view.
66   *
67   * @var string
68   */
69  protected $label;
70
71  /**
72   * The description of the view, which is used only in the interface.
73   *
74   * @var string
75   */
76  protected $description = '';
77
78  /**
79   * The "tags" of a view.
80   *
81   * The tags are stored as a single string, though it is used as multiple tags
82   * for example in the views overview.
83   *
84   * @var string
85   */
86  protected $tag = '';
87
88  /**
89   * Stores all display handlers of this view.
90   *
91   * An array containing Drupal\views\Plugin\views\display\DisplayPluginBase
92   * objects.
93   *
94   * @var array
95   */
96  protected $display = [];
97
98  /**
99   * The name of the base field to use.
100   *
101   * @var string
102   */
103  protected $base_field = 'nid';
104
105  /**
106   * Stores a reference to the executable version of this view.
107   *
108   * @var \Drupal\views\ViewExecutable
109   */
110  protected $executable;
111
112  /**
113   * The module implementing this view.
114   *
115   * @var string
116   */
117  protected $module = 'views';
118
119  /**
120   * {@inheritdoc}
121   */
122  public function getExecutable() {
123    // Ensure that an executable View is available.
124    if (!isset($this->executable)) {
125      $this->executable = Views::executableFactory()->get($this);
126    }
127
128    return $this->executable;
129  }
130
131  /**
132   * {@inheritdoc}
133   */
134  public function createDuplicate() {
135    $duplicate = parent::createDuplicate();
136    unset($duplicate->executable);
137    return $duplicate;
138  }
139
140  /**
141   * {@inheritdoc}
142   */
143  public function label() {
144    if (!$label = $this->get('label')) {
145      $label = $this->id();
146    }
147    return $label;
148  }
149
150  /**
151   * {@inheritdoc}
152   */
153  public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) {
154    if (empty($plugin_id)) {
155      return FALSE;
156    }
157
158    $plugin = Views::pluginManager('display')->getDefinition($plugin_id);
159
160    if (empty($plugin)) {
161      $plugin['title'] = t('Broken');
162    }
163
164    if (empty($id)) {
165      $id = $this->generateDisplayId($plugin_id);
166
167      // Generate a unique human-readable name by inspecting the counter at the
168      // end of the previous display ID, e.g., 'page_1'.
169      if ($id !== 'default') {
170        preg_match("/[0-9]+/", $id, $count);
171        $count = $count[0];
172      }
173      else {
174        $count = '';
175      }
176
177      if (empty($title)) {
178        // If there is no title provided, use the plugin title, and if there are
179        // multiple displays, append the count.
180        $title = $plugin['title'];
181        if ($count > 1) {
182          $title .= ' ' . $count;
183        }
184      }
185    }
186
187    $display_options = [
188      'display_plugin' => $plugin_id,
189      'id' => $id,
190      // Cast the display title to a string since it is an object.
191      // @see \Drupal\Core\StringTranslation\TranslatableMarkup
192      'display_title' => (string) $title,
193      'position' => $id === 'default' ? 0 : count($this->display),
194      'display_options' => [],
195    ];
196
197    // Add the display options to the view.
198    $this->display[$id] = $display_options;
199    return $id;
200  }
201
202  /**
203   * Generates a display ID of a certain plugin type.
204   *
205   * @param string $plugin_id
206   *   Which plugin should be used for the new display ID.
207   *
208   * @return string
209   */
210  protected function generateDisplayId($plugin_id) {
211    // 'default' is singular and is unique, so just go with 'default'
212    // for it. For all others, start counting.
213    if ($plugin_id == 'default') {
214      return 'default';
215    }
216    // Initial ID.
217    $id = $plugin_id . '_1';
218    $count = 1;
219
220    // Loop through IDs based upon our style plugin name until
221    // we find one that is unused.
222    while (!empty($this->display[$id])) {
223      $id = $plugin_id . '_' . ++$count;
224    }
225
226    return $id;
227  }
228
229  /**
230   * {@inheritdoc}
231   */
232  public function &getDisplay($display_id) {
233    return $this->display[$display_id];
234  }
235
236  /**
237   * {@inheritdoc}
238   */
239  public function duplicateDisplayAsType($old_display_id, $new_display_type) {
240    $executable = $this->getExecutable();
241    $display = $executable->newDisplay($new_display_type);
242    $new_display_id = $display->display['id'];
243    $displays = $this->get('display');
244
245    // Let the display title be generated by the addDisplay method and set the
246    // right display plugin, but keep the rest from the original display.
247    $display_duplicate = $displays[$old_display_id];
248    unset($display_duplicate['display_title']);
249    unset($display_duplicate['display_plugin']);
250    unset($display_duplicate['new_id']);
251
252    $displays[$new_display_id] = NestedArray::mergeDeep($displays[$new_display_id], $display_duplicate);
253    $displays[$new_display_id]['id'] = $new_display_id;
254
255    // First set the displays.
256    $this->set('display', $displays);
257
258    // Ensure that we just copy display options, which are provided by the new
259    // display plugin.
260    $executable->setDisplay($new_display_id);
261
262    $executable->display_handler->filterByDefinedOptions($displays[$new_display_id]['display_options']);
263    // Update the display settings.
264    $this->set('display', $displays);
265
266    return $new_display_id;
267  }
268
269  /**
270   * {@inheritdoc}
271   */
272  public function calculateDependencies() {
273    parent::calculateDependencies();
274
275    // Ensure that the view is dependant on the module that implements the view.
276    $this->addDependency('module', $this->module);
277
278    $executable = $this->getExecutable();
279    $executable->initDisplay();
280    $executable->initStyle();
281
282    foreach ($executable->displayHandlers as $display) {
283      // Calculate the dependencies each display has.
284      $this->calculatePluginDependencies($display);
285    }
286
287    return $this;
288  }
289
290  /**
291   * {@inheritdoc}
292   */
293  public function preSave(EntityStorageInterface $storage) {
294    parent::preSave($storage);
295
296    $displays = $this->get('display');
297
298    // @todo Remove this line and support for pre-8.3 table names in Drupal 9.
299    // @see https://www.drupal.org/project/drupal/issues/3069405 .
300    $this->fixTableNames($displays);
301
302    // Sort the displays.
303    ksort($displays);
304    $this->set('display', ['default' => $displays['default']] + $displays);
305
306    // Calculating the cacheability metadata is only needed when the view is
307    // saved through the UI or API. It should not be done when we are syncing
308    // configuration or installing modules.
309    if (!$this->isSyncing() && !$this->hasTrustedData()) {
310      $this->addCacheMetadata();
311    }
312  }
313
314  /**
315   * Fixes table names for revision metadata fields of revisionable entities.
316   *
317   * Views for revisionable entity types using revision metadata fields might
318   * be using the wrong table to retrieve the fields after system_update_8300
319   * has moved them correctly to the revision table. This method updates the
320   * views to use the correct tables.
321   *
322   * @param array &$displays
323   *   An array containing display handlers of a view.
324   *
325   * @todo Remove this method and its usage in Drupal 9. See
326   *   https://www.drupal.org/project/drupal/issues/3069405.
327   * @see https://www.drupal.org/node/2831499
328   */
329  private function fixTableNames(array &$displays) {
330    // Fix wrong table names for entity revision metadata fields.
331    foreach ($displays as $display => $display_data) {
332      if (isset($display_data['display_options']['fields'])) {
333        foreach ($display_data['display_options']['fields'] as $property_name => $property_data) {
334          if (isset($property_data['entity_type']) && isset($property_data['field']) && isset($property_data['table'])) {
335            $entity_type = $this->entityTypeManager()->getDefinition($property_data['entity_type']);
336            // We need to update the table name only for revisionable entity
337            // types, otherwise the view is already using the correct table.
338            if (($entity_type instanceof ContentEntityTypeInterface) && is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && $entity_type->isRevisionable()) {
339              $revision_metadata_fields = $entity_type->getRevisionMetadataKeys();
340              // @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout()
341              $revision_table = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
342
343              // Check if this is a revision metadata field and if it uses the
344              // wrong table.
345              if (in_array($property_data['field'], $revision_metadata_fields) && $property_data['table'] != $revision_table) {
346                @trigger_error('Support for pre-8.3.0 revision table names in imported views is deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Imported views must reference the correct tables. See https://www.drupal.org/node/2831499', E_USER_DEPRECATED);
347                $displays[$display]['display_options']['fields'][$property_name]['table'] = $revision_table;
348              }
349            }
350          }
351        }
352      }
353    }
354  }
355
356  /**
357   * Fills in the cache metadata of this view.
358   *
359   * Cache metadata is set per view and per display, and ends up being stored in
360   * the view's configuration. This allows Views to determine very efficiently:
361   * - the max-age
362   * - the cache contexts
363   * - the cache tags
364   *
365   * In other words: this allows us to do the (expensive) work of initializing
366   * Views plugins and handlers to determine their effect on the cacheability of
367   * a view at save time rather than at runtime.
368   */
369  protected function addCacheMetadata() {
370    $executable = $this->getExecutable();
371
372    $current_display = $executable->current_display;
373    $displays = $this->get('display');
374    foreach (array_keys($displays) as $display_id) {
375      $display =& $this->getDisplay($display_id);
376      $executable->setDisplay($display_id);
377
378      $cache_metadata = $executable->getDisplay()->calculateCacheMetadata();
379      $display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge();
380      $display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts();
381      $display['cache_metadata']['tags'] = $cache_metadata->getCacheTags();
382      // Always include at least the 'languages:' context as there will most
383      // probably be translatable strings in the view output.
384      $display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['languages:' . LanguageInterface::TYPE_INTERFACE]);
385    }
386    // Restore the previous active display.
387    $executable->setDisplay($current_display);
388  }
389
390  /**
391   * {@inheritdoc}
392   */
393  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
394    parent::postSave($storage, $update);
395
396    // @todo Remove if views implements a view_builder controller.
397    views_invalidate_cache();
398    $this->invalidateCaches();
399
400    // Rebuild the router if this is a new view, or its status changed.
401    if (!isset($this->original) || ($this->status() != $this->original->status())) {
402      \Drupal::service('router.builder')->setRebuildNeeded();
403    }
404  }
405
406  /**
407   * {@inheritdoc}
408   */
409  public static function postLoad(EntityStorageInterface $storage, array &$entities) {
410    parent::postLoad($storage, $entities);
411    foreach ($entities as $entity) {
412      $entity->mergeDefaultDisplaysOptions();
413    }
414  }
415
416  /**
417   * {@inheritdoc}
418   */
419  public static function preCreate(EntityStorageInterface $storage, array &$values) {
420    parent::preCreate($storage, $values);
421
422    // If there is no information about displays available add at least the
423    // default display.
424    $values += [
425      'display' => [
426        'default' => [
427          'display_plugin' => 'default',
428          'id' => 'default',
429          'display_title' => 'Master',
430          'position' => 0,
431          'display_options' => [],
432        ],
433      ],
434    ];
435  }
436
437  /**
438   * {@inheritdoc}
439   */
440  public function postCreate(EntityStorageInterface $storage) {
441    parent::postCreate($storage);
442
443    $this->mergeDefaultDisplaysOptions();
444  }
445
446  /**
447   * {@inheritdoc}
448   */
449  public static function preDelete(EntityStorageInterface $storage, array $entities) {
450    parent::preDelete($storage, $entities);
451
452    // Call the remove() hook on the individual displays.
453    /** @var \Drupal\views\ViewEntityInterface $entity */
454    foreach ($entities as $entity) {
455      $executable = Views::executableFactory()->get($entity);
456      foreach ($entity->get('display') as $display_id => $display) {
457        $executable->setDisplay($display_id);
458        $executable->getDisplay()->remove();
459      }
460    }
461  }
462
463  /**
464   * {@inheritdoc}
465   */
466  public static function postDelete(EntityStorageInterface $storage, array $entities) {
467    parent::postDelete($storage, $entities);
468
469    $tempstore = \Drupal::service('tempstore.shared')->get('views');
470    foreach ($entities as $entity) {
471      $tempstore->delete($entity->id());
472    }
473  }
474
475  /**
476   * {@inheritdoc}
477   */
478  public function mergeDefaultDisplaysOptions() {
479    $displays = [];
480    foreach ($this->get('display') as $key => $options) {
481      $options += [
482        'display_options' => [],
483        'display_plugin' => NULL,
484        'id' => NULL,
485        'display_title' => '',
486        'position' => NULL,
487      ];
488      // Add the defaults for the display.
489      $displays[$key] = $options;
490    }
491    $this->set('display', $displays);
492  }
493
494  /**
495   * {@inheritdoc}
496   */
497  public function isInstallable() {
498    $table_definition = \Drupal::service('views.views_data')->get($this->base_table);
499    // Check whether the base table definition exists and contains a base table
500    // definition. For example, taxonomy_views_data_alter() defines
501    // node_field_data even if it doesn't exist as a base table.
502    return $table_definition && isset($table_definition['table']['base']);
503  }
504
505  /**
506   * {@inheritdoc}
507   */
508  public function __sleep() {
509    $keys = parent::__sleep();
510    unset($keys[array_search('executable', $keys)]);
511    return $keys;
512  }
513
514  /**
515   * Invalidates cache tags.
516   */
517  public function invalidateCaches() {
518    // Invalidate cache tags for cached rows.
519    $tags = $this->getCacheTags();
520    \Drupal::service('cache_tags.invalidator')->invalidateTags($tags);
521  }
522
523  /**
524   * {@inheritdoc}
525   */
526  public function onDependencyRemoval(array $dependencies) {
527    $changed = FALSE;
528
529    // Don't intervene if the views module is removed.
530    if (isset($dependencies['module']) && in_array('views', $dependencies['module'])) {
531      return FALSE;
532    }
533
534    // If the base table for the View is provided by a module being removed, we
535    // delete the View because this is not something that can be fixed manually.
536    $views_data = Views::viewsData();
537    $base_table = $this->get('base_table');
538    $base_table_data = $views_data->get($base_table);
539    if (!empty($base_table_data['table']['provider']) && in_array($base_table_data['table']['provider'], $dependencies['module'])) {
540      return FALSE;
541    }
542
543    $current_display = $this->getExecutable()->current_display;
544    $handler_types = Views::getHandlerTypes();
545
546    // Find all the handlers and check whether they want to do something on
547    // dependency removal.
548    foreach ($this->display as $display_id => $display_plugin_base) {
549      $this->getExecutable()->setDisplay($display_id);
550      $display = $this->getExecutable()->getDisplay();
551
552      foreach (array_keys($handler_types) as $handler_type) {
553        $handlers = $display->getHandlers($handler_type);
554        foreach ($handlers as $handler_id => $handler) {
555          if ($handler instanceof DependentWithRemovalPluginInterface) {
556            if ($handler->onDependencyRemoval($dependencies)) {
557              // Remove the handler and indicate we made changes.
558              unset($this->display[$display_id]['display_options'][$handler_types[$handler_type]['plural']][$handler_id]);
559              $changed = TRUE;
560            }
561          }
562        }
563      }
564    }
565
566    // Disable the View if we made changes.
567    // @todo https://www.drupal.org/node/2832558 Give better feedback for
568    // disabled config.
569    if ($changed) {
570      // Force a recalculation of the dependencies if we made changes.
571      $this->getExecutable()->current_display = NULL;
572      $this->calculateDependencies();
573      $this->disable();
574    }
575
576    $this->getExecutable()->setDisplay($current_display);
577    return $changed;
578  }
579
580}
581