PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.0
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.0
4.0.8 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 All 149 releases
← All changes | classes/Visualizer/Module/Chart.php +123 -29 3.11.154.0.0 View file →
@@ -69,9 +69,8 @@
69 69
70 70 $this->_addAjaxAction( Visualizer_Plugin::ACTION_SAVE_FILTER_QUERY, 'saveFilter' );
71 71
72 72 $this->_addFilter( 'visualizer_get_sidebar', 'getSidebar', 10, 2 );
73 -
74 73 }
75 74
76 75 /**
77 76 * Generates the HTML of the sidebar for the chart.
@@ -402,8 +401,13 @@
402 401 }
403 402
404 403 $date_formats = Visualizer_Source::get_date_formats_if_exists( $series, $data );
405 404
405 + $code = '';
406 + if ( 'd3' === $library ) {
407 + $code = get_post_meta( $chart->ID, Visualizer_Module_AIBuilder::CF_D3_CODE, true );
408 + }
409 +
406 410 return array(
407 411 'type' => $type,
408 412 'series' => $series,
409 413 'settings' => $settings,
@@ -408,8 +412,9 @@
408 412 'series' => $series,
409 413 'settings' => $settings,
410 414 'data' => $data,
411 415 'library' => $library,
416 + 'code' => $code,
412 417 'css' => $css,
413 418 'date_formats' => $date_formats,
414 419 );
415 420 }
@@ -538,9 +543,10 @@
538 543 $chart_id = isset( $_GET['chart'] ) ? filter_var( $_GET['chart'], FILTER_VALIDATE_INT ) : '';
539 544 if ( ! empty( $_POST ) ) {
540 545 $_POST = map_deep( $_POST, 'wp_strip_all_tags' );
541 546 }
542 - if ( ! $chart_id || ! ( $chart = get_post( $chart_id ) ) || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) {
547 + $chart = $chart_id ? get_post( $chart_id ) : null;
548 + if ( ! $chart_id || ! $chart || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) {
543 549 if ( empty( $_GET['lang'] ) || empty( $_GET['parent_chart_id'] ) ) {
544 550 $this->deleteOldCharts();
545 551 $default_type = isset( $_GET['type'] ) && ! empty( $_GET['type'] ) ? $_GET['type'] : 'line';
546 552 $chart_status = Visualizer_Module_Admin::checkChartStatus( $default_type );
@@ -574,9 +580,9 @@
574 580
575 581 do_action( 'visualizer_pro_new_chart_defaults', $chart_id );
576 582 }
577 583 } else {
578 - $parent_chart_id = isset( $_GET['parent_chart_id'] ) ? filter_var( $_GET['parent_chart_id'], FILTER_VALIDATE_INT ) : '';
584 + $parent_chart_id = filter_var( $_GET['parent_chart_id'], FILTER_VALIDATE_INT );
579 585 $success = false;
580 586 if ( $parent_chart_id ) {
581 587 $parent_chart = get_post( $parent_chart_id );
582 588 $success = $parent_chart && $parent_chart->post_type === Visualizer_Plugin::CPT_VISUALIZER;
@@ -737,9 +743,9 @@
737 743 wp_register_script( 'visualizer-codemirror-matchbrackets', '//codemirror.net/addon/edit/matchbrackets.js', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
738 744 wp_register_script( 'visualizer-codemirror-closebrackets', '//codemirror.net/addon/edit/closebrackets.js', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
739 745 wp_register_script( 'visualizer-codemirror-sql', '//codemirror.net/mode/sql/sql.js', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
740 746 wp_register_script( 'visualizer-codemirror-sql-hint', '//codemirror.net/addon/hint/sql-hint.js', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
741 - wp_register_script( 'visualizer-codemirror-hint', '//codemirror.net/addon/hint/show-hint.js', array( 'visualizer-codemirror-sql', 'visualizer-codemirror-sql-hint', 'visualizer-codemirror-placeholder', 'visualizer-codemirror-matchbrackets', 'visualizer-codemirror-closebrackets' ), Visualizer_Plugin::VERSION );
747 + wp_register_script( 'visualizer-codemirror-hint', '//codemirror.net/addon/hint/show-hint.js', array( 'visualizer-codemirror-sql', 'visualizer-codemirror-sql-hint', 'visualizer-codemirror-placeholder', 'visualizer-codemirror-matchbrackets', 'visualizer-codemirror-closebrackets' ), Visualizer_Plugin::VERSION );
742 748 wp_register_style( 'visualizer-codemirror-core', '//codemirror.net/lib/codemirror.css', array(), Visualizer_Plugin::VERSION );
743 749 wp_register_style( 'visualizer-codemirror-hint', '//codemirror.net/addon/hint/show-hint.css', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
744 750
745 751 wp_enqueue_script( 'visualizer-codemirror-hint' );
@@ -803,9 +809,14 @@
803 809 wp_update_post( $this->_chart->to_array() );
804 810 }
805 811 // save meta data only when it is NOT being canceled.
806 812 if ( ! $is_canceled ) {
807 - update_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, $_POST );
813 + $post_settings = $_POST;
814 + $existing = get_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, true );
815 + if ( isset( $existing['colors'] ) && is_array( $existing['colors'] ) && ! isset( $post_settings['colors'] ) ) {
816 + $post_settings['colors'] = $existing['colors'];
817 + }
818 + update_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, $post_settings );
808 819
809 820 // we will keep a parameter called 'internal_title' that will be set to the given title or, if empty, the chart ID
810 821 // this will help in searching with the chart id.
811 822 $settings = get_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, true );
@@ -852,8 +863,9 @@
852 863 wp_enqueue_style( 'visualizer-frame' );
853 864 wp_enqueue_script( 'visualizer-preview' );
854 865 wp_enqueue_script( 'visualizer-chosen' );
855 866 wp_enqueue_script( 'visualizer-render' );
867 + wp_enqueue_code_editor( array( 'type' => 'application/json' ) );
856 868
857 869 if ( Visualizer_Module::can_show_feature( 'simple-editor' ) ) {
858 870 wp_enqueue_script( 'visualizer-editor-simple' );
859 871 wp_localize_script(
@@ -861,12 +873,10 @@
861 873 'visualizer1',
862 874 array(
863 875 'ajax' => array(
864 876 'url' => admin_url( 'admin-ajax.php' ),
865 - 'nonces' => array(
866 - ),
867 - 'actions' => array(
868 - ),
877 + 'nonces' => array(),
878 + 'actions' => array(),
869 879 ),
870 880 )
871 881 );
872 882 }
@@ -877,14 +887,15 @@
877 887 'visualizer-render',
878 888 'visualizer',
879 889 array(
880 890 'l10n' => array(
881 - 'invalid_source' => esc_html__( 'You have entered an invalid URL. Please provide a valid URL.', 'visualizer' ),
882 - 'loading' => esc_html__( 'Loading...', 'visualizer' ),
883 - 'json_error' => esc_html__( 'An error occured in fetching data.', 'visualizer' ),
884 - 'select_columns' => esc_html__( 'Please select a few columns to include in the chart.', 'visualizer' ),
885 - 'save_settings' => __( 'You have modified the chart\'s settings. To modify the source/data again, you must save this chart and reopen it for editing. If you continue without saving the chart, you may lose your changes.', 'visualizer' ),
886 - 'copied' => __( 'The data has been copied to your clipboard. Hit Ctrl-V/Cmd-V in your spreadsheet editor to paste the data.', 'visualizer' ),
891 + 'invalid_source' => esc_html__( 'The URL you entered is invalid. Please enter a valid URL.', 'visualizer' ),
892 + 'loading' => esc_html__( 'Loading...', 'visualizer' ),
893 + 'json_error' => esc_html__( 'An error occured in fetching data.', 'visualizer' ),
894 + 'select_columns' => esc_html__( 'Please select a few columns to include in the chart.', 'visualizer' ),
895 + 'save_settings' => __( 'You have modified the chart\'s settings. To modify the source/data again, you must save this chart and reopen it for editing. If you continue without saving the chart, you may lose your changes.', 'visualizer' ),
896 + 'copied' => __( 'The data has been copied to your clipboard. Hit Ctrl-V/Cmd-V in your spreadsheet editor to paste the data.', 'visualizer' ),
897 + 'invalid_format' => esc_html__( 'This format pattern is not supported in the series settings field. Use the Manual Configuration option instead.', 'visualizer' ),
887 898 ),
888 899 'charts' => array(
889 900 'canvas' => $data,
890 901 'id' => $this->_chart->ID,
@@ -1011,8 +1022,80 @@
1011 1022 * Processes the CSV that is sent in the request as a string.
1012 1023 *
1013 1024 * @since 3.2.0
1014 1025 */
1026 + /**
1027 + * Determines whether a remote URL serves an XLSX file.
1028 + *
1029 + * Used as a fallback when the URL path has no recognisable file extension
1030 + * (e.g. SharePoint, signed S3 URLs, or "download?id=…" endpoints).
1031 + *
1032 + * Uses wp_safe_remote_get() to block requests to private/loopback addresses,
1033 + * and streams the response to a temp file so no body data is held in memory
1034 + * regardless of whether the server honours the Range header.
1035 + *
1036 + * The check relies on the ZIP magic number (PK\x03\x04) that every XLSX
1037 + * file begins with, making it immune to misleading Content-Type headers
1038 + * such as application/octet-stream. Content-Type is used as a last-resort
1039 + * fallback only when the temp file is empty (e.g. a HEAD-only server).
1040 + *
1041 + * @access private
1042 + * @param string $url The remote URL to probe.
1043 + * @return bool TRUE if the file appears to be XLSX, FALSE otherwise.
1044 + */
1045 + private static function _url_is_xlsx( $url ) {
1046 + $tmpfile = wp_tempnam( 'visualizer_xlsx_probe' );
1047 + if ( ! $tmpfile ) {
1048 + return false;
1049 + }
1050 +
1051 + $response = wp_safe_remote_get(
1052 + $url,
1053 + array(
1054 + 'timeout' => 10,
1055 + 'user-agent' => 'WordPress/' . get_bloginfo( 'version' ),
1056 + 'headers' => array( 'Range' => 'bytes=0-3' ),
1057 + 'stream' => true,
1058 + 'filename' => $tmpfile,
1059 + )
1060 + );
1061 +
1062 + if ( is_wp_error( $response ) ) {
1063 + @unlink( $tmpfile ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
1064 + return false;
1065 + }
1066 +
1067 + $magic = '';
1068 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
1069 + $fh = @fopen( $tmpfile, 'rb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
1070 + if ( $fh ) {
1071 + $magic = fread( $fh, 4 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
1072 + fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1073 + }
1074 + @unlink( $tmpfile ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
1075 +
1076 + if ( strlen( $magic ) >= 4 ) {
1077 + // XLSX (and all ZIP-based Office formats) start with PK\x03\x04.
1078 + return $magic === "PK\x03\x04";
1079 + }
1080 +
1081 + // Last resort: server returned an empty body (e.g. ignored Range and
1082 + // returned only headers). Check Content-Type from the same response.
1083 + // application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
1084 + return false !== strpos(
1085 + wp_remote_retrieve_header( $response, 'content-type' ),
1086 + 'spreadsheetml'
1087 + );
1088 + }
1089 +
1090 + /**
1091 + * Parses a raw CSV string or editor payload and returns a source object.
1092 + *
1093 + * @access private
1094 + * @param string $data The raw CSV data string.
1095 + * @param string $editor_type The editor type ('text' or 'tabular').
1096 + * @return Visualizer_Source|null The populated source object, or null on failure.
1097 + */
1015 1098 private function handleCSVasString( $data, $editor_type ) {
1016 1099 $source = null;
1017 1100
1018 1101 switch ( $editor_type ) {
@@ -1027,9 +1110,9 @@
1027 1110 continue;
1028 1111 }
1029 1112 $row = explode( ',', $row );
1030 1113 $row = array_map(
1031 - function( $r ) {
1114 + function ( $r ) {
1032 1115 return '' === $r ? ' ' : $r;
1033 1116 },
1034 1117 $row
1035 1118 );
@@ -1078,9 +1161,9 @@
1078 1161 foreach ( $types as $type ) {
1079 1162 if ( empty( $type ) ) {
1080 1163 $exclude[] = $index;
1081 1164 }
1082 - $index++;
1165 + ++$index;
1083 1166 }
1084 1167
1085 1168 // when N headers are being renamed, the number of headers increases by N
1086 1169 // because of the way datatable duplicates header information
@@ -1155,11 +1238,12 @@
1155 1238
1156 1239 // check chart, if chart exists
1157 1240 // do not use filter_input as it does not work for phpunit test cases, use filter_var instead
1158 1241 $chart_id = isset( $_GET['chart'] ) ? filter_var( $_GET['chart'], FILTER_VALIDATE_INT ) : '';
1242 + $chart = $chart_id ? get_post( $chart_id ) : null;
1159 1243 if (
1160 1244 ! $chart_id ||
1161 - ! ( $chart = get_post( $chart_id ) ) ||
1245 + ! $chart ||
1162 1246 $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ||
1163 1247 ! current_user_can( 'edit_post', $chart_id )
1164 1248 ) {
1165 1249 if ( ! $can_die ) {
@@ -1209,15 +1293,24 @@
1209 1293 if ( isset( $_POST['remote_data'] ) && function_exists( 'wp_http_validate_url' ) ) {
1210 1294 $remote_data = wp_http_validate_url( $_POST['remote_data'] );
1211 1295 }
1212 1296 if ( false !== $remote_data ) {
1213 - $source = new Visualizer_Source_Csv_Remote( $remote_data );
1297 + $remote_ext = strtolower( pathinfo( parse_url( $remote_data, PHP_URL_PATH ), PATHINFO_EXTENSION ) );
1298 + if ( 'xlsx' === $remote_ext || ( 'csv' !== $remote_ext && self::_url_is_xlsx( $remote_data ) ) ) {
1299 + $source = new Visualizer_Source_Xlsx_Remote( $remote_data );
1300 + } else {
1301 + $source = new Visualizer_Source_Csv_Remote( $remote_data );
1302 + }
1214 1303 if ( isset( $_POST['vz-import-time'] ) ) {
1215 1304 apply_filters( 'visualizer_pro_chart_schedule', $chart_id, $remote_data, $_POST['vz-import-time'] );
1216 1305 }
1217 - // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
1218 - } elseif ( isset( $_FILES['local_data'] ) && $_FILES['local_data']['error'] == 0 ) {
1219 - $source = new Visualizer_Source_Csv( $_FILES['local_data']['tmp_name'] );
1306 + } elseif ( isset( $_FILES['local_data'] ) && $_FILES['local_data']['error'] === 0 ) {
1307 + $local_ext = strtolower( pathinfo( isset( $_FILES['local_data']['name'] ) ? $_FILES['local_data']['name'] : '', PATHINFO_EXTENSION ) );
1308 + if ( 'xlsx' === $local_ext ) {
1309 + $source = new Visualizer_Source_Xlsx( $_FILES['local_data']['tmp_name'] );
1310 + } else {
1311 + $source = new Visualizer_Source_Csv( $_FILES['local_data']['tmp_name'] );
1312 + }
1220 1313 } elseif ( isset( $_POST['chart_data'] ) && strlen( $_POST['chart_data'] ) > 0 ) {
1221 1314 $source = $this->handleCSVasString( $_POST['chart_data'], $_POST['editor-type'] );
1222 1315 update_post_meta( $chart_id, Visualizer_Plugin::CF_EDITOR, $_POST['editor-type'] );
1223 1316 } elseif ( isset( $_POST['table_data'] ) && 'yes' === $_POST['table_data'] ) {
@@ -1224,10 +1317,10 @@
1224 1317 $source = $this->handleTabularData();
1225 1318 update_post_meta( $chart_id, Visualizer_Plugin::CF_EDITOR, $_POST['editor-type'] );
1226 1319 } else {
1227 1320 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'CSV file with chart data was not uploaded for chart %d.', $chart_id ), 'error', __FILE__, __LINE__ );
1228 - $render->message = esc_html__( 'CSV file with chart data was not uploaded. Please try again.', 'visualizer' );
1229 - update_post_meta( $chart_id, Visualizer_Plugin::CF_ERROR, esc_html__( 'CSV file with chart data was not uploaded. Please try again.', 'visualizer' ) );
1321 + $render->message = esc_html__( 'No CSV file was received. Select a file and try uploading again.', 'visualizer' );
1322 + update_post_meta( $chart_id, Visualizer_Plugin::CF_ERROR, esc_html__( 'No CSV file was received. Select a file and try uploading again.', 'visualizer' ) );
1230 1323 }
1231 1324
1232 1325 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Uploaded data for chart %d with source %s', $chart_id, print_r( $source, true ) ), 'debug', __FILE__, __LINE__ );
1233 1326
@@ -1275,9 +1368,9 @@
1275 1368 $render->settings = json_encode( $settings );
1276 1369 } else {
1277 1370 $error = $source->get_error();
1278 1371 if ( empty( $error ) ) {
1279 - $error = esc_html__( 'CSV file is broken or invalid. Please try again.', 'visualizer' );
1372 + $error = esc_html__( 'The CSV file couldn\'t be read. Check that it\'s properly formatted and try again.', 'visualizer' );
1280 1373 }
1281 1374 $render->message = $error;
1282 1375 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( '%s for chart %d.', $error, $chart_id ), 'error', __FILE__, __LINE__ );
1283 1376 update_post_meta( $chart_id, Visualizer_Plugin::CF_ERROR, $error );
@@ -1405,10 +1498,11 @@
1405 1498 'visualizer-render',
1406 1499 'visualizer',
1407 1500 array(
1408 1501 'l10n' => array(
1409 - 'invalid_source' => esc_html__( 'You have entered an invalid URL. Please provide a valid URL.', 'visualizer' ),
1410 - 'loading' => esc_html__( 'Loading...', 'visualizer' ),
1502 + 'invalid_source' => esc_html__( 'The URL you entered is invalid. Please enter a valid URL.', 'visualizer' ),
1503 + 'loading' => esc_html__( 'Loading...', 'visualizer' ),
1504 + 'invalid_format' => esc_html__( 'This format pattern is not supported in the series settings field. To display percentages, use the Manual Configuration option instead.', 'visualizer' ),
1411 1505 ),
1412 1506 'charts' => array(
1413 1507 'canvas' => $data,
1414 1508 ),
@@ -1570,9 +1664,9 @@
1570 1664
1571 1665 $hours = filter_input(
1572 1666 INPUT_POST,
1573 1667 'refresh',
1574 - FILTER_VALIDATE_INT,
1668 + FILTER_VALIDATE_FLOAT,
1575 1669 array(
1576 1670 'options' => array(
1577 1671 'min_range' => -1,
1578 1672 'max_range' => apply_filters( 'visualizer_is_business', false ) ? PHP_INT_MAX : -1,
@@ -1579,9 +1673,9 @@
1579 1673 ),
1580 1674 )
1581 1675 );
1582 1676
1583 - if ( 0 !== $hours && empty( $hours ) ) {
1677 + if ( ! is_numeric( $hours ) ) {
1584 1678 $hours = -1;
1585 1679 }
1586 1680
1587 1681 do_action( 'visualizer_save_filter', $chart_id, $hours );