PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 3.10.2
Visualizer – Tables & Charts Manager with Built-in AI Generator v3.10.2
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 3.10.4 All 148 releases
visualizer / classes / Visualizer / Source / Json.php

Json.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 3.10.2, at classes/Visualizer/Source/Json.php

504 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // +----------------------------------------------------------------------+
4 // | Copyright 2013 Madpixels (email : visualizer@madpixels.net) |
5 // +----------------------------------------------------------------------+
6 // | This program is free software; you can redistribute it and/or modify |
7 // | it under the terms of the GNU General Public License, version 2, as |
8 // | published by the Free Software Foundation. |
9 // | |
10 // | This program is distributed in the hope that it will be useful, |
11 // | but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 // | GNU General Public License for more details. |
14 // | |
15 // | You should have received a copy of the GNU General Public License |
16 // | along with this program; if not, write to the Free Software |
17 // | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, |
18 // | MA 02110-1301 USA |
19 // +----------------------------------------------------------------------+
20 // | Author: Eugene Manuilov <eugene@manuilov.org> |
21 // +----------------------------------------------------------------------+
22 /**
23 * Source manager for JSON URLs.
24 *
25 * @category Visualizer
26 * @package Source
27 *
28 * @since 1.0.0
29 */
30 class Visualizer_Source_Json extends Visualizer_Source {
31
32 /**
33 * The url to the data.
34 *
35 * @since 1.0.0
36 *
37 * @access protected
38 * @var string
39 */
40 protected $_url;
41
42 /**
43 * The root to the data.
44 *
45 * @since 1.0.0
46 *
47 * @access protected
48 * @var string
49 */
50 protected $_root;
51
52 /**
53 * The paging element.
54 *
55 * @since 1.0.0
56 *
57 * @access protected
58 * @var string
59 */
60 protected $_paging;
61
62 /**
63 * The headers.
64 *
65 * @since ?
66 *
67 * @access protected
68 * @var array
69 */
70 protected $_headers;
71
72 /**
73 * The other headers.
74 *
75 * @since 3.4.11
76 *
77 * @access protected
78 * @var array
79 */
80 protected $_additional_headers;
81
82 /**
83 * The array that contains the definition of the data.
84 *
85 * @since 1.0.0
86 *
87 * @access protected
88 * @var array
89 */
90 protected $_args;
91
92 const TAG_SEPARATOR = '>';
93 const TAG_SEPARATOR_VIEW = ' &#x27A4; ';
94
95 /**
96 * Constructor.
97 *
98 * @since 1.0.0
99 *
100 * @access public
101 * @param array $params The array that contains the definition of the data.
102 */
103 public function __construct( $params = null ) {
104 $this->_args = $params;
105 if ( isset( $this->_args['url'] ) ) {
106 $this->_url = trim( $this->_args['url'] );
107 }
108 if ( isset( $this->_args['root'] ) ) {
109 $this->_root = trim( $this->_args['root'] );
110 }
111 if ( isset( $this->_args['paging'] ) ) {
112 $this->_paging = trim( $this->_args['paging'] );
113 }
114 if ( isset( $this->_args['auth'] ) && ! empty( $this->_args['auth'] ) ) {
115 $this->_headers = array( 'auth' => $this->_args['auth'] );
116 } elseif ( isset( $this->_args['username'] ) && isset( $this->_args['password'] ) ) {
117 $this->_headers = array( 'username' => $this->_args['username'], 'password' => $this->_args['password'] );
118 }
119 if ( isset( $this->_args['method'] ) ) {
120 $this->_headers['method'] = $this->_args['method'];
121 }
122 if ( isset( $this->_args['additional_headers'] ) ) {
123 $this->_additional_headers = $this->_args['additional_headers'];
124 }
125 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Constructor called for params = %s', print_r( $params, true ) ), 'debug', __FILE__, __LINE__ );
126 }
127
128 /**
129 * Get the root elements for JSON-endpoint.
130 *
131 * @since ?
132 *
133 * @access public
134 */
135 public function fetchRoots() {
136 $roots = apply_filters( 'visualizer_json_get_root_elements', false, $this->_url );
137 if ( false !== $roots ) {
138 return $roots;
139 }
140 $roots = $this->getRootElements( 'root', '', array(), $this->getJSON() );
141 if ( is_null( $roots ) ) {
142 $this->_error = esc_html__( 'This does not appear to be a valid JSON feed. Please try again.', 'visualizer' );
143 return false;
144 }
145 return $roots;
146 }
147
148 /**
149 * Get the name of the elements that are likely to contain the paginated URLs.
150 *
151 * @since ?
152 *
153 * @access public
154 */
155 public function getPaginationElements() {
156 $pages = array();
157 $root = explode( self::TAG_SEPARATOR, $this->_root );
158 // the pagination element is usually one level above the root element we are going to use
159 // e.g. if the data is in root > results, the pagination element (let's call it "next") would be root > next and not root > results > next.
160 array_pop( $root );
161
162 // base of the next element.
163 $base = implode( self::TAG_SEPARATOR, $root );
164 // get rid of the first element as that is the faux root element indicator
165 array_shift( $root );
166
167 $array = $this->getJSON();
168 if ( is_null( $array ) ) {
169 return $pages;
170 }
171
172 $leaf = $array;
173 if ( ! empty( $root ) ) {
174 foreach ( $root as $tag ) {
175 if ( array_key_exists( $tag, $leaf ) ) {
176 $leaf = $array[ $tag ];
177 } else {
178 // if the tag does not exist, we assume it is present in the 0th element of the current array.
179 $leaf = $leaf[0][ $tag ];
180 }
181 }
182 }
183
184 $paging = array();
185 foreach ( $leaf as $key => $value ) {
186 // the paging element's value will most probably contain the url of the feed.
187 if ( ! is_string( $value ) ) {
188 continue;
189 }
190
191 // strip the url off the request parameters e.g. format=json as sometimes the pagination urls may not contain them.
192 $url = wp_parse_url( $value );
193 if ( 0 === stripos( $value, $this->_url ) || false !== stripos( $this->_url, $url['path'] ) ) {
194 $paging[] = $key;
195 }
196 }
197
198 foreach ( array_filter( array_unique( $paging ) ) as $page ) {
199 $pages[] = $base . self::TAG_SEPARATOR . $page;
200 }
201 return $pages;
202 }
203
204 /**
205 * Parse the JSON-endpoint from the chosen root as the base.
206 *
207 * @since ?
208 *
209 * @access public
210 */
211 public function fetch() {
212 $url = $this->_url;
213 $data = array();
214 $page = 1;
215
216 while ( ! is_null( $url ) && $page++ < apply_filters( 'visualizer_json_fetch_pages', 5, $this->_url ) ) {
217 $array = $this->getJSON( $url );
218 if ( is_null( $array ) ) {
219 break;
220 }
221
222 $root = explode( self::TAG_SEPARATOR, $this->_root );
223 if ( count( $root ) > 1 ) {
224 // get rid of the first element as that is the faux root element indicator
225 array_shift( $root );
226 }
227 $leaf = $array;
228 foreach ( $root as $tag ) {
229 if ( array_key_exists( $tag, $leaf ) ) {
230 $leaf = $leaf[ $tag ];
231 } else {
232 // if the tag does not exist, we assume it is present in every element of the current array.
233 $temp = array();
234 for ( $depth = 0; $depth < count( $leaf ); $depth++ ) {
235 if ( ! isset( $leaf[ $depth ][ $tag ] ) ) {
236 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Element %s not present at depth %d in %d elements. Ignoring.', $tag, $depth, count( $leaf ) ), 'debug', __FILE__, __LINE__ );
237 continue;
238 }
239 $temp[] = $leaf[ $depth ][ $tag ];
240 }
241 $leaf = $temp;
242 }
243 }
244
245 // JSONs that do not have a root element may fall into this condition e.g. /wp-json/wp/v2/posts
246 if ( empty( $leaf ) ) {
247 $leaf = $array;
248 }
249
250 // now that we have got the final array we need to operate on, we will use this as the collection of series.
251 // lets check if the series is a flat-series e.g. https://api.exchangeratesapi.io/latest
252 // in this, all values of `$leaf` would be a string, not an array.
253 $values = array_values( $leaf );
254 if ( ! is_array( $values[0] ) ) {
255 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Looks like a flat-series = %s', print_r( $leaf, true ) ), 'debug', __FILE__, __LINE__ );
256 $inner_data = array();
257 foreach ( $leaf as $datum => $value ) {
258 $inner_data[ $datum ] = $value;
259 }
260 $data[] = $inner_data;
261 } else {
262 $row_num = 0;
263 // we will filter out all elements of this array that have array as a value.
264 foreach ( $leaf as $datum ) {
265 $inner_data = array();
266 foreach ( $datum as $key => $value ) {
267 if ( is_array( $value ) ) {
268 continue;
269 }
270 $inner_data[ $key ] = $value;
271 }
272 // if we want to exclude entire rows on the basis of some data/key or row index.
273 if ( apply_filters( 'visualizer_json_include_row', true, $inner_data, $this->_root, $this->_url, ++$row_num ) ) {
274 $data[] = $inner_data;
275 }
276 }
277 }
278
279 $data = $this->collectCommonElements( $data );
280
281 $url = $this->getNextPage( $array );
282 }
283
284 $this->_data = $data;
285
286 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Parsed data endpoint %s with root %s is %s', $this->_url, $this->_root, print_r( $data, true ) ), 'debug', __FILE__, __LINE__ );
287
288 return true;
289 }
290
291 /**
292 * Determines which keys are common to all the data and returns an array with only those elements.
293 * This is to ensure that the data set displayed on the table is consistent.
294 * Inconsistent data causes the table to not display.
295 *
296 * @since ?
297 *
298 * @access private
299 */
300 private function collectCommonElements( $data ) {
301 $keys = array();
302 foreach ( $data as $datum ) {
303 if ( empty( $keys ) ) {
304 $keys = array_keys( $datum );
305 continue;
306 }
307 $keys = array_intersect( $keys, array_keys( $datum ) );
308 }
309
310 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Extracting data only for the common keys = %s', print_r( $keys, true ) ), 'debug', __FILE__, __LINE__ );
311
312 $rows = array();
313 foreach ( $data as $datum ) {
314 $row = array();
315 foreach ( $datum as $key => $value ) {
316 if ( in_array( $key, $keys, true ) ) {
317 $row[ $key ] = $value;
318 }
319 }
320 $rows[] = $row;
321 }
322
323 return $rows;
324
325 }
326
327 /**
328 * Get the next page URL.
329 *
330 * @since ?
331 *
332 * @access private
333 */
334 private function getNextPage( $array ) {
335 if ( empty( $this->_paging ) ) {
336 return null;
337 }
338
339 $root = explode( self::TAG_SEPARATOR, $this->_paging );
340 // get rid of the first element as that is the faux root element indicator
341 array_shift( $root );
342 $leaf = $array;
343 foreach ( $root as $tag ) {
344 if ( array_key_exists( $tag, $leaf ) ) {
345 $leaf = $array[ $tag ];
346 } else {
347 // if the tag does not exist, we assume it is present in the 0th element of the current array.
348 // TODO: we may want to change this to a filter later.
349 $leaf = $leaf[0][ $tag ];
350 }
351 }
352
353 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Got next page as %s for paging element %s', $leaf, $this->_paging ), 'debug', __FILE__, __LINE__ );
354
355 return $leaf;
356 }
357
358 /**
359 * Get the root elements for JSON-endpoint.
360 *
361 * @since ?
362 *
363 * @access private
364 */
365 private function getRootElements( $parent, $now, $root, $array ) {
366 if ( is_null( $array ) ) {
367 return null;
368 }
369
370 $root[] = $parent;
371 foreach ( $array as $key => $value ) {
372 if ( is_array( $value ) && ! empty( $value ) ) {
373 if ( ! is_numeric( $key ) ) {
374 $now = sprintf( '%s%s%s', $parent, self::TAG_SEPARATOR, $key );
375 $root[] = $now;
376 } else {
377 $now = $parent;
378 }
379 $root = $this->getRootElements( $now, $key, $root, $value );
380 }
381 }
382 $roots = array_unique( $root );
383 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Roots found for %s = %s', $this->_url, print_r( $roots, true ) ), 'debug', __FILE__, __LINE__ );
384 return $roots;
385 }
386
387 /**
388 * Get the JSON for the JSON-endpoint.
389 *
390 * @since ?
391 *
392 * @access private
393 */
394 private function getJSON( $url = null ) {
395 if ( is_null( $url ) ) {
396 $url = $this->_url;
397 }
398
399 $response = $this->connect( $url );
400 if ( is_wp_error( $response ) || ! in_array( intval( $response['response']['code'] ), array( 200, 201 ), true ) ) {
401 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Error while fetching JSON endpoint %s = %s', $url, print_r( $response, true ) ), 'error', __FILE__, __LINE__ );
402 return null;
403 }
404
405 // this filter can be used to rearrange columns e.g. in candlestick charts
406 // or to add additional data to the root.
407 $bom = pack( 'H*', 'EFBBBF' );
408 $response_body = wp_remote_retrieve_body( $response );
409 $response_body = preg_replace( "/^$bom/", '', $response_body );
410 $array = apply_filters( 'visualizer_json_massage_data', json_decode( $response_body, true ), $url );
411
412 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'JSON array for the endpoint is %s = ', print_r( $array, true ) ), 'debug', __FILE__, __LINE__ );
413
414 return $array;
415 }
416
417 /**
418 * Sets the authentication/authorization headers and connects to the endpoint.
419 *
420 * @since ?
421 *
422 * @access private
423 */
424 private function connect( $url ) {
425 $args = array( 'method' => 'GET' );
426 if ( ! empty( $this->_headers ) ) {
427 $args = array( 'method' => strtoupper( $this->_headers['method'] ) );
428 if ( array_key_exists( 'auth', $this->_headers ) ) {
429 if ( ! empty( $this->_headers['auth'] ) ) {
430 $header_auth = explode( ':', $this->_headers['auth'] );
431 $header_key = explode( ' ', reset( $header_auth ) );
432 $args['headers'] = array( 'Authorization' => reset( $header_key ) . ' ' . base64_encode( end( $header_key ) . ':' . end( $header_auth ) ) );
433 }
434 } elseif ( array_key_exists( 'username', $this->_headers ) && array_key_exists( 'password', $this->_headers ) && ! empty( $this->_headers['username'] ) && ! empty( $this->_headers['password'] ) ) {
435 $args['headers'] = array( 'Authorization' => 'Basic ' . base64_encode( $this->_headers['username'] . ':' . $this->_headers['password'] ) );
436 }
437 }
438
439 $args = apply_filters( 'visualizer_json_args', $args, $url );
440 if ( ! empty( $this->_additional_headers ) ) {
441 $this->_additional_headers = explode( ',', $this->_additional_headers );
442 $this->_additional_headers = array_filter( $this->_additional_headers );
443 if ( ! empty( $this->_additional_headers ) ) {
444 $this->_additional_headers = array_map(
445 function( $headers ) {
446 $headers = explode( ':', $headers );
447 $headers = array_map( 'trim', $headers );
448 return $headers;
449 },
450 $this->_additional_headers
451 );
452 foreach ( $this->_additional_headers as $additional_headers ) {
453 $header_key = reset( $additional_headers );
454 $header_value = end( $additional_headers );
455 $args['headers'][ $header_key ] = $header_value;
456 }
457 }
458 }
459
460 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Connecting to %s with args = %s ', $url, print_r( $args, true ) ), 'debug', __FILE__, __LINE__ );
461 return wp_remote_request( $url, $args );
462 }
463
464 /**
465 * Refresh the data for the provided series.
466 *
467 * @since ?
468 *
469 * @access public
470 */
471 public function refresh( $series ) {
472 $this->_series = $series;
473 $this->fetch();
474
475 $headers = wp_list_pluck( $this->_series, 'label' );
476 $data = array();
477 foreach ( $this->_data as $line ) {
478 $data_row = array();
479 foreach ( $line as $header => $value ) {
480 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
481 if ( in_array( $header, $headers ) ) {
482 $data_row[] = $value;
483 }
484 }
485 $data[] = $this->_normalizeData( $data_row );
486 }
487 $this->_data = $data;
488 return true;
489 }
490
491 /**
492 * Returns source name.
493 *
494 * @since 1.0.0
495 *
496 * @access public
497 * @return string The name of source.
498 */
499 public function getSourceName() {
500 return __CLASS__;
501 }
502
503 }
504