PluginProbe
SQL Chart Builder / 3.0.3
SQL Chart Builder v3.0.3
3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 trunk 1.0.2 1.0.3 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.7.1 2.3.7.2 2.3.8 3.0.0
← All changes | functions.php +36 -118 3.0.53.0.3 View file →
@@ -110,9 +110,8 @@
110 110 add_action('admin_notices', 'guaven_sqlcharts_onboarding_notice');
111 111
112 112 function guaven_sqlcharts_onboarding_notice_dismissed(){
113 113 check_ajax_referer('notice_dismissed', 'nonce');
114 - if (!current_user_can('manage_options')) return;
115 114
116 115 if(empty($_POST['type']))return;
117 116 switch ($_POST['type']){
118 117 case 'onboarding_notice':
@@ -225,31 +224,9 @@
225 224 'item_updated' => __('Chart updated.','guaven_sqlcharts'),
226 225 ),
227 226
228 227 'public' => true,
229 - 'show_in_rest' => false,
230 228 'menu_icon' => 'dashicons-chart-pie',
231 - // Charts execute SQL, so every primitive capability of this post type maps to manage_options.
232 - // Contributors/Authors cannot create, edit, publish or delete charts through any WordPress
233 - // entry point (admin UI, XML-RPC, REST). Published charts stay viewable on the front end.
234 - // Only primitive capabilities are remapped: mapping the meta capabilities edit_post/read_post/
235 - // delete_post to manage_options would make WordPress treat manage_options itself as a meta
236 - // capability and break that check site-wide.
237 - 'capability_type' => 'post',
238 - 'map_meta_cap' => true,
239 - 'capabilities' => array(
240 - 'edit_posts' => 'manage_options',
241 - 'edit_others_posts' => 'manage_options',
242 - 'edit_published_posts' => 'manage_options',
243 - 'edit_private_posts' => 'manage_options',
244 - 'publish_posts' => 'manage_options',
245 - 'read_private_posts' => 'manage_options',
246 - 'delete_posts' => 'manage_options',
247 - 'delete_private_posts' => 'manage_options',
248 - 'delete_published_posts' => 'manage_options',
249 - 'delete_others_posts' => 'manage_options',
250 - 'create_posts' => 'manage_options',
251 - ),
252 229 'supports' => array(
253 230 'title',
254 231 'postmeta'
255 232 ),
@@ -258,14 +235,8 @@
258 235
259 236 guaven_sqlcharts_load_defaults();
260 237 }
261 238
262 -// All guaven_sqlcharts_* meta keys are protected: they cannot be written through the Custom Fields box,
263 -// XML-RPC or the REST API. The plugin's own save handler (update_post_meta) is not affected.
264 -add_filter('is_protected_meta', function ($protected, $meta_key) {
265 - return strpos((string) $meta_key, 'guaven_sqlcharts_') === 0 ? true : $protected;
266 -}, 10, 2);
267 -
268 239 // "Add title" placeholder on the chart edit screen
269 240 add_filter('enter_title_here', function ($title, $post) {
270 241 if (!empty($post) and $post->post_type == 'gvn_schart') return __('Chart name', 'guaven_sqlcharts');
271 242 return $title;
@@ -452,11 +423,8 @@
452 423 {
453 424 if (!isset($_POST['meta_box_nonce_field']) or !wp_verify_nonce($_POST['meta_box_nonce_field'], 'meta_box_nonce_action')) {
454 425 return $post->ID;
455 426 }
456 - if ($post->post_type != 'gvn_schart' or !current_user_can('manage_options') or (defined('DOING_AUTOSAVE') and DOING_AUTOSAVE)) {
457 - return $post->ID;
458 - }
459 427 $fields = array(
460 428 "guaven_sqlcharts_chartheight",
461 429 "guaven_sqlcharts_chartwidth",
462 430 "guaven_sqlcharts_graphtype",
@@ -512,57 +480,13 @@
512 480 // save the custom fields
513 481
514 482
515 483
516 -// Removes string literals (contents only), backtick identifiers and comments from SQL so keyword checks
517 -// see the same code MySQL will execute. "/*!" and "/*+" comments are executable in MySQL and are kept.
518 -function guaven_sqlcharts_strip_sql_literals($sql)
519 -{
520 - $out = ''; $len = strlen($sql); $i = 0;
521 - while ($i < $len) {
522 - $c = $sql[$i];
523 - if ($c === "'" or $c === '"' or $c === '`') {
524 - $out .= $c . $c; $i++;
525 - while ($i < $len) {
526 - if ($sql[$i] === '\\' and $c !== '`') { $i += 2; continue; }
527 - if ($sql[$i] === $c) { if ($i + 1 < $len and $sql[$i + 1] === $c) { $i += 2; continue; } $i++; break; }
528 - $i++;
529 - }
530 - continue;
531 - }
532 - if ($c === '#' or ($c === '-' and substr($sql, $i, 2) === '--' and ($i + 2 >= $len or ctype_space($sql[$i + 2])))) {
533 - $nl = strpos($sql, "\n", $i); $i = ($nl === false) ? $len : $nl; continue;
534 - }
535 - if ($c === '/' and substr($sql, $i, 2) === '/*' and !in_array(substr($sql, $i + 2, 1), array('!', '+'), true)) {
536 - $close = strpos($sql, '*/', $i + 2); $i = ($close === false) ? $len : $close + 2; $out .= ' '; continue;
537 - }
538 - $out .= $c; $i++;
539 - }
540 - return $out;
541 -}
542 -
543 -// Returns 1 when the (fully substituted) SQL must not run, 0 when it is a read-only query.
544 -// Called after every {tag}/{argN} replacement so user-supplied values are covered too.
545 484 function gvn_chart_check_sql_query($sql)
546 485 {
547 - // 1) data-changing statements: checked on the raw text, exactly as in every previous version
548 - $write = '/\b(delete|update|insert|replace|drop|truncate|alter|create|rename|grant|revoke|call|handler|load\s+data|load_file|outfile|dumpfile)\b/i';
549 - if (preg_match($write, $sql)) return 1;
550 -
551 - // 2) further dangerous statements, matched outside string literals and comments so that ordinary
552 - // values such as status = 'reset' keep working
553 - $danger = '/\b(prepare|execute|deallocate|lock|unlock|kill|shutdown|flush|reset|purge|install|uninstall|import'
554 - . '|set\s+(?:global|session|persist|persist_only|password|@@)|start\s+(?:replica|slave|group_replication)|stop\s+(?:replica|slave)|change\s+(?:master|replication))\b/i';
555 - if (preg_match($danger, guaven_sqlcharts_strip_sql_literals($sql))) return 1;
556 -
557 - // 3) every ";"-separated statement must be a read statement. The renderer sends each segment to the
558 - // database on its own, so this stops a value from smuggling a second statement behind a ";".
559 - foreach (explode(';', $sql) as $segment) {
560 - $segment = ltrim(guaven_sqlcharts_strip_sql_literals($segment), " \t\r\n(");
561 - if ($segment === '') continue;
562 - if (!preg_match('/^(select|with|show|describe|desc|explain)\b/i', $segment)) return 1;
563 - }
564 - return 0;
486 + // case-insensitive, word-boundary check: only read-only SELECT queries are allowed
487 + $pattern = '/\b(delete|update|insert|replace|drop|truncate|alter|create|rename|grant|revoke|call|handler|load\s+data|load_file|outfile|dumpfile)\b/i';
488 + return preg_match($pattern, $sql) ? 1 : 0;
565 489 }
566 490
567 491 function guaven_get_labels_and_values($id, $fvs)
568 492 {
@@ -656,9 +580,9 @@
656 580 if (count($varfield_arr)<3) continue;
657 581 $varfield_arr=array_map("trim",$varfield_arr);
658 582 if (!empty($_GET[$varfield_arr[0]])) {
659 583 // User-supplied input: no () bypass allowed — sanitize strictly
660 - $varreplacement = str_replace(';', '', sanitize_text_field(wp_unslash($_GET[$varfield_arr[0]])));
584 + $varreplacement = sanitize_text_field(wp_unslash($_GET[$varfield_arr[0]]));
661 585 if (is_numeric($varreplacement)) {
662 586 $varreplacement = $varreplacement + 0;
663 587 } else {
664 588 $varreplacement = '"' . esc_sql($varreplacement) . '"';
@@ -752,10 +676,8 @@
752 676
753 677 function guaven_sqlcharts_local_shortcode($atts) {
754 678 if(empty($atts['id']))return 'ID is missing.';
755 679 $atts['id']=intval($atts['id']);
756 - $post_g = get_post($atts['id']);
757 - if (!$post_g or $post_g->post_type != 'gvn_schart') return 'Chart not found.';
758 680 $remote_host=get_post_meta($atts['id'], 'guaven_sqlcharts_dbhost', true);
759 681 if ($remote_host!=''){
760 682 $remote_db=get_post_meta($atts['id'], 'guaven_sqlcharts_dbname', true);
761 683 $remote_login=get_post_meta($atts['id'], 'guaven_sqlcharts_dblogin', true);
@@ -772,34 +694,34 @@
772 694 $GLOBALS["guaven_sqlcharts_atts"]=$atts;
773 695
774 696 $sql = guaven_sqlcharts_get_code($atts['id']);
775 697 if(empty($sql))return 'SQL query is missing.';
698 + $sql=gvn_chart_put_variables($sql,$atts['id']);
776 699
700 +
701 + $sql=apply_filters('guaven_sqlcharts_rendered_sql',$sql,$atts);
702 +
703 + $blacklister_f = gvn_chart_check_sql_query($sql);
704 + if ($blacklister_f == 1)return 'You given SQL code contains forbidden commands. Remember that you should only use SELECT queries';
705 + $tip_g = guaven_sqlcharts_normalize_type(get_post_meta($atts['id'], 'guaven_sqlcharts_graphtype', true));
706 +
777 707 // {arg1}..{arg19} come from shortcode attributes: [gvn_schart_2 id="1" arg1="41"].
778 708 // Substituted directly (not via wpdb::prepare) so the same tag may appear any number of times,
779 709 // e.g. in every query of a ";"-separated comparison chart. Numbers are inserted as-is, anything
780 710 // else is escaped and quoted; a tag already wrapped in quotes ('{arg1}') is not double-quoted.
781 - // ";" is removed from values because the finished SQL is split on ";" below.
782 711 for($i=1;$i<20;$i++){
783 712 $tag = '{arg'.$i.'}';
784 713 if (strpos($sql, $tag) === false) continue;
785 714 $replacearg = !empty($atts['arg'.$i]) ? $atts['arg'.$i] : 0;
786 715 if (is_numeric($replacearg)) $replacearg = $replacearg + 0;
787 - else $replacearg = "'" . esc_sql(str_replace(';', '', sanitize_text_field((string) $replacearg))) . "'";
716 + else $replacearg = "'" . esc_sql($replacearg) . "'";
788 717 $sql = str_replace(array("'".$tag."'", '"'.$tag.'"', $tag), $replacearg, $sql);
789 718 }
790 719
791 - $sql=gvn_chart_put_variables($sql,$atts['id']);
792 - $sql=apply_filters('guaven_sqlcharts_rendered_sql',$sql,$atts);
793 -
794 - // command check on the final SQL, after every shortcode argument and filter value is in place
795 - $blacklister_f = gvn_chart_check_sql_query($sql);
796 - if ($blacklister_f == 1)return 'You given SQL code contains forbidden commands. Remember that you should only use SELECT queries';
797 - $tip_g = guaven_sqlcharts_normalize_type(get_post_meta($atts['id'], 'guaven_sqlcharts_graphtype', true));
798 -
799 720 $sql_split = explode(';', $sql);
800 721 $labels_and_values = array();
801 722 $labels = $values = $ylabel = $xlabel = array();
723 + $post_g = get_post($atts['id']);
802 724
803 725 global $sqlcharts_inserted_script;
804 726 ob_start();
805 727 for ($i = 0; $i < count($sql_split); $i++) {
@@ -879,20 +801,14 @@
879 801 // distinct set of attributes gets its own cache entry. Append ?force_sql_cache_reload to the URL to bypass.
880 802 add_shortcode("gvn_schart_2_cached",function($atts){
881 803 if(empty($atts["id"]))return;
882 804 $atts["id"]=intval($atts["id"]);
805 + $is_logged_in=is_user_logged_in()?'':'_guest';
883 806 $expire=!empty($atts["expire"])?intval($atts["expire"]):3600;
884 807 $inner_atts=$atts;
885 808 unset($inner_atts['expire']);
886 - // One cache entry per user (charts may use {current_user_*} tags), per set of shortcode attributes
887 - // and per value of every dynamic filter this chart reads from the URL. A visitor can therefore
888 - // never be served, or pre-seed, a result computed for someone else or for other filter values.
889 - $key_parts = array('atts' => $inner_atts, 'user' => is_user_logged_in() ? get_current_user_id() : 0, 'get' => array());
890 - foreach (explode('|', (string) get_post_meta($atts['id'], 'guaven_sqlcharts_variables', true)) as $vrow) {
891 - $vname = trim(current(explode('~', $vrow)));
892 - if ($vname !== '' and isset($_GET[$vname])) $key_parts['get'][$vname] = sanitize_text_field(wp_unslash($_GET[$vname]));
893 - }
894 - $key = 'cached_sql_charts_' . $atts["id"] . '_' . md5(serialize($key_parts));
809 + $key='cached_sql_charts_'.$atts["id"].$is_logged_in;
810 + if (count($inner_atts) > 1) $key .= '_'.md5(serialize($inner_atts));
895 811 $cached=get_transient($key);
896 812 if(!empty($cached) and !isset($_GET["force_sql_cache_reload"]) )return $cached;
897 813 $tobecached=guaven_sqlcharts_local_shortcode($inner_atts);
898 814 set_transient($key, $tobecached,$expire);
@@ -942,17 +858,8 @@
942 858 if ($text === '' or ($which == 'y' and strpos($text, ';') !== false)) return '';
943 859 return 'title: {display: true, text: ' . wp_json_encode($text) . '},';
944 860 }
945 861
946 -// "params" shortcode attribute: extra Chart.js dataset options, e.g. params="borderWidth: 3, borderDash: [5,5],".
947 -// The text is placed inside the inline <script>, so only a conservative character set is accepted:
948 -// no parentheses, semicolons, "=", "<", ">", "/", "\\", "+" or backticks, which rules out executable JavaScript.
949 -function guaven_sqlcharts_dataset_params(){
950 - $params = isset($GLOBALS["guaven_sqlcharts_atts"]["params"]) ? (string) $GLOBALS["guaven_sqlcharts_atts"]["params"] : '';
951 - if ($params === '' or !preg_match('/^[A-Za-z0-9_\s,:.\'"#%\-\[\]{}]+$/', $params)) return '';
952 - return $params;
953 -}
954 -
955 862 // dataset label as a safe JS string literal (labels saved before 3.0.1 may hold HTML entities)
956 863 function guaven_sqlcharts_js_label($label){
957 864 return wp_json_encode(html_entity_decode((string) $label, ENT_QUOTES, 'UTF-8'));
958 865 }
@@ -985,12 +892,11 @@
985 892 }
986 893 return $has_point ? $out : false;
987 894 }
988 895
989 -// X scale options for time-axis mode (globals from asset/front.js): gvnSqlChartsTimeTicks replaces the evenly
990 -// spaced ticks Chart.js generates on a linear scale with the actual data dates, gvnSqlChartsTimeTick formats them
896 +// X scale options for time-axis mode; gvnSqlChartsTimeTick (asset/front.js) formats the ticks as dates
991 897 function guaven_sqlcharts_time_axis_scale(){
992 - return "type: 'linear', offset: true, afterBuildTicks: gvnSqlChartsTimeTicks, ticks: {callback: gvnSqlChartsTimeTick, maxRotation: 45, autoSkip: true},";
898 + return "type: 'linear', offset: true, ticks: {callback: gvnSqlChartsTimeTick, maxRotation: 45},";
993 899 }
994 900 // extra entry for the Chart.js "plugins" object in time-axis mode (tooltip title shown as a date)
995 901 function guaven_sqlcharts_time_axis_plugins($time_points){
996 902 return $time_points !== false ? 'tooltip: {callbacks: {title: gvnSqlChartsTimeTooltipTitle}}' : '';
@@ -1014,9 +920,12 @@
1014 920 $points = $time_points !== false ? $time_points[$key_ak] : $values_new[$key_ak];
1015 921 ?>
1016 922 {
1017 923 <?php
1018 - echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
924 + if(!empty($GLOBALS["guaven_sqlcharts_atts"]["params"])){
925 + //passing chartJS params via the shortcode
926 + echo wp_kses($GLOBALS["guaven_sqlcharts_atts"]["params"],[]);
927 + }
1019 928 ?>
1020 929 label: <?php echo guaven_sqlcharts_js_label($ylabel[$key_ak]); ?>,
1021 930 backgroundColor: [
1022 931 <?php
@@ -1028,9 +937,9 @@
1028 937 echo wp_kses(guaven_sqlcharts_colorgenerator(count($points), 0, 0.2, guaven_sqlcharts_colors($i, $pid)),[]);
1029 938 ?>
1030 939 ],
1031 940 borderWidth: 1,
1032 - <?php if ($time_points !== false) echo 'barThickness: 24,'; // fixed width: on a time axis Chart.js would otherwise size bars from the closest pair of dates ?>
941 + <?php if ($time_points !== false) echo 'maxBarThickness: 48,'; ?>
1033 942 data: [<?php
1034 943 echo wp_kses(implode(",", $points),[]);
1035 944 ?>],
1036 945 },
@@ -1100,9 +1009,12 @@
1100 1009 else $fill = ($i == 0 and $dataset_count > 1) ? '"+1"' : '"origin"';
1101 1010 ?>
1102 1011 {
1103 1012 <?php
1104 - echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1013 + if(!empty($GLOBALS["guaven_sqlcharts_atts"]["params"])){
1014 + //passing chartJS params via the shortcode
1015 + echo wp_kses($GLOBALS["guaven_sqlcharts_atts"]["params"],[]);
1016 + }
1105 1017 ?>
1106 1018 label: <?php echo guaven_sqlcharts_js_label($ylabel[$key_ak]); ?>,
1107 1019 fill: <?php echo wp_kses($fill,[]);
1108 1020 ?>,
@@ -1189,9 +1101,12 @@
1189 1101 }
1190 1102 ?>
1191 1103 {
1192 1104 <?php
1193 - echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1105 + if(!empty($GLOBALS["guaven_sqlcharts_atts"]["params"])){
1106 + //passing chartJS params via the shortcode
1107 + echo wp_kses($GLOBALS["guaven_sqlcharts_atts"]["params"],[]);
1108 + }
1194 1109 ?>
1195 1110 label: <?php echo guaven_sqlcharts_js_label(isset($ylabel[$key_ak])?$ylabel[$key_ak]:''); ?>,
1196 1111 backgroundColor: <?php
1197 1112 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
@@ -1271,9 +1186,12 @@
1271 1186 for ($i = 0; $i < count($values); $i++) {
1272 1187 ?>
1273 1188 {
1274 1189 <?php
1275 - echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1190 + if(!empty($GLOBALS["guaven_sqlcharts_atts"]["params"])){
1191 + //passing chartJS params via the shortcode
1192 + echo wp_kses($GLOBALS["guaven_sqlcharts_atts"]["params"],[]);
1193 + }
1276 1194 ?>
1277 1195 data: [<?php
1278 1196 echo wp_kses(implode(",", $values[$i]),[]);
1279 1197 ?>],