Skip to content
views_plugin_display.inc 108 KiB
Newer Older
/**
 * @file
 * Contains the base display plugin.
 */

/**
 * @defgroup views_display_plugins Views display plugins
 * @{
 * Display plugins control how Views interact with the rest of Drupal.
 *
 * They can handle creating Views from a Drupal page hook; they can
 * handle creating Views from a Drupal block hook. They can also
 * handle creating Views from an external module source, such as
 * a Panels pane, or an insert view, or a CCK field type.
 *
 */

/**
 * The default display plugin handler. Display plugins handle options and
 * basic mechanisms for different output methods.
 */
class views_plugin_display extends views_plugin {
   * Stores all available display extenders.
  function init(&$view, &$display, $options = NULL) {
    $this->view = &$view;
    $this->display = &$display;
    // Load extenders as soon as possible.
    $this->extender = array();
    $extenders = views_get_enabled_display_extenders();
    // If you update to the dev version the registry might not be loaded yet.
    if (!empty($extenders) && class_exists('views_plugin_display_extender')) {
      foreach ($extenders as $extender) {
        $plugin = views_get_plugin('display_extender', $extender);
        if ($plugin) {
          $plugin->init($this->view, $this);
          $this->extender[$extender] = $plugin;
        }
        else {
          vpr('Invalid display extender @extender', array('@handler' => $extender));
        }
      }
    }

    // Track changes that the user should know about.
    $changed = FALSE;

    if (!isset($options) && isset($display->display_options)) {
      $options = $display->display_options;
    }

    if ($this->is_default_display() && isset($options['defaults'])) {
      unset($options['defaults']);
    }

    // Cache for unpack_options, but not if we are in the ui.
    static $unpack_options = array();
    if (empty($view->editing)) {
      $cid = 'unpack_options:' . md5(serialize(array($this->options, $options)));
      if (empty($unpack_options[$cid])) {
        $cache = views_cache_get($cid, TRUE);
        if (!empty($cache->data)) {
          $this->options = $cache->data;
        }
        else {
          $this->unpack_options($this->options, $options);
          views_cache_set($cid, $this->options, TRUE);
        }
        $unpack_options[$cid] = $this->options;
      }
      else {
        $this->options = $unpack_options[$cid];
      }
    }
    else {
      $this->unpack_options($this->options, $options);
    }

    // Translate changed settings:
    $items_per_page = $this->get_option('items_per_page');
    $offset = $this->get_option('offset');
    $use_pager = $this->get_option('use_pager');
    $pager = $this->get_option('pager');
    // Check if the pager options were already converted.
    // The pager settings of a Views 2.x view specifying 10 items with an
    // offset of 0 and no pager is the same as of a Views 3.x view with
    // default settings. In this case, the only way to determine which case we
    // are dealing with is checking the API version but that's only available
    // for exported Views as it's not stored in the database.
    // If you would like to change this code, really take care that you thought
    // of every possibility.
    // @TODO: Provide a way to convert the database views as well.
    if (((!empty($items_per_page) && $items_per_page != 10) || !empty($offset) || !empty($use_pager))
      || (!empty($view->api_version) && $view->api_version == 2)) {
      // Find out the right pager type.
      // If the view "use pager" it's a normal/full pager.
      if ($use_pager) {
      // If it does not use pager, but 0 items per page it should not page
      // else it should display just a certain amount of items.
      else {
        $type = $items_per_page ? 'some' : 'none';
      }
      // Setup the pager options.
      $pager = array(
        'type' => $type,
        'options' => array(
          'offset' => intval($offset)
        ),
      );

      if ($items_per_page) {
        $pager['options']['items_per_page'] = $items_per_page;
      }
      // Setup the pager element.
      if ($id = $this->get_option('pager_element')) {
        $pager['options']['id'] = $id;
      }

      // Unset the previous options
      // After edit and save the view they will be erased
      $this->set_option('items_per_page', NULL);
      $this->set_option('offset', NULL);
      $this->set_option('use_pager', NULL);
      $this->set_option('pager', $pager);
    // Plugable headers, footer and empty texts are
    // not compatible with previous version of views
    // This code converts old values into a configured handler for each area
    foreach (array('header', 'footer', 'empty') as $area) {
      if (isset($this->options[$area]) && !is_array($this->options[$area])) {
        if (!empty($this->options[$area])) {
          $content = $this->get_option($area);
          if (!empty($content) && !is_array($content)) {
            $format = $this->get_option($area . '_format');
            $options = array(
              'id' => 'area',
              'table' => 'views',
              'field' => 'area',
              'label' => '',
              'relationship' => 'none',
              'group_type' => 'group',
              'content' => $content,
              'format' => !empty($format) ? $format : filter_default_format(),
            );

            if ($area != 'empty' && $empty = $this->get_option($area . '_empty')) {
              $options['empty'] = $empty;
            }
            $this->set_option($area, array('text' => $options));
        // Ensure that options are at least an empty array
        if (!$converted) {
          $this->set_option($area, array());
        }

    // Convert distinct setting from display to query settings.
    $distinct = $this->get_option('distinct');
    if (!empty($distinct)) {
      $query_settings = $this->get_option('query');
      $query_settings['options']['distinct'] = $distinct;
      $this->set_option('query', $query_settings);
      // Clear the values
      $this->set_option('distinct', NULL);

    // Convert field language settings.
    $query_options = $this->get_option('query');
    if (isset($query_options['options']['field_language'])) {
      $this->set_option('field_language', $query_options['options']['field_language']);
      unset($query_options['options']['field_language']);
    }
    if (isset($query_options['options']['field_language_add_to_query'])) {
      $this->set_option('field_language_add_to_query', $query_options['options']['field_language_add_to_query']);
      unset($query_options['options']['field_language_add_to_query']);
    }
    $this->set_option('query', $query_options);

    // Convert filter groups.
    $filter_groups = $this->get_option('filter_groups');
    // Only convert if it wasn't converted yet, which is the case if there is a 0 group.
    if (isset($filter_groups['groups'][0])) {
      // Update filter groups.
      $filter_groups ['groups'] = views_array_key_plus($filter_groups['groups']);
      $this->set_option('filter_groups', $filter_groups);
      // Update the filter group on each filter.
      $filters = $this->get_option('filters');
      foreach ($filters as &$filter) {
        if (isset($filter['group'])) {
          $filter['group']++;
        }
        else {
          $filter['group'] = 1;
        }
    // Filter groups were allowed to be rewritten without its filters, so
    // before this update the view was using the default values. To be sure that
    // the existing view isn't broken, don't use this overridden values but copy
    // them from the default display. Only do this if the filters are overridden
    // but the filter_groups are not marked as so.
    if (!$this->is_default_display() && !$this->options['defaults']['filters'] && $this->options['defaults']['filter_groups']) {
      // Set filter_groups to be overridden and save the value in the
      // display_options as well.
      $this->options['defaults']['filter_groups'] = FALSE;
      $this->display->display_options['defaults']['filter_groups'] = $this->options['defaults']['filter_groups'];
      // Copy the filter_groups from the default, and add them to the
      // display_options as well. $this->default_display is not initialized at
      // this point.
      $this->options['filter_groups'] = $this->view->display['default']->handler->options['filter_groups'];
      $this->display->display_options['filter_groups'] = $this->options['filter_groups'];

      $changed = TRUE;
    }

    // Mark the view as changed so the user has a chance to save it.
    if ($changed) {
      $this->view->changed = TRUE;
  }

  function destroy() {
    parent::destroy();

    foreach ($this->handlers as $type => $handlers) {
      foreach ($handlers as $id => $handler) {
        if (is_object($handler)) {
          $this->handlers[$type][$id]->destroy();
        }
      }
    }

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

    foreach ($this->extender as $extender) {
      $extender->destroy();
    }
  }

  /**
   * Determine if this display is the 'default' display which contains
   * fallback settings
   */
  function is_default_display() { return FALSE; }

  /**
   * Determine if this display uses exposed filters, so the view
   * will know whether or not to build them.
   */
  function uses_exposed() {
    if (!isset($this->has_exposed)) {
      foreach ($this->handlers as $type => $value) {
        foreach ($this->view->$type as $id => $handler) {
          if ($handler->can_expose() && $handler->is_exposed()) {
            // one is all we need; if we find it, return true.
            $this->has_exposed = TRUE;
            return TRUE;
          }
        }
      }
      $pager = $this->get_plugin('pager');
      if (isset($pager) && $pager->uses_exposed()) {
        $this->has_exposed = TRUE;
        return TRUE;
      }
      $this->has_exposed = FALSE;
    }

    return $this->has_exposed;
  }

  /**
   * Determine if this display should display the exposed
   * filters widgets, so the view will know whether or not
   * to render them.
   *
   * Regardless of what this function
   * returns, exposed filters will not be used nor
   * displayed unless uses_exposed() returns TRUE.
   */
  function displays_exposed() {
    return TRUE;
  }

  /**
   * Does the display use AJAX?
   */
  function use_ajax() {
    if (!empty($this->definition['use ajax'])) {
      return $this->get_option('use_ajax');
    }
    return FALSE;
  }

  /**
   * Does the display have a pager enabled?
   */
  function use_pager() {
    $pager = $this->get_plugin('pager');
    if ($pager) {
      return $pager->use_pager();
  /**
   * Does the display have a more link enabled?
   */
  function use_more() {
    if (!empty($this->definition['use more'])) {
      return $this->get_option('use_more');
    }
    return FALSE;
  }

   * Does the display have groupby enabled?
   */
  function use_group_by() {
    return $this->get_option('group_by');
  }

  /**
   * Should the enabled display more link be shown when no more items?
   */
  function use_more_always() {
    if (!empty($this->definition['use more'])) {
      return $this->get_option('use_more_always');
    }
    return FALSE;
  }

  /**
   * Does the display have custom link text?
   */
  function use_more_text() {
    if (!empty($this->definition['use more'])) {
      return $this->get_option('use_more_text');
    }
    return FALSE;
  }

  /**
   * Can this display accept attachments?
   */
  function accept_attachments() {
    if (empty($this->definition['accept attachments'])) {
      return FALSE;
    }
    if (!empty($this->view->argument) && $this->get_option('hide_attachment_summary')) {
      foreach ($this->view->argument as $argument_id => $argument) {
        if ($argument->needs_style_plugin() && empty($argument->argument_validated)) {
          return FALSE;
        }
      }
    }
    return TRUE;
  }

  /**
   * Allow displays to attach to other views.
   */
  function attach_to($display_id) { }

  /**
   * Static member function to list which sections are defaultable
   * and what items each section contains.
   */
  function defaultable_sections($section = NULL) {
    $sections = array(
      'access' => array('access', 'access_options'),
      'access_options' => array('access', 'access_options'),
      'cache' => array('cache', 'cache_options'),
      'cache_options' => array('cache', 'cache_options'),
      'css_class' => array('css_class'),
      'hide_attachment_summary' => array('hide_attachment_summary'),
      'use_more' => array('use_more', 'use_more_always', 'use_more_text'),
      'use_more_always' => array('use_more', 'use_more_always', 'use_more_text'),
      'use_more_text' => array('use_more', 'use_more_always', 'use_more_text'),
      'link_display' => array('link_display', 'link_url'),

      // Force these to cascade properly.
      'style_plugin' => array('style_plugin', 'style_options', 'row_plugin', 'row_options'),
      'style_options' => array('style_plugin', 'style_options', 'row_plugin', 'row_options'),
      'row_plugin' => array('style_plugin', 'style_options', 'row_plugin', 'row_options'),
      'row_options' => array('style_plugin', 'style_options', 'row_plugin', 'row_options'),

      'pager' => array('pager', 'pager_options'),
      'pager_options' => array('pager', 'pager_options'),

      'exposed_form' => array('exposed_form', 'exposed_form_options'),
      'exposed_form_options' => array('exposed_form', 'exposed_form_options'),

      'footer' => array('footer'),
      'relationships' => array('relationships'),
      'fields' => array('fields'),
      'sorts' => array('sorts'),
      'arguments' => array('arguments'),
      'filters' => array('filters', 'filter_groups'),
      'filter_groups' => array('filters', 'filter_groups'),

    // If the display cannot use a pager, then we cannot default it.
    if (empty($this->definition['use pager'])) {
      unset($sections['pager']);
      unset($sections['items_per_page']);
    }

    foreach ($this->extender as $extender) {
      $extender->defaultable_sections($sections, $section);
    }

    if ($section) {
      if (!empty($sections[$section])) {
        return $sections[$section];
      }
    }
    else {
      return $sections;
    }
  }

  function option_definition() {
    $options = array(
      'defaults' => array(
        'default' => array(
          'access' => TRUE,
          'cache' => TRUE,
          'display_description' => FALSE,
          'hide_attachment_summary' => TRUE,
          'pager_options' => TRUE,
          'use_more_always' => TRUE,
          'exposed_form_options' => TRUE,

          'style_plugin' => TRUE,
          'style_options' => TRUE,
          'row_plugin' => TRUE,
          'row_options' => TRUE,

          'header' => TRUE,
          'footer' => TRUE,
          'empty' => TRUE,

          'relationships' => TRUE,
          'fields' => TRUE,
          'sorts' => TRUE,
          'arguments' => TRUE,
          'filters' => TRUE,
      'title' => array(
        'default' => '',
        'translatable' => TRUE,
      ),
      'enabled' => array(
        'default' => TRUE,
        'translatable' => FALSE,
        'bool' => TRUE,
      ),
      'css_class' => array(
        'default' => '',
        'translatable' => FALSE,
      ),
      'display_description' => array(
        'default' => '',
        'translatable' => TRUE,
      ),
      'hide_attachment_summary' => array(
        'default' => FALSE,
        'bool' => TRUE,
      ),
      'hide_admin_links' => array(
        'default' => FALSE,
        'bool' => TRUE,
      ),
      // This is legacy code:
      // Items_per/offset/use_pager is moved to the pager plugin
      // but the automatic update path needs this items defined, so don't remove it.
      // @see views_plugin_display::init()
      'items_per_page' => array(
        'default' => 10,
      ),
      'offset' => array(
        'default' => 0,
      ),
      'use_pager' => array(
        'default' => FALSE,
      'use_more_always' => array(
        'default' => FALSE,
        'bool' => TRUE,
        'export' => 'export_option_always',
      'use_more_text' => array(
        'default' => 'more',
        'translatable' => TRUE,
      ),
      'link_display' => array(
        'default' => '',
      ),
      'field_language' => array(
        'default' => '***CURRENT_LANGUAGE***',
      ),
      'field_language_add_to_query' => array(
        'default' => 1,
      ),

      // These types are all plugins that can have individual settings
      // and therefore need special handling.
      'access' => array(
        'contains' => array(
          'type' => array('default' => 'none', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'),
          'type' => array('default' => 'none', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'),
      'query' => array(
        'contains' => array(
          'type' => array('default' => 'views_query', 'export' => 'export_plugin'),
          'options' => array('default' => array(), 'export' => FALSE),
         ),
      ),
      // Note that exposed_form plugin has options in a separate array,
      // while access and cache do not. access and cache are legacy and
      // that pattern should not be repeated, but it is left as is to
      // reduce the need to modify older views. Let's consider the
      // pattern used here to be the template from which future plugins
      // should be copied.
      'exposed_form' => array(
        'contains' => array(
          'type' => array('default' => 'basic', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'),
          'options' => array('default' => array(), 'export' => FALSE),
         ),
          'type' => array('default' => 'full', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'),
          'options' => array('default' => array(), 'export' => FALSE),
         ),
      ),
      // Note that the styles have their options completely independent.
      // Like access and cache above, this is a legacy pattern and
      // should not be repeated.
      'style_plugin' => array(
        'default' => 'default',
        'unpack_translatable' => 'unpack_style',
      ),
      'style_options' => array(
        'default' => array(),
        'unpack_translatable' => 'unpack_style',
      'header' => array(
        'default' => array(),
        'export' => 'export_handler',
        'unpack_translatable' => 'unpack_handler',
      ),
      'footer' => array(
        'default' => array(),
        'export' => 'export_handler',
        'unpack_translatable' => 'unpack_handler',
      ),
      'empty' => array(
        'default' => array(),
        'export' => 'export_handler',
        'unpack_translatable' => 'unpack_handler',
      // We want these to export last.
      // These are the 5 handler types.
      'relationships' => array(
        'default' => array(),
        'export' => 'export_handler',
      ),
      'fields' => array(
        'default' => array(),
        'export' => 'export_handler',
        'unpack_translatable' => 'unpack_handler',
      ),
      'sorts' => array(
        'default' => array(),
        'export' => 'export_handler',
        'unpack_translatable' => 'unpack_handler',
      ),
      'arguments' => array(
        'default' => array(),
        'export' => 'export_handler',
        'unpack_translatable' => 'unpack_handler',
      'filter_groups' => array(
        'contains' => array(
          'operator' => array('default' => 'AND'),
          'groups' => array('default' => array(1 => 'AND')),
      'filters' => array(
        'default' => array(),
        'export' => 'export_handler',
        'unpack_translatable' => 'unpack_handler',
    if (empty($this->definition['use pager'])) {
      $options['defaults']['default']['use_pager'] = FALSE;
      $options['defaults']['default']['items_per_page'] = FALSE;
      $options['defaults']['default']['offset'] = FALSE;
      $options['defaults']['default']['pager'] = FALSE;
      $options['pager']['contains']['type']['default'] = 'some';
    }

    if ($this->is_default_display()) {
      unset($options['defaults']);
    }

    foreach ($this->extender as $extender) {
      $extender->options_definition_alter($options);
    return $options;
  }

  /**
   * Check to see if the display has a 'path' field.
   *
   * This is a pure function and not just a setting on the definition
   * because some displays (such as a panel pane) may have a path based
   * upon configuration.
   *
   * By default, displays do not have a path.
   */
  function has_path() { return FALSE; }

  /**
   * Check to see if the display has some need to link to another display.
   *
   * For the most part, displays without a path will use a link display. However,
   * sometimes displays that have a path might also need to link to another display.
   * This is true for feeds.
   */
  function uses_link_display() { return !$this->has_path(); }

  /**
   * Check to see if the display can put the exposed formin a block.
   *
   * By default, displays that do not have a path cannot disconnect
   * the exposed form and put it in a block, because the form has no
   * place to go and Views really wants the forms to go to a specific
   * page.
   */
  function uses_exposed_form_in_block() { return $this->has_path(); }

  /**
   * Check to see which display to use when creating links within
   * a view using this display.
   */
  function get_link_display() {
    $display_id = $this->get_option('link_display');
    // If unknown, pick the first one.
    if (empty($display_id) || empty($this->view->display[$display_id])) {
      foreach ($this->view->display as $display_id => $display) {
        if (!empty($display->handler) && $display->handler->has_path()) {
          return $display_id;
        }
      }
    }
    else {
      return $display_id;
    }
    // fall-through returns NULL
  }

  /**
   * Return the base path to use for this display.
   *
   * This can be overridden for displays that do strange things
   * with the path.
   */
  function get_path() {
    if ($this->has_path()) {
      return $this->get_option('path');
    }

    $display_id = $this->get_link_display();
    if ($display_id && !empty($this->view->display[$display_id]) && is_object($this->view->display[$display_id]->handler)) {
      return $this->view->display[$display_id]->handler->get_path();
    }
  }

  /**
   * Check to see if the display needs a breadcrumb
   *
   * By default, displays do not need breadcrumbs
   */
  function uses_breadcrumb() { return FALSE; }

  /**
   * Determine if a given option is set to use the default display or the
   * current display
   *
   * @return
   *   TRUE for the default display
   */
  function is_defaulted($option) {
    return !$this->is_default_display() && !empty($this->default_display) && !empty($this->options['defaults'][$option]);
  }

  /**
   * Intelligently get an option either from this display or from the
   * default display, if directed to do so.
   */
  function get_option($option) {
    if ($this->is_defaulted($option)) {
      return $this->default_display->get_option($option);
    }

    if (array_key_exists($option, $this->options)) {
      return $this->options[$option];
    }
  }

  /**
   * Determine if the display's style uses fields.
   */
  function uses_fields() {
    $plugin = $this->get_plugin();
    if ($plugin) {
      return $plugin->uses_fields();
    }
  }

  /**
   * Get the instance of a plugin, for example style or row.
   *
   * @param string $type
   *   The type of the plugin.
   * @param string $name
   *   The name of the plugin defined in hook_views_plugins.
   *
   * @return views_plugin|FALSE
   */
  function get_plugin($type = 'style', $name = NULL) {
    static $cache = array();
    if (!isset($cache[$type][$name])) {
      switch ($type) {
        case 'style':
        case 'row':
          $option_name = $type . '_plugin';
          $options = $this->get_option($type . '_options');
          if (!$name) {
            $name = $this->get_option($option_name);
          }
        case 'query':
          $views_data = views_fetch_data($this->view->base_table);
          $name = !empty($views_data['table']['base']['query class']) ? $views_data['table']['base']['query class'] : 'views_query';
        default:
          $option_name = $type;
          $options = $this->get_option($type);
          if (!$name) {
            $name = $options['type'];
          }
          // access & cache store their options as siblings with the
          // type; all others use an 'options' array.
          if ($type != 'access' && $type != 'cache') {
            $options = $options['options'];
          }
      }
      $plugin = views_get_plugin($type, $name);
      if ($type != 'query') {
        $plugin->init($this->view, $this->display, $options);
      }
      else {
        $display_id = $this->is_defaulted($option_name) ? $this->display->id : 'default';
        $plugin->localization_keys = array($display_id, $type);

        if (!isset($this->base_field)) {
          $views_data = views_fetch_data($this->view->base_table);
          $this->view->base_field = !empty($views_data['table']['base']['field']) ? $views_data['table']['base']['field'] : '';
        $plugin->init($this->view->base_table, $this->view->base_field, $options);
      }
  /**
   * Get the handler object for a single handler.
   */
  function &get_handler($type, $id) {
    if (!isset($this->handlers[$type])) {
      $this->get_handlers($type);
    }

    if (isset($this->handlers[$type][$id])) {
      return $this->handlers[$type][$id];
    }

    // So we can return a reference.
    $null = NULL;
    return $null;
  }

  /**
   * Get a full array of handlers for $type. This caches them.
   */
  function get_handlers($type) {
    if (!isset($this->handlers[$type])) {
      $this->handlers[$type] = array();
      $types = views_object_types();
      $plural = $types[$type]['plural'];
      foreach ($this->get_option($plural) as $id => $info) {
        // If this is during form submission and there are temporary options
        // which can only appear if the view is in the edit cache, use those
        // options instead. This is used for AJAX multi-step stuff.
        if (isset($_POST['form_id']) && isset($this->view->temporary_options[$type][$id])) {
          $info = $this->view->temporary_options[$type][$id];
        }

        // If aggregation is on, the group type might override the actual
        // handler that is in use. This piece of code checks that and,
        // if necessary, sets the override handler.
        $override = NULL;
        if ($this->use_group_by() && !empty($info['group_type'])) {
          if (empty($this->view->query)) {
            $this->view->init_query();
          }
          $aggregate = $this->view->query->get_aggregation_info();
          if (!empty($aggregate[$info['group_type']]['handler'][$type])) {
            $override = $aggregate[$info['group_type']]['handler'][$type];
          }
        }

        if (!empty($types[$type]['type'])) {
          $handler_type = $types[$type]['type'];
        }
        else {
          $handler_type = $type;
        }

        $handler = views_get_handler($info['table'], $info['field'], $handler_type, $override);
          // Special override for area types so they know where they come from.
          if ($handler_type == 'area') {
            $handler->handler_type = $type;
          }

          $this->handlers[$type][$id] = &$handler;
        }

        // Prevent reference problems.
        unset($handler);
      }
    }

    return $this->handlers[$type];
  }

  /**
   * Retrieve a list of fields for the current display with the
   * relationship associated if it exists.
   *
   * @param $groupable_only
   *  Return only an array of field labels from handler that return TRUE
   *  from use_string_group_by method.
    // Use func_get_arg so the function signature isn't amended
    // but we can still pass TRUE into the function to filter
    // by groupable handlers.
    $args = func_get_args();
    $groupable_only = isset($args[0]) ? $args[0] : FALSE;
    $options = array();
    foreach ($this->get_handlers('relationship') as $relationship => $handler) {
      if ($label = $handler->label()) {
        $relationships[$relationship] = $label;
      }
      else {
        $relationships[$relationship] = $handler->ui_name();
      }
    }

    foreach ($this->get_handlers('field') as $id => $handler) {