ViewUI.php 39.4 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Definition of Drupal\views_ui\ViewUI.
6 7
 */

8
namespace Drupal\views_ui;
9

10
use Drupal\Component\Utility\Html;
11
use Drupal\Component\Utility\Timer;
12
use Drupal\Component\Utility\Xss;
13
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
14
use Drupal\Core\Form\FormStateInterface;
15
use Drupal\Core\Url;
16
use Drupal\views\Views;
17
use Drupal\Core\Entity\EntityStorageInterface;
18
use Drupal\views\ViewExecutable;
19
use Drupal\Core\Database\Database;
20
use Drupal\Core\TypedData\TypedDataInterface;
21
use Drupal\Core\Session\AccountInterface;
22
use Drupal\views\Plugin\views\query\Sql;
23
use Drupal\views\Entity\View;
24
use Drupal\views\ViewEntityInterface;
25 26 27
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
28

29 30 31
/**
 * Stores UI related temporary settings.
 */
32
class ViewUI implements ViewEntityInterface {
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71

  /**
   * Indicates if a view is currently being edited.
   *
   * @var bool
   */
  public $editing = FALSE;

  /**
   * Stores an array of displays that have been changed.
   *
   * @var array
   */
  public $changed_display;

  /**
   * How long the view takes to build.
   *
   * @var int
   */
  public $build_time;

  /**
   * How long the view takes to render.
   *
   * @var int
   */
  public $render_time;

  /**
   * How long the view takes to execute.
   *
   * @var int
   */
  public $execute_time;

  /**
   * If this view is locked for editing.
   *
72
   * If this view is locked it will contain the result of
73 74
   * \Drupal\user\SharedTempStore::getMetadata(). Which can be a stdClass or
   * NULL.
75 76
   *
   * @var stdClass
77
   */
78
  public $lock;
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

  /**
   * If this view has been changed.
   *
   * @var bool
   */
  public $changed;

  /**
   * Stores options temporarily while editing.
   *
   * @var array
   */
  public $temporary_options;

  /**
   * Stores a stack of UI forms to display.
   *
   * @var array
   */
  public $stack;

  /**
102
   * Is the view run in a context of the preview in the admin interface.
103 104 105 106
   *
   * @var bool
   */
  public $live_preview;
107

108
  public $renderPreview = FALSE;
109 110

  /**
111
   * The View storage object.
112
   *
113
   * @var \Drupal\views\ViewEntityInterface
114
   */
115
  protected $storage;
116

117 118 119 120 121 122 123 124 125
  /**
   * Stores a list of database queries run beside the main one from views.
   *
   * @var array
   *
   * @see \Drupal\Core\Database\Log
   */
  protected $additionalQueries;

126 127 128 129 130
  /**
   * Contains an array of form keys and their respective classes.
   *
   * @var array
   */
131
  public static $forms = array(
132
    'add-handler' => '\Drupal\views_ui\Form\Ajax\AddItem',
133
    'analyze' => '\Drupal\views_ui\Form\Ajax\Analyze',
134 135 136
    'handler' => '\Drupal\views_ui\Form\Ajax\ConfigHandler',
    'handler-extra' => '\Drupal\views_ui\Form\Ajax\ConfigHandlerExtra',
    'handler-group' => '\Drupal\views_ui\Form\Ajax\ConfigHandlerGroup',
137 138 139 140 141
    'display' => '\Drupal\views_ui\Form\Ajax\Display',
    'edit-details' => '\Drupal\views_ui\Form\Ajax\EditDetails',
    'rearrange' => '\Drupal\views_ui\Form\Ajax\Rearrange',
    'rearrange-filter' => '\Drupal\views_ui\Form\Ajax\RearrangeFilter',
    'reorder-displays' => '\Drupal\views_ui\Form\Ajax\ReorderDisplays',
142 143
  );

144 145 146 147 148 149 150 151
  /**
   * Whether the config is being created, updated or deleted through the
   * import process.
   *
   * @var bool
   */
  private $isSyncing = FALSE;

152 153 154 155 156 157 158
  /**
   * Whether the config is being deleted through the uninstall process.
   *
   * @var bool
   */
  private $isUninstalling = FALSE;

159
  /**
160
   * Constructs a View UI object.
161
   *
162
   * @param \Drupal\views\ViewEntityInterface $storage
163
   *   The View storage object to wrap.
164
   */
165
  public function __construct(ViewEntityInterface $storage) {
166 167
    $this->entityType = 'view';
    $this->storage = $storage;
168 169 170
  }

