View source
<?php
namespace Drupal\views_xml_backend\Plugin\views\query;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Site\Settings;
use Drupal\views\Plugin\views\join\JoinPluginBase;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Drupal\views_xml_backend\Messenger;
use Drupal\views_xml_backend\MessengerInterface;
use Drupal\views_xml_backend\Plugin\views\argument\XmlArgumentInterface;
use Drupal\views_xml_backend\Plugin\views\filter\XmlFilterInterface;
use Drupal\views_xml_backend\Xpath;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\RequestOptions;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Xml extends QueryPluginBase {
const DEFAULT_CACHE_DIR = 'public://views_xml_backend';
public $orderby = [];
protected $arguments = [];
protected $cacheBackend;
protected $extraFields = [];
protected $httpClient;
protected $filters = [];
protected $livePreview = FALSE;
protected $logger;
protected $messenger;
public function __construct(array $configuration, $plugin_id, $plugin_definition, ClientInterface $http_client, CacheBackendInterface $cache_backend, LoggerInterface $logger, MessengerInterface $messenger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->httpClient = $http_client;
$this->cacheBackend = $cache_backend;
$this->logger = $logger;
$this->messenger = $messenger;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container
->get('http_client'), $container
->get('cache.views_xml_backend_download'), $container
->get('logger.factory')
->get('views_xml_backend'), new Messenger());
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['xml_file']['default'] = '';
$options['row_xpath']['default'] = '';
$options['default_namespace']['default'] = 'default';
$options['show_errors']['default'] = TRUE;
return $options;
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['xml_file'] = [
'#type' => 'textfield',
'#title' => $this
->t('XML File'),
'#default_value' => $this->options['xml_file'],
'#description' => $this
->t('The URL or path to the XML file.'),
'#maxlength' => 1024,
'#required' => TRUE,
];
$form['row_xpath'] = [
'#type' => 'textfield',
'#title' => $this
->t('Row Xpath'),
'#default_value' => $this->options['row_xpath'],
'#description' => $this
->t('An xpath function that selects rows.'),
'#maxlength' => 1024,
'#required' => TRUE,
];
$form['default_namespace'] = [
'#type' => 'textfield',
'#title' => $this
->t('Default namespace'),
'#default_value' => $this->options['default_namespace'],
'#description' => $this
->t("If the xml contains a default namespace, it will be accessible as 'default:element'. If you want something different, declare it here."),
];
$form['show_errors'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Show XML errors'),
'#default_value' => $this->options['show_errors'],
'#description' => $this
->t('If there were any errors during XML parsing, display them. It is recommended to leave this on during development.'),
];
}
public function ensureTable($table, $relationship = NULL, JoinPluginBase $join = NULL) {
return $table;
}
public function addArgument(XmlArgumentInterface $argument) {
$this->arguments[] = $argument;
}
public function addField($field, $xpath) {
$this->extraFields[$field] = $xpath;
}
public function addFilter(XmlFilterInterface $filter) {
$this->filters[] = $filter;
}
public function addOrderBy($table, $field = NULL, $order = 'ASC', $alias = '', $params = []) {
if ($table === 'rand') {
$this->orderby[] = 'shuffle';
}
}
public function addSort(callable $callback) {
$this->orderby[] = $callback;
}
public function addWhere($group, $field, $value = NULL, $operator = NULL) {
if ($group && $operator === 'in') {
if (strpos($field, '.') !== FALSE) {
list(, $field) = explode('.', $field);
}
if (!isset($this->view->field[$field])) {
return;
}
$xpath = $this->view->field[$field]->options['xpath_selector'];
$values = [];
foreach ($value as $v) {
$v = Xpath::escapeXpathString($v);
$values[] = "{$xpath} = {$v}";
}
$this->filters[] = implode(' or ', $values);
}
}
public function query($get_count = FALSE) {
$row_xpath = $this->options['row_xpath'];
if ($this->filters) {
$row_xpath .= '[' . implode(' and ', $this->filters) . ']';
}
if ($this->arguments) {
$row_xpath .= '[' . implode(' and ', $this->arguments) . ']';
}
return $row_xpath;
}
public function build(ViewExecutable $view) {
$this->view = $view;
$view
->initPager();
$view->pager
->query();
$view->build_info['query'] = $this
->query();
$view->build_info['count_query'] = '';
$this->livePreview = !empty($view->live_preview);
}
public function execute(ViewExecutable $view) {
if ($view->build_info['query'] === '') {
$this->messenger
->setMessage($this
->t('Please configure the query settings.'), 'warning');
return;
}
$start = microtime(TRUE);
libxml_clear_errors();
$use_errors = libxml_use_internal_errors(TRUE);
$this
->doExecute($view);
if ($this->livePreview && $this->options['show_errors']) {
foreach (libxml_get_errors() as $error) {
$type = $error->level === LIBXML_ERR_FATAL ? 'error' : 'warning';
$args = [
'%error' => trim($error->message),
'%num' => $error->line,
'%code' => $error->code,
];
$this->messenger
->setMessage($this
->t('%error on line %num. Error code: %code', $args), $type);
}
}
libxml_use_internal_errors($use_errors);
libxml_clear_errors();
$view->execute_time = microtime(TRUE) - $start;
}
protected function doExecute(ViewExecutable $view) {
$xpath = $this
->getXpath($this
->fetchFileContents($this
->getXmlDocumentPath($view)));
if ($view->pager
->useCountQuery() || !empty($view->get_total_rows)) {
$view->pager->total_items = $xpath
->query($view->build_info['query'])->length;
$view->pager->total_items -= $view->pager
->getOffset();
}
foreach ($xpath
->query($view->build_info['query']) as $row) {
$result_row = new ResultRow();
$view->result[] = $result_row;
foreach ($view->field as $field_name => $field) {
if (!isset($field->options['xpath_selector']) || $field->options['xpath_selector'] === '') {
continue;
}
$result_row->{$field_name} = $this
->executeRowQuery($xpath, $field->options['xpath_selector'], $row);
}
foreach ($this->extraFields as $field_name => $selector) {
$result_row->{$field_name} = $this
->executeRowQuery($xpath, $selector, $row);
}
}
$this
->executeSorts($view);
if (!empty($this->limit) || !empty($this->offset)) {
$view->result = array_slice($view->result, (int) $this->offset, (int) $this->limit);
}
$this
->reIndexResults($view);
$view->pager
->postExecute($view->result);
$view->pager
->updatePageInfo();
$view->total_rows = $view->pager
->getTotalItems();
}
public function getCacheMaxAge() {
return 0;
}
protected function getXmlDocumentPath(ViewExecutable $view) {
return strtr($this->options['xml_file'], $view
->getDisplay()
->getArgumentsTokens());
}
protected function getXpath($contents) {
if ($contents === '') {
return new \DOMXPath(new \DOMDocument());
}
$xpath = new \DOMXPath($this
->createDomDocument($contents));
$this
->registerNamespaces($xpath);
$xpath
->registerPhpFunctions('views_xml_backend_date');
$xpath
->registerPhpFunctions('views_xml_backend_format_value');
return $xpath;
}
protected function createDomDocument($contents) {
$document = new \DOMDocument();
$document->strictErrorChecking = FALSE;
$document->resolveExternals = FALSE;
$document->substituteEntities = TRUE;
$document->recover = TRUE;
$options = LIBXML_NONET;
if (defined('LIBXML_COMPACT')) {
$options |= LIBXML_COMPACT;
}
if (defined('LIBXML_PARSEHUGE')) {
$options |= LIBXML_PARSEHUGE;
}
if (defined('LIBXML_BIGLINES')) {
$options |= LIBXML_BIGLINES;
}
$disable_entities = libxml_disable_entity_loader(TRUE);
$document
->loadXML($contents, $options);
foreach ($document->childNodes as $child) {
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
if ($this->livePreview) {
$this->messenger
->setMessage($this
->t('A suspicious document was detected.'), 'error');
}
$this->logger
->error('A suspicious document was detected.');
$document = new \DOMDocument();
break;
}
}
libxml_disable_entity_loader($disable_entities);
return $document;
}
protected function fetchFileContents($uri) {
if ($uri === '') {
if ($this->livePreview) {
$this->messenger
->setMessage($this
->t('Please enter a file path or URL in the query settings.'));
}
return '';
}
$parsed = parse_url($uri);
if (empty($parsed['host'])) {
return $this
->fetchLocalFile($uri);
}
return $this
->fetchRemoteFile($uri);
}
protected function fetchLocalFile($uri) {
if (file_exists($uri)) {
return file_get_contents($uri);
}
if ($this->livePreview) {
$this->messenger
->setMessage($this
->t('Local file not found: @uri', [
'@uri' => $uri,
]), 'error');
}
$this->logger
->error('Local file not found: @uri', [
'@uri' => $uri,
]);
return '';
}
protected function fetchRemoteFile($uri) {
$destination = Settings::get('views_xml_backend_cache_directory', static::DEFAULT_CACHE_DIR);
if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
if ($this->livePreview) {
$this->messenger
->setMessage($this
->t('File cache directory either cannot be created or is not writable.'), 'error');
}
$this->logger
->error('File cache directory either cannot be created or is not writable.');
return (string) $this
->doGetRequest($uri)
->getBody();
}
$cache_key = hash('sha256', $uri);
$cache_file = "{$destination}/{$cache_key}";
$options = [];
if ($cache = $this->cacheBackend
->get($cache_key)) {
if (isset($cache->data['etag'])) {
$options[RequestOptions::HEADERS]['If-None-Match'] = $cache->data['etag'];
}
if (isset($cache->data['last-modified'])) {
$options[RequestOptions::HEADERS]['If-Modified-Since'] = $cache->data['last-modified'];
}
}
$response = $this
->doGetRequest($uri, $options);
if ($response
->getStatusCode() === 304) {
if (file_exists($cache_file)) {
return file_get_contents($cache_file);
}
$this->cacheBackend
->delete($cache_key);
return $this
->fetchRemoteFile($uri);
}
if ($response
->getStatusCode() === -100) {
if (file_exists($cache_file)) {
return file_get_contents($cache_file);
}
}
$data = trim($response
->getBody());
file_unmanaged_save_data($data, $cache_file, FILE_EXISTS_REPLACE);
$this->cacheBackend
->set($cache_key, array_change_key_case($response
->getHeaders()));
return $data;
}
protected function doGetRequest($url, $options = []) {
try {
return $this->httpClient
->get($url, $options);
} catch (\RuntimeException $e) {
$args = [
'@url' => $url,
'%message' => $e
->getMessage(),
];
$this->logger
->error('An error occured while downloading @url: %message.', $args);
if ($this->livePreview) {
$this->messenger
->setMessage($this
->t('An error occured while downloading @url: %message.', $args), 'error');
}
}
return new Response(-100, [], '');
}
protected function registerNamespaces(\DOMXPath $xpath) {
$xpath
->registerNamespace('php', 'http://php.net/xpath');
if (!($simple = @simplexml_import_dom($xpath->document))) {
return;
}
foreach ($simple
->getNamespaces(TRUE) as $prefix => $namespace) {
if ($prefix === '') {
$prefix = $this->options['default_namespace'];
}
$xpath
->registerNamespace($prefix, $namespace);
}
}
protected function calculatePager(ViewExecutable $view) {
if (empty($this->limit) && empty($this->offset)) {
return;
}
$limit = intval(!empty($this->limit) ? $this->limit : 999999);
$offset = intval(!empty($this->offset) ? $this->offset : 0);
$limit += $offset;
$view->build_info['query'] .= "[position() > {$offset} and not(position() > {$limit})]";
}
protected function executeRowQuery(\DOMXPath $xpath, $selector, \DOMNode $row) {
$node_list = $xpath
->query($selector, $row);
if ($node_list === FALSE) {
return [];
}
$values = [];
foreach ($node_list as $node) {
$values[] = $node->nodeValue;
}
return $values;
}
protected function executeSorts(ViewExecutable $view) {
foreach (array_reverse($this->orderby) as $sort) {
$this
->reIndexResults($view);
$sort($view->result);
}
}
protected function reIndexResults(ViewExecutable $view) {
$index = 0;
foreach ($view->result as $row) {
$row->index = $index++;
}
}
}