You are here

views_navigation.inc in Views navigation 7

Views navigation main include file.

File

views_navigation.inc
View source
<?php

/**
 * @file
 * Views navigation main include file.
 */

/**
 * Get a view query from cache.
 */
function views_navigation_get_cached_query($cid) {
  $cache = cache_get('query-' . $cid, VIEWS_NAVIGATION_CACHE_BIN);
  if ($cache && _views_navigation_query_is_supported($cache->data)) {
    return $cache->data;
  }
}

/**
 * Get a view result from cache.
 */
function views_navigation_get_cached_result($cid) {
  $cache = cache_get('result-' . $cid, VIEWS_NAVIGATION_CACHE_BIN);
  return $cache ? $cache->data : FALSE;
}

/**
 * Store a view query in cache.
 *
 * Return the cid or FALSE if the query is not stored.
 */
function views_navigation_store_query($view) {
  if (_views_navigation_query_is_supported($view->query)) {
    $query_to_store = clone $view->query;
    $view_to_store = clone $view;
    $dh_to_store = clone $view_to_store->display_handler;

    // Handle the case when there is no pager.
    if (!isset($view_to_store->total_rows) || empty($view_to_store->total_rows)) {
      $view_to_store->total_rows = count($view_to_store->result);
    }

    // If there is zero or one result, we do nothing.
    if ($view_to_store->total_rows < 2) {
      return FALSE;
    }

    // Store the back destination if needed.
    if ($view->display_handler
      ->get_option('views_navigation_back')) {
      $raw_destination = drupal_get_destination();

      // We must remove the "page=X" query parameter to keep the stored info
      // identical across pages (same hash needed).
      // Thus the back link will always lead to the first page (we can't easily
      // know which page the user was on when he left the view : that would need
      // an extra GET parameter).
      $parsed_destination = drupal_parse_url(urldecode(reset($raw_destination)));
      unset($parsed_destination['query']['page']);
      $view_to_store->back_destination = $parsed_destination;

      // Store the view's title if needed.
      if ($view->display_handler
        ->get_option('views_navigation_title')) {
        $view_to_store->back_title = $view_to_store
          ->get_title();
      }
    }

    // Remove useless properties and query limit. This is the not easy part.
    // We must ensure that the stored query is the same across pagination.
    $query_to_store->pager_backup = $query_to_store->pager;
    unset($query_to_store->display);
    unset($query_to_store->header);
    unset($query_to_store->pager);
    $plugin = _views_navigation_get_query_plugin($query_to_store);
    switch ($plugin) {
      case 'default':
        unset($query_to_store->limit);
        $query_to_store->offset = 0;
        break;
      case 'search_api':
        $query_to_store
          ->set_limit(NULL);
        $query_to_store
          ->set_offset(0);
        break;
    }
    $keys = [
      'base_database',
      'name',
      'total_rows',
      'back_destination',
      'back_title',
    ];
    foreach ($view_to_store as $key => $value) {
      if (!in_array($key, $keys)) {
        unset($view_to_store->{$key});
      }
    }

    // Most of the display handler info is unneeded and might change across
    // pagination. But we need some info though.
    // Unsure we keep inherited cache options.
    $dh_to_store->options['cache'] = $dh_to_store
      ->get_option('cache');
    foreach ($dh_to_store as $key => $value) {

      // For now, we found no bug by keeping all the options, and only that.
      if (!in_array($key, [
        'options',
      ])) {
        unset($dh_to_store->{$key});
      }
    }
    $view_to_store->display_handler = $dh_to_store;
    $query_to_store->view = $view_to_store;

    // Allow modules to alter the stored info.
    drupal_alter('views_navigation_stored_query', $query_to_store, $view);
    $cid = views_navigation_get_query_cid($query_to_store);

    // The query may be stored already.
    if (!views_navigation_get_cached_query($cid)) {

      // We need to store the query as long as the user navigates across the
      // result set. One day should be far enough.
      // @see views_navigation_get_links()
      cache_set('query-' . $cid, $query_to_store, VIEWS_NAVIGATION_CACHE_BIN, REQUEST_TIME + 86400);
    }
    return $cid;
  }
  return FALSE;
}

/**
 * Get the unique cid corresponding to a view query.
 */
function views_navigation_get_query_cid($query) {
  return drupal_hash_base64(serialize($query));
}

/**
 * Redirect to the next or previous entity.
 */
