| @@ -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 | ?>], |