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 +123 -63 4.0.34.0.8 View file →
@@ -107,11 +107,10 @@
107 107 */
108 108 public function setJsonSchedule() {
109 109 check_ajax_referer( Visualizer_Plugin::ACTION_JSON_SET_SCHEDULE . Visualizer_Plugin::VERSION, 'security' );
110 110
111 - $chart_id = filter_input(
112 - INPUT_POST,
113 - 'chart',
111 + $chart_id = isset( $_POST['chart'] ) ? filter_var(
112 + $_POST['chart'],
114 113 FILTER_VALIDATE_INT,
115 114 array(
116 115 'options' => array(
117 116 'min_range' => 1,
@@ -116,14 +115,18 @@
116 115 'options' => array(
117 116 'min_range' => 1,
118 117 ),
119 118 )
120 - );
119 + ) : false;
121 120
122 121 if ( ! $chart_id ) {
123 122 wp_send_json_error();
124 123 }
125 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 +
126 129 $time = filter_input(
127 130 INPUT_POST,
128 131 'time',
129 132 FILTER_VALIDATE_INT,
@@ -169,8 +172,12 @@
169 172 */
170 173 public function getJsonRoots() {
171 174 check_ajax_referer( Visualizer_Plugin::ACTION_JSON_GET_ROOTS . Visualizer_Plugin::VERSION, 'security' );
172 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 +
173 180 $params = wp_parse_args( $_POST['params'] );
174 181
175 182 $source = new Visualizer_Source_Json( $params );
176 183
@@ -191,13 +198,18 @@
191 198 */
192 199 public function getJsonData() {
193 200 check_ajax_referer( Visualizer_Plugin::ACTION_JSON_GET_DATA . Visualizer_Plugin::VERSION, 'security' );
194 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 +
195 206 $params = wp_parse_args( $_POST['params'] );
196 207
197 208 $chart_id = $params['chart'];
198 209
199 - 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 ) ) {
200 212 wp_die();
201 213 }
202 214
203 215 $source = new Visualizer_Source_Json( $params );
@@ -222,12 +234,12 @@
222 234 public function setJsonData() {
223 235 check_ajax_referer( Visualizer_Plugin::ACTION_JSON_SET_DATA . Visualizer_Plugin::VERSION, 'security' );
224 236
225 237 $params = $_POST;
226 - $chart_id = $_GET['chart'];
238 + $chart_id = isset( $_GET['chart'] ) ? absint( $_GET['chart'] ) : 0;
227 239
228 - if ( empty( $chart_id ) ) {
229 - 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 ) );
230 242 }
231 243
232 244 $chart = get_post( $chart_id );
233 245
@@ -309,8 +321,14 @@
309 321 *
310 322 * @access public
311 323 */
312 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 +
313 331 $query_args = array(
314 332 'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
315 333 'posts_per_page' => 9,
316 334 'paged' => filter_input(
@@ -324,8 +342,11 @@
324 342 ),
325 343 )
326 344 ),
327 345 );
346 + if ( ! current_user_can( 'edit_others_posts' ) ) {
347 + $query_args['author'] = get_current_user_id();
348 + }
328 349 $filter = filter_input( INPUT_GET, 's', FILTER_SANITIZE_STRING );
329 350 if ( empty( $filter ) ) {
330 351 // 'filter' is from the modal from the add media button.
331 352 $filter = filter_input( INPUT_GET, 'filter', FILTER_SANITIZE_STRING );
@@ -444,16 +465,14 @@
444 465 * @access public
445 466 */
446 467 public function deleteChart() {
447 468 $is_post = $_SERVER['REQUEST_METHOD'] === 'POST';
448 - $input_method = $is_post ? INPUT_POST : INPUT_GET;
469 + $input = $is_post ? $_POST : $_GET;
449 470 $chart_id = $success = false;
450 - $nonce = wp_verify_nonce( filter_input( $input_method, 'nonce' ) );
451 - $capable = current_user_can( 'delete_posts' );
452 - if ( $nonce && $capable ) {
453 - $chart_id = filter_input(
454 - $input_method,
455 - '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'],
456 475 FILTER_VALIDATE_INT,
457 476 array(
458 477 'options' => array(
459 478 'min_range' => 1,
@@ -458,12 +477,17 @@
458 477 'options' => array(
459 478 'min_range' => 1,
460 479 ),
461 480 )
462 - );
481 + ) : false;
463 482 if ( $chart_id ) {
464 483 $chart = get_post( $chart_id );
465 - $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 + );
466 490 }
467 491 }
468 492 if ( $success ) {
469 493 global $sitepress;
@@ -544,8 +568,11 @@
544 568 if ( ! empty( $_POST ) ) {
545 569 $_POST = map_deep( $_POST, 'wp_strip_all_tags' );
546 570 }
547 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 + }
548 575 if ( ! $chart_id || ! $chart || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) {
549 576 if ( empty( $_GET['lang'] ) || empty( $_GET['parent_chart_id'] ) ) {
550 577 $this->deleteOldCharts();
551 578 $default_type = isset( $_GET['type'] ) && ! empty( $_GET['type'] ) ? $_GET['type'] : 'line';
@@ -582,9 +609,9 @@
582 609 }
583 610 } else {
584 611 $parent_chart_id = filter_var( $_GET['parent_chart_id'], FILTER_VALIDATE_INT );
585 612 $success = false;
586 - if ( $parent_chart_id ) {
613 + if ( $parent_chart_id && self::can_edit_chart( $parent_chart_id ) ) {
587 614 $parent_chart = get_post( $parent_chart_id );
588 615 $success = $parent_chart && $parent_chart->post_type === Visualizer_Plugin::CPT_VISUALIZER;
589 616 }
590 617 if ( $success ) {
@@ -604,9 +631,9 @@
604 631 $post_meta = get_post_meta( $parent_chart_id );
605 632 $chart_id = $new_chart_id;
606 633 foreach ( $post_meta as $key => $value ) {
607 634 if ( strpos( $key, 'visualizer-' ) !== false ) {
608 - add_post_meta( $new_chart_id, $key, maybe_unserialize( $value[0] ) );
635 + add_post_meta( $new_chart_id, $key, self::maybe_decode_content( $value[0] ) );
609 636 }
610 637 }
611 638 }
612 639 }
@@ -790,16 +817,16 @@
790 817 /**
791 818 * Handle data and settings page
792 819 */
793 820 private function _handleDataAndSettingsPage() {
794 - if ( isset( $_POST['map_api_key'] ) ) {
795 - update_option( 'visualizer-map-api-key', $_POST['map_api_key'] );
796 - }
797 -
798 821 if ( $_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'] ) ) {
799 822 $is_canceled = isset( $_POST['cancel'] ) && 1 === intval( $_POST['cancel'] );
800 823 $is_newly_created = $this->_chart->post_status === 'auto-draft';
801 824
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 +
802 829 if ( $is_newly_created && ! $is_canceled ) {
803 830 $this->_chart->post_status = 'publish';
804 831
805 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.
@@ -814,8 +841,9 @@
814 841 $existing = get_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, true );
815 842 if ( isset( $existing['colors'] ) && is_array( $existing['colors'] ) && ! isset( $post_settings['colors'] ) ) {
816 843 $post_settings['colors'] = $existing['colors'];
817 844 }
845 + $post_settings = $this->sanitizeSettings( $post_settings );
818 846 update_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, $post_settings );
819 847
820 848 // we will keep a parameter called 'internal_title' that will be set to the given title or, if empty, the chart ID
821 849 // this will help in searching with the chart id.
@@ -1006,8 +1034,35 @@
1006 1034 wp_iframe( array( $render, 'render' ) );
1007 1035 }
1008 1036
1009 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 + /**
1010 1065 * Renders flattr script in the iframe <head>
1011 1066 *
1012 1067 * @since 1.4.2
1013 1068 * @action admin_head
@@ -1028,9 +1083,9 @@
1028 1083 *
1029 1084 * Used as a fallback when the URL path has no recognisable file extension
1030 1085 * (e.g. SharePoint, signed S3 URLs, or "download?id=…" endpoints).
1031 1086 *
1032 - * Uses wp_safe_remote_get() to block requests to private/loopback addresses,
1087 + * Uses the shared remote-fetch policy to block non-public destinations,
1033 1088 * and streams the response to a temp file so no body data is held in memory
1034 1089 * regardless of whether the server honours the Range header.
1035 1090 *
1036 1091 * The check relies on the ZIP magic number (PK\x03\x04) that every XLSX
@@ -1047,16 +1102,17 @@
1047 1102 if ( ! $tmpfile ) {
1048 1103 return false;
1049 1104 }
1050 1105
1051 - $response = wp_safe_remote_get(
1106 + $response = Visualizer_Remote_Fetch::request(
1052 1107 $url,
1053 1108 array(
1054 - 'timeout' => 10,
1055 - 'user-agent' => 'WordPress/' . get_bloginfo( 'version' ),
1056 - 'headers' => array( 'Range' => 'bytes=0-3' ),
1057 - 'stream' => true,
1058 - 'filename' => $tmpfile,
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,
1059 1115 )
1060 1116 );
1061 1117
1062 1118 if ( is_wp_error( $response ) ) {
@@ -1221,14 +1277,15 @@
1221 1277 public function uploadData() {
1222 1278 // if this is being called internally from pro and VISUALIZER_DO_NOT_DIE is set.
1223 1279 // otherwise, assume this is a normal web request.
1224 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).
1225 1282
1226 - // validate nonce
1283 + // validate nonce; capability check applies to web requests only, not trusted internal calls.
1227 1284 if (
1228 1285 ! isset( $_GET['nonce'] ) ||
1229 1286 ! wp_verify_nonce( $_GET['nonce'], 'visualizer-upload-data' ) ||
1230 - ! current_user_can( 'edit_posts' )
1287 + ( $can_die && ! current_user_can( 'edit_posts' ) )
1231 1288 ) {
1232 1289 if ( ! $can_die ) {
1233 1290 return;
1234 1291 }
@@ -1243,9 +1300,9 @@
1243 1300 if (
1244 1301 ! $chart_id ||
1245 1302 ! $chart ||
1246 1303 $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ||
1247 - ! current_user_can( 'edit_post', $chart_id )
1304 + ( $can_die && ! current_user_can( 'edit_post', $chart_id ) )
1248 1305 ) {
1249 1306 if ( ! $can_die ) {
1250 1307 return;
1251 1308 }
@@ -1327,10 +1384,10 @@
1327 1384 if ( $source ) {
1328 1385 if ( $source->fetch() ) {
1329 1386 $content = $source->getData( get_post_meta( $chart_id, Visualizer_Plugin::CF_EDITABLE_TABLE, true ) );
1330 1387 $populate = true;
1331 - if ( is_string( $content ) && is_array( unserialize( $content ) ) ) {
1332 - $json = unserialize( $content );
1388 + $json = self::decode_content( $content );
1389 + if ( is_array( $json ) ) {
1333 1390 // if source exists, so should data. if source exists but data is blank, do not populate the chart.
1334 1391 // if we populate the data even if it is empty, the chart will show "Table has no columns".
1335 1392 if ( array_key_exists( 'source', $json ) && ! empty( $json['source'] ) && ( ! array_key_exists( 'data', $json ) || empty( $json['data'] ) ) ) {
1336 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__ );
@@ -1394,12 +1451,11 @@
1394 1451 */
1395 1452 public function cloneChart() {
1396 1453 $chart_id = $success = false;
1397 1454 $nonce = isset( $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'], Visualizer_Plugin::ACTION_CLONE_CHART );
1398 - $capable = current_user_can( 'edit_posts' );
1399 - if ( $nonce && $capable ) {
1455 + if ( $nonce ) {
1400 1456 $chart_id = isset( $_GET['chart'] ) ? filter_var( $_GET['chart'], FILTER_VALIDATE_INT ) : '';
1401 - if ( $chart_id ) {
1457 + if ( $chart_id && self::can_edit_chart( $chart_id ) ) {
1402 1458 $chart = get_post( $chart_id );
1403 1459 $success = $chart && $chart->post_type === Visualizer_Plugin::CPT_VISUALIZER;
1404 1460 }
1405 1461 }
@@ -1419,9 +1475,9 @@
1419 1475 } else {
1420 1476 $post_meta = get_post_meta( $chart_id );
1421 1477 foreach ( $post_meta as $key => $value ) {
1422 1478 if ( strpos( $key, 'visualizer-' ) !== false ) {
1423 - 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] ) );
1424 1480 }
1425 1481 }
1426 1482 $redirect = esc_url(
1427 1483 add_query_arg(
@@ -1453,24 +1509,21 @@
1453 1509 * @access public
1454 1510 */
1455 1511 public function exportData() {
1456 1512 check_ajax_referer( Visualizer_Plugin::ACTION_EXPORT_DATA . Visualizer_Plugin::VERSION, 'security' );
1457 - $capable = current_user_can( 'edit_posts' );
1458 - if ( $capable ) {
1459 - $chart_id = isset( $_GET['chart'] ) ? filter_var(
1460 - $_GET['chart'],
1461 - FILTER_VALIDATE_INT,
1462 - array(
1463 - 'options' => array(
1464 - 'min_range' => 1,
1465 - ),
1466 - )
1467 - ) : '';
1468 - if ( $chart_id ) {
1469 - $data = $this->_getDataAs( $chart_id, 'csv' );
1470 - if ( $data ) {
1471 - echo wp_send_json_success( $data );
1472 - }
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 );
1473 1526 }
1474 1527 }
1475 1528
1476 1529 ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
@@ -1650,11 +1703,10 @@
1650 1703 */
1651 1704 public function saveFilter() {
1652 1705 check_ajax_referer( Visualizer_Plugin::ACTION_SAVE_FILTER_QUERY . Visualizer_Plugin::VERSION, 'security' );
1653 1706
1654 - $chart_id = filter_input(
1655 - INPUT_GET,
1656 - 'chart',
1707 + $chart_id = isset( $_GET['chart'] ) ? filter_var(
1708 + $_GET['chart'],
1657 1709 FILTER_VALIDATE_INT,
1658 1710 array(
1659 1711 'options' => array(
1660 1712 'min_range' => 1,
@@ -1659,10 +1711,14 @@
1659 1711 'options' => array(
1660 1712 'min_range' => 1,
1661 1713 ),
1662 1714 )
1663 - );
1715 + ) : false;
1664 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 +
1665 1721 $hours = filter_input(
1666 1722 INPUT_POST,
1667 1723 'refresh',
1668 1724 FILTER_VALIDATE_FLOAT,
@@ -1690,9 +1746,9 @@
1690 1746 *
1691 1747 * @param string $base64_img Chart image.
1692 1748 * @param int $chart_id Chart ID.
1693 1749 * @param bool $save_attachment Save attachment.
1694 - * @return attachment ID
1750 + * @return int Attachment ID, or 0 when no attachment was saved.
1695 1751 */
1696 1752 public function save_chart_image( $base64_img, $chart_id, $save_attachment = true ) {
1697 1753 // Delete old chart image.
1698 1754 $old_attachment_id = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_IMAGE, true );
@@ -1707,11 +1763,15 @@
1707 1763 // Upload dir.
1708 1764 $upload_dir = wp_upload_dir();
1709 1765 $upload_path = str_replace( '/', DIRECTORY_SEPARATOR, $upload_dir['path'] ) . DIRECTORY_SEPARATOR;
1710 1766
1711 - $img = str_replace( 'data:image/png;base64,', '', $base64_img );
1712 - $img = str_replace( ' ', '+', $img );
1713 - $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 + }
1714 1774 $filename = 'visualization-' . $chart_id . '.png';
1715 1775 $file_type = 'image/png';
1716 1776 $hashed_filename = $filename;
1717 1777