PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.8
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.8
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 +319 -127 3.10.104.0.8 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.
@@ -108,11 +107,10 @@
108 107 */
109 108 public function setJsonSchedule() {
110 109 check_ajax_referer( Visualizer_Plugin::ACTION_JSON_SET_SCHEDULE . Visualizer_Plugin::VERSION, 'security' );
111 110
112 - $chart_id = filter_input(
113 - INPUT_POST,
114 - 'chart',
111 + $chart_id = isset( $_POST['chart'] ) ? filter_var(
112 + $_POST['chart'],
115 113 FILTER_VALIDATE_INT,
116 114 array(
117 115 'options' => array(
118 116 'min_range' => 1,
@@ -117,14 +115,18 @@
117 115 'options' => array(
118 116 'min_range' => 1,
119 117 ),
120 118 )
121 - );
119 + ) : false;
122 120
123 121 if ( ! $chart_id ) {
124 122 wp_send_json_error();
125 123 }
126 124
125 + if ( ! self::can_edit_chart( $chart_id ) ) {
126 + wp_send_json_error( array( 'msg' => esc_html__( 'You do not have permission to perform this action.', 'visualizer' ) ), 403 );
127 + }
128 +
127 129 $time = filter_input(
128 130 INPUT_POST,
129 131 'time',
130 132 FILTER_VALIDATE_INT,
@@ -170,8 +172,12 @@
170 172 */
171 173 public function getJsonRoots() {
172 174 check_ajax_referer( Visualizer_Plugin::ACTION_JSON_GET_ROOTS . Visualizer_Plugin::VERSION, 'security' );
173 175
176 + if ( ! current_user_can( 'edit_posts' ) ) {
177 + wp_send_json_error( array( 'msg' => esc_html__( 'You do not have permission to perform this action.', 'visualizer' ) ) );
178 + }
179 +
174 180 $params = wp_parse_args( $_POST['params'] );
175 181
176 182 $source = new Visualizer_Source_Json( $params );
177 183
@@ -192,13 +198,18 @@
192 198 */
193 199 public function getJsonData() {
194 200 check_ajax_referer( Visualizer_Plugin::ACTION_JSON_GET_DATA . Visualizer_Plugin::VERSION, 'security' );
195 201
202 + if ( ! current_user_can( 'edit_posts' ) ) {
203 + wp_send_json_error( array( 'msg' => esc_html__( 'You do not have permission to perform this action.', 'visualizer' ) ) );
204 + }
205 +
196 206 $params = wp_parse_args( $_POST['params'] );
197 207
198 208 $chart_id = $params['chart'];
199 209
200 - if ( empty( $chart_id ) ) {
210 + $chart = $chart_id ? get_post( $chart_id ) : null;
211 + if ( ! $chart || Visualizer_Plugin::CPT_VISUALIZER !== $chart->post_type || ! current_user_can( 'edit_post', $chart_id ) ) {
201 212 wp_die();
202 213 }
203 214
204 215 $source = new Visualizer_Source_Json( $params );
@@ -223,12 +234,12 @@
223 234 public function setJsonData() {
224 235 check_ajax_referer( Visualizer_Plugin::ACTION_JSON_SET_DATA . Visualizer_Plugin::VERSION, 'security' );
225 236
226 237 $params = $_POST;
227 - $chart_id = $_GET['chart'];
238 + $chart_id = isset( $_GET['chart'] ) ? absint( $_GET['chart'] ) : 0;
228 239
229 - if ( empty( $chart_id ) ) {
230 - wp_die();
240 + if ( ! self::can_edit_chart( $chart_id ) ) {
241 + wp_die( esc_html__( 'You do not have permission to perform this action.', 'visualizer' ), '', array( 'response' => 403 ) );
231 242 }
232 243
233 244 $chart = get_post( $chart_id );
234 245
@@ -296,9 +307,9 @@
296 307 $render->data = json_encode( $source->getRawData( get_post_meta( $chart_id, Visualizer_Plugin::CF_EDITABLE_TABLE, true ) ) );
297 308 $render->series = json_encode( $source->getSeries() );
298 309 $render->render();
299 310
300 - defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
311 + ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
301 312 }
302 313
303 314
304 315 /**
@@ -310,8 +321,14 @@
310 321 *
311 322 * @access public
312 323 */
313 324 public function getCharts() {
325 + check_ajax_referer( Visualizer_Plugin::ACTION_GET_CHARTS, 'nonce' );
326 +
327 + if ( ! current_user_can( 'edit_posts' ) ) {
328 + wp_send_json_error( array( 'msg' => esc_html__( 'You do not have permission to perform this action.', 'visualizer' ) ), 403 );
329 + }
330 +
314 331 $query_args = array(
315 332 'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
316 333 'posts_per_page' => 9,
317 334 'paged' => filter_input(
@@ -325,8 +342,11 @@
325 342 ),
326 343 )
327 344 ),
328 345 );
346 + if ( ! current_user_can( 'edit_others_posts' ) ) {
347 + $query_args['author'] = get_current_user_id();
348 + }
329 349 $filter = filter_input( INPUT_GET, 's', FILTER_SANITIZE_STRING );
330 350 if ( empty( $filter ) ) {
331 351 // 'filter' is from the modal from the add media button.
332 352 $filter = filter_input( INPUT_GET, 'filter', FILTER_SANITIZE_STRING );
@@ -374,13 +394,13 @@
374 394 * @since 1.0.0
375 395 *
376 396 * @access private
377 397 *
378 - * @param WP_Post $chart The chart object.
398 + * @param WP_Post|null $chart The chart object.
379 399 *
380 400 * @return array The array of chart data.
381 401 */
382 - private function _getChartArray( WP_Post $chart = null ) {
402 + private function _getChartArray( $chart = null ) {
383 403 if ( is_null( $chart ) ) {
384 404 $chart = $this->_chart;
385 405 }
386 406 $type = get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_TYPE, true );
@@ -402,8 +422,13 @@
402 422 }
403 423
404 424 $date_formats = Visualizer_Source::get_date_formats_if_exists( $series, $data );
405 425
426 + $code = '';
427 + if ( 'd3' === $library ) {
428 + $code = get_post_meta( $chart->ID, Visualizer_Module_AIBuilder::CF_D3_CODE, true );
429 + }
430 +
406 431 return array(
407 432 'type' => $type,
408 433 'series' => $series,
409 434 'settings' => $settings,
@@ -408,8 +433,9 @@
408 433 'series' => $series,
409 434 'settings' => $settings,
410 435 'data' => $data,
411 436 'library' => $library,
437 + 'code' => $code,
412 438 'css' => $css,
413 439 'date_formats' => $date_formats,
414 440 );
415 441 }
@@ -426,9 +452,9 @@
426 452 public static function _sendResponse( $results ) {
427 453 header( 'Content-type: application/json' );
428 454 nocache_headers();
429 455 echo json_encode( $results );
430 - defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
456 + ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
431 457 }
432 458
433 459 /**
434 460 * Deletes a chart from database.
@@ -439,16 +465,14 @@
439 465 * @access public
440 466 */
441 467 public function deleteChart() {
442 468 $is_post = $_SERVER['REQUEST_METHOD'] === 'POST';
443 - $input_method = $is_post ? INPUT_POST : INPUT_GET;
469 + $input = $is_post ? $_POST : $_GET;
444 470 $chart_id = $success = false;
445 - $nonce = wp_verify_nonce( filter_input( $input_method, 'nonce' ) );
446 - $capable = current_user_can( 'delete_posts' );
447 - if ( $nonce && $capable ) {
448 - $chart_id = filter_input(
449 - $input_method,
450 - 'chart',
471 + $nonce = isset( $input['nonce'] ) && wp_verify_nonce( $input['nonce'] );
472 + if ( $nonce ) {
473 + $chart_id = isset( $input['chart'] ) ? filter_var(
474 + $input['chart'],
451 475 FILTER_VALIDATE_INT,
452 476 array(
453 477 'options' => array(
454 478 'min_range' => 1,
@@ -453,12 +477,17 @@
453 477 'options' => array(
454 478 'min_range' => 1,
455 479 ),
456 480 )
457 - );
481 + ) : false;
458 482 if ( $chart_id ) {
459 483 $chart = get_post( $chart_id );
460 - $success = $chart && $chart->post_type === Visualizer_Plugin::CPT_VISUALIZER;
484 + $success = $chart
485 + && $chart->post_type === Visualizer_Plugin::CPT_VISUALIZER
486 + && (
487 + current_user_can( 'delete_post', $chart_id )
488 + || ( (int) $chart->post_author === get_current_user_id() && current_user_can( 'delete_posts' ) )
489 + );
461 490 }
462 491 }
463 492 if ( $success ) {
464 493 global $sitepress;
@@ -523,8 +552,12 @@
523 552 *
524 553 * @access public
525 554 */
526 555 public function renderChartPages() {
556 + if ( ! current_user_can( 'edit_posts' ) ) {
557 + wp_die( __( 'You do not have permission to access this page.', 'visualizer' ) );
558 + }
559 +
527 560 defined( 'IFRAME_REQUEST' ) || define( 'IFRAME_REQUEST', 1 );
528 561 if ( ! defined( 'ET_BUILDER_PRODUCT_VERSION' ) && function_exists( 'et_get_theme_version' ) ) {
529 562 define( 'ET_BUILDER_PRODUCT_VERSION', et_get_theme_version() );
530 563 }
@@ -534,9 +567,13 @@
534 567 $chart_id = isset( $_GET['chart'] ) ? filter_var( $_GET['chart'], FILTER_VALIDATE_INT ) : '';
535 568 if ( ! empty( $_POST ) ) {
536 569 $_POST = map_deep( $_POST, 'wp_strip_all_tags' );
537 570 }
538 - if ( ! $chart_id || ! ( $chart = get_post( $chart_id ) ) || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) {
571 + $chart = $chart_id ? get_post( $chart_id ) : null;
572 + if ( $chart && ! self::can_edit_chart( $chart_id ) ) {
573 + wp_die( esc_html__( 'You do not have permission to access this page.', 'visualizer' ), '', array( 'response' => 403 ) );
574 + }
575 + if ( ! $chart_id || ! $chart || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) {
539 576 if ( empty( $_GET['lang'] ) || empty( $_GET['parent_chart_id'] ) ) {
540 577 $this->deleteOldCharts();
541 578 $default_type = isset( $_GET['type'] ) && ! empty( $_GET['type'] ) ? $_GET['type'] : 'line';
542 579 $chart_status = Visualizer_Module_Admin::checkChartStatus( $default_type );
@@ -570,35 +607,33 @@
570 607
571 608 do_action( 'visualizer_pro_new_chart_defaults', $chart_id );
572 609 }
573 610 } else {
574 - if ( current_user_can( 'edit_posts' ) ) {
575 - $parent_chart_id = isset( $_GET['parent_chart_id'] ) ? filter_var( $_GET['parent_chart_id'], FILTER_VALIDATE_INT ) : '';
576 - $success = false;
577 - if ( $parent_chart_id ) {
578 - $parent_chart = get_post( $parent_chart_id );
579 - $success = $parent_chart && $parent_chart->post_type === Visualizer_Plugin::CPT_VISUALIZER;
580 - }
581 - if ( $success ) {
582 - $new_chart_id = wp_insert_post(
583 - array(
584 - 'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
585 - 'post_title' => 'Visualization',
586 - 'post_author' => get_current_user_id(),
587 - 'post_status' => $parent_chart->post_status,
588 - 'post_content' => $parent_chart->post_content,
589 - )
590 - );
611 + $parent_chart_id = filter_var( $_GET['parent_chart_id'], FILTER_VALIDATE_INT );
612 + $success = false;
613 + if ( $parent_chart_id && self::can_edit_chart( $parent_chart_id ) ) {
614 + $parent_chart = get_post( $parent_chart_id );
615 + $success = $parent_chart && $parent_chart->post_type === Visualizer_Plugin::CPT_VISUALIZER;
616 + }
617 + if ( $success ) {
618 + $new_chart_id = wp_insert_post(
619 + array(
620 + 'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
621 + 'post_title' => 'Visualization',
622 + 'post_author' => get_current_user_id(),
623 + 'post_status' => $parent_chart->post_status,
624 + 'post_content' => $parent_chart->post_content,
625 + )
626 + );
591 627
592 - if ( is_wp_error( $new_chart_id ) ) {
593 - do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Error while cloning chart %d = %s', $parent_chart_id, print_r( $new_chart_id, true ) ), 'error', __FILE__, __LINE__ );
594 - } else {
595 - $post_meta = get_post_meta( $parent_chart_id );
596 - $chart_id = $new_chart_id;
597 - foreach ( $post_meta as $key => $value ) {
598 - if ( strpos( $key, 'visualizer-' ) !== false ) {
599 - add_post_meta( $new_chart_id, $key, maybe_unserialize( $value[0] ) );
600 - }
628 + if ( is_wp_error( $new_chart_id ) ) {
629 + do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Error while cloning chart %d = %s', $parent_chart_id, print_r( $new_chart_id, true ) ), 'error', __FILE__, __LINE__ );
630 + } else {
631 + $post_meta = get_post_meta( $parent_chart_id );
632 + $chart_id = $new_chart_id;
633 + foreach ( $post_meta as $key => $value ) {
634 + if ( strpos( $key, 'visualizer-' ) !== false ) {
635 + add_post_meta( $new_chart_id, $key, self::maybe_decode_content( $value[0] ) );
601 636 }
602 637 }
603 638 }
604 639 }
@@ -605,9 +640,9 @@
605 640 do_action( 'visualizer_pro_new_chart_defaults', $chart_id );
606 641 }
607 642 wp_redirect( esc_url_raw( add_query_arg( 'chart', (int) $chart_id ) ) );
608 643
609 - if ( defined( 'WP_TESTS_DOMAIN' ) ) {
644 + if ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) {
610 645 wp_die();
611 646 }
612 647 exit();
613 648 }
@@ -703,9 +738,9 @@
703 738 default:
704 739 // this should never happen.
705 740 break;
706 741 }
707 - defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
742 + ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
708 743 }
709 744
710 745 /**
711 746 * Load code editor assets.
@@ -735,9 +770,9 @@
735 770 wp_register_script( 'visualizer-codemirror-matchbrackets', '//codemirror.net/addon/edit/matchbrackets.js', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
736 771 wp_register_script( 'visualizer-codemirror-closebrackets', '//codemirror.net/addon/edit/closebrackets.js', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
737 772 wp_register_script( 'visualizer-codemirror-sql', '//codemirror.net/mode/sql/sql.js', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
738 773 wp_register_script( 'visualizer-codemirror-sql-hint', '//codemirror.net/addon/hint/sql-hint.js', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
739 - 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 );
774 + 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 );
740 775 wp_register_style( 'visualizer-codemirror-core', '//codemirror.net/lib/codemirror.css', array(), Visualizer_Plugin::VERSION );
741 776 wp_register_style( 'visualizer-codemirror-hint', '//codemirror.net/addon/hint/show-hint.css', array( 'visualizer-codemirror-core' ), Visualizer_Plugin::VERSION );
742 777
743 778 wp_enqueue_script( 'visualizer-codemirror-hint' );
@@ -751,9 +786,9 @@
751 786 'lineWrapping' => true,
752 787 'dragDrop' => false,
753 788 'matchBrackets' => true,
754 789 'autoCloseBrackets' => true,
755 - 'extraKeys' => array( 'Ctrl-Space' => 'autocomplete' ),
790 + 'extraKeys' => array( 'Shift-Space' => 'autocomplete' ),
756 791 'hintOptions' => array( 'tables' => $table_col_mapping ),
757 792 ),
758 793 )
759 794 );
@@ -782,14 +817,17 @@
782 817 /**
783 818 * Handle data and settings page
784 819 */
785 820 private function _handleDataAndSettingsPage() {
786 - if ( isset( $_POST['map_api_key'] ) ) {
787 - update_option( 'visualizer-map-api-key', $_POST['map_api_key'] );
788 - }
821 + if ( $_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'] ) ) {
822 + $is_canceled = isset( $_POST['cancel'] ) && 1 === intval( $_POST['cancel'] );
823 + $is_newly_created = $this->_chart->post_status === 'auto-draft';
789 824
790 - if ( $_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'] ) ) {
791 - if ( $this->_chart->post_status === 'auto-draft' ) {
825 + if ( isset( $_POST['map_api_key'] ) && current_user_can( 'manage_options' ) ) {
826 + update_option( 'visualizer-map-api-key', sanitize_text_field( wp_unslash( $_POST['map_api_key'] ) ) );
827 + }
828 +
829 + if ( $is_newly_created && ! $is_canceled ) {
792 830 $this->_chart->post_status = 'publish';
793 831
794 832 // ensure that a revision is not created. If a revision is created it will have the proper data and the parent of the revision will have default data.
795 833 // we do not want any difference in data so disable revisions temporarily.
@@ -797,10 +835,16 @@
797 835
798 836 wp_update_post( $this->_chart->to_array() );
799 837 }
800 838 // save meta data only when it is NOT being canceled.
801 - if ( ! ( isset( $_POST['cancel'] ) && 1 === intval( $_POST['cancel'] ) ) ) {
802 - update_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, $_POST );
839 + if ( ! $is_canceled ) {
840 + $post_settings = $_POST;
841 + $existing = get_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, true );
842 + if ( isset( $existing['colors'] ) && is_array( $existing['colors'] ) && ! isset( $post_settings['colors'] ) ) {
843 + $post_settings['colors'] = $existing['colors'];
844 + }
845 + $post_settings = $this->sanitizeSettings( $post_settings );
846 + update_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, $post_settings );
803 847
804 848 // we will keep a parameter called 'internal_title' that will be set to the given title or, if empty, the chart ID
805 849 // this will help in searching with the chart id.
806 850 $settings = get_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, true );
@@ -847,8 +891,9 @@
847 891 wp_enqueue_style( 'visualizer-frame' );
848 892 wp_enqueue_script( 'visualizer-preview' );
849 893 wp_enqueue_script( 'visualizer-chosen' );
850 894 wp_enqueue_script( 'visualizer-render' );
895 + wp_enqueue_code_editor( array( 'type' => 'application/json' ) );
851 896
852 897 if ( Visualizer_Module::can_show_feature( 'simple-editor' ) ) {
853 898 wp_enqueue_script( 'visualizer-editor-simple' );
854 899 wp_localize_script(
@@ -856,12 +901,10 @@
856 901 'visualizer1',
857 902 array(
858 903 'ajax' => array(
859 904 'url' => admin_url( 'admin-ajax.php' ),
860 - 'nonces' => array(
861 - ),
862 - 'actions' => array(
863 - ),
905 + 'nonces' => array(),
906 + 'actions' => array(),
864 907 ),
865 908 )
866 909 );
867 910 }
@@ -872,13 +915,15 @@
872 915 'visualizer-render',
873 916 'visualizer',
874 917 array(
875 918 'l10n' => array(
876 - 'invalid_source' => esc_html__( 'You have entered an invalid URL. Please provide a valid URL.', 'visualizer' ),
877 - 'loading' => esc_html__( 'Loading...', 'visualizer' ),
878 - 'json_error' => esc_html__( 'An error occured in fetching data.', 'visualizer' ),
879 - 'select_columns' => esc_html__( 'Please select a few columns to include in the chart.', 'visualizer' ),
880 - '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' ),
919 + 'invalid_source' => esc_html__( 'The URL you entered is invalid. Please enter a valid URL.', 'visualizer' ),
920 + 'loading' => esc_html__( 'Loading...', 'visualizer' ),
921 + 'json_error' => esc_html__( 'An error occured in fetching data.', 'visualizer' ),
922 + 'select_columns' => esc_html__( 'Please select a few columns to include in the chart.', 'visualizer' ),
923 + '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' ),
924 + 'copied' => __( 'The data has been copied to your clipboard. Hit Ctrl-V/Cmd-V in your spreadsheet editor to paste the data.', 'visualizer' ),
925 + 'invalid_format' => esc_html__( 'This format pattern is not supported in the series settings field. Use the Manual Configuration option instead.', 'visualizer' ),
881 926 ),
882 927 'charts' => array(
883 928 'canvas' => $data,
884 929 'id' => $this->_chart->ID,
@@ -922,11 +967,10 @@
922 967 if ( filter_input( INPUT_GET, 'library', FILTER_VALIDATE_BOOLEAN ) ) {
923 968 $render->button = filter_input( INPUT_GET, 'action' ) === Visualizer_Plugin::ACTION_EDIT_CHART
924 969 ? esc_html__( 'Save Chart', 'visualizer' )
925 970 : esc_html__( 'Create Chart', 'visualizer' );
926 - if ( filter_input( INPUT_GET, 'action' ) === Visualizer_Plugin::ACTION_EDIT_CHART ) {
927 - $render->cancel_button = esc_html__( 'Cancel', 'visualizer' );
928 - }
971 +
972 + $render->cancel_button = esc_html__( 'Cancel', 'visualizer' );
929 973 } else {
930 974 $render->button = esc_attr__( 'Insert Chart', 'visualizer' );
931 975 }
932 976
@@ -949,9 +993,9 @@
949 993 * @access private
950 994 */
951 995 private function _handleTypesPage() {
952 996 // process post request
953 - if ( $_SERVER['REQUEST_METHOD'] === 'POST' && wp_verify_nonce( filter_input( INPUT_POST, 'nonce' ) ) ) {
997 + if ( $_SERVER['REQUEST_METHOD'] === 'POST' && wp_verify_nonce( filter_input( INPUT_POST, 'nonce' ), 'visualizer-upload-data' ) ) {
954 998 $type = filter_input( INPUT_POST, 'type' );
955 999 $library = filter_input( INPUT_POST, 'chart-library' );
956 1000 if ( Visualizer_Module_Admin::checkChartStatus( $type ) ) {
957 1001 if ( empty( $library ) ) {
@@ -990,8 +1034,35 @@
990 1034 wp_iframe( array( $render, 'render' ) );
991 1035 }
992 1036
993 1037 /**
1038 + * Sanitize settings data from the request.
1039 + *
1040 + * @param array<string, mixed> $post_data The POST data to sanitize.
1041 + * @return array<string, mixed> The sanitized settings data.
1042 + */
1043 + private function sanitizeSettings( $post_data ): array {
1044 + $chart_img = '';
1045 + if ( isset( $post_data['chart-img'] ) ) {
1046 + $chart_img = wp_unslash( $post_data['chart-img'] );
1047 + unset( $post_data['chart-img'] );
1048 + }
1049 +
1050 + $post_data = map_deep(
1051 + $post_data,
1052 + 'sanitize_textarea_field'
1053 + );
1054 +
1055 + // The value is a client-side canvas export; keep it only when it is a
1056 + // base64 image data URI so nothing else is ever stored unsanitized.
1057 + if ( is_string( $chart_img ) && preg_match( '#^data:image/(png|jpeg|webp);base64,[A-Za-z0-9+/ ]+=*$#', $chart_img ) ) {
1058 + $post_data['chart-img'] = $chart_img;
1059 + }
1060 +
1061 + return $post_data;
1062 + }
1063 +
1064 + /**
994 1065 * Renders flattr script in the iframe <head>
995 1066 *
996 1067 * @since 1.4.2
997 1068 * @action admin_head
@@ -1006,8 +1077,81 @@
1006 1077 * Processes the CSV that is sent in the request as a string.
1007 1078 *
1008 1079 * @since 3.2.0
1009 1080 */
1081 + /**
1082 + * Determines whether a remote URL serves an XLSX file.
1083 + *
1084 + * Used as a fallback when the URL path has no recognisable file extension
1085 + * (e.g. SharePoint, signed S3 URLs, or "download?id=…" endpoints).
1086 + *
1087 + * Uses the shared remote-fetch policy to block non-public destinations,
1088 + * and streams the response to a temp file so no body data is held in memory
1089 + * regardless of whether the server honours the Range header.
1090 + *
1091 + * The check relies on the ZIP magic number (PK\x03\x04) that every XLSX
1092 + * file begins with, making it immune to misleading Content-Type headers
1093 + * such as application/octet-stream. Content-Type is used as a last-resort
1094 + * fallback only when the temp file is empty (e.g. a HEAD-only server).
1095 + *
1096 + * @access private
1097 + * @param string $url The remote URL to probe.
1098 + * @return bool TRUE if the file appears to be XLSX, FALSE otherwise.
1099 + */
1100 + private static function _url_is_xlsx( $url ) {
1101 + $tmpfile = wp_tempnam( 'visualizer_xlsx_probe' );
1102 + if ( ! $tmpfile ) {
1103 + return false;
1104 + }
1105 +
1106 + $response = Visualizer_Remote_Fetch::request(
1107 + $url,
1108 + array(
1109 + 'timeout' => 10,
1110 + 'user-agent' => 'WordPress/' . get_bloginfo( 'version' ),
1111 + 'headers' => array( 'Range' => 'bytes=0-3' ),
1112 + 'stream' => true,
1113 + 'filename' => $tmpfile,
1114 + 'limit_response_size' => 4,
1115 + )
1116 + );
1117 +
1118 + if ( is_wp_error( $response ) ) {
1119 + @unlink( $tmpfile ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
1120 + return false;
1121 + }
1122 +
1123 + $magic = '';
1124 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
1125 + $fh = @fopen( $tmpfile, 'rb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
1126 + if ( $fh ) {
1127 + $magic = fread( $fh, 4 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
1128 + fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1129 + }
1130 + @unlink( $tmpfile ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
1131 +
1132 + if ( strlen( $magic ) >= 4 ) {
1133 + // XLSX (and all ZIP-based Office formats) start with PK\x03\x04.
1134 + return $magic === "PK\x03\x04";
1135 + }
1136 +
1137 + // Last resort: server returned an empty body (e.g. ignored Range and
1138 + // returned only headers). Check Content-Type from the same response.
1139 + // application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
1140 + return false !== strpos(
1141 + wp_remote_retrieve_header( $response, 'content-type' ),
1142 + 'spreadsheetml'
1143 + );
1144 + }
1145 +
1146 + /**
1147 + * Parses a raw CSV string or editor payload and returns a source object.
1148 + *
1149 + * @access private
1150 + * @param string $data The raw CSV data string.
1151 + * @param string $editor_type The editor type ('text' or 'tabular').
1152 + * @return Visualizer_Source|null The populated source object, or null on failure.
1153 + */
1010 1154 private function handleCSVasString( $data, $editor_type ) {
1011 1155 $source = null;
1012 1156
1013 1157 switch ( $editor_type ) {
@@ -1022,9 +1166,9 @@
1022 1166 continue;
1023 1167 }
1024 1168 $row = explode( ',', $row );
1025 1169 $row = array_map(
1026 - function( $r ) {
1170 + function ( $r ) {
1027 1171 return '' === $r ? ' ' : $r;
1028 1172 },
1029 1173 $row
1030 1174 );
@@ -1073,9 +1217,9 @@
1073 1217 foreach ( $types as $type ) {
1074 1218 if ( empty( $type ) ) {
1075 1219 $exclude[] = $index;
1076 1220 }
1077 - $index++;
1221 + ++$index;
1078 1222 }
1079 1223
1080 1224 // when N headers are being renamed, the number of headers increases by N
1081 1225 // because of the way datatable duplicates header information
@@ -1133,11 +1277,16 @@
1133 1277 public function uploadData() {
1134 1278 // if this is being called internally from pro and VISUALIZER_DO_NOT_DIE is set.
1135 1279 // otherwise, assume this is a normal web request.
1136 1280 $can_die = ! ( defined( 'VISUALIZER_DO_NOT_DIE' ) && VISUALIZER_DO_NOT_DIE );
1281 + // $can_die also gates the capability checks below, so VISUALIZER_DO_NOT_DIE must stay internal-only (never set from request input or globally).
1137 1282
1138 - // validate nonce
1139 - if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'] ) ) {
1283 + // validate nonce; capability check applies to web requests only, not trusted internal calls.
1284 + if (
1285 + ! isset( $_GET['nonce'] ) ||
1286 + ! wp_verify_nonce( $_GET['nonce'], 'visualizer-upload-data' ) ||
1287 + ( $can_die && ! current_user_can( 'edit_posts' ) )
1288 + ) {
1140 1289 if ( ! $can_die ) {
1141 1290 return;
1142 1291 }
1143 1292 status_header( 403 );
@@ -1146,9 +1295,15 @@
1146 1295
1147 1296 // check chart, if chart exists
1148 1297 // do not use filter_input as it does not work for phpunit test cases, use filter_var instead
1149 1298 $chart_id = isset( $_GET['chart'] ) ? filter_var( $_GET['chart'], FILTER_VALIDATE_INT ) : '';
1150 - if ( ! $chart_id || ! ( $chart = get_post( $chart_id ) ) || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) {
1299 + $chart = $chart_id ? get_post( $chart_id ) : null;
1300 + if (
1301 + ! $chart_id ||
1302 + ! $chart ||
1303 + $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ||
1304 + ( $can_die && ! current_user_can( 'edit_post', $chart_id ) )
1305 + ) {
1151 1306 if ( ! $can_die ) {
1152 1307 return;
1153 1308 }
1154 1309 status_header( 400 );
@@ -1195,15 +1350,24 @@
1195 1350 if ( isset( $_POST['remote_data'] ) && function_exists( 'wp_http_validate_url' ) ) {
1196 1351 $remote_data = wp_http_validate_url( $_POST['remote_data'] );
1197 1352 }
1198 1353 if ( false !== $remote_data ) {
1199 - $source = new Visualizer_Source_Csv_Remote( $remote_data );
1354 + $remote_ext = strtolower( pathinfo( parse_url( $remote_data, PHP_URL_PATH ), PATHINFO_EXTENSION ) );
1355 + if ( 'xlsx' === $remote_ext || ( 'csv' !== $remote_ext && self::_url_is_xlsx( $remote_data ) ) ) {
1356 + $source = new Visualizer_Source_Xlsx_Remote( $remote_data );
1357 + } else {
1358 + $source = new Visualizer_Source_Csv_Remote( $remote_data );
1359 + }
1200 1360 if ( isset( $_POST['vz-import-time'] ) ) {
1201 1361 apply_filters( 'visualizer_pro_chart_schedule', $chart_id, $remote_data, $_POST['vz-import-time'] );
1202 1362 }
1203 - // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
1204 - } elseif ( isset( $_FILES['local_data'] ) && $_FILES['local_data']['error'] == 0 ) {
1205 - $source = new Visualizer_Source_Csv( $_FILES['local_data']['tmp_name'] );
1363 + } elseif ( isset( $_FILES['local_data'] ) && $_FILES['local_data']['error'] === 0 ) {
1364 + $local_ext = strtolower( pathinfo( isset( $_FILES['local_data']['name'] ) ? $_FILES['local_data']['name'] : '', PATHINFO_EXTENSION ) );
1365 + if ( 'xlsx' === $local_ext ) {
1366 + $source = new Visualizer_Source_Xlsx( $_FILES['local_data']['tmp_name'] );
1367 + } else {
1368 + $source = new Visualizer_Source_Csv( $_FILES['local_data']['tmp_name'] );
1369 + }
1206 1370 } elseif ( isset( $_POST['chart_data'] ) && strlen( $_POST['chart_data'] ) > 0 ) {
1207 1371 $source = $this->handleCSVasString( $_POST['chart_data'], $_POST['editor-type'] );
1208 1372 update_post_meta( $chart_id, Visualizer_Plugin::CF_EDITOR, $_POST['editor-type'] );
1209 1373 } elseif ( isset( $_POST['table_data'] ) && 'yes' === $_POST['table_data'] ) {
@@ -1210,10 +1374,10 @@
1210 1374 $source = $this->handleTabularData();
1211 1375 update_post_meta( $chart_id, Visualizer_Plugin::CF_EDITOR, $_POST['editor-type'] );
1212 1376 } else {
1213 1377 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__ );
1214 - $render->message = esc_html__( 'CSV file with chart data was not uploaded. Please try again.', 'visualizer' );
1215 - update_post_meta( $chart_id, Visualizer_Plugin::CF_ERROR, esc_html__( 'CSV file with chart data was not uploaded. Please try again.', 'visualizer' ) );
1378 + $render->message = esc_html__( 'No CSV file was received. Select a file and try uploading again.', 'visualizer' );
1379 + update_post_meta( $chart_id, Visualizer_Plugin::CF_ERROR, esc_html__( 'No CSV file was received. Select a file and try uploading again.', 'visualizer' ) );
1216 1380 }
1217 1381
1218 1382 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__ );
1219 1383
@@ -1220,10 +1384,10 @@
1220 1384 if ( $source ) {
1221 1385 if ( $source->fetch() ) {
1222 1386 $content = $source->getData( get_post_meta( $chart_id, Visualizer_Plugin::CF_EDITABLE_TABLE, true ) );
1223 1387 $populate = true;
1224 - if ( is_string( $content ) && is_array( unserialize( $content ) ) ) {
1225 - $json = unserialize( $content );
1388 + $json = self::decode_content( $content );
1389 + if ( is_array( $json ) ) {
1226 1390 // if source exists, so should data. if source exists but data is blank, do not populate the chart.
1227 1391 // if we populate the data even if it is empty, the chart will show "Table has no columns".
1228 1392 if ( array_key_exists( 'source', $json ) && ! empty( $json['source'] ) && ( ! array_key_exists( 'data', $json ) || empty( $json['data'] ) ) ) {
1229 1393 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Not populating chart data as source exists (%s) but data is empty!', $json['source'] ), 'warn', __FILE__, __LINE__ );
@@ -1261,9 +1425,9 @@
1261 1425 $render->settings = json_encode( $settings );
1262 1426 } else {
1263 1427 $error = $source->get_error();
1264 1428 if ( empty( $error ) ) {
1265 - $error = esc_html__( 'CSV file is broken or invalid. Please try again.', 'visualizer' );
1429 + $error = esc_html__( 'The CSV file couldn\'t be read. Check that it\'s properly formatted and try again.', 'visualizer' );
1266 1430 }
1267 1431 $render->message = $error;
1268 1432 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( '%s for chart %d.', $error, $chart_id ), 'error', __FILE__, __LINE__ );
1269 1433 update_post_meta( $chart_id, Visualizer_Plugin::CF_ERROR, $error );
@@ -1274,9 +1438,9 @@
1274 1438 $render->render();
1275 1439 if ( ! $can_die ) {
1276 1440 return;
1277 1441 }
1278 - defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
1442 + ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
1279 1443 }
1280 1444
1281 1445 /**
1282 1446 * Clones the chart.
@@ -1287,12 +1451,11 @@
1287 1451 */
1288 1452 public function cloneChart() {
1289 1453 $chart_id = $success = false;
1290 1454 $nonce = isset( $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'], Visualizer_Plugin::ACTION_CLONE_CHART );
1291 - $capable = current_user_can( 'edit_posts' );
1292 - if ( $nonce && $capable ) {
1455 + if ( $nonce ) {
1293 1456 $chart_id = isset( $_GET['chart'] ) ? filter_var( $_GET['chart'], FILTER_VALIDATE_INT ) : '';
1294 - if ( $chart_id ) {
1457 + if ( $chart_id && self::can_edit_chart( $chart_id ) ) {
1295 1458 $chart = get_post( $chart_id );
1296 1459 $success = $chart && $chart->post_type === Visualizer_Plugin::CPT_VISUALIZER;
1297 1460 }
1298 1461 }
@@ -1312,9 +1475,9 @@
1312 1475 } else {
1313 1476 $post_meta = get_post_meta( $chart_id );
1314 1477 foreach ( $post_meta as $key => $value ) {
1315 1478 if ( strpos( $key, 'visualizer-' ) !== false ) {
1316 - add_post_meta( $new_chart_id, $key, maybe_unserialize( $value[0] ) );
1479 + add_post_meta( $new_chart_id, $key, self::maybe_decode_content( $value[0] ) );
1317 1480 }
1318 1481 }
1319 1482 $redirect = esc_url(
1320 1483 add_query_arg(
@@ -1330,9 +1493,9 @@
1330 1493 );
1331 1494 }
1332 1495 }
1333 1496
1334 - if ( defined( 'WP_TESTS_DOMAIN' ) ) {
1497 + if ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) {
1335 1498 wp_die();
1336 1499 }
1337 1500 wp_redirect( $redirect );
1338 1501 exit;
@@ -1346,28 +1509,25 @@
1346 1509 * @access public
1347 1510 */
1348 1511 public function exportData() {
1349 1512 check_ajax_referer( Visualizer_Plugin::ACTION_EXPORT_DATA . Visualizer_Plugin::VERSION, 'security' );
1350 - $capable = current_user_can( 'edit_posts' );
1351 - if ( $capable ) {
1352 - $chart_id = isset( $_GET['chart'] ) ? filter_var(
1353 - $_GET['chart'],
1354 - FILTER_VALIDATE_INT,
1355 - array(
1356 - 'options' => array(
1357 - 'min_range' => 1,
1358 - ),
1359 - )
1360 - ) : '';
1361 - if ( $chart_id ) {
1362 - $data = $this->_getDataAs( $chart_id, 'csv' );
1363 - if ( $data ) {
1364 - echo wp_send_json_success( $data );
1365 - }
1513 + $chart_id = isset( $_GET['chart'] ) ? filter_var(
1514 + $_GET['chart'],
1515 + FILTER_VALIDATE_INT,
1516 + array(
1517 + 'options' => array(
1518 + 'min_range' => 1,
1519 + ),
1520 + )
1521 + ) : '';
1522 + if ( $chart_id && self::can_edit_chart( $chart_id ) ) {
1523 + $data = $this->_getDataAs( $chart_id, 'csv' );
1524 + if ( $data ) {
1525 + echo wp_send_json_success( $data );
1366 1526 }
1367 1527 }
1368 1528
1369 - defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
1529 + ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
1370 1530 }
1371 1531
1372 1532 /**
1373 1533 * Handles chart data page.
@@ -1391,10 +1551,11 @@
1391 1551 'visualizer-render',
1392 1552 'visualizer',
1393 1553 array(
1394 1554 'l10n' => array(
1395 - 'invalid_source' => esc_html__( 'You have entered an invalid URL. Please provide a valid URL.', 'visualizer' ),
1396 - 'loading' => esc_html__( 'Loading...', 'visualizer' ),
1555 + 'invalid_source' => esc_html__( 'The URL you entered is invalid. Please enter a valid URL.', 'visualizer' ),
1556 + 'loading' => esc_html__( 'Loading...', 'visualizer' ),
1557 + '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' ),
1397 1558 ),
1398 1559 'charts' => array(
1399 1560 'canvas' => $data,
1400 1561 ),
@@ -1420,12 +1581,24 @@
1420 1581 */
1421 1582 public function getQueryData() {
1422 1583 check_ajax_referer( Visualizer_Plugin::ACTION_FETCH_DB_DATA . Visualizer_Plugin::VERSION, 'security' );
1423 1584
1585 + if ( ! current_user_can( 'administrator' ) ) {
1586 + wp_send_json_error( array( 'msg' => __( 'Action not allowed for this user.', 'visualizer' ) ) );
1587 + }
1588 + if ( ! is_super_admin() ) {
1589 + wp_send_json_error( array( 'msg' => __( 'Action not allowed for this user.', 'visualizer' ) ) );
1590 + }
1591 +
1592 + if ( ! Visualizer_Module::is_pro() ) {
1593 + wp_send_json_error( array( 'msg' => __( 'Feature is not available.', 'visualizer' ) ) );
1594 + }
1595 +
1424 1596 $params = wp_parse_args( $_POST['params'] );
1425 1597 $chart_id = filter_var( $params['chart_id'], FILTER_VALIDATE_INT );
1598 + $query = trim( $params['query'], ';' );
1426 1599
1427 - $source = new Visualizer_Source_Query( stripslashes( $params['query'] ), $chart_id, $params );
1600 + $source = new Visualizer_Source_Query( stripslashes( $query ), $chart_id, $params );
1428 1601 $html = $source->fetch( true );
1429 1602 $error = $source->get_error();
1430 1603 if ( ! empty( $error ) ) {
1431 1604 wp_send_json_error( array( 'msg' => $error ) );
@@ -1440,8 +1613,19 @@
1440 1613 */
1441 1614 public function saveQuery() {
1442 1615 check_ajax_referer( Visualizer_Plugin::ACTION_SAVE_DB_QUERY . Visualizer_Plugin::VERSION, 'security' );
1443 1616
1617 + if ( ! current_user_can( 'administrator' ) ) {
1618 + wp_send_json_error( array( 'msg' => __( 'Action not allowed for this user.', 'visualizer' ) ) );
1619 + }
1620 + if ( ! is_super_admin() ) {
1621 + wp_send_json_error( array( 'msg' => __( 'Action not allowed for this user.', 'visualizer' ) ) );
1622 + }
1623 +
1624 + if ( ! Visualizer_Module::is_pro() ) {
1625 + wp_send_json_error( array( 'msg' => __( 'Feature is not available.', 'visualizer' ) ) );
1626 + }
1627 +
1444 1628 $chart_id = filter_input(
1445 1629 INPUT_GET,
1446 1630 'chart',
1447 1631 FILTER_VALIDATE_INT,
@@ -1470,13 +1654,14 @@
1470 1654
1471 1655 $render = new Visualizer_Render_Page_Update();
1472 1656 if ( $chart_id ) {
1473 1657 $params = wp_parse_args( $_POST['params'] );
1474 - $source = new Visualizer_Source_Query( stripslashes( $params['query'] ), $chart_id, $params );
1658 + $query = trim( $params['query'], ';' );
1659 + $source = new Visualizer_Source_Query( stripslashes( $query ), $chart_id, $params );
1475 1660 $source->fetch( false );
1476 1661 $error = $source->get_error();
1477 1662 if ( empty( $error ) ) {
1478 - update_post_meta( $chart_id, Visualizer_Plugin::CF_DB_QUERY, stripslashes( $params['query'] ) );
1663 + update_post_meta( $chart_id, Visualizer_Plugin::CF_DB_QUERY, stripslashes( $query ) );
1479 1664 update_post_meta( $chart_id, Visualizer_Plugin::CF_SOURCE, $source->getSourceName() );
1480 1665 update_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, $source->getSeries() );
1481 1666 update_post_meta( $chart_id, Visualizer_Plugin::CF_DB_SCHEDULE, $hours );
1482 1667 update_post_meta( $chart_id, Visualizer_Plugin::CF_DEFAULT_DATA, 0 );
@@ -1505,9 +1690,9 @@
1505 1690 }
1506 1691 }
1507 1692 $render->render();
1508 1693 if ( ! ( defined( 'VISUALIZER_DO_NOT_DIE' ) && VISUALIZER_DO_NOT_DIE ) ) {
1509 - defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
1694 + ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
1510 1695 }
1511 1696 }
1512 1697
1513 1698
@@ -1518,11 +1703,10 @@
1518 1703 */
1519 1704 public function saveFilter() {
1520 1705 check_ajax_referer( Visualizer_Plugin::ACTION_SAVE_FILTER_QUERY . Visualizer_Plugin::VERSION, 'security' );
1521 1706
1522 - $chart_id = filter_input(
1523 - INPUT_GET,
1524 - 'chart',
1707 + $chart_id = isset( $_GET['chart'] ) ? filter_var(
1708 + $_GET['chart'],
1525 1709 FILTER_VALIDATE_INT,
1526 1710 array(
1527 1711 'options' => array(
1528 1712 'min_range' => 1,
@@ -1527,14 +1711,18 @@
1527 1711 'options' => array(
1528 1712 'min_range' => 1,
1529 1713 ),
1530 1714 )
1531 - );
1715 + ) : false;
1532 1716
1717 + if ( ! self::can_edit_chart( $chart_id ) ) {
1718 + wp_send_json_error( array( 'msg' => esc_html__( 'You do not have permission to perform this action.', 'visualizer' ) ), 403 );
1719 + }
1720 +
1533 1721 $hours = filter_input(
1534 1722 INPUT_POST,
1535 1723 'refresh',
1536 - FILTER_VALIDATE_INT,
1724 + FILTER_VALIDATE_FLOAT,
1537 1725 array(
1538 1726 'options' => array(
1539 1727 'min_range' => -1,
1540 1728 'max_range' => apply_filters( 'visualizer_is_business', false ) ? PHP_INT_MAX : -1,
@@ -1541,9 +1729,9 @@
1541 1729 ),
1542 1730 )
1543 1731 );
1544 1732
1545 - if ( 0 !== $hours && empty( $hours ) ) {
1733 + if ( ! is_numeric( $hours ) ) {
1546 1734 $hours = -1;
1547 1735 }
1548 1736
1549 1737 do_action( 'visualizer_save_filter', $chart_id, $hours );
@@ -1548,9 +1736,9 @@
1548 1736
1549 1737 do_action( 'visualizer_save_filter', $chart_id, $hours );
1550 1738
1551 1739 if ( ! ( defined( 'VISUALIZER_DO_NOT_DIE' ) && VISUALIZER_DO_NOT_DIE ) ) {
1552 - defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
1740 + ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
1553 1741 }
1554 1742 }
1555 1743
1556 1744 /**
@@ -1558,9 +1746,9 @@
1558 1746 *
1559 1747 * @param string $base64_img Chart image.
1560 1748 * @param int $chart_id Chart ID.
1561 1749 * @param bool $save_attachment Save attachment.
1562 - * @return attachment ID
1750 + * @return int Attachment ID, or 0 when no attachment was saved.
1563 1751 */
1564 1752 public function save_chart_image( $base64_img, $chart_id, $save_attachment = true ) {
1565 1753 // Delete old chart image.
1566 1754 $old_attachment_id = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_IMAGE, true );
@@ -1575,11 +1763,15 @@
1575 1763 // Upload dir.
1576 1764 $upload_dir = wp_upload_dir();
1577 1765 $upload_path = str_replace( '/', DIRECTORY_SEPARATOR, $upload_dir['path'] ) . DIRECTORY_SEPARATOR;
1578 1766
1579 - $img = str_replace( 'data:image/png;base64,', '', $base64_img );
1580 - $img = str_replace( ' ', '+', $img );
1581 - $decoded = base64_decode( $img );
1767 + $img = str_replace( 'data:image/png;base64,', '', (string) $base64_img );
1768 + $img = str_replace( ' ', '+', $img );
1769 + $decoded = base64_decode( $img, true );
1770 + // The value comes from an untrusted request; only write real PNG bytes to uploads.
1771 + if ( false === $decoded || 0 !== strncmp( $decoded, "\x89PNG\r\n\x1a\n", 8 ) ) {
1772 + return 0;
1773 + }
1582 1774 $filename = 'visualization-' . $chart_id . '.png';
1583 1775 $file_type = 'image/png';
1584 1776 $hashed_filename = $filename;
1585 1777