function views_navigation_router($cid, $pos) {
  $back_pos = arg(3);
  if (list($path, $options) = _views_navigation_get_data($cid, $pos, $back_pos)) {
    drupal_goto($path, $options);
  }

  // Something went wrong.
  watchdog('views_navigation', 'Could not find the requested page.', \func_get_args(), WATCHDOG_ERROR);
  return t("Sorry, could not find the requested page.");
}

/**
 * Get the result of a query, as an array of etids keyed by position.
 */
function views_navigation_get_result($cid) {
  if (!($result = views_navigation_get_cached_result($cid))) {
    if ($query = views_navigation_get_cached_query($cid)) {
      $result = [];
      $plugin = _views_navigation_get_query_plugin($query);
      $entity_type = _views_navigation_get_entity_type($query);
      $id_key = _views_navigation_get_id_key($entity_type);
      views_module_include('views');
      switch ($plugin) {
        case 'default':
          $real_query = $query
            ->query();
          $rows = $real_query
            ->execute();
          foreach ($rows as $pos => $row) {
            $result[$pos] = $row->{$id_key};
          }
          break;
        case 'search_api':
          $real_query = $query
            ->getSearchApiQuery();
          $real_query
            ->range(0, NULL);
          $response = $real_query
            ->execute();
          $rows = $response['results'];
          $pos = 0;
          foreach ($rows as $row) {
            $result[$pos] = $row['id'];
            $pos++;
          }
          break;
      }

      // Try to store the result set for a relevant time according to the view's
      // cache settings.
      switch ($query->view->display_handler->options['cache']['type']) {
        case 'none':

          // Never cache the result. We assume the view is lightweight and
          // results won't change during navigation.
          break;
        case 'time':

          // This cache could be set at a different time from the normal view's
          // one. So that this can still lead to inconsistency with fast-moving
          // views. But this is better than nothing as we have a idea on how
          // often this view changes.
          $ttl = REQUEST_TIME + $query->view->display_handler->options['cache']['results_lifespan'];
          break;
        default:

          // Unhandled cache backends. Let's store the result set during one
          // hour. This is better than never or for ever.
          $ttl = REQUEST_TIME + 3600;
      }
      if (isset($ttl)) {
        cache_set('result-' . $cid, $result, VIEWS_NAVIGATION_CACHE_BIN, $ttl);
      }
    }
  }
  return $result;
}

/**
 * Build and render the previous/next links for the entity being viewed.
 */
function views_navigation_get_links($view_mode = 'full') {

  // Ensure we can show links.
  $cid = isset($_GET[VIEWS_NAVIGATION_CACHE_ID_PARAMETER]) ? $_GET[VIEWS_NAVIGATION_CACHE_ID_PARAMETER] : NULL;
  $pos = isset($_GET[VIEWS_NAVIGATION_POSITION_PARAMETER]) ? $_GET[VIEWS_NAVIGATION_POSITION_PARAMETER] : NULL;
  if (!isset($cid) || !isset($pos) || $view_mode != 'full') {
    return FALSE;
  }
  $query = views_navigation_get_cached_query($cid);
  if (!$query) {
    return FALSE;
  }
  $data = [];
  if ($query->view->display_handler
    ->get_option('views_navigation') && ($query->view->display_handler
    ->get_option('views_navigation_cycle') || $pos > 0)) {
    $data['previous'] = [
      'default_title' => t('Previous'),
      'pos' => $pos - 1,
      'back pos' => NULL,
    ];
  }
  if (isset($query->view->back_destination)) {
    $data['back'] = [
      'default_title' => isset($query->view->back_title) ? t('Back to %title', [
        '%title' => $query->view->back_title,
      ]) : t('Back'),
      'pos' => 'back',
      'back pos' => $pos,
    ];
  }
  if ($query->view->display_handler
    ->get_option('views_navigation') && ($query->view->display_handler
    ->get_option('views_navigation_cycle') || $pos < $query->view->total_rows - 1)) {
    $data['next'] = [
      'default_title' => t('Next'),
      'pos' => $pos + 1,
      'back pos' => NULL,
    ];
  }
  $links = [];
  foreach ($data as $key => $value) {
    if ($query->view->display_handler
      ->get_option('views_navigation_seo_first')) {
      list($path, $options, $text) = _views_navigation_get_data($cid, $value['pos'], $value['back pos'], TRUE);
      $links[$key] = $options;
      $links[$key]['title'] = isset($text) ? $text : $value['default_title'];
      $links[$key]['href'] = $path;
    }
    else {
      $path_parts = [
        'views_navigation',
        $cid,
        $value['pos'],
      ];
      if (!empty($value['back pos'])) {
        $path_parts[] = $value['back pos'];
      }
      $links[$key]['title'] = $value['default_title'];
      $links[$key]['href'] = implode('/', $path_parts);
      $links[$key]['attributes']['rel'] = 'nofollow';
    }
    $links[$key]['html'] = TRUE;
    $links[$key]['attributes']['class'] = [
      'views-navigation-' . $key,
    ];
  }

  // Allow modules to alter the navigation links.
  drupal_alter('views_navigation_navigation_links', $links, $cid, $pos);
  return [
    '#theme' => 'links',
    '#links' => $links,
  ];
}