  /**
171
   * Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::get().
172
   */
173 174 175
  public function get($property_name, $langcode = NULL) {
    if (property_exists($this->storage, $property_name)) {
      return $this->storage->get($property_name, $langcode);
176 177
    }

178
    return isset($this->{$property_name}) ? $this->{$property_name} : NULL;
179 180
  }

181 182 183 184 185 186 187
  /**
   * Implements \Drupal\Core\Config\Entity\ConfigEntityInterface::setStatus().
   */
  public function setStatus($status) {
    return $this->storage->setStatus($status);
  }

188
  /**
189
   * Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::set().
190
   */
191
  public function set($property_name, $value, $notify = TRUE) {
192 193
    if (property_exists($this->storage, $property_name)) {
      $this->storage->set($property_name, $value);
194 195
    }
    else {
196
      $this->{$property_name} = $value;
197 198 199
    }
  }

200 201 202 203 204 205 206
  /**
   * {@inheritdoc}
   */
  public function setSyncing($syncing) {
    $this->isSyncing = $syncing;
  }

207 208 209 210 211 212 213
  /**
   * {@inheritdoc}
   */
  public function setUninstalling($isUninstalling) {
    $this->isUninstalling = $isUninstalling;
  }

214 215 216 217 218 219 220
  /**
   * {@inheritdoc}
   */
  public function isSyncing() {
    return $this->isSyncing;
  }

221 222 223 224 225 226 227
  /**
   * {@inheritdoc}
   */
  public function isUninstalling() {
    return $this->isUninstalling;
  }

228 229 230 231 232 233 234
  /**
   * Basic submit handler applicable to all 'standard' forms.
   *
   * This submit handler determines whether the user wants the submitted changes
   * to apply to the default display or to the current display, and dispatches
   * control appropriately.
   */
235
  public function standardSubmit($form, FormStateInterface $form_state) {
236 237 238 239 240 241 242
    // Determine whether the values the user entered are intended to apply to
    // the current display or the default display.

    list($was_defaulted, $is_defaulted, $revert) = $this->getOverrideValues($form, $form_state);

    // Based on the user's choice in the display dropdown, determine which display
    // these changes apply to.
243
    $display_id = $form_state->get('display_id');
244 245
    if ($revert) {
      // If it's revert just change the override and return.
246
      $display = &$this->getExecutable()->displayHandlers->get($display_id);
247 248 249
      $display->optionsOverride($form, $form_state);

      // Don't execute the normal submit handling but still store the changed view into cache.
250
      $this->cacheSet();
251 252 253 254 255 256 257 258 259
      return;
    }
    elseif ($was_defaulted === $is_defaulted) {
      // We're not changing which display these form values apply to.
      // Run the regular submit handler for this form.
    }
    elseif ($was_defaulted && !$is_defaulted) {
      // We were using the default display's values, but we're now overriding
      // the default display and saving values specific to this display.
260
      $display = &$this->getExecutable()->displayHandlers->get($display_id);
261 262 263 264 265 266 267 268 269
      // optionsOverride toggles the override of this section.
      $display->optionsOverride($form, $form_state);
      $display->submitOptionsForm($form, $form_state);
    }
    elseif (!$was_defaulted && $is_defaulted) {
      // We used to have an override for this display, but the user now wants
      // to go back to the default display.
      // Overwrite the default display with the current form values, and make
      // the current display use the new default values.
270
      $display = &$this->getExecutable()->displayHandlers->get($display_id);
271 272 273 274 275
      // optionsOverride toggles the override of this section.
      $display->optionsOverride($form, $form_state);
      $display->submitOptionsForm($form, $form_state);
    }

276 277
    $submit_handler = [$form_state->getFormObject(), 'submitForm'];
    call_user_func_array($submit_handler, [&$form, $form_state]);
278 279 280 281 282
  }

  /**
   * Submit handler for cancel button
   */
283
  public function standardCancel($form, FormStateInterface $form_state) {
284 285
    if (!empty($this->changed) && isset($this->form_cache)) {
      unset($this->form_cache);
286
      $this->cacheSet();
287 288
    }

289
    $form_state->setRedirectUrl($this->urlInfo('edit-form'));
290 291 292 293 294 295 296 297 298 299
  }

