You are here

class views_xml_backend_plugin_query_xml in Views XML Backend 7

Same name and namespace in other branches
  1. 6 views_xml_backend_plugin_query_xml.inc \views_xml_backend_plugin_query_xml

@file Contains views_xml_backend_plugin_query_xml.

Hierarchy

Expanded class hierarchy of views_xml_backend_plugin_query_xml

1 string reference to 'views_xml_backend_plugin_query_xml'
views_xml_backend_views_plugins in ./views_xml_backend.views.inc
Implements hook_views_plugins().

File

./views_xml_backend_plugin_query_xml.inc, line 8
Contains views_xml_backend_plugin_query_xml.

View source
class views_xml_backend_plugin_query_xml extends views_plugin_query {

  /**
   * Document and initialize the attributes we use.
   */
  public $filter = array();
  public $argument = array();
  public $orderby = array();
  public $where = array();
  public $fields = array();
  public $options = array();
  public $group_operator = 'AND';
  public $pager;
  public $limit;
  public $offset;

  /**
   * Generates a query and a countquery from all of the information supplied to
   * the object.
   *
   * @param bool $get_count
   *   (Optional) Provide a countquery if this is true, otherwise provide a
   *   normal query. Defaults to FALSE.
   */
  public function query($get_count = FALSE) {
    $row_xpath = $this->options['row_xpath'];
    if ($this->filter) {
      $filters = array();
      foreach ($this->filter as $group_id => $group) {

        // $group is an array of views_xml_backend_handler_filter()'s. They
        // implement __toString(), so imploding calls their generate() method.
        if (count($group) == 1) {
          $filters[] = reset($group);
        }
        else {

          // Type could be 'AND' or 'OR'.
          $op = strtolower($this->where[$group_id]['type']);
          $filters[] = '(' . implode(" {$op} ", $group) . ')';
        }
      }
      $op = strtolower($this->group_operator);
      $row_xpath .= '[' . implode(" {$op} ", $filters) . ']';
    }
    if ($this->argument) {
      $row_xpath .= '[' . implode(' and ', $this->argument) . ']';
    }
    return $row_xpath;
  }

  /**
   * Builds the necessary info to execute the query.
   */
  public function build(&$view) {
    $view
      ->init_pager();

    // Let the pager modify the query to add limits.
    $this->pager
      ->query();
    $view->build_info['query'] = $this
      ->query();
    $view->build_info['count_query'] = 'count(' . $view->build_info['query'] . ')';
    $view->build_info['query_args'] = array();
  }
  public function fetch_file($uri) {
    $parsed = parse_url($uri);

    // Check for local file.
    if (empty($parsed['host'])) {
      if (!file_exists($uri)) {
        throw new Exception(t('Local file not found.'));
      }
      return file_get_contents($uri);
    }
    $destination = 'public://views_xml_backend';
    if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
      throw new Exception(t('Files directory either cannot be created or is not writable.'));
    }
    $headers = array();
    $cache_file = 'views_xml_backend_' . md5($uri);
    if ($cache = cache_get($cache_file)) {
      $last_headers = $cache->data;
      if (!empty($last_headers['etag'])) {
        $headers['If-None-Match'] = $last_headers['etag'];
      }
      if (!empty($last_headers['last-modified'])) {
        $headers['If-Modified-Since'] = $last_headers['last-modified'];
      }
    }
    if (empty($headers['Accept'])) {

      // Tell the sever we're looking for XML.
      $headers['Accept'] = 'application/xml';
    }
    $result = drupal_http_request($uri, array(
      'headers' => $headers,
    ));
    if (isset($result->error)) {
      if ($this->options['show_errors']) {
        $message = format_string('HTTP response: %error. URI: %uri', array(
          '%error' => $result->error,
          '%uri' => $uri,
        ));
        throw new Exception($message);
      }
      return;
    }
    $cache_file_uri = "{$destination}/{$cache_file}";
    if ($result->code == 304) {
      if (file_exists($cache_file_uri)) {
        return file_get_contents($cache_file_uri);
      }

      // We have the headers but no cache file. :(
      // Run it back.
      cache_clear_all($cache_file, 'cache');
      return $this
        ->fetch_file($uri);
    }

