PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.6
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.6
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 / Module.php

Module.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 4.0.6, at classes/Visualizer/Module.php

971 lines 30.3 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 * Base class for all modules. Implements routine methods required by all modules.
24 *
25 * @category Visualizer
26 * @package Module
27 *
28 * @since 1.0.0
29 */
30 class Visualizer_Module {
31
32 /**
33 * The instance of wpdb class.
34 *
35 * @since 1.0.0
36 *
37 * @access protected
38 * @var wpdb
39 */
40 protected $_wpdb = null;
41
42 /**
43 * The plugin instance.
44 *
45 * @since 1.0.0
46 *
47 * @access protected
48 * @var Visualizer_Plugin
49 */
50 protected $_plugin = null;
51
52 /**
53 * Constructor.
54 *
55 * @since 1.0.0
56 * @global wpdb $wpdb Current database connection.
57 *
58 * @access public
59 * @param Visualizer_Plugin $plugin The instance of the plugin.
60 */
61 public function __construct( Visualizer_Plugin $plugin ) {
62 global $wpdb;
63
64 $this->_wpdb = $wpdb;
65 $this->_plugin = $plugin;
66
67 $this->_addFilter( Visualizer_Plugin::FILTER_UNDO_REVISIONS, 'undoRevisions', 10, 2 );
68 $this->_addFilter( Visualizer_Plugin::FILTER_HANDLE_REVISIONS, 'handleExistingRevisions', 10, 2 );
69 $this->_addFilter( Visualizer_Plugin::FILTER_GET_CHART_DATA_AS, 'getDataAs', 10, 3 );
70 $this->_addAction( 'pre_get_posts', 'PreGetPosts' );
71 register_shutdown_function( array( $this, 'onShutdown' ) );
72 }
73
74 /**
75 * Register a shutdown hook to catch fatal errors.
76 *
77 * @since ?
78 *
79 * @access public
80 */
81 public function onShutdown() {
82 $error = error_get_last();
83 if ( $error && $error['type'] === E_ERROR && false !== strpos( $error['file'], 'Visualizer/' ) ) {
84 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Critical error %s', print_r( $error, true ) ), 'error', __FILE__, __LINE__ );
85 }
86 }
87
88 /**
89 * Registers an action hook.
90 *
91 * @since 1.0.0
92 * @uses add_action() To register action hook.
93 *
94 * @access protected
95 * @param string $tag The name of the action to which the $method is hooked.
96 * @param string $method The name of the method to be called.
97 * @param bool $methodClass The root of the method.
98 * @param int $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default: 10). Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
99 * @param int $accepted_args optional. The number of arguments the function accept (default 1).
100 * @return Visualizer_Module
101 */
102 protected function _addAction( $tag, $method, $methodClass = null, $priority = 10, $accepted_args = 1 ) {
103 add_action( $tag, array( $methodClass ? $methodClass : $this, $method ), $priority, $accepted_args );
104 return $this;
105 }
106
107 /**
108 * Registers AJAX action hook.
109 *
110 * @since 1.0.0
111 *
112 * @access public
113 * @param string $tag The name of the AJAX action to which the $method is hooked.
114 * @param string $method Optional. The name of the method to be called. If the name of the method is not provided, tag name will be used as method name.
115 * @param bool $methodClass The root of the method.
116 * @param boolean $logged_in Optional. Determines if we should register hook for logged in users.
117 * @param boolean $logged_out Optional. Determines if we should register hook for not logged in users.
118 * @return Visualizer_Module
119 */
120 protected function _addAjaxAction( $tag, $method = '', $methodClass = null, $logged_in = true, $logged_out = false ) {
121 if ( $logged_in ) {
122 $this->_addAction( 'wp_ajax_' . $tag, $method, $methodClass );
123 }
124
125 if ( $logged_out ) {
126 $this->_addAction( 'wp_ajax_nopriv_' . $tag, $method, $methodClass );
127 }
128
129 return $this;
130 }
131
132 /**
133 * Registers a filter hook.
134 *
135 * @since 1.0.0
136 * @uses add_filter() To register filter hook.
137 *
138 * @access protected
139 * @param string $tag The name of the filter to hook the $method to.
140 * @param string $method The name of the method to be called when the filter is applied.
141 * @param int $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default: 10). Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
142 * @param int $accepted_args optional. The number of arguments the function accept (default 1).
143 * @return Visualizer_Module
144 */
145 protected function _addFilter( $tag, $method, $priority = 10, $accepted_args = 1 ) {
146 add_filter( $tag, array( $this, $method ), $priority, $accepted_args );
147 return $this;
148 }
149
150 /**
151 * Registers a hook for shortcode tag.
152 *
153 * @since 1.0.0
154 * @uses add_shortcode() To register shortcode hook.
155 *
156 * @access protected
157 * @param string $tag Shortcode tag to be searched in post content.
158 * @param string $method Hook to run when shortcode is found.
159 * @return Visualizer_Module
160 */
161 protected function _addShortcode( $tag, $method ) {
162 add_shortcode( $tag, array( $this, $method ) );
163 return $this;
164 }
165
166 /**
167 * A wrapper around the actual function _getDataAs. This function is invoked as a filter.
168 *
169 * @since 3.2.0
170 */
171 public function getDataAs( $data, $chart_id, $type ) {
172 return $this->_getDataAs( $chart_id, $type );
173 }
174
175 /**
176 * Extracts the data for a chart and prepares it for the given type.
177 *
178 * @access public
179 * @param int $chart_id The chart id.
180 * @param string $type The exported type.
181 */
182 public function _getDataAs( $chart_id, $type ) {
183 $final = null;
184 $success = false;
185 if ( $chart_id ) {
186 $chart = get_post( $chart_id );
187 $success = $chart && $chart->post_type === Visualizer_Plugin::CPT_VISUALIZER;
188 }
189 if ( $success ) {
190 $settings = get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true );
191 $rows = array();
192 $series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true );
193 $data = self::get_chart_data( $chart, $type, false );
194 if ( ! empty( $series ) ) {
195 $row = array();
196 foreach ( $series as $array ) {
197 $row[] = $array['label'];
198 }
199 $rows[] = $row;
200 $row = array();
201 foreach ( $series as $array ) {
202 $row[] = $array['type'];
203 }
204 $rows[] = $row;
205 }
206 if ( ! empty( $data ) ) {
207 foreach ( $data as $array ) {
208 // ignore strings
209 if ( ! is_array( $array ) ) {
210 continue;
211 }
212 // if this is an array of arrays...
213 if ( is_array( $array[0] ) ) {
214 foreach ( $array as $arr ) {
215 $rows[] = $arr;
216 }
217 } else {
218 // just an array
219 $rows[] = $array;
220 }
221 }
222 }
223
224 $title = 'visualizer#' . $chart_id;
225 if ( ! empty( $settings['title'] ) ) {
226 $title = $settings['title'];
227 }
228 // for ChartJS, title is an array.
229 if ( is_array( $title ) && isset( $title['text'] ) ) {
230 $title = $title['text'];
231 }
232 if ( empty( $title ) ) {
233 $title = 'visualizer#' . $chart_id;
234 }
235
236 $filename = $title;
237 switch ( $type ) {
238 case 'csv':
239 $final = $this->_getCSV( $rows, $filename, false );
240 break;
241 case 'csv-display':
242 $final = $this->_getCSV( $rows, $filename, true );
243 break;
244 case 'xls':
245 $final = $this->_getExcel( $rows, $filename );
246 break;
247 case 'print':
248 $final = $this->_getHTML( $rows );
249 break;
250 case 'image':
251 $final = $this->_getImage( $chart );
252 break;
253 }
254 }
255 return $final;
256 }
257
258 /**
259 * Prepares a CSV.
260 *
261 * @access private
262 * @param array $rows The array of data.
263 * @param string $filename The name of the file to use.
264 * @param bool $enclose Enclose strings that have commas in them in double quotes.
265 */
266 private function _getCSV( $rows, $filename, $enclose ) {
267 $filename .= '.csv';
268
269 $bom = chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF );
270 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
271 $fp = function_exists( 'tmpfile' ) ? @tmpfile() : null;
272 if ( ! $fp ) {
273 if ( ! function_exists( 'wp_tempnam' ) ) {
274 require_once ABSPATH . 'wp-admin/includes/file.php';
275 }
276 $fp = fopen( wp_tempnam(), 'w+' );
277 }
278 if ( ! $fp ) {
279 return array( 'csv' => '', 'name' => $filename, 'string' => '' );
280 }
281 if ( ! apply_filters( 'vizualizer_export_include_series_type', true ) ) {
282 unset( $rows[1] );
283 $rows = array_values( $rows );
284 }
285 // support for MS Excel
286 fprintf( $fp, $bom );
287 foreach ( $rows as $row ) {
288 fputcsv( $fp, $row );
289 }
290 rewind( $fp );
291 $csv = '';
292 $array = fgetcsv( $fp );
293 while ( $array !== false ) {
294 if ( strlen( $csv ) > 0 ) {
295 $csv .= PHP_EOL;
296 }
297 // if enclosure is required, check every item of this line
298 // if a comma exists in the item, add enclosure.
299 if ( $enclose ) {
300 $temp_array = array();
301 foreach ( $array as $item ) {
302 if ( strpos( $item, ',' ) !== false ) {
303 $item = VISUALIZER_CSV_ENCLOSURE . $item . VISUALIZER_CSV_ENCLOSURE;
304 }
305 $temp_array[] = $item;
306 }
307 $array = $temp_array;
308 }
309 $csv .= implode( ',', $array );
310 $array = fgetcsv( $fp );
311 }
312 fclose( $fp );
313
314 return array(
315 'csv' => $csv,
316 'name' => $filename,
317 'string' => str_replace( $bom, '', $csv ),
318 );
319 }
320
321 /**
322 * Prepares an Excel file.
323 *
324 * @access private
325 * @param array $rows The array of data.
326 * @param string $filename The name of the file to use.
327 */
328 private function _getExcel( $rows, $filename ) {
329 // OpenSpout allows for long sheet names, but let's keep the same limit for compatibility.
330 $chart = substr( $filename, 0, 30 );
331 $filename .= '.xlsx';
332 if ( ! apply_filters( 'vizualizer_export_include_series_type', true ) ) {
333 unset( $rows[1] );
334 $rows = array_values( $rows );
335 $rows = array_map(
336 function ( $r ) {
337 return array_map( 'strval', $r );
338 },
339 $rows
340 );
341 }
342 $vendor_file = VISUALIZER_ABSPATH . '/vendor/autoload.php';
343 if ( is_readable( $vendor_file ) ) {
344 include_once $vendor_file;
345 }
346 $xlsData = '';
347 if ( class_exists( 'OpenSpout\Writer\Common\Creator\WriterEntityFactory' ) ) {
348 try {
349 // Use OpenSpout to create the XLSX file in memory.
350 $writer = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createXLSXWriter();
351 $writer->openToFile( 'php://output' ); // Open to output instead of a file.
352 $writer->getCurrentSheet()->setName( sanitize_title( $chart ) );
353
354 // Write rows.
355 foreach ( $rows as $row ) {
356 $rowFromValues = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createRowFromArray( $row );
357 $writer->addRow( $rowFromValues );
358 }
359
360 ob_start();
361 $writer->close(); // Saves and closes the file in the output buffer.
362 $xlsData = ob_get_clean();
363 } catch ( Exception $e ) {
364 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, 'OpenSpout writer error: ' . $e->getMessage(), 'error', __FILE__, __LINE__ );
365 error_log( 'OpenSpout writer error: ' . $e->getMessage() );
366 }
367 } else {
368 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, 'Class OpenSpout\Writer\Common\Creator\WriterEntityFactory does not exist!', 'error', __FILE__, __LINE__ );
369 error_log( 'Class OpenSpout\Writer\Common\Creator\WriterEntityFactory does not exist!' );
370 }
371 return array(
372 'csv' => 'data:application/vnd.ms-excel;base64,' . base64_encode( $xlsData ),
373 'name' => $filename,
374 'raw' => base64_encode( $xlsData ),
375 );
376 }
377
378 /**
379 * Prepares an HTML table.
380 *
381 * @access private
382 * @param array $rows The array of data.
383 */
384 private function _getHTML( $rows ) {
385 $css = '
386 table.visualizer-print {
387 border-collapse: collapse;
388 }
389 table.visualizer-print, table.visualizer-print th, table.visualizer-print td {
390 border: 1px solid #000;
391 }
392 ';
393 $html = '';
394 $html .= '
395 <html>
396 <head>
397 <style>
398 ' . apply_filters( 'visualizer_print_css', $css ) . '
399 </style>
400 </head>
401 <body>';
402
403 $table = '<table class="visualizer-print">';
404 $index = 0;
405 foreach ( $rows as $row ) {
406 // skip the data type row.
407 if ( 1 === $index ) {
408 ++$index;
409 continue;
410 }
411
412 $table .= '<tr>';
413 foreach ( $row as $col ) {
414 if ( $index === 0 ) {
415 $table .= '<th>' . $col . '</th>';
416 } else {
417 $table .= '<td>' . $col . '</td>';
418 }
419 }
420 $table .= '</tr>';
421 ++$index;
422 }
423 $table .= '</table>';
424
425 $html .= apply_filters( 'visualizer_print_table', $table ) . '
426 </body>
427 </html>';
428 return array(
429 'csv' => $html,
430 );
431 }
432
433 /**
434 * Disable revisions temporarily for visualizer post type.
435 */
436 final protected function disableRevisionsTemporarily() {
437 add_filter(
438 'wp_revisions_to_keep', function ( $num, $post ) {
439 if ( $post->post_type === Visualizer_Plugin::CPT_VISUALIZER ) {
440 return 0;
441 }
442 return $num;
443 }, 10, 2
444 );
445 }
446
447 /**
448 * Undo revisions for the chart, and if necessary, restore the earliest version.
449 *
450 * @return bool If any revisions were found.
451 */
452 final public function undoRevisions( $chart_id, $restore = false ) {
453 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'undoRevisions for %d with%s restore', $chart_id, ( $restore ? '' : 'out' ) ), 'debug', __FILE__, __LINE__ );
454 if ( get_post_type( $chart_id ) !== Visualizer_Plugin::CPT_VISUALIZER ) {
455 return false;
456 }
457 $revisions = wp_get_post_revisions( $chart_id, array( 'order' => 'ASC' ) );
458 if ( count( $revisions ) > 1 ) {
459 $revision_ids = array_keys( $revisions );
460
461 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'found %d revisions = %s', count( $revisions ), print_r( $revision_ids, true ) ), 'debug', __FILE__, __LINE__ );
462
463 // when we restore, a new revision is likely to be created. so, let's disable revisions for the time being.
464 $this->disableRevisionsTemporarily();
465
466 if ( $restore ) {
467 // restore to the oldest one i.e. the first one.
468 wp_restore_post_revision( array_shift( $revision_ids ) );
469 }
470
471 // delete all revisions.
472 foreach ( $revision_ids as $id ) {
473 wp_delete_post_revision( $id );
474 }
475
476 return true;
477 }
478 return false;
479 }
480
481 /**
482 * If existing revisions exist for the chart, restore the earliest version and then create a new revision to initiate editing.
483 */
484 final public function handleExistingRevisions( $chart_id, $chart ) {
485
486 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'handleExistingRevisions for %d', $chart_id ), 'debug', __FILE__, __LINE__ );
487 if ( get_post_type( $chart_id ) !== Visualizer_Plugin::CPT_VISUALIZER ) {
488 return $chart_id;
489 }
490 // AI Builder (D3) charts use their own publish flow — skip the classic revision-restore
491 // mechanism, which would otherwise overwrite the saved title and settings.
492 if ( 'd3' === get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true ) ) {
493 return $chart;
494 }
495 // undo revisions.
496 $revisions_found = $this->undoRevisions( $chart_id, true );
497
498 // create revision for the edit action.
499 wp_save_post_revision( $chart_id );
500
501 if ( $revisions_found ) {
502 // fetch chart data again in case it was updated by an earlier revision.
503 $chart = get_post( $chart_id );
504 }
505 return $chart;
506 }
507
508 /**
509 * Returns the language of the locale.
510 *
511 * @access protected
512 */
513 protected function get_language() {
514 $locale = get_locale();
515 if ( empty( $locale ) ) {
516 return '';
517 }
518 $array = explode( '_', $locale );
519 if ( count( $array ) < 2 ) {
520 return '';
521 }
522 return reset( $array );
523 }
524
525 /**
526 * Gets/creates the JS where user-specific customizations can be/have been added.
527 */
528 protected function get_user_customization_js() {
529 // use this as the JS file in case we are not able to create the file in uploads.
530 $default = VISUALIZER_ABSURL . 'js/customization.js';
531
532 $uploads = wp_get_upload_dir();
533 $specific = $uploads['baseurl'] . '/visualizer/customization.js';
534
535 // for testing on user sites (before we send them the correctly customized file).
536 if ( VISUALIZER_TEST_JS_CUSTOMIZATION ) {
537 return $default;
538 }
539
540 try {
541 require_once ABSPATH . 'wp-admin/includes/file.php';
542 WP_Filesystem();
543 global $wp_filesystem;
544 if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) {
545 return $default;
546 }
547
548 $multisite_arg = '/';
549 if ( is_multisite() && ! is_main_site() ) {
550 $multisite_arg = '/sites/' . get_current_blog_id() . '/';
551 }
552
553 $dir = $wp_filesystem->wp_content_dir() . 'uploads' . $multisite_arg . 'visualizer';
554 $file = $wp_filesystem->wp_content_dir() . 'uploads' . $multisite_arg . 'visualizer/customization.js';
555
556 if ( $wp_filesystem->is_readable( $file ) ) {
557 return $specific;
558 }
559
560 if ( $wp_filesystem->exists( $file ) && ! $wp_filesystem->is_readable( $file ) ) {
561 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to read file %s', $file ), 'error', __FILE__, __LINE__ );
562 return $default;
563 }
564
565 if ( ! $wp_filesystem->exists( $dir ) ) {
566 $done = $wp_filesystem->mkdir( $dir );
567 if ( $done === false ) {
568 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to create directory %s', $dir ), 'error', __FILE__, __LINE__ );
569 return $default;
570 }
571 }
572
573 // if file does not exist, copy.
574 if ( ! $wp_filesystem->exists( $file ) ) {
575 $src = str_replace( ABSPATH, $wp_filesystem->abspath(), VISUALIZER_ABSPATH . '/js/customization.js' );
576 $done = $wp_filesystem->copy( $src, $file );
577 if ( $done === false ) {
578 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to copy file %s to %s', $src, $file ), 'error', __FILE__, __LINE__ );
579 return $default;
580 }
581 }
582
583 return $specific;
584 } catch ( \Throwable $e ) {
585 return $default;
586 }
587 }
588
589 /**
590 * Load the class for the given chart's chart type so that its assets can be loaded.
591 */
592 protected function load_chart_type( $chart_id ) {
593 // D3/AI charts have no traditional type class — return the library string directly.
594 $lib = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true );
595 if ( 'd3' === $lib ) {
596 return 'd3';
597 }
598
599 $name = $this->load_chart_class_name( $chart_id );
600 $class = null;
601 if ( class_exists( $name ) || true === apply_filters( 'visualizer_load_chart', false, $name ) ) {
602 $class = new $name();
603 }
604
605 if ( is_null( $class ) && Visualizer_Module::is_pro() ) {
606 // lets see if this type exists in pro. New Lite(3.1.0+) & old Pro(1.8.0-).
607 $type = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, true );
608 $class = apply_filters( 'visualizer_pro_chart_type_sidebar', null, array( 'id' => $chart_id, 'type' => $type, 'settings' => get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true ) ) );
609 }
610
611 return is_null( $class ) ? null : $class->getLibrary();
612 }
613
614 /**
615 * Returns the class name for the given chart's chart type.
616 */
617 protected function load_chart_class_name( $chart_id ) {
618 $type = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, true );
619 $lib = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true );
620
621 // backward compatibility.
622 if ( empty( $lib ) ) {
623 $lib = 'GoogleCharts';
624 if ( 'dataTable' === $type ) {
625 $lib = 'DataTable';
626 }
627 }
628 $name = 'Visualizer_Render_Sidebar_Type_' . $lib . '_' . ucwords( $type );
629 return $name;
630 }
631
632 /**
633 * Generates the inline CSS to apply to the chart and adds these classes to the settings.
634 *
635 * @access public
636 * @param int $id The id of the chart.
637 * @param array $settings The settings of the chart.
638 */
639 protected function get_inline_custom_css( $id, $settings ) {
640 $css = '';
641
642 $classes = array();
643 $css = '<style type="text/css" name="visualizer-custom-css" id="customcss-' . $id . '">';
644 if ( ! empty( $settings['customcss'] ) ) {
645 foreach ( $settings['customcss'] as $name => $element ) {
646 $attributes = array();
647 foreach ( $element as $property => $value ) {
648 $attributes[] = $this->handle_css_property( $property, $value );
649 }
650 $class_name = $id . $name;
651 $properties = implode( ' !important; ', array_filter( $attributes ) );
652 if ( ! empty( $properties ) ) {
653 $css .= '.' . $class_name . ' {' . $properties . ' !important;}';
654 $classes[ $name ] = $class_name;
655 }
656 }
657 $settings['cssClassNames'] = $classes;
658 }
659
660 $img_path = VISUALIZER_ABSURL . 'images';
661 $css .= ".locker,.locker-loader{position:absolute;top:0;left:0;width:100%;height:100%}.locker{z-index:1000;opacity:.8;background-color:#fff;-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=80)\";filter:alpha(opacity=80)}.locker-loader{z-index:1001;background:url($img_path/ajax-loader.gif) no-repeat center center}.dt-button{display:none!important}.visualizer-front-container.visualizer-lazy-render{content-visibility: auto;}.google-visualization-controls-categoryfilter label.google-visualization-controls-label {vertical-align: middle;}.google-visualization-controls-categoryfilter li.goog-inline-block {margin: 0 0.2em;}.google-visualization-controls-categoryfilter li {padding: 0 0.2em;}.visualizer-front-container .dataTables_scrollHeadInner{margin: 0 auto;}";
662 $css .= '</style>';
663
664 $arguments = array( $css, $settings );
665 apply_filters_ref_array( 'visualizer_inline_css', array( &$arguments ) );
666
667 return $arguments;
668 }
669
670 /**
671 * Handles CSS properties that might need special syntax.
672 *
673 * @access private
674 * @param string $property The name of the css property.
675 * @param string $value The value of the css property.
676 */
677 private function handle_css_property( $property, $value ) {
678 if ( empty( $property ) || empty( $value ) ) {
679 return '';
680 }
681
682 switch ( $property ) {
683 case 'transform':
684 $value = 'rotate(' . $value . 'deg)';
685 break;
686 }
687 return $property . ': ' . $value;
688 }
689
690 /**
691 * Determines if charts have been created of the particular chart type.
692 */
693 protected static function hasChartType( $type ) {
694 $args = array(
695 'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
696 'fields' => 'ids',
697 'post_status' => 'publish',
698 'meta_query' => array(
699 array(
700 'key' => Visualizer_Plugin::CF_CHART_TYPE,
701 'value' => $type,
702 ),
703 ),
704 );
705
706 $q = new WP_Query( $args );
707 return $q->found_posts > 0;
708 }
709
710 /**
711 * Determines how many charts have been created.
712 */
713 protected static function numberOfCharts() {
714 $args = array(
715 'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
716 'fields' => 'ids',
717 'post_status' => 'publish',
718 'posts_per_page' => 300,
719 );
720
721 $q = new WP_Query( $args );
722 return $q->found_posts;
723 }
724
725 /**
726 * Checks whether the current user may edit a specific chart.
727 *
728 * @param int $chart_id Chart ID.
729 * @return bool
730 */
731 public static function can_edit_chart( $chart_id ) {
732 $chart_id = absint( $chart_id );
733 if ( ! $chart_id ) {
734 return false;
735 }
736
737 $chart = get_post( $chart_id );
738 return $chart
739 && Visualizer_Plugin::CPT_VISUALIZER === $chart->post_type
740 && (
741 current_user_can( 'edit_post', $chart_id )
742 || ( (int) $chart->post_author === get_current_user_id() && current_user_can( 'edit_posts' ) )
743 );
744 }
745
746 /**
747 * Checks if the PRO version is active.
748 *
749 * @since 3.3.0
750 */
751 public static function is_pro() {
752 // versions of pro before 1.9.0 will use the constant VISUALIZER_PRO
753 // versions of pro 1.9.0 onwards will use the filter
754 return apply_filters( 'visualizer_is_pro', VISUALIZER_PRO );
755 }
756
757 /**
758 * Checks if the PRO version is older than a particular version.
759 *
760 * @since 3.3.0
761 */
762 public static function is_pro_older_than( $version ) {
763 return version_compare( VISUALIZER_PRO_VERSION, $version, '<' );
764 }
765
766 /**
767 * Should we show some specific feature on the basis of the version?
768 *
769 * @since 3.4.0
770 */
771 public static function can_show_feature( $feature ) {
772 switch ( $feature ) {
773 case 'simple-editor':
774 // if user has pro but an older version, then don't load the simple editor functionality
775 // as the select box will not behave as expected because the pro editor's functionality will supercede.
776 return ! Visualizer_Module::is_pro() || ! Visualizer_Module::is_pro_older_than( '1.9.2' );
777 }
778 return false;
779 }
780
781 /**
782 * Gets the features for the provided license type.
783 */
784 final public static function get_features_for_license( $plan ) {
785 $is_new_personal = apply_filters( 'visualizer_is_new_personal', false );
786 switch ( $plan ) {
787 case 1:
788 $features = array( 'import-wp', 'import-wc-report', 'import-file', 'import-url' );
789 if ( ! $is_new_personal ) {
790 $features[] = 'db-query';
791 }
792 return $features;
793 case 2:
794 $features = array( 'schedule-chart', 'chart-permissions', 'import-chart', 'data-filter-configuration', 'frontend-actions' );
795 if ( $is_new_personal ) {
796 $features[] = 'db-query';
797 }
798 return $features;
799 }
800 }
801
802 /**
803 * Safely unserialize chart/source content, blocking PHP object injection.
804 *
805 * Single guarded chokepoint shared by chart/source content sinks so the
806 * allowed_classes guard cannot be dropped from one call site independently.
807 *
808 * @param mixed $content The serialized content (only strings are decoded).
809 * @return mixed The decoded value (array for valid chart data), or false.
810 */
811 public static function decode_content( $content ) {
812 if ( ! is_string( $content ) ) {
813 return false;
814 }
815 $value = unserialize( trim( $content ), array( 'allowed_classes' => false ) );
816 if ( self::contains_references( $value ) ) {
817 return false;
818 }
819 return self::strip_incomplete_objects( $value );
820 }
821
822 /**
823 * Check decoded arrays for references before recursively processing them.
824 *
825 * Cyclic serialized arrays necessarily contain a reference. Rejecting all
826 * references also prevents shared references from becoming cycles later,
827 * so strip_incomplete_objects() cannot recurse without terminating.
828 *
829 * @param mixed $value The decoded value.
830 * @return bool Whether the value contains an array reference.
831 */
832 private static function contains_references( $value ) {
833 if ( ! is_array( $value ) ) {
834 return false;
835 }
836 foreach ( array_keys( $value ) as $key ) {
837 if ( null !== ReflectionReference::fromArrayElement( $value, $key ) ) {
838 return true;
839 }
840 if ( is_array( $value[ $key ] ) && self::contains_references( $value[ $key ] ) ) {
841 return true;
842 }
843 }
844 return false;
845 }
846
847 /**
848 * Remove the __PHP_Incomplete_Class stubs the allowed_classes guard leaves
849 * behind; they crash map_deep() when the decoded value is written back to
850 * post meta. Legitimate chart content is nested arrays/scalars only.
851 *
852 * @param mixed $value The decoded value.
853 * @return mixed The value without object stubs; false for a top-level stub.
854 */
855 private static function strip_incomplete_objects( $value ) {
856 if ( $value instanceof __PHP_Incomplete_Class ) {
857 return false;
858 }
859 if ( is_array( $value ) ) {
860 foreach ( $value as $key => $item ) {
861 if ( $item instanceof __PHP_Incomplete_Class ) {
862 unset( $value[ $key ] );
863 } elseif ( is_array( $item ) ) {
864 $value[ $key ] = self::strip_incomplete_objects( $item );
865 }
866 }
867 }
868 return $value;
869 }
870
871 /**
872 * Object-injection-safe drop-in for maybe_unserialize().
873 *
874 * @param mixed $value Raw meta/content value.
875 * @return mixed The decoded value for serialized input, the value unchanged otherwise.
876 */
877 public static function maybe_decode_content( $value ) {
878 return is_serialized( $value ) ? self::decode_content( $value ) : $value;
879 }
880
881 /**
882 * Gets the chart content after common manipulations.
883 */
884 public static function get_chart_data( $chart, $type, $run_filter = true ) {
885 // change HTML entities
886 $post_content = html_entity_decode( htmlentities( $chart->post_content ) );
887 $post_content = preg_replace_callback(
888 '!s:(\d+):"(.*?)";!s',
889 function ( $matches ) {
890 if ( isset( $matches[2] ) ) {
891 return 's:' . strlen( $matches[2] ) . ':"' . $matches[2] . '";';
892 }
893 },
894 $post_content
895 );
896 $data = self::decode_content( $post_content );
897 $altered = array();
898 if ( ! empty( $data ) ) {
899 foreach ( $data as $index => $array ) {
900 if ( ! is_array( $index ) && is_array( $array ) ) {
901 foreach ( $array as &$datum ) {
902 if ( is_string( $datum ) ) {
903 $datum = stripslashes( $datum );
904 }
905 }
906 $altered[ $index ] = $array;
907 }
908 }
909 }
910 // if something goes wrong and the end result is empty, be safe and use the original data
911 if ( empty( $altered ) ) {
912 $altered = $data;
913 }
914 if ( $run_filter ) {
915 return apply_filters( Visualizer_Plugin::FILTER_GET_CHART_DATA, $altered, $chart->ID, $type );
916 }
917 return $altered;
918 }
919
920 /**
921 * Get chart image.
922 *
923 * @param object $chart Chart data.
924 * @return string
925 */
926 public function _getImage( $chart = null ) {
927 $image = '';
928 if ( $chart ) {
929 $chart_image = get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_IMAGE, true );
930 if ( ! empty( $chart_image ) ) {
931 $image = wp_get_attachment_image( $chart_image, 'full' );
932 }
933 }
934 return array(
935 'csv' => $image,
936 );
937 }
938
939 /**
940 * Filter chart title if visualizer post type.
941 *
942 * @param object $query WP Query object.
943 * @return void
944 */
945 public function PreGetPosts( $query ) {
946 if ( ! $query->is_main_query() ) {
947 $post_type = $query->get( 'post_type' );
948 if ( 'visualizer' === $post_type ) {
949 $this->_addFilter( Visualizer_Plugin::FILTER_CHART_TITLE, 'filterChartTitle', 10, 2 );
950 }
951 }
952 }
953
954 /**
955 * Filter chart title.
956 *
957 * @access public
958 * @param string $post_title Post title.
959 * @param int $post_id Post ID.
960 * @return string
961 */
962 public function filterChartTitle( $post_title, $post_id ) {
963 $post_type = get_post_type( $post_id );
964 $post_title = trim( $post_title );
965 if ( 'visualizer' === $post_type && 'Visualization' === $post_title ) {
966 return sprintf( '%s #%d', $post_title, $post_id );
967 }
968 return $post_title;
969 }
970 }
971