  /**
   * Provide a standard set of Apply/Cancel/OK buttons for the forms. Also provide
   * a hidden op operator because the forms plugin doesn't seem to properly
   * provide which button was clicked.
   *
   * TODO: Is the hidden op operator still here somewhere, or is that part of the
   * docblock outdated?
   */
300
  public function getStandardButtons(&$form, FormStateInterface $form_state, $form_id, $name = NULL) {
301 302
    $form['actions'] = array(
      '#type' => 'actions',
303 304 305 306 307 308 309 310 311 312
    );

    if (empty($name)) {
      $name = t('Apply');
      if (!empty($this->stack) && count($this->stack) > 1) {
        $name = t('Apply and continue');
      }
      $names = array(t('Apply'), t('Apply and continue'));
    }

313 314 315
    // Views provides its own custom handling of AJAX form submissions. Usually
    // this happens at the same path, but custom paths may be specified in
    // $form_state.
316
    $form_url = $form_state->get('url') ?: Url::fromRouteMatch(\Drupal::routeMatch());
317

318 319
    // Forms that are purely informational set an ok_button flag, so we know not
    // to create an "Apply" button for them.
320
    if (!$form_state->get('ok_button')) {
321
      $form['actions']['submit'] = array(
322 323
        '#type' => 'submit',
        '#value' => $name,
324
        '#id' => 'edit-submit-' . Html::getUniqueId($form_id),
325 326 327 328 329 330
        // The regular submit handler ($form_id . '_submit') does not apply if
        // we're updating the default display. It does apply if we're updating
        // the current display. Since we have no way of knowing at this point
        // which display the user wants to update, views_ui_standard_submit will
        // take care of running the regular submit handler as appropriate.
        '#submit' => array(array($this, 'standardSubmit')),
331
        '#button_type' => 'primary',
332
        '#ajax' => array(
333
          'url' => $form_url,
334
        ),
335 336
      );
      // Form API button click detection requires the button's #value to be the
337 338 339
      // same between the form build of the initial page request, and the
      // initial form build of the request processing the form submission.
      // Ideally, the button's #value shouldn't change until the form rebuild
340
      // step. However, \Drupal\views_ui\Form\Ajax\ViewsFormBase::getForm()
341 342 343 344
      // implements a different multistep form workflow than the Form API does,
      // and adjusts $view->stack prior to form processing, so we compensate by
      // extending button click detection code to support any of the possible
      // button labels.
345
      if (isset($names)) {
346
        $form['actions']['submit']['#values'] = $names;
347
        $form['actions']['submit']['#process'] = array_merge(array('views_ui_form_button_was_clicked'), \Drupal::service('element_info')->getInfoProperty($form['actions']['submit']['#type'], '#process', array()));
348 349
      }
      // If a validation handler exists for the form, assign it to this button.
350
      $form['actions']['submit']['#validate'][] = [$form_state->getFormObject(), 'validateForm'];
351 352 353 354
    }

    // Create a "Cancel" button. For purely informational forms, label it "OK".
    $cancel_submit = function_exists($form_id . '_cancel') ? $form_id . '_cancel' : array($this, 'standardCancel');
355
    $form['actions']['cancel'] = array(
356
      '#type' => 'submit',
357
      '#value' => !$form_state->get('ok_button') ? t('Cancel') : t('Ok'),
358 359
      '#submit' => array($cancel_submit),
      '#validate' => array(),
360
      '#ajax' => array(
361
        'path' => $form_url,
362
      ),
363
      '#limit_validation_errors' => array(),
364 365 366 367 368
    );

    // Compatibility, to be removed later: // TODO: When is "later"?
    // We used to set these items on the form, but now we want them on the $form_state:
    if (isset($form['#title'])) {
369
      $form_state->set('title', $form['#title']);
370 371
    }
    if (isset($form['#section'])) {
372
      $form_state->set('#section', $form['#section']);
373 374 375 376 377 378 379 380
    }
    // Finally, we never want these cached -- our object cache does that for us.
    $form['#no_cache'] = TRUE;
  }