    // Cache data to a file.
    file_unmanaged_save_data($result->data, $cache_file_uri, FILE_EXISTS_REPLACE);
    cache_set($cache_file, $result->headers);
    return $result->data;
  }
  public function execute(&$view) {
    $start = microtime(TRUE);

    // Avoid notices about $view->execute_time being undefined if the query
    // doesn't finish.
    $view->execute_time = NULL;

    // Make sure that an xml file exists. This could happen if you come from the
    // add wizard to the actual views edit page.
    if (empty($this->options['xml_file'])) {
      return FALSE;
    }
    $data = new stdClass();
    try {

      // Do token replacement. At this point, only argument values are
      // available.
      $path = $this
        ->replace_arguments($view, $this->options['xml_file']);
      $data->contents = $this
        ->fetch_file($path);
    } catch (Exception $e) {
      $cache = $view->display_handler
        ->get_plugin('cache')
        ->cache_flush();
      drupal_set_message(t('Views XML Backend: ' . $e
        ->getMessage()), 'error');
      return;
    }

    // Allow other modules to alter the data. Could be used for adding Tidy
    // support.
    // @todo Document this.
    drupal_alter('views_xml_backend_data', $data, $view->name);

    // When content is empty, parsing it is pointless.
    if (!$data->contents) {
      if ($this->options['show_errors']) {
        drupal_set_message(t('Views XML Backend: File is empty.'), 'warning');
      }
      return;
    }
    $use = $this
      ->errorStart();

    // Go!
    $this
      ->parse($view, $data);
    $view->execute_time = microtime(TRUE) - $start;
    $this
      ->errorStop($use, $this->options['show_errors']);
  }
  public function replace_arguments($view, $string) {
    if (!empty($view->build_info['substitutions'])) {
      return strtr($string, $view->build_info['substitutions']);
    }
    return $string;
  }
  public function parse(&$view, $data) {

    // The appropriate error messages will be displayed automatically.
    if (!($doc = $this
      ->get_dom_document($data->contents))) {
      return;
    }
    $xpath = new DOMXPath($doc);

    // Create a simplexml object so that we can use
    // SimpleXMLElement::getNamespaces().
    // Does anyone know a better way to do this?
    $simple = simplexml_import_dom($doc);
    if (!$simple) {
      return;
    }
    $namespaces = $simple
      ->getNamespaces(TRUE);

    // Register namespaces. Allow for overriding the default namespace.
    foreach ($namespaces as $prefix => $namespace) {
      if ($prefix === '') {
        if (empty($this->options['default_namespace'])) {
          $prefix = 'default';
        }
        else {
          $prefix = $this->options['default_namespace'];
        }
      }
      $xpath
        ->registerNamespace($prefix, $namespace);
    }
    try {
      if ($this->pager
        ->use_count_query() || !empty($view->get_total_rows)) {

        // $this->pager->execute_count_query($count_query);
        // Hackish execute_count_query implementation.
        $this->pager->total_items = $xpath
          ->evaluate($view->build_info['count_query']);
        if (!empty($this->pager->options['offset'])) {
          $this->pager->total_items -= $this->pager->options['offset'];
        }
        $this->pager
          ->update_page_info();
      }

      // Let the pager modify the query to add limits.
      $this->pager
        ->pre_execute($view->build_info['query']);

      // Used to keep track of query misses when previewing.
      $in_preview = !empty($view->live_preview);

      // Get the rows.
      $rows = $xpath
        ->query($view->build_info['query']);
      $result = array();
      $duds = array();
      foreach ($rows as $row) {
        $item = new stdClass();

        // Query each field per row.
        foreach ($this->fields as $field) {
          $field_key = $field['field'];
          if (!$field_key) {
            continue;
          }
          $node_list = $xpath
            ->evaluate($field_key, $row);

          // It can happen that a field is not present, but the path is valid.
          // In that case, the evaluate function returns an empty list.
          if ($node_list && $node_list->length > 0) {

            // Allow multiple values in a field.
            if (!empty($field['multiple'])) {
              $item->{$field_key} = array();
              foreach ($node_list as $node) {
                $item->{$field_key}[] = $node->nodeValue;
              }
            }
            else {
              $item->{$field_key} = $node_list
                ->item(0)->nodeValue;
            }
          }
          else {

            // Make sure all of the fields are set. Allows us to do less error
            // checking later on.
            $item->{$field_key} = NULL;

            // Track empty results during preview for later assistance.
            if ($in_preview) {
              if (!isset($duds[$field_key])) {
                $duds[$field_key] = 0;
              }
              $duds[$field_key]++;
            }
          }
        }
        $result[] = $item;
      }

      // Give some feedback if we are previewing and a field is not returning
      // any values.
      if ($in_preview) {
        $count = count($result);
        foreach ($duds as $field_key => $empty_count) {
          if ($count == $empty_count) {
            drupal_set_message(t('Field %field never returned a valid result.', array(
              '%field' => $field_key,
            )), 'warning');
          }
        }
      }
      if (!empty($this->orderby)) {

        // Array reverse, because the most specific are first.
        foreach (array_reverse($this->orderby) as $orderby) {
          if ($orderby == 'rand') {
            shuffle($result);
          }
          else {
            $orderby
              ->sort($result);
          }
        }
      }
      if (!empty($this->limit) || !empty($this->offset)) {
        $result = array_slice($result, $this->offset, $this->limit, TRUE);
      }
      $view->result = $result;
      $view->total_rows = count($result);
      $this->pager
        ->post_execute($view->result);
    } catch (Exception $e) {
      $view->result = array();
      if (!empty($view->live_preview)) {
        drupal_set_message(time());
        drupal_set_message($e
          ->getMessage(), 'error');
      }
      else {
        debug($e
          ->getMessage(), 'Views XML Backend');
      }
    }
  }

  /**
   * Returns the DOM document to use for parsing.
   *
   * This is easily overridable so that subclasses can change the method of
   * loading. For example, using DOMDocument::loadHTML(), or html5-php's
   * HTML::loadHTML().
   *
   * @param string $content
   *   The content to load in the DOM.
   *
   * @return DOMDocument|NULL
   *   A new DOM document, or NULL on failure.
   */
  public function get_dom_document($content) {
    $doc = new DOMDocument();
    if (!($success = $doc
      ->loadXML($content))) {
      return;
    }
    return $doc;
  }
  public function add_signature(&$view) {
  }
  public function option_definition() {
    $options = parent::option_definition();
    $options['xml_file'] = array(
      'default' => '',
    );
    $options['row_xpath'] = array(
      'default' => '',
    );
    $options['default_namespace'] = array(
      'default' => '',
    );
    $options['show_errors'] = array(
      'default' => TRUE,
    );
    return $options;
  }
  public function options_form(&$form, &$form_state) {
    $form['xml_file'] = array(
      '#type' => 'textfield',
      '#title' => t('XML File'),
      '#default_value' => $this->options['xml_file'],
      '#description' => t('The URL or path to the XML file.'),
      '#maxlength' => 1024,
    );
    $form['row_xpath'] = array(
      '#type' => 'textfield',
      '#title' => t('Row Xpath'),
      '#default_value' => $this->options['row_xpath'],
      '#description' => t('An xpath function that selects rows.'),
      '#required' => TRUE,
    );
    $form['default_namespace'] = array(
      '#type' => 'textfield',
      '#title' => t('Default namespace'),
      '#default_value' => $this->options['default_namespace'],
      '#description' => t("If the xml contains a default namespace, it will be accessible as 'default:element'. If you want something different, declare it here."),
      '#required' => FALSE,
    );
    $form['show_errors'] = array(
      '#type' => 'checkbox',
      '#title' => t('Show errors'),
      '#default_value' => $this->options['show_errors'],
      '#description' => t('If there were any errors during XML parsing or downloading the file, display them. It is recommended to leave this on during development.'),
      '#required' => FALSE,
    );
  }
  public function add_field($table, $field, $alias = '', $params = array()) {
    $alias = $field;

    // Add field info array.
    if (empty($this->fields[$field])) {
      $this->fields[$field] = array(
        'field' => $field,
        'table' => $table,
        'alias' => $alias,
      ) + $params;
    }
    return $field;
  }
  public function add_orderby($orderby) {
    $this->orderby[] = $orderby;
  }
  public function add_filter($filter) {
    $this->filter[$filter->options['group']][] = $filter;
  }
  public function add_argument($argument) {
    $this->argument[] = $argument;
  }

  /**
   * Returns info to base the uniqueness of the result on.
   *
   * @return array $cache_info
   *   Array with query unique data.
   */
  public function get_cache_info() {
    return array(
      'xml_file' => $this->options['xml_file'],
      'row_xpath' => $this->options['row_xpath'],
      'default_namespace' => $this->options['default_namespace'],
    );
  }

  /**
   * Starts custom error handling.
   *
   * @return bool
   *   The previous value of use_errors.
   */
  protected function errorStart() {
    return libxml_use_internal_errors(TRUE);
  }

  /**
   * Stops custom error handling.
   *
   * @param bool $use
   *   The previous value of use_errors.
   * @param bool $print
   *   (Optional) Whether to print errors to the screen. Defaults to TRUE.
   */
  protected function errorStop($use, $print = TRUE) {
    if ($print) {
      foreach (libxml_get_errors() as $error) {
        switch ($error->level) {
          case LIBXML_ERR_WARNING:
          case LIBXML_ERR_ERROR:
            $type = 'warning';
            break;
          case LIBXML_ERR_FATAL:
            $type = 'error';
            break;
        }
        $message = t('%error on line %num. Error code: %code', array(
          '%error' => trim($error->message),
          '%num' => $error->line,
          '%code' => $error->code,
        ));
        drupal_set_message($message, $type, FALSE);
      }
    }
    libxml_clear_errors();
    libxml_use_internal_errors($use);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
views_object::$definition public property Handler's definition.
views_object::altered_option_definition function Collect this handler's option definition and alter them, ready for use.
views_object::construct public function Views handlers use a special construct function. 4
views_object::destroy public function Destructor. 2
views_object::export_option public function 1
views_object::export_options public function
views_object::export_option_always public function Always exports the option, regardless of the default value.
views_object::options Deprecated public function Set default options on this object. 1
views_object::set_default_options public function Set default options.
views_object::set_definition public function Let the handler know what its full definition is.
views_object::unpack_options public function Unpack options over our existing defaults, drilling down into arrays so that defaults don't get totally blown away.
views_object::unpack_translatable public function Unpack a single option definition.
views_object::unpack_translatables public function Unpacks each handler to store translatable texts.
views_object::_set_option_defaults public function
views_plugin::$display public property The current used views display.
views_plugin::$plugin_name public property The plugin name of this plugin, for example table or full.
views_plugin::$plugin_type public property The plugin type of this plugin, for example style or query.
views_plugin::$view public property The top object of a view. Overrides views_object::$view 1
views_plugin::additional_theme_functions public function Provide a list of additional theme functions for the theme info page.
views_plugin::plugin_title public function Return the human readable name of the display.
views_plugin::theme_functions public function Provide a full list of possible theme templates used by this style.
views_plugin::validate public function Validate that the plugin is correct and can be saved. 3
views_plugin_query::alter public function Let modules modify the query just prior to finalizing it. 1
views_plugin_query::get_aggregation_info public function Get aggregation info for group by queries. 1
views_plugin_query::get_result_entities public function Returns the according entity objects for the given query results. 1
views_plugin_query::init public function Constructor; Create the basic query object and fill with default values. 1
views_plugin_query::options_submit public function Handle any special handling on the validate form. Overrides views_plugin::options_submit 1
views_plugin_query::options_validate public function Validate the options form. Overrides views_plugin::options_validate
views_plugin_query::render_pager public function Render the pager, if necessary.
views_plugin_query::set_group_operator public function Control how all WHERE and HAVING groups are put together.
views_plugin_query::set_limit public function Set a LIMIT on the query, specifying a maximum number of results.
views_plugin_query::set_offset public function Set an OFFSET on the query, specifying a number of results to skip
views_plugin_query::set_where_group public function Create a new grouping for the WHERE or HAVING clause.
views_plugin_query::summary_title public function Returns the summary of the settings in the display. Overrides views_plugin::summary_title
views_xml_backend_plugin_query_xml::$argument public property
views_xml_backend_plugin_query_xml::$fields public property
views_xml_backend_plugin_query_xml::$filter public property Document and initialize the attributes we use.
views_xml_backend_plugin_query_xml::$group_operator public property
views_xml_backend_plugin_query_xml::$limit public property
views_xml_backend_plugin_query_xml::$offset public property
views_xml_backend_plugin_query_xml::$options public property Except for displays, options for the object will be held here. Overrides views_object::$options
views_xml_backend_plugin_query_xml::$orderby public property
views_xml_backend_plugin_query_xml::$pager public property A pager plugin that should be provided by the display. Overrides views_plugin_query::$pager
views_xml_backend_plugin_query_xml::$where public property
views_xml_backend_plugin_query_xml::add_argument public function
views_xml_backend_plugin_query_xml::add_field public function
views_xml_backend_plugin_query_xml::add_filter public function
views_xml_backend_plugin_query_xml::add_orderby public function
views_xml_backend_plugin_query_xml::add_signature public function Add a signature to the query, if such a thing is feasible. Overrides views_plugin_query::add_signature
views_xml_backend_plugin_query_xml::build public function Builds the necessary info to execute the query. Overrides views_plugin_query::build
views_xml_backend_plugin_query_xml::errorStart protected function Starts custom error handling.
views_xml_backend_plugin_query_xml::errorStop protected function Stops custom error handling.
views_xml_backend_plugin_query_xml::execute public function Executes the query and fills the associated view object with according values. Overrides views_plugin_query::execute
views_xml_backend_plugin_query_xml::fetch_file public function
views_xml_backend_plugin_query_xml::get_cache_info public function Returns info to base the uniqueness of the result on.
views_xml_backend_plugin_query_xml::get_dom_document public function Returns the DOM document to use for parsing.
views_xml_backend_plugin_query_xml::options_form public function Add settings for the ui. Overrides views_plugin_query::options_form
views_xml_backend_plugin_query_xml::option_definition public function Information about options for all kinds of purposes will be held here. Overrides views_object::option_definition
views_xml_backend_plugin_query_xml::parse public function
views_xml_backend_plugin_query_xml::query public function Generates a query and a countquery from all of the information supplied to the object. Overrides views_plugin_query::query
views_xml_backend_plugin_query_xml::replace_arguments public function