diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 0000000000000000000000000000000000000000..623863d19a5bd724e1bda548d52bb633d31b71a8 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,20 @@ +{ + "version": "0.2", + "language": "en", + "ignoreWords": [ + "Apath", + "Holovachek", + "Pradeep", + "Venugopal", + "Viktor", + "adipiscing", + "amet", + "apath", + "clicksorter", + "countquery", + "elit", + "firstname", + "lastname", + "venugopp" + ] +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..3c39db76a058e974fb032a404b993489262ef726 --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "drupal/views_json_source", + "description": "Views Json Source is an extension of views module to work with external JSON data.", + "type": "drupal-module", + "license": "GPL-2.0-or-later", + "keywords": ["Drupal"], + "homepage": "https://www.drupal.org/project/views_json_source", + "authors": [ + { + "name": "Pradeep Venugopal (venugopp)", + "homepage": "https://www.drupal.org/u/venugopp", + "role": "Maintainer" + }, + { + "name": "Viktor Holovachek (AstonVictor)", + "homepage": "https://www.drupal.org/u/astonvictor", + "role": "Maintainer" + } + ], + "minimum-stability": "dev", + "support": { + "issues": "https://www.drupal.org/project/issues/views_json_source", + "source": "https://git.drupalcode.org/project/views_json_source" + }, + "require": { } +} diff --git a/config/optional/views.view.views_json_source.yml b/config/optional/views.view.views_json_source.yml index 77d81030ab4999ed1d50b489cd397331f8936194..d43e5dc9714454a132cb616de28c019049a40388 100644 --- a/config/optional/views.view.views_json_source.yml +++ b/config/optional/views.view.views_json_source.yml @@ -27,6 +27,7 @@ display: type: views_query options: json_file: '[site:url]modules/contrib/views_json_source/data/sample/example1.json' + request_method: 'get' row_apath: data/nodes show_errors: 1 exposed_form: @@ -196,6 +197,7 @@ display: type: views_query options: json_file: '[site:url]modules/contrib/views_json_source/data/sample/example1.json' + request_method: 'get' row_apath: data/nodes headers: '' show_errors: 1 @@ -225,6 +227,7 @@ display: type: views_query options: json_file: '/modules/contrib/views_json_source/data/sample/example2.json' + request_method: 'get' row_apath: data/%/contents headers: '' show_errors: 1 @@ -278,6 +281,7 @@ display: type: views_query options: json_file: '/modules/contrib/views_json_source/data/sample/example3.json' + request_method: 'get' row_apath: nid=2/related headers: '' show_errors: 1 diff --git a/src/Plugin/views/filter/ViewsJsonFilter.php b/src/Plugin/views/filter/ViewsJsonFilter.php index f9ea4a9618a2ebbffd79d52bf1f9cc5bd4d401a2..33f80c764596ae760389ff80adae0a0f400b6877 100644 --- a/src/Plugin/views/filter/ViewsJsonFilter.php +++ b/src/Plugin/views/filter/ViewsJsonFilter.php @@ -231,11 +231,19 @@ class ViewsJsonFilter extends FilterPluginBase { * Generate the filter criteria. */ public function generate() { + $options = $this->options; + $operator = $this->options['operator']; - $key = $this->options['key']; - $value = !empty($this->value) && $this->isExposed() ? reset($this->value) : $this->options['value']; + if ($options['exposed'] && $options['expose']['use_operator']) { + $operator = $this->operator; + } + + $value = $this->options['value']; + if ($options['exposed'] && !empty($this->value)) { + $value = $options['expose']['multiple'] ? $this->value : reset($this->value); + } - return [$key, $operator, $value]; + return !empty($value) ? [$this->options['key'], $operator, $value] : []; } } diff --git a/src/Plugin/views/query/ViewsJsonQuery.php b/src/Plugin/views/query/ViewsJsonQuery.php index dd790a6c02f7079fccc4fb5b54d9d3fcfb5e11b7..ac107274ab24d0831aeb596c9be675edc65ed72e 100755 --- a/src/Plugin/views/query/ViewsJsonQuery.php +++ b/src/Plugin/views/query/ViewsJsonQuery.php @@ -3,7 +3,9 @@ namespace Drupal\views_json_source\Plugin\views\query; use Drupal\Component\Datetime\TimeInterface; -use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher; +use Drupal\Component\Serialization\Json; +use Drupal\Component\Transliteration\TransliterationInterface; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\FormStateInterface; @@ -15,6 +17,7 @@ use Drupal\views_json_source\Event\PreCacheEvent; use GuzzleHttp\ClientInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Base query handler for views_json_source. @@ -58,7 +61,7 @@ class ViewsJsonQuery extends QueryPluginBase { /** * The event dispatcher. * - * @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */ protected $eventDispatcher; @@ -121,6 +124,13 @@ class ViewsJsonQuery extends QueryPluginBase { */ public $filter = []; + /** + * Transliteration service. + * + * @var \Drupal\Component\Transliteration\TransliterationInterface + */ + protected $transliteration; + /** * {@inheritdoc} */ @@ -206,9 +216,13 @@ class ViewsJsonQuery extends QueryPluginBase { else { // Add the request headers if available. $headers = $this->options['headers'] - ? json_decode($this->options['headers'], TRUE) ?? [] + ? Json::decode($this->options['headers']) ?? [] : []; + foreach ($headers as $key => $value) { + $headers[$key] = $this->token->replacePlain($value); + } + $result = $this->getRequestResponse($uri, $headers); if (isset($result->error)) { $args = ['%error' => $result->error, '%uri' => $uri]; @@ -359,6 +373,10 @@ class ViewsJsonQuery extends QueryPluginBase { * Define ops for using in filter. */ public function ops($op, $l, $r) { + // Transliterate values before comparison. + $l = $this->transliterateValue($l); + $r = $this->transliterateValue($r); + $table = [ '=' => function ($l, $r) { return $l == $r; @@ -367,44 +385,51 @@ class ViewsJsonQuery extends QueryPluginBase { return $l != $r; }, 'contains' => function ($l, $r) { - return stripos($l, $r) !== FALSE; + return $l !== NULL && $r !== NULL ? stripos($l, $r) !== FALSE : FALSE; }, 'starts' => function ($l, $r) { - return strpos($l, $r) === 0; + return $l !== NULL && $r !== NULL ? strpos($l, $r) === 0 : FALSE; }, 'not_starts' => function ($l, $r) { - return strpos($l, $r) !== 0; + return $l !== NULL && $r !== NULL ? strpos($l, $r) !== 0 : FALSE; }, 'ends' => function ($l, $r) { - $len = strlen($r); + $len = $l !== NULL && $r !== NULL ? strlen($r) : 0; return $len > 0 ? substr($l, -$len) === $r : TRUE; }, 'not_ends' => function ($l, $r) { - $len = strlen($r); + $len = $l !== NULL && $r !== NULL ? strlen($r) : 0; return $len > 0 ? substr($l, -$len) !== $r : TRUE; }, 'not' => function ($l, $r) { - return stripos($l, $r) === FALSE; + return $l !== NULL && $r !== NULL ? stripos($l, $r) === FALSE : FALSE; }, 'shorterthan' => function ($l, $r) { - return strlen($l) < $r; + return $l !== NULL ? strlen($l) < $r : FALSE; }, 'longerthan' => function ($l, $r) { - return strlen($l) > $r; + return $l !== NULL ? strlen($l) > $r : FALSE; }, 'regular_expression' => function ($l, $r) { - return preg_match($r, $l) === 1; + return $l !== NULL && $r !== NULL ? preg_match($r, $l) === 1 : FALSE; }, ]; - return call_user_func_array($table[$op], [$l, $r]); + return array_key_exists($op, $table) ? call_user_func($table[$op], $l, $r) : FALSE; + } + + /** + * Transliterate. + */ + protected function transliterateValue($value) { + return $this->transliteration->transliterate($value); } /** * Parse. */ public function parse(ViewExecutable &$view, $data) { - $ret = json_decode($data->contents, TRUE); + $ret = Json::decode($data->contents); if (!$ret) { return FALSE; } @@ -422,7 +447,8 @@ class ViewsJsonQuery extends QueryPluginBase { foreach ($view->build_info['query'] as $filter) { // Filter only when value is present. if (!empty($filter[0])) { - $l = $row[$filter[0]]; + $filter_keys = explode('/', trim($filter[0], '//')); + $l = NestedArray::getValue($row, $filter_keys); $check = $this->ops($filter[1], $l, $filter[2]); if ($group_conditional_operator === "AND") { // With AND condition. @@ -443,6 +469,9 @@ class ViewsJsonQuery extends QueryPluginBase { } } + // Save the total number of results in the view. + $total_number_results = count($ret); + try { if ($view->pager->useCountQuery() || !empty($view->get_total_rows)) { // Hackish execute_count_query implementation. @@ -487,7 +516,8 @@ class ViewsJsonQuery extends QueryPluginBase { $row->index = $index++; } - $view->total_rows = count($result); + // Pass the total number of the results. + $view->total_rows = $total_number_results; $view->pager->postExecute($view->result); @@ -547,6 +577,8 @@ class ViewsJsonQuery extends QueryPluginBase { $options['json_file'] = ['default' => '']; $options['row_apath'] = ['default' => '']; $options['headers'] = ['default' => '']; + $options['request_method'] = ['default' => 'get']; + $options['request_body'] = ['default' => '']; $options['single_payload'] = ['default' => '']; $options['show_errors'] = ['default' => TRUE]; @@ -578,6 +610,29 @@ class ViewsJsonQuery extends QueryPluginBase { '#description' => $this->t("Headers to be passed for the REST call.<br />Pass the headers as JSON string. Ex:<br /><pre>{"Authorization":"Basic xxxxx","Content-Type":"application/json"}</pre><br />.Here we are passing 2 headers for making the REST API call."), '#required' => FALSE, ]; + $form['request_method'] = [ + '#type' => 'select', + '#title' => $this->t('Request method'), + '#default_value' => $this->options['request_method'], + '#options' => [ + 'get' => $this->t('GET'), + 'post' => $this->t('POST'), + ], + '#description' => $this->t('The request method to the REST call.'), + ]; + $form['request_body'] = [ + '#type' => 'textarea', + '#title' => $this->t('Request body'), + '#default_value' => $this->options['request_body'], + '#states' => [ + 'visible' => [ + 'select[name="query[options][request_method]"]' => [ + 'value' => 'post', + ], + ], + ], + '#description' => $this->t('The POST request body to the REST call.<br/>Pass the form values as JSON string. Ex: <br/><pre>[{"name":"item_key","contents":"item value","headers":{"Content-type":"application/json"}}]</pre> See <a href="https://docs.guzzlephp.org/en/stable/request-options.html#multipart" target="_blank">the documentation for GuzzleHttp multipart request options</a>.'), + ]; $form['single_payload'] = [ '#type' => 'checkbox', '#title' => $this->t('Response contain single node.'), @@ -722,7 +777,23 @@ class ViewsJsonQuery extends QueryPluginBase { * The request response. */ private function getRequestResponse(string $uri, array $headers = []) { - return $this->httpClient->get($uri, ['headers' => $headers]); + $this->options['request_method'] = $this->options['request_method'] ?? 'get'; + switch ($this->options['request_method']) { + case 'post': + $options = []; + $options['headers'] = $headers; + if (!empty($this->options['request_body'])) { + $options['multipart'] = Json::decode($this->options['request_body']); + } + $result = $this->httpClient->post($uri, $options); + break; + + default: + $result = $this->httpClient->get($uri, ['headers' => $headers]); + break; + } + + return $result; } } diff --git a/views_json_source.services.yml b/views_json_source.services.yml index 7657bbed28a1c25b683c5dda0edf4d47b509ca1c..2f2f0e49f445ee232f2afcee05d70d7f430ecd3a 100644 --- a/views_json_source.services.yml +++ b/views_json_source.services.yml @@ -1,4 +1,4 @@ services: logger.channel.views_json_source: parent: logger.channel_base - arguments: [ 'views_json_source' ] \ No newline at end of file + arguments: [ 'views_json_source' ]