  /**
   * Return the was_defaulted, is_defaulted and revert state of a form.
   */
381
  public function getOverrideValues($form, FormStateInterface $form_state) {
382
    // Make sure the dropdown exists in the first place.
383
    if ($form_state->hasValue(array('override', 'dropdown'))) {
384 385 386
      // #default_value is used to determine whether it was the default value or not.
      // So the available options are: $display, 'default' and 'default_revert', not 'defaults'.
      $was_defaulted = ($form['override']['dropdown']['#default_value'] === 'defaults');
387 388 389
      $dropdown = $form_state->getValue(array('override', 'dropdown'));
      $is_defaulted = ($dropdown === 'default');
      $revert = ($dropdown === 'default_revert');
390 391 392 393

      if ($was_defaulted !== $is_defaulted && isset($form['#section'])) {
        // We're changing which display these values apply to.
        // Update the #section so it knows what to mark changed.
394
        $form['#section'] = str_replace('default-', $form_state->get('display_id') . '-', $form['#section']);
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
      }
    }
    else {
      // The user didn't get the dropdown for overriding the default display.
      $was_defaulted = FALSE;
      $is_defaulted = FALSE;
      $revert = FALSE;
    }

    return array($was_defaulted, $is_defaulted, $revert);
  }

  /**
   * Add another form to the stack; clicking 'apply' will go to this form
   * rather than closing the ajax popup.
   */
411
  public function addFormToStack($key, $display_id, $type, $id = NULL, $top = FALSE, $rebuild_keys = FALSE) {
412 413 414
    // Reset the cache of IDs. Drupal rather aggressively prevents ID
    // duplication but this causes it to remember IDs that are no longer even
    // being used.
415
    Html::resetSeenIds();
416

417 418 419 420
    if (empty($this->stack)) {
      $this->stack = array();
    }

421
    $stack = array(implode('-', array_filter(array($key, $this->id(), $display_id, $type, $id))), $key, $display_id, $type, $id);
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
    // If we're being asked to add this form to the bottom of the stack, no
    // special logic is required. Our work is equally easy if we were asked to add
    // to the top of the stack, but there's nothing in it yet.
    if (!$top || empty($this->stack)) {
      $this->stack[] = $stack;
    }
    // If we're adding to the top of an existing stack, we have to maintain the
    // existing integer keys, so they can be used for the "2 of 3" progress
    // indicator (which will now read "2 of 4").
    else {
      $keys = array_keys($this->stack);
      $first = current($keys);
      $last = end($keys);
      for ($i = $last; $i >= $first; $i--) {
        if (!isset($this->stack[$i])) {
          continue;
        }
        // Move form number $i to the next position in the stack.
        $this->stack[$i + 1] = $this->stack[$i];
        unset($this->stack[$i]);
      }
      // Now that the previously $first slot is free, move the new form into it.
      $this->stack[$first] = $stack;
      ksort($this->stack);

      // Start the keys from 0 again, if requested.
      if ($rebuild_keys) {
        $this->stack = array_values($this->stack);
      }
    }
  }

  /**
   * Submit handler for adding new item(s) to a view.
   */
457
  public function submitItemAdd($form, FormStateInterface $form_state) {
458
    $type = $form_state->get('type');
459
    $types = ViewExecutable::getHandlerTypes();
460
    $section = $types[$type]['plural'];
461
    $display_id = $form_state->get('display_id');
462 463 464 465 466 467

    // Handle the override select.
    list($was_defaulted, $is_defaulted) = $this->getOverrideValues($form, $form_state);
    if ($was_defaulted && !$is_defaulted) {
      // We were using the default display's values, but we're now overriding
      // the default display and saving values specific to this display.
468
      $display = &$this->getExecutable()->displayHandlers->get($display_id);
469 470 471 472 473 474 475 476
      // setOverride toggles the override of this section.
      $display->setOverride($section);
    }
    elseif (!$was_defaulted && $is_defaulted) {
      // We used to have an override for this display, but the user now wants
      // to go back to the default display.
      // Overwrite the default display with the current form values, and make
      // the current display use the new default values.
477
      $display = &$this->getExecutable()->displayHandlers->get($display_id);
478 479 480 481
      // optionsOverride toggles the override of this section.
      $display->setOverride($section);
    }

482
    if (!$form_state->isValueEmpty('name') && is_array($form_state->getValue('name'))) {
483
      // Loop through each of the items that were checked and add them to the view.
484
      foreach (array_keys(array_filter($form_state->getValue('name'))) as $field) {
485 486 487 488 489
        list($table, $field) = explode('.', $field, 2);

        if ($cut = strpos($field, '$')) {
          $field = substr($field, 0, $cut);
        }
490
        $id = $this->getExecutable()->addHandler($display_id, $type, $table, $field);
491 492 493 494 495 496 497

        // check to see if we have group by settings
        $key = $type;
        // Footer,header and empty text have a different internal handler type(area).
        if (isset($types[$type]['type'])) {
          $key = $types[$type]['type'];
        }
498 499 500 501
        $item = array(
          'table' => $table,
          'field' => $field,
        );
502
        $handler = Views::handlerManager($key)->getHandler($item);
503
        if ($this->getExecutable()->displayHandlers->get('default')->useGroupBy() && $handler->usesGroupBy()) {
504
          $this->addFormToStack('handler-group', $display_id, $type, $id);
505 506 507 508
        }

        // check to see if this type has settings, if so add the settings form first
        if ($handler && $handler->hasExtraOptions()) {
509
          $this->addFormToStack('handler-extra', $display_id, $type, $id);
510 511
        }
        // Then add the form to the stack
512
        $this->addFormToStack('handler', $display_id, $type, $id);
513 514 515 516 517 518 519 520
      }
    }

    if (isset($this->form_cache)) {
      unset($this->form_cache);
    }

    // Store in cache
521
    $this->cacheSet();
522 523
  }

524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
  /**
   * Set up query capturing.
   *
   * \Drupal\Core\Database\Database stores the queries that it runs, if logging
   * is enabled.
   *
   * @see ViewUI::endQueryCapture()
   */
  public function startQueryCapture() {
    Database::startLog('views');
  }