/**
 * Add the query parameters to append to the entity url.
 */
function _views_navigation_build_query($etid, $view, $query = []) {
  if (isset($view->views_navigation_cid)) {

    // Todo handle the case when the etid is in the result more than one time.
    $etids = [];
    $plugin = _views_navigation_get_query_plugin($view->query);
    $entity_type = _views_navigation_get_entity_type($view->query);
    $id_key = _views_navigation_get_id_key($entity_type);
    foreach ($view->result as $result) {
      switch ($plugin) {
        case 'default':
          $etids[] = $result->{$id_key};
          break;
        case 'search_api':
          if (is_object($result->entity)) {
            $etids[] = $result->entity->{$id_key};
          }
          else {
            $etids[] = $result->entity;
          }
          break;
      }
    }
    $index = array_search($etid, $etids);

    // Index of first item on second page must be items_per_page, not 0.
    $pager = $view->query->pager;
    if ($pager
      ->use_pager()) {
      $index += $pager
        ->get_items_per_page() * $pager
        ->get_current_page();
    }
    $query = array_merge($query, [
      VIEWS_NAVIGATION_POSITION_PARAMETER => $index,
      VIEWS_NAVIGATION_CACHE_ID_PARAMETER => $view->views_navigation_cid,
    ]);
  }

  // Allow modules to alter the query string.
  drupal_alter('views_navigation_query_string', $query, $index, $view);
  return $query;
}

/**
 * Used when the view handler needs an already built url.
 *
 * This is the case when it will be passed to parse_url() as in
 * views_handler_field::render_as_link().
 *
 * @param int $etid
 *   The entity ID.
 * @param object $view
 *   The views object.
 * @param array $options
 *   Link options.
 *
 * @return string
 *   The generated HTML for the link.
 */
function _views_navigation_build_url($etid, $view, array $options = [
  'absolute' => TRUE,
]) {
  $options['query'] = _views_navigation_build_query($etid, $view, isset($options['query']) ? $options['query'] : []);
  $entity_type = _views_navigation_get_entity_type($view->query);
  $entities = entity_load($entity_type, [
    $etid,
  ]);
  $uri = entity_uri($entity_type, reset($entities));
  return url($uri['path'], array_merge($options, $uri['options']));
}

/**
 * Function for check views navigation query is supported.
 *
 * @param object $query
 *   The views object.
 *
 * @return bool
 *   Whether the query type is supported.
 */
function _views_navigation_query_is_supported($query) {
  $plugin = _views_navigation_get_query_plugin($query);
  return isset($plugin);
}

/**
 * Function for getting the query plugin type.
 *
 * @param object $query
 *   The query object.
 *
 * @return string
 *   Either search_api or default.
 */
function _views_navigation_get_query_plugin($query) {

  // ATM we only handle the default and Search API views queries.
  if (is_a($query, 'views_plugin_query_default')) {
    return 'default';
  }
  if (is_a($query, 'SearchApiViewsQuery')) {
    return 'search_api';
  }
}

/**
 * Based on EntityFieldHandlerHelper::render_entity_link().
 *
 * @param object $handler
 *   The field handler whose field is rendered.
 * @param string $value
 *   The single value to render.
 * @param object $values
 *   The values for the current row retrieved from the Views query, as an
 *   object.
 *
 * @return string
 *   The rendered value.
 */
function _views_navigation_render_entity_link($handler, $value, $values) {
  $render = EntityFieldHandlerHelper::render_single_value($handler, $value, $values);
  if (!$handler->options['link_to_entity']) {
    return $render;
  }
  $entity = $handler
    ->get_value($values, 'entity object');
  if (is_object($entity) && ($uri = entity_uri($handler->entity_type, $entity))) {
    $id_key = _views_navigation_get_id_key($handler->entity_type);
    if (isset($entity->{$id_key})) {
      $uri['options']['query'] = _views_navigation_build_query($entity->{$id_key}, $handler->view);
    }
    return l($render, $uri['path'], [
      'html' => TRUE,
    ] + $uri['options']);
  }
  return $render;
}

