PluginProbe
SQL Chart Builder / 3.0.6
SQL Chart Builder v3.0.6
3.0.6 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 +133 -39 3.0.33.0.6 View file →
@@ -110,8 +110,9 @@
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;
114 115
115 116 if(empty($_POST['type']))return;
116 117 switch ($_POST['type']){
117 118 case 'onboarding_notice':
@@ -224,9 +225,31 @@
224 225 'item_updated' => __('Chart updated.','guaven_sqlcharts'),
225 226 ),
226 227
227 228 'public' => true,
229 + 'show_in_rest' => false,
228 230 '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 + ),
229 252 'supports' => array(
230 253 'title',
231 254 'postmeta'
232 255 ),
@@ -235,8 +258,14 @@
235 258
236 259 guaven_sqlcharts_load_defaults();
237 260 }
238 261
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 +
239 268 // "Add title" placeholder on the chart edit screen
240 269 add_filter('enter_title_here', function ($title, $post) {
241 270 if (!empty($post) and $post->post_type == 'gvn_schart') return __('Chart name', 'guaven_sqlcharts');
242 271 return $title;
@@ -423,8 +452,11 @@
423 452 {
424 453 if (!isset($_POST['meta_box_nonce_field']) or !wp_verify_nonce($_POST['meta_box_nonce_field'], 'meta_box_nonce_action')) {
425 454 return $post->ID;
426 455 }
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 + }
427 459 $fields = array(
428 460 "guaven_sqlcharts_chartheight",
429 461 "guaven_sqlcharts_chartwidth",
430 462 "guaven_sqlcharts_graphtype",
@@ -480,13 +512,57 @@
480 512 // save the custom fields
481 513
482 514
483 515
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.
484 545 function gvn_chart_check_sql_query($sql)
485 546 {
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;
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;
489 565 }
490 566
491 567 function guaven_get_labels_and_values($id, $fvs)
492 568 {
@@ -580,9 +656,9 @@
580 656 if (count($varfield_arr)<3) continue;
581 657 $varfield_arr=array_map("trim",$varfield_arr);
582 658 if (!empty($_GET[$varfield_arr[0]])) {
583 659 // User-supplied input: no () bypass allowed — sanitize strictly
584 - $varreplacement = sanitize_text_field(wp_unslash($_GET[$varfield_arr[0]]));
660 + $varreplacement = str_replace(';', '', sanitize_text_field(wp_unslash($_GET[$varfield_arr[0]])));
585 661 if (is_numeric($varreplacement)) {
586 662 $varreplacement = $varreplacement + 0;
587 663 } else {
588 664 $varreplacement = '"' . esc_sql($varreplacement) . '"';
@@ -601,8 +677,14 @@
601 677 }
602 678
603 679 function gvn_chart_top_form($atts){
604 680 if (get_post_meta($atts["id"],'guaven_sqlcharts_formpartrole',true)!='' and !is_user_logged_in()) return;
681 + // Inside the chart builder's live preview the filter inputs are rendered disabled and without a <form>
682 + // or submit button: the preview sits inside WordPress's own post edit form, nested forms are dropped
683 + // by the browser, and a filter named e.g. "post_type" would otherwise be submitted with the post and
684 + // make WordPress stop with "A post type mismatch has been detected." (chart settings not saved).
685 + $preview = !empty($GLOBALS['guaven_sqlcharts_admin_preview']);
686 + $dis = $preview ? ' disabled' : '';
605 687 $topform='';$dateexists=false;
606 688 $variables_raw=get_post_meta($atts['id'],'guaven_sqlcharts_variables',true);
607 689 $variables_raw=explode("|",$variables_raw);
608 690 foreach ($variables_raw as $vrow){
@@ -614,14 +696,14 @@
614 696 if ($vrow_arr[3]=='date') {
615 697 $dateexists=true;
616 698 $topform.= '<span class="gvn-filter-field"><label>'.$vrow_arr[2].'</label> <input class="gws_datepicker" autocomplete="off" type="text"
617 699 value="'.$gvalue.'"
618 - data-toggle="datepicker" name="'.$vrow_arr[0].'" placeholder="'.$dvalue.'"></span>
700 + data-toggle="datepicker" name="'.$vrow_arr[0].'" placeholder="'.$dvalue.'"'.$dis.'></span>
619 701 ';}
620 702 else {
621 703 $topform.= '<span class="gvn-filter-field"><label>'.$vrow_arr[2].'</label> <input autocomplete="off"
622 704 type="'.$vrow_arr[3].'"
623 - value="'.$gvalue.'" name="'.$vrow_arr[0].'" placeholder="'.$dvalue.'"></span>
705 + value="'.$gvalue.'" name="'.$vrow_arr[0].'" placeholder="'.$dvalue.'"'.$dis.'></span>
624 706 ';
625 707 }
626 708 }
627 709 if (!empty($topform)) {
@@ -638,12 +720,14 @@
638 720 'class' => array(),
639 721 'data-toggle'=>array(),
640 722 'placeholder'=>array(),
641 723 'autocomplete'=>array(),
724 + 'disabled'=>array(),
642 725 'style'=>[]
643 726 ),
644 727 'span' => array('class' => array()),
645 728 'label' => array(),
729 + 'div' => array('class' => array()),
646 730 );
647 731
648 732 $submit_button_value = get_post_meta($atts['id'], 'guaven_sqlcharts_formpartbutton', true) != ''
649 733 ? esc_attr(get_post_meta($atts['id'], 'guaven_sqlcharts_formpartbutton', true))
@@ -648,10 +732,14 @@
648 732 $submit_button_value = get_post_meta($atts['id'], 'guaven_sqlcharts_formpartbutton', true) != ''
649 733 ? esc_attr(get_post_meta($atts['id'], 'guaven_sqlcharts_formpartbutton', true))
650 734 : 'OK';
651 735
652 - $topform = '<form method="get" action="" class="guaven_sqlcharts_form">' . $topform . '
736 + if ($preview) {
737 + $topform = '<div class="guaven_sqlcharts_form">' . $topform . '<input type="submit" value="' . $submit_button_value . '" disabled></div>';
738 + } else {
739 + $topform = '<form method="get" action="" class="guaven_sqlcharts_form">' . $topform . '
653 740 <input type="submit" value="' . $submit_button_value . '"></form>';
741 + }
654 742
655 743 echo wp_kses($topform, $allowed_html);
656 744 }
657 745 }
@@ -676,8 +764,10 @@
676 764
677 765 function guaven_sqlcharts_local_shortcode($atts) {
678 766 if(empty($atts['id']))return 'ID is missing.';
679 767 $atts['id']=intval($atts['id']);
768 + $post_g = get_post($atts['id']);
769 + if (!$post_g or $post_g->post_type != 'gvn_schart') return 'Chart not found.';
680 770 $remote_host=get_post_meta($atts['id'], 'guaven_sqlcharts_dbhost', true);
681 771 if ($remote_host!=''){
682 772 $remote_db=get_post_meta($atts['id'], 'guaven_sqlcharts_dbname', true);
683 773 $remote_login=get_post_meta($atts['id'], 'guaven_sqlcharts_dblogin', true);
@@ -694,34 +784,34 @@
694 784 $GLOBALS["guaven_sqlcharts_atts"]=$atts;
695 785
696 786 $sql = guaven_sqlcharts_get_code($atts['id']);
697 787 if(empty($sql))return 'SQL query is missing.';
698 - $sql=gvn_chart_put_variables($sql,$atts['id']);
699 788
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 -
707 789 // {arg1}..{arg19} come from shortcode attributes: [gvn_schart_2 id="1" arg1="41"].
708 790 // Substituted directly (not via wpdb::prepare) so the same tag may appear any number of times,
709 791 // e.g. in every query of a ";"-separated comparison chart. Numbers are inserted as-is, anything
710 792 // else is escaped and quoted; a tag already wrapped in quotes ('{arg1}') is not double-quoted.
793 + // ";" is removed from values because the finished SQL is split on ";" below.
711 794 for($i=1;$i<20;$i++){
712 795 $tag = '{arg'.$i.'}';
713 796 if (strpos($sql, $tag) === false) continue;
714 797 $replacearg = !empty($atts['arg'.$i]) ? $atts['arg'.$i] : 0;
715 798 if (is_numeric($replacearg)) $replacearg = $replacearg + 0;
716 - else $replacearg = "'" . esc_sql($replacearg) . "'";
799 + else $replacearg = "'" . esc_sql(str_replace(';', '', sanitize_text_field((string) $replacearg))) . "'";
717 800 $sql = str_replace(array("'".$tag."'", '"'.$tag.'"', $tag), $replacearg, $sql);
718 801 }
719 802
803 + $sql=gvn_chart_put_variables($sql,$atts['id']);
804 + $sql=apply_filters('guaven_sqlcharts_rendered_sql',$sql,$atts);
805 +
806 + // command check on the final SQL, after every shortcode argument and filter value is in place
807 + $blacklister_f = gvn_chart_check_sql_query($sql);
808 + if ($blacklister_f == 1)return 'You given SQL code contains forbidden commands. Remember that you should only use SELECT queries';
809 + $tip_g = guaven_sqlcharts_normalize_type(get_post_meta($atts['id'], 'guaven_sqlcharts_graphtype', true));
810 +
720 811 $sql_split = explode(';', $sql);
721 812 $labels_and_values = array();
722 813 $labels = $values = $ylabel = $xlabel = array();
723 - $post_g = get_post($atts['id']);
724 814
725 815 global $sqlcharts_inserted_script;
726 816 ob_start();
727 817 for ($i = 0; $i < count($sql_split); $i++) {
@@ -801,14 +891,20 @@
801 891 // distinct set of attributes gets its own cache entry. Append ?force_sql_cache_reload to the URL to bypass.
802 892 add_shortcode("gvn_schart_2_cached",function($atts){
803 893 if(empty($atts["id"]))return;
804 894 $atts["id"]=intval($atts["id"]);
805 - $is_logged_in=is_user_logged_in()?'':'_guest';
806 895 $expire=!empty($atts["expire"])?intval($atts["expire"]):3600;
807 896 $inner_atts=$atts;
808 897 unset($inner_atts['expire']);
809 - $key='cached_sql_charts_'.$atts["id"].$is_logged_in;
810 - if (count($inner_atts) > 1) $key .= '_'.md5(serialize($inner_atts));
898 + // One cache entry per user (charts may use {current_user_*} tags), per set of shortcode attributes
899 + // and per value of every dynamic filter this chart reads from the URL. A visitor can therefore
900 + // never be served, or pre-seed, a result computed for someone else or for other filter values.
901 + $key_parts = array('atts' => $inner_atts, 'user' => is_user_logged_in() ? get_current_user_id() : 0, 'get' => array());
902 + foreach (explode('|', (string) get_post_meta($atts['id'], 'guaven_sqlcharts_variables', true)) as $vrow) {
903 + $vname = trim(current(explode('~', $vrow)));
904 + if ($vname !== '' and isset($_GET[$vname])) $key_parts['get'][$vname] = sanitize_text_field(wp_unslash($_GET[$vname]));
905 + }
906 + $key = 'cached_sql_charts_' . $atts["id"] . '_' . md5(serialize($key_parts));
811 907 $cached=get_transient($key);
812 908 if(!empty($cached) and !isset($_GET["force_sql_cache_reload"]) )return $cached;
813 909 $tobecached=guaven_sqlcharts_local_shortcode($inner_atts);
814 910 set_transient($key, $tobecached,$expire);
@@ -858,8 +954,17 @@
858 954 if ($text === '' or ($which == 'y' and strpos($text, ';') !== false)) return '';
859 955 return 'title: {display: true, text: ' . wp_json_encode($text) . '},';
860 956 }
861 957
958 +// "params" shortcode attribute: extra Chart.js dataset options, e.g. params="borderWidth: 3, borderDash: [5,5],".
959 +// The text is placed inside the inline <script>, so only a conservative character set is accepted:
960 +// no parentheses, semicolons, "=", "<", ">", "/", "\\", "+" or backticks, which rules out executable JavaScript.
961 +function guaven_sqlcharts_dataset_params(){
962 + $params = isset($GLOBALS["guaven_sqlcharts_atts"]["params"]) ? (string) $GLOBALS["guaven_sqlcharts_atts"]["params"] : '';
963 + if ($params === '' or !preg_match('/^[A-Za-z0-9_\s,:.\'"#%\-\[\]{}]+$/', $params)) return '';
964 + return $params;
965 +}
966 +
862 967 // dataset label as a safe JS string literal (labels saved before 3.0.1 may hold HTML entities)
863 968 function guaven_sqlcharts_js_label($label){
864 969 return wp_json_encode(html_entity_decode((string) $label, ENT_QUOTES, 'UTF-8'));
865 970 }
@@ -892,11 +997,12 @@
892 997 }
893 998 return $has_point ? $out : false;
894 999 }
895 1000
896 -// X scale options for time-axis mode; gvnSqlChartsTimeTick (asset/front.js) formats the ticks as dates
1001 +// X scale options for time-axis mode (globals from asset/front.js): gvnSqlChartsTimeTicks replaces the evenly
1002 +// spaced ticks Chart.js generates on a linear scale with the actual data dates, gvnSqlChartsTimeTick formats them
897 1003 function guaven_sqlcharts_time_axis_scale(){
898 - return "type: 'linear', offset: true, ticks: {callback: gvnSqlChartsTimeTick, maxRotation: 45},";
1004 + return "type: 'linear', offset: true, afterBuildTicks: gvnSqlChartsTimeTicks, ticks: {callback: gvnSqlChartsTimeTick, maxRotation: 45, autoSkip: true},";
899 1005 }
900 1006 // extra entry for the Chart.js "plugins" object in time-axis mode (tooltip title shown as a date)
901 1007 function guaven_sqlcharts_time_axis_plugins($time_points){
902 1008 return $time_points !== false ? 'tooltip: {callbacks: {title: gvnSqlChartsTimeTooltipTitle}}' : '';
@@ -920,12 +1026,9 @@
920 1026 $points = $time_points !== false ? $time_points[$key_ak] : $values_new[$key_ak];
921 1027 ?>
922 1028 {
923 1029 <?php
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 - }
1030 + echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
928 1031 ?>
929 1032 label: <?php echo guaven_sqlcharts_js_label($ylabel[$key_ak]); ?>,
930 1033 backgroundColor: [
931 1034 <?php
@@ -937,9 +1040,9 @@
937 1040 echo wp_kses(guaven_sqlcharts_colorgenerator(count($points), 0, 0.2, guaven_sqlcharts_colors($i, $pid)),[]);
938 1041 ?>
939 1042 ],
940 1043 borderWidth: 1,
941 - <?php if ($time_points !== false) echo 'maxBarThickness: 48,'; ?>
1044 + <?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 ?>
942 1045 data: [<?php
943 1046 echo wp_kses(implode(",", $points),[]);
944 1047 ?>],
945 1048 },
@@ -1009,12 +1112,9 @@
1009 1112 else $fill = ($i == 0 and $dataset_count > 1) ? '"+1"' : '"origin"';
1010 1113 ?>
1011 1114 {
1012 1115 <?php
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 - }
1116 + echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1017 1117 ?>
1018 1118 label: <?php echo guaven_sqlcharts_js_label($ylabel[$key_ak]); ?>,
1019 1119 fill: <?php echo wp_kses($fill,[]);
1020 1120 ?>,
@@ -1101,12 +1201,9 @@
1101 1201 }
1102 1202 ?>
1103 1203 {
1104 1204 <?php
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 - }
1205 + echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1109 1206 ?>
1110 1207 label: <?php echo guaven_sqlcharts_js_label(isset($ylabel[$key_ak])?$ylabel[$key_ak]:''); ?>,
1111 1208 backgroundColor: <?php
1112 1209 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
@@ -1186,12 +1283,9 @@
1186 1283 for ($i = 0; $i < count($values); $i++) {
1187 1284 ?>
1188 1285 {
1189 1286 <?php
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 - }
1287 + echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1194 1288 ?>
1195 1289 data: [<?php
1196 1290 echo wp_kses(implode(",", $values[$i]),[]);
1197 1291 ?>],