  /**
   * Add the list of queries run during render to buildinfo.
   *
   * @see ViewUI::startQueryCapture()
   */
  public function endQueryCapture() {
    $queries = Database::getLog('views');

    $this->additionalQueries = $queries;
  }

547 548
  public function renderPreview($display_id, $args = array()) {
    // Save the current path so it can be restored before returning from this function.
549 550
    $request_stack = \Drupal::requestStack();
    $current_request = $request_stack->getCurrentRequest();
551
    $executable = $this->getExecutable();
552 553

    // Determine where the query and performance statistics should be output.
554
    $config = \Drupal::config('views.settings');
555 556 557 558 559 560 561 562 563 564 565 566 567
    $show_query = $config->get('ui.show.sql_query.enabled');
    $show_info = $config->get('ui.show.preview_information');
    $show_location = $config->get('ui.show.sql_query.where');

    $show_stats = $config->get('ui.show.performance_statistics');
    if ($show_stats) {
      $show_stats = $config->get('ui.show.sql_query.where');
    }

    $combined = $show_query && $show_stats;

    $rows = array('query' => array(), 'statistics' => array());

568
    $errors = $executable->validate();
569
    $executable->destroy();
570
    if (empty($errors)) {
571
      $this->ajax = TRUE;
572
      $executable->live_preview = TRUE;
573

574
      // AJAX happens via HTTP POST but everything expects exposed data to
575 576
      // be in GET. Copy stuff but remove ajax-framework specific keys.
      // If we're clicking on links in a preview, though, we could actually
577 578 579
      // have some input in the query parameters, so we merge request() and
      // query() to ensure we get it all.
      $exposed_input = array_merge(\Drupal::request()->request->all(), \Drupal::request()->query->all());
580 581 582 583 584
      foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) {
        if (isset($exposed_input[$key])) {
          unset($exposed_input[$key]);
        }
      }
585
      $executable->setExposedInput($exposed_input);
586

587
      if (!$executable->setDisplay($display_id)) {
588 589 590
        return [
          '#markup' => t('Invalid display id @display', array('@display' => $display_id)),
        ];
591 592
      }

593
      $executable->setArguments($args);
594 595

      // Store the current view URL for later use:
596
      if ($executable->hasUrl() && $executable->display_handler->getOption('path')) {
597
        $path = $executable->getUrl();
598 599 600 601
      }

      // Make view links come back to preview.

602 603 604
      // Also override the current path so we get the pager, and make sure the
      // Request object gets all of the proper values from $_SERVER.
      $request = Request::createFromGlobals();
605 606 607 608 609 610 611 612 613 614 615
      $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'entity.view.preview_form');
      $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, \Drupal::service('router.route_provider')->getRouteByName('entity.view.preview_form'));
      $request->attributes->set('view', $this->storage);
      $request->attributes->set('display_id', $display_id);
      $raw_parameters = new ParameterBag();
      $raw_parameters->set('view', $this->id());
      $raw_parameters->set('display_id', $display_id);
      $request->attributes->set('_raw_variables', $raw_parameters);

      foreach ($args as $key => $arg) {
        $request->attributes->set('arg_' . $key, $arg);
616
      }