/**
 * Helper function to get the data.
 *
 * Return an array containing $path and $options, as needed by url() and
 * drupal_goto() functions. If $get_label is set, the third value will be the
 * entity's label, else NULL.
 */
function _views_navigation_get_data($cid, $pos, $back_pos = NULL, $get_label = FALSE) {
  if ($query = views_navigation_get_cached_query($cid)) {
    if ($pos === 'back') {
      if (isset($query->view->back_destination)) {
        $options = $query->view->back_destination;
        if ($pager = $query->pager_backup) {
          if (!empty($back_pos)) {
            $options['query']['page'] = floor($back_pos / $pager->options['items_per_page']);
          }
        }
        return [
          $options['path'],
          $options,
          NULL,
        ];
      }
    }
    elseif ($result = views_navigation_get_result($cid)) {

      // Manage array ends, cycling behavior.
      $max_index = count($result) - 1;
      $pos = $pos > $max_index ? 0 : ($pos < 0 ? $max_index : $pos);
      if ($etid = $result[$pos]) {
        $params = [
          VIEWS_NAVIGATION_POSITION_PARAMETER => $pos,
          VIEWS_NAVIGATION_CACHE_ID_PARAMETER => $cid,
        ];
        $entity_type = _views_navigation_get_entity_type($query);
        $entities = entity_load($entity_type, [
          $etid,
        ]);
        $entity = reset($entities);
        $uri = entity_uri($entity_type, $entity);
        $uri['options']['query'] = $params;
        $return = [
          $uri['path'],
          $uri['options'],
        ];
        if ($get_label) {
          $return[] = entity_label($entity_type, $entity);
        }
        else {
          $return[] = NULL;
        }
        return $return;
      }
    }
  }
}

/**
 * Function to get the id of the entity key.
 *
 * @param string $entity_type
 *   The entity type.
 *
 * @return int
 *   The id of the entity key.
 */
function _views_navigation_get_id_key($entity_type) {
  $info = entity_get_info($entity_type);
  return $info['entity keys']['id'];
}

/**
 * Function to get the entity type.
 *
 * @param object $query
 *   The query object.
 *
 * @return string
 *   The entity type.
 */
function _views_navigation_get_entity_type($query) {

  // TODO Have it work with search API.
  $entity_type = $query->base_table;
  if ($entity_type == 'taxonomy_term_data') {
    $entity_type = 'taxonomy_term';
  }
  if (get_class($query) == 'SearchApiViewsQuery') {
    $entity_type = $query
      ->getIndex()
      ->getEntityType();
  }
  return $entity_type;
}

/**
 * Helper function to replace the links in HTML.
 *
 * @param string $html
 *   The HTML to replace links in.
 * @param object $entity
 *   The entity to render.
 * @param object $view
 *   The views object.
 */
function _views_navigation_replace_href_in_html(&$html, $entity, $view) {
  $entity_type = _views_navigation_get_entity_type($view->query);
  $uri = entity_uri($entity_type, $entity);
  $alias = base_path() . drupal_get_path_alias($uri['path']);
  $path = url($uri['path'], array(
    'absolute' => TRUE,
  ));
  $pattern = '@href="(' . $path . '|' . $alias . ')"@';
  if (preg_match($pattern, $html)) {
    $id_key = _views_navigation_get_id_key($entity_type);
    $url = _views_navigation_build_url($entity->{$id_key}, $view, [
      'absolute' => FALSE,
    ]);
    $html = preg_replace($pattern, 'href="' . $url . '"', $html);
  }
}

Functions

Namesort descending Description
views_navigation_get_cached_query Get a view query from cache.
views_navigation_get_cached_result Get a view result from cache.
views_navigation_get_links Build and render the previous/next links for the entity being viewed.
views_navigation_get_query_cid Get the unique cid corresponding to a view query.
views_navigation_get_result Get the result of a query, as an array of etids keyed by position.
views_navigation_router Redirect to the next or previous entity.
views_navigation_store_query Store a view query in cache.
_views_navigation_build_query Add the query parameters to append to the entity url.
_views_navigation_build_url Used when the view handler needs an already built url.
_views_navigation_get_data Helper function to get the data.
_views_navigation_get_entity_type Function to get the entity type.
_views_navigation_get_id_key Function to get the id of the entity key.
_views_navigation_get_query_plugin Function for getting the query plugin type.
_views_navigation_query_is_supported Function for check views navigation query is supported.
_views_navigation_render_entity_link Based on EntityFieldHandlerHelper::render_entity_link().
_views_navigation_replace_href_in_html Helper function to replace the links in HTML.