617
      $request_stack->push($request);
618 619 620 621 622 623

      // Suppress contextual links of entities within the result set during a
      // Preview.
      // @todo We'll want to add contextual links specific to editing the View, so
      //   the suppression may need to be moved deeper into the Preview pipeline.
      views_ui_contextual_links_suppress_push();
624 625 626

      $show_additional_queries = $config->get('ui.show.additional_queries');

627
      Timer::start('entity.view.preview_form');
628 629 630 631 632 633

      if ($show_additional_queries) {
        $this->startQueryCapture();
      }

      // Execute/get the view preview.
634
      $preview = $executable->preview($display_id, $args);
635 636 637 638 639

      if ($show_additional_queries) {
        $this->endQueryCapture();
      }

640
      $this->render_time = Timer::stop('entity.view.preview_form');
641

642 643 644 645 646 647
      views_ui_contextual_links_suppress_pop();

      // Prepare the query information and statistics to show either above or
      // below the view preview.
      if ($show_info || $show_query || $show_stats) {
        // Get information from the preview for display.
648
        if (!empty($executable->build_info['query'])) {
649
          if ($show_query) {
650
            $query_string = $executable->build_info['query'];
651 652 653
            // Only the sql default class has a method getArguments.
            $quoted = array();

654
            if ($executable->query instanceof Sql) {
655
              $quoted = $query_string->getArguments();
656 657 658 659 660 661 662 663 664 665
              $connection = Database::getConnection();
              foreach ($quoted as $key => $val) {
                if (is_array($val)) {
                  $quoted[$key] = implode(', ', array_map(array($connection, 'quote'), $val));
                }
                else {
                  $quoted[$key] = $connection->quote($val);
                }
              }
            }
666
            $rows['query'][] = array(
667 668 669 670 671 672 673 674 675 676 677 678 679
              array(
                'data' => array(
                  '#type' => 'inline_template',
                  '#template' => "<strong>{% trans 'Query' %}</strong>",
                ),
              ),
              array(
                'data' => array(
                  '#type' => 'inline_template',
                  '#template' => '<pre>{{ query }}</pre>',
                  '#context' => array('query' => strtr($query_string, $quoted)),
                ),
              ),
680
            );
681
            if (!empty($this->additionalQueries)) {
682 683 684 685 686
              $queries[] = array(
                '#prefix' => '<strong>',
                '#markup' => t('These queries were run during view rendering:'),
                '#suffix' => '</strong>',
              );
687 688
              foreach ($this->additionalQueries as $query) {
                $query_string = strtr($query['query'], $query['args']);
689 690 691 692
                $queries[] = array(
                  '#prefix' => "\n",
                  '#markup' => t('[@time ms] @query', array('@time' => round($query['time'] * 100000, 1) / 100000.0, '@query' => $query_string)),
                );
693 694
              }

695
              $rows['query'][] = array(
696 697 698 699 700 701
                array(
                  'data' => array(
                    '#type' => 'inline_template',
                    '#template' => "<strong>{% trans 'Other queries' %}</strong>",
                  ),
                ),
702 703 704 705 706 707 708
                array(
                  'data' => array(
                    '#prefix' => '<pre>',
                     'queries' => $queries,
                     '#suffix' => '</pre>',
                    ),
                ),
709
              );
710 711 712
            }
          }
          if ($show_info) {
713
            $rows['query'][] = array(
714 715 716 717 718 719
              array(
                'data' => array(
                  '#type' => 'inline_template',
                  '#template' => "<strong>{% trans 'Title' %}</strong>",
                ),
              ),
720
              Xss::filterAdmin($executable->getTitle()),
721
            );
722
            if (isset($path)) {
723 724
              // @todo Views should expect and store a leading /. See:
              //   https://www.drupal.org/node/2423913
725
              $path = \Drupal::l($path->toString(), $path);
726 727 728 729
            }
            else {
              $path = t('This display has no path.');
            }
730 731 732 733 734 735 736 737 738 739 740 741 742 743
            $rows['query'][] = array(
              array(
                'data' => array(
                  '#prefix' => '<strong>',
                  '#markup' => t('Path'),
                  '#suffix' => '</strong>',
                ),
              ),
              array(
                'data' => array(
                  '#markup' => $path,
                ),
              )
            );
744 745
          }
          if ($show_stats) {
746
            $rows['statistics'][] = array(
747 748 749 750 751 752
              array(
                'data' => array(
                  '#type' => 'inline_template',
                  '#template' => "<strong>{% trans 'Query build time' %}</strong>",
                ),
              ),
753
              t('@time ms', array('@time' => intval($executable->build_time * 100000) / 100)),
754 755 756
            );

            $rows['statistics'][] = array(
757 758 759 760 761 762
              array(
                'data' => array(
                  '#type' => 'inline_template',
                  '#template' => "<strong>{% trans 'Query execute time' %}</strong>",
                ),
              ),
763
              t('@time ms', array('@time' => intval($executable->execute_time * 100000) / 100)),
764
            );
765

766
            $rows['statistics'][] = array(
767 768 769 770 771 772
              array(
                'data' => array(
                  '#type' => 'inline_template',
                  '#template' => "<strong>{% trans 'View render time' %}</strong>",
                ),
              ),
773
              t('@time ms', array('@time' => intval($executable->render_time * 100000) / 100)),
774
            );
775
          }
776
          \Drupal::moduleHandler()->alter('views_preview_info', $rows, $executable);
777 778 779 780 781
        }
        else {
          // No query was run. Display that information in place of either the
          // query or the performance statistics, whichever comes first.
          if ($combined || ($show_location === 'above')) {
782 783 784 785 786 787 788 789 790 791 792 793 794 795
            $rows['query'][] = array(
              array(
                'data' => array(
                  '#prefix' => '<strong>',
                  '#markup' => t('Query'),
                  '#suffix' => '</strong>',
                ),
              ),
              array(
                'data' => array(
                  '#markup' => t('No query was run'),
                ),
              ),
            );
796 797
          }
          else {
798 799 800 801 802 803 804 805 806 807 808 809 810 811
            $rows['statistics'][] = array(
              array(
                'data' => array(
                  '#prefix' => '<strong>',
                  '#markup' => t('Query'),
                  '#suffix' => '</strong>',
                ),
              ),
              array(
                'data' => array(
                  '#markup' => t('No query was run'),
                ),
              ),
            );
812 813 814 815 816
          }
        }
      }
    }
    else {
817 818 819 820
      foreach ($errors as $display_errors) {
        foreach ($display_errors as $error) {
          drupal_set_message($error, 'error');
        }
821 822 823 824 825 826
      }
      $preview = t('Unable to preview due to validation errors.');
    }

    // Assemble the preview, the query info, and the query statistics in the
    // requested order.
827
    $table = array(
828
      '#type' => 'table',
829 830 831 832
      '#prefix' => '<div class="views-query-info">',
      '#suffix' => '</div>',
    );
    if ($show_location === 'above' || $show_location === 'below') {
833
      if ($combined) {
834
        $table['#rows'] = array_merge($rows['query'], $rows['statistics']);
835 836
      }
      else {
837
        $table['#rows'] = $rows['query'];
838 839
      }
    }
840 841
    elseif ($show_stats === 'above' || $show_stats === 'below') {
      $table['#rows'] = $rows['statistics'];
842 843
    }

844
    if ($show_location === 'above' || $show_stats === 'above') {
845 846 847 848
      $output = [
        'table' => $table,
        'preview' => $preview,
      ];
849
    }
850
    elseif ($show_location === 'below' || $show_stats === 'below') {
851 852 853 854
      $output = [
        'preview' => $preview,
        'table' => $table,
      ];
855 856
    }

857 858 859 860 861
    // Ensure that we just remove an additional request we pushed earlier.
    // This could happen if $errors was not empty.
    if ($request_stack->getCurrentRequest() != $current_request) {
      $request_stack->pop();
    }
862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
    return $output;
  }

  /**
   * Get the user's current progress through the form stack.
   *
   * @return
   *   FALSE if the user is not currently in a multiple-form stack. Otherwise,
   *   an associative array with the following keys:
   *   - current: The number of the current form on the stack.
   *   - total: The total number of forms originally on the stack.
   */
  public function getFormProgress() {
    $progress = FALSE;
    if (!empty($this->stack)) {
      // The forms on the stack have integer keys that don't change as the forms
      // are completed, so we can see which ones are still left.
      $keys = array_keys($this->stack);
      // Add 1 to the array keys for the benefit of humans, who start counting
      // from 1 and not 0.
      $current = reset($keys) + 1;
      $total = end($keys) + 1;
      if ($total > 1) {
        $progress = array();
        $progress['current'] = $current;
        $progress['total'] = $total;
      }
    }
    return $progress;
  }

893 894 895 896 897 898 899 900 901 902 903 904
  /**
   * Sets a cached view object in the user tempstore.
   */
  public function cacheSet() {
    if ($this->isLocked()) {
      drupal_set_message(t('Changes cannot be made to a locked view.'), 'error');
      return;
    }

    // Let any future object know that this view has changed.
    $this->changed = TRUE;

905
    $executable = $this->getExecutable();
906 907 908
    if (isset($executable->current_display)) {
      // Add the knowledge of the changed display, too.
      $this->changed_display[$executable->current_display] = TRUE;
909
      $executable->current_display = NULL;
910 911
    }

912 913 914
    // Unset handlers. We don't want to write these into the cache.
    $executable->display_handler = NULL;
    $executable->default_display = NULL;
915
    $executable->query = NULL;
916
    $executable->displayHandlers = NULL;
917
    \Drupal::service('user.shared_tempstore')->get('views')->set($this->id(), $this);
918 919 920 921 922 923 924 925 926
  }

  /**
   * Returns whether the current view is locked.
   *
   * @return bool
   *   TRUE if the view is locked, FALSE otherwise.
   */
  public function isLocked() {
927
    return is_object($this->lock) && ($this->lock->owner != \Drupal::currentUser()->id());
928 929
  }

930
  /**
931 932 933 934 935 936
   * Passes through all unknown calls onto the storage object.
   */
  public function __call($method, $args) {
    return call_user_func_array(array($this->storage, $method), $args);
  }

937 938 939 940 941 942 943
  /**
   * {@inheritdoc}
   */
  public function &getDisplay($display_id) {
    return $this->storage->getDisplay($display_id);
  }

944 945 946 947
  /**
   * Implements \Drupal\Core\Entity\EntityInterface::id().
   */
  public function id() {
948
    return $this->storage->id();
949 950 951 952 953 954
  }

  /**
   * Implements \Drupal\Core\Entity\EntityInterface::uuid().
   */
  public function uuid() {
955
    return $this->storage->uuid();
956 957 958 959 960 961
  }

  /**
   * Implements \Drupal\Core\Entity\EntityInterface::isNew().
   */
  public function isNew() {
962
    return $this->storage->isNew();
963 964 965
  }

  /**
966
   * {@inheritdoc}
967
   */
968 969
  public function getEntityTypeId() {
    return $this->storage->getEntityTypeId();
970 971 972 973 974 975
  }

  /**
   * Implements \Drupal\Core\Entity\EntityInterface::bundle().
   */
  public function bundle() {
976
    return $this->storage->bundle();
977 978 979
  }

  /**
980
   * {@inheritdoc}
981
   */
982 983
  public function getEntityType() {
    return $this->storage->getEntityType();
984 985 986 987 988 989
  }

  /**
   * Implements \Drupal\Core\Entity\EntityInterface::createDuplicate().
   */
  public function createDuplicate() {
990
    return $this->storage->createDuplicate();
991 992
  }

993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006
  /**
   * {@inheritdoc}
   */
  public static function load($id) {
    return View::load($id);
  }

  /**
   * {@inheritdoc}
   */
  public static function loadMultiple(array $ids = NULL) {
    return View::loadMultiple($ids);
  }

1007 1008 1009 1010 1011 1012 1013
  /**
   * {@inheritdoc}
   */
  public static function create(array $values = array()) {
    return View::create($values);
  }

1014 1015 1016 1017
  /**
   * Implements \Drupal\Core\Entity\EntityInterface::delete().
   */
  public function delete() {
1018
    return $this->storage->delete();
1019 1020 1021 1022 1023 1024
  }

  /**
   * Implements \Drupal\Core\Entity\EntityInterface::save().
   */
  public function save() {
1025
    return $this->storage->save();
1026 1027 1028 1029 1030
  }

  /**
   * Implements \Drupal\Core\Entity\EntityInterface::uri().
   */
1031 1032
  public function urlInfo($rel = 'edit-form', array $options = []) {
    return $this->storage->urlInfo($rel, $options);