--- a/tablesome/components/cell-types/email.php
+++ b/tablesome/components/cell-types/email.php
@@ -16,9 +16,9 @@
return $cell;
}
- $cell["html"] = $cell['value'];
+ $cell["html"] = esc_html($cell['value']);
if (filter_var(trim($cell['value']), FILTER_VALIDATE_EMAIL)) {
- $cell["html"] = '<a href="mailto:' . $cell['value'] . '?subject = Feedback&body = Message">' . $cell['value'] . '</a>';
+ $cell["html"] = '<a href="' . esc_url('mailto:' . $cell['value'] . '?subject=Feedback&body=Message') . '">' . esc_html($cell['value']) . '</a>';
}
return $cell;
--- a/tablesome/components/cell-types/file/view.php
+++ b/tablesome/components/cell-types/file/view.php
@@ -32,15 +32,13 @@
break;
}
- // error_log('html : ' . $html);
-
return $html;
}
public function get_image($data, $is_preview = false)
{
- $url = isset($data["url"]) && !empty($data["url"]) ? $data["url"] : "";
- $link = isset($data["link"]) && !empty($data["link"]) ? $data["link"] : "";
+ $url = isset($data["url"]) && !empty($data["url"]) ? esc_url($data["url"]) : "";
+ $link = isset($data["link"]) && !empty($data["link"]) ? esc_url($data["link"]) : "";
$html = '<img class="tablesome__inputMediaPreview tablesome__inputMediaPreview--image" src="' . $url . '" />';
@@ -54,7 +52,7 @@
public function get_video($url = '', $mime_type = '')
{
$html = '<video controls class="tablesome__inputMediaPreview tablesome__inputMediaPreview--video">';
- $html .= '<source src="' . $url . '" type="' . $mime_type . '">';
+ $html .= '<source src="' . esc_url($url) . '" type="' . esc_attr($mime_type) . '">';
$html .= __('Your browser does not support HTML video', 'tablesome');
$html .= '</video>';
@@ -64,7 +62,7 @@
public function get_audio($url = '', $mime_type = '')
{
$html = '<audio controls class="tablesome__inputMediaPreview tablesome__inputMediaPreview--audio">';
- $html .= '<source src="' . $url . '" type="' . $mime_type . '">';
+ $html .= '<source src="' . esc_url($url) . '" type="' . esc_attr($mime_type) . '">';
$html .= __('Your browser does not support HTML audio', 'tablesome');
$html .= '</audio>';
@@ -73,7 +71,7 @@
public function get_media_link($url = '', $name = '')
{
- return '<a class="tablesome__inputMediaPreview tablesome__inputMediaPreview--link" href="' . $url . '" target="_blank">' . $name . '</a>';
+ return '<a class="tablesome__inputMediaPreview tablesome__inputMediaPreview--link" href="' . esc_url($url) . '" target="_blank">' . esc_html($name) . '</a>';
}
}
}
--- a/tablesome/components/table/other-cpt-model.php
+++ b/tablesome/components/table/other-cpt-model.php
@@ -472,11 +472,11 @@
} else if ('post_author' === $source_operand_id) {
$statements .= " AND {$wpdb->posts}.{$source_operand_id} ";
if (in_array($operator, ['contains', 'does_not_contain'])) {
- $author_ids = empty($destination_value) ? [0] : $destination_value;
+ $author_ids = empty($destination_value) ? [0] : array_map('intval', (array) $destination_value);
$statements .= $operator_map_value . ' (' . implode(',', $author_ids) . ')';
} else if (in_array($operator, ['is', 'is_not']) && !empty($destination_value)) {
$operator_map_value = ('is' === $operator) ? '=' : '<>';
- $author_id = is_array($destination_value) ? $destination_value[0] : $destination_value;
+ $author_id = intval(is_array($destination_value) ? $destination_value[0] : $destination_value);
$statements .= $operator_map_value . ''' . $author_id . ''';
} else if (in_array($operator, ['empty', 'not_empty'])) {
$operator_map_value = ('empty' === $operator) ? '=' : '<>';
--- a/tablesome/includes/actions.php
+++ b/tablesome/includes/actions.php
@@ -24,6 +24,8 @@
public $async_email_handler;
+ public $redirect_token;
+
public function __construct() {
$this->utils = new TablesomeIncludesUtils();
/** plugin activation Hook */
@@ -243,6 +245,29 @@
// Belows are callback functions of adding Actions order wise
public function init_hook() {
$this->setGlobalCurrentUserID();
+ // Set the redirect token cookie early, before any output is sent.
+ // This must happen in init (not wp_enqueue_scripts) because themes
+ // output HTML before wp_head(), making headers_sent() return true.
+ // Exclude wp-login.php: is_admin() returns false for it, but setting
+ // a cookie there creates stale tokens that mismatch the JS token
+ // generated on the actual form page.
+ $is_login_page = isset( $GLOBALS['pagenow'] ) && $GLOBALS['pagenow'] === 'wp-login.php';
+ // REST_REQUEST constant is not defined until parse_request (after init),
+ // so detect REST API requests via REQUEST_URI instead.
+ $request_uri = $_SERVER['REQUEST_URI'] ?? '';
+ $is_rest_request = defined( 'REST_REQUEST' ) || strpos( $request_uri, '/wp-json/' ) !== false || strpos( $request_uri, 'rest_route=' ) !== false;
+ if ( !is_admin() && !$is_login_page && !wp_doing_ajax() && !$is_rest_request && !headers_sent() ) {
+ $this->redirect_token = wp_generate_password( 32, false );
+ setcookie(
+ 'tablesome_redirect_token',
+ $this->redirect_token,
+ 0,
+ COOKIEPATH,
+ COOKIE_DOMAIN,
+ is_ssl(),
+ false
+ );
+ }
$this->cron->action( 'start' );
/* Tablesome Table-Actions Ajax Hooks */
new TablesomeIncludesAjax_Handler();
@@ -961,6 +986,11 @@
),
"site_domain" => $_SERVER['SERVER_NAME'],
);
+ // Use the redirect token that was generated and set as a cookie
+ // during init_hook (before any output was sent)
+ if ( !empty( $this->redirect_token ) ) {
+ $tablesome_ajax_object['redirect_token'] = $this->redirect_token;
+ }
return $tablesome_ajax_object;
}
--- a/tablesome/includes/ajax-handler.php
+++ b/tablesome/includes/ajax-handler.php
@@ -27,11 +27,32 @@
add_action('wp_ajax_update_feature_notice_dismissal_data_via_ajax', array(new TablesomeIncludesModulesFeature_Notice(), 'update_feature_notice_dismissal_data_via_ajax'));
add_action("wp_ajax_get_redirection_data", array($this, 'get_redirection_data'));
+ add_action("wp_ajax_nopriv_get_redirection_data", array($this, 'get_redirection_data'));
}
+ private function verify_nonce()
+ {
+ // Check both _wpnonce and nonce fields for backwards compatibility
+ if (check_ajax_referer('tablesome_nonce', '_wpnonce', false)) {
+ return true;
+ }
+ if (check_ajax_referer('tablesome_nonce', 'nonce', false)) {
+ return true;
+ }
+ return false;
+ }
+
public function save_table()
{
+ if (!$this->verify_nonce()) {
+ wp_send_json_error(array(
+ 'message' => 'Security check failed. Please refresh the page and try again.',
+ 'code' => 'invalid_nonce'
+ ), 403);
+ wp_die();
+ }
+
if (!current_user_can('manage_options')) {
wp_send_json_error(array(
'message' => 'Unauthorized access. You do not have permission to perform this action.',
@@ -74,6 +95,14 @@
public function get_table_columns_by_table_id()
{
+ if (!$this->verify_nonce()) {
+ wp_send_json_error(array(
+ 'message' => 'Security check failed. Please refresh the page and try again.',
+ 'code' => 'invalid_nonce'
+ ), 403);
+ wp_die();
+ }
+
if (!current_user_can('edit_posts')) {
wp_send_json_error(array(
'message' => 'Unauthorized access. You do not have permission to view table columns.',
@@ -126,18 +155,29 @@
public function get_redirection_data()
{
- if (!is_user_logged_in()) {
+ if (!$this->verify_nonce()) {
wp_send_json_error(array(
- 'message' => 'Unauthorized access. You must be logged in to access this resource.',
- 'code' => 'unauthorized'
+ 'message' => 'Security check failed. Please refresh the page and try again.',
+ 'code' => 'invalid_nonce'
), 403);
wp_die();
}
- $redirection_data = get_option('workflow_redirection_data');
+ $redirection_data = [];
- error_log('*** get_redirection_data ***');
- $redirection_data = isset($redirection_data) && !empty($redirection_data) ? $redirection_data : [];
+ // Read redirect data from the per-session transient keyed by redirect token
+ $redirect_token = isset($_REQUEST['redirect_token'])
+ ? sanitize_text_field($_REQUEST['redirect_token'])
+ : '';
+
+ if (!empty($redirect_token)) {
+ $transient_key = 'tablesome_redir_' . substr(md5($redirect_token), 0, 20);
+ $transient_data = get_transient($transient_key);
+ if (!empty($transient_data)) {
+ $redirection_data = $transient_data;
+ delete_transient($transient_key);
+ }
+ }
$response = array(
'status' => 'success',
@@ -145,7 +185,6 @@
'data' => $redirection_data,
);
- delete_option('workflow_redirection_data');
wp_send_json($response);
wp_die();
}
--- a/tablesome/includes/lib/freemius-integrator.php
+++ b/tablesome/includes/lib/freemius-integrator.php
@@ -12,25 +12,26 @@
// }
// require_once $freemius_wordpress_sdk;
$tablesome_fs = fs_dynamic_init( array(
- 'id' => '7163',
- 'slug' => 'tablesome',
- 'type' => 'plugin',
- 'public_key' => 'pk_12b7206bfde98e6b6646e8714b8f2',
- 'is_premium' => false,
- 'premium_suffix' => '',
- 'has_addons' => false,
- 'has_paid_plans' => true,
- 'trial' => array(
+ 'id' => '7163',
+ 'slug' => 'tablesome',
+ 'type' => 'plugin',
+ 'public_key' => 'pk_12b7206bfde98e6b6646e8714b8f2',
+ 'is_premium' => false,
+ 'premium_suffix' => '',
+ 'has_addons' => false,
+ 'has_paid_plans' => true,
+ 'trial' => array(
'days' => 7,
'is_require_payment' => true,
),
- 'menu' => array(
+ 'menu' => array(
'slug' => 'edit.php?post_type=' . TABLESOME_CPT,
'first-path' => 'edit.php?post_type=' . TABLESOME_CPT . '&page=tablesome-onboarding',
'contact' => false,
'support' => true,
),
- 'is_live' => true,
+ 'is_live' => true,
+ 'is_org_compliant' => true,
) );
}
return $tablesome_fs;
--- a/tablesome/includes/modules/datatable/record.php
+++ b/tablesome/includes/modules/datatable/record.php
@@ -68,16 +68,27 @@
{
$prevent_field_column = isset($insert_args['prevent_field_column']) ? $insert_args['prevent_field_column'] : "";
-
+
// Return false if no duplicate prevention column is specified
if (empty($prevent_field_column)) {
return false;
}
-
- $prevent_field_value = isset($data[$prevent_field_column]) ? $data[$prevent_field_column] : "";
+
+ // Sanitize column name to prevent SQL injection
+ $mysql = new TablesomeIncludesModulesMyqueMysql();
+ $sanitized_column = $mysql->sanitize_column_name($prevent_field_column);
+ if (empty($sanitized_column)) {
+ error_log('check_if_duplicate: rejected invalid prevent_field_column');
+ return false;
+ }
+
+ $prevent_field_value = isset($data[$sanitized_column]) ? $data[$sanitized_column] : "";
global $wpdb;
- $query = "SELECT * FROM $table_name WHERE `$prevent_field_column` = '$prevent_field_value' LIMIT 1;";
+ $query = $wpdb->prepare(
+ "SELECT * FROM `$table_name` WHERE `$sanitized_column` = %s LIMIT 1;",
+ $prevent_field_value
+ );
$result = $wpdb->get_results($query);
return !empty($result) ? true : false;
}
--- a/tablesome/includes/modules/feature-notice.php
+++ b/tablesome/includes/modules/feature-notice.php
@@ -103,6 +103,15 @@
public function update_feature_notice_dismissal_data_via_ajax()
{
+ if (!check_ajax_referer('tablesome_nonce', '_wpnonce', false) &&
+ !check_ajax_referer('tablesome_nonce', 'nonce', false)) {
+ wp_send_json_error(array(
+ 'message' => 'Security check failed.',
+ 'code' => 'invalid_nonce'
+ ), 403);
+ wp_die();
+ }
+
update_option($this->dismissed_option_name, TABLESOME_VERSION);
$response = array(
--- a/tablesome/includes/modules/myque/mysql.php
+++ b/tablesome/includes/modules/myque/mysql.php
@@ -50,12 +50,13 @@
public function get_row_count($table_id)
{
- if( !isset($table_id) || empty($table_id) || is_null($table_id) || $table_id == 0 ){
+ $table_id = intval($table_id);
+ if( empty($table_id) || $table_id == 0 ){
return 0;
}
$table_name = TABLESOME_TABLE_NAME . '_' . $table_id;
$table_name = $this->wpdb->prefix . $table_name;
- $query = "SELECT COUNT(*) FROM $table_name";
+ $query = "SELECT COUNT(*) FROM `$table_name`";
return $this->wpdb->get_var($query);
}
@@ -116,52 +117,33 @@
return 0;
}
- $query = "INSERT INTO $table_name (";
-
- $ii = 0;
-
if (count($record) == 0) {
return 0;
}
- // Check if record has columns missing in table
- // $this->check_and_add_missing_columns($record, $table_name);
-
-
- foreach ($record as $key => $cell) {
- # code...
- // $column_name = $this->get_column_name($cell);
- $column_name = $key;
- $query .= " `$column_name`";
-
- // Add comma if not the last item
- if ($ii < count($record) - 1) {
- $query .= ",";
- }
- $ii++;
- } // END of cell loop
-
- $query .= ") SELECT ";
-
- $ii = 0;
+ // Filter to only valid column names, keeping columns and values in sync
+ $columns = [];
+ $values = [];
foreach ($record as $key => $value) {
- # code...
- // $value = $cell['value'];
- $value = esc_sql($value);
- $query .= " '$value'";
- // Add comma if not the last item
- if ($ii < count($record) - 1) {
- $query .= ",";
+ $column_name = $this->sanitize_column_name($key);
+ if (empty($column_name)) {
+ continue;
}
+ $columns[] = "`$column_name`";
+ $values[] = "'" . esc_sql($value) . "'";
+ }
- $ii++;
- } // END of cell loop
+ if (empty($columns)) {
+ return 0;
+ }
+
+ $query = "INSERT INTO $table_name (" . implode(", ", $columns) . ") SELECT " . implode(", ", $values);
$query .= " ";
$enabled_prevent_duplication = isset($insert_args['enable_duplication_prevention']) && $insert_args['enable_duplication_prevention'] == 1 ? true : false;
$enabled_limit_submission = isset($insert_args['enable_submission_limit']) && $insert_args['enable_submission_limit'] == 1 ? true : false;
$submission_limit = isset($insert_args['max_allowed_submissions']) && !empty($insert_args['max_allowed_submissions']) ? intval($insert_args['max_allowed_submissions']) : 100;
- $prevent_field_column = isset($insert_args['prevent_field_column']) ? $insert_args['prevent_field_column'] : "";
+ $prevent_field_column = isset($insert_args['prevent_field_column']) ? $this->sanitize_column_name($insert_args['prevent_field_column']) : "";
$can_add_prevent_query = ($enabled_prevent_duplication || $enabled_limit_submission);
if ($can_add_prevent_query) {
@@ -171,7 +153,10 @@
$query .= "WHERE ";
if ($enabled_prevent_duplication && !empty($prevent_field_column) && isset($record[$prevent_field_column])) {
- $query .= "NOT EXISTS (SELECT 1 FROM " . $table_name . " WHERE " . $prevent_field_column . " = '" . esc_sql($record[$prevent_field_column]) . "' ) ";
+ $query .= $wpdb->prepare(
+ "NOT EXISTS (SELECT 1 FROM " . $table_name . " WHERE `" . $prevent_field_column . "` = %s ) ",
+ $record[$prevent_field_column]
+ );
}
$query .= " "; // space
@@ -185,11 +170,6 @@
}
}
- // error_log('$query : ' . $query);
-
- // Todo: Add wpdb->prepare() to $query
- // Example: $wpdb->query( $wpdb->prepare($query) );
-
$insert_success_bool = $wpdb->query($query);
$inserted_record_id = $wpdb->insert_id;
// $inserted_record_id = $wpdb->query("SELECT LAST_INSERT_ID();");
@@ -204,22 +184,35 @@
{
// error_log('update_record $record : ' . print_r($record, true));
- $prevent_field_column = isset($insert_args['prevent_field_column']) ? $insert_args['prevent_field_column'] : "";
+ $prevent_field_column = isset($insert_args['prevent_field_column']) ? $this->sanitize_column_name($insert_args['prevent_field_column']) : "";
$prevent_field_value = isset($record[$prevent_field_column]) ? $record[$prevent_field_column] : "";
+ if (empty($prevent_field_column)) {
+ error_log('update_record: invalid prevent_field_column');
+ return $record;
+ }
+
global $wpdb;
- $query = "UPDATE $table_name SET ";
- $ii = 0;
+ $assignments = [];
foreach ($record as $key => $value) {
- $value = esc_sql($value);
- $query .= " `$key` = '$value'";
- // Add comma if not the last item
- if ($ii < count($record) - 1) {
- $query .= ",";
+ $sanitized_key = $this->sanitize_column_name($key);
+ if (empty($sanitized_key)) {
+ continue;
}
- $ii++;
- } // END of cell loop
- $query .= " WHERE `$prevent_field_column` = '$prevent_field_value' LIMIT 1;";
+ $value = esc_sql($value);
+ $assignments[] = "`$sanitized_key` = '$value'";
+ }
+
+ if (empty($assignments)) {
+ error_log('update_record: no valid columns to update');
+ return $record;
+ }
+
+ $query = "UPDATE `$table_name` SET " . implode(", ", $assignments);
+ $query = $wpdb->prepare(
+ $query . " WHERE `$prevent_field_column` = %s LIMIT 1;",
+ $prevent_field_value
+ );
$wpdb->query($query);
return $record;
}
@@ -228,10 +221,16 @@
global $wpdb;
$table_name = $args['table_name'];
$args['table_name'] = $table_name;
- $column_name = $args['name'];
- $column_type = $args['format'];
+ $column_name = $this->sanitize_column_name($args['name']);
+ $column_type = $this->sanitize_column_type($args['format']);
+
+ if (empty($column_name) || empty($column_type)) {
+ error_log('insert_column: invalid column name or type');
+ $response['new_column_created'] = false;
+ return $response;
+ }
- $query = "ALTER TABLE $table_name ADD $column_name $column_type DEFAULT ''";
+ $query = "ALTER TABLE `$table_name` ADD `$column_name` $column_type DEFAULT ''";
// error_log('insert_column $query : ' . $query);
$response['new_column_created'] = $wpdb->query($query);
return $response;
@@ -242,15 +241,15 @@
global $wpdb;
$table_name = $args['table_name'];
$args['table_name'] = $table_name;
- $source_column = $args['source_column'];
- $target_column = $args['target_column'];
+ $source_column = $this->sanitize_column_name($args['source_column']);
+ $target_column = $this->sanitize_column_name($args['target_column']);
if(empty($source_column) || empty($target_column)){
- error_log('copy_column_content: $source_column or $target_column is empty');
+ error_log('copy_column_content: $source_column or $target_column is empty or invalid');
return $response;
}
- $query = "UPDATE $table_name SET $target_column = $source_column";
+ $query = "UPDATE `$table_name` SET `$target_column` = `$source_column`";
$response['copied_column_records'] = $wpdb->query($query);
return $response;
}
@@ -262,11 +261,11 @@
global $wpdb;
$table_name = $args['table_name'];
$args['table_name'] = $table_name;
- $source_column = $args['source_column'];
- $target_column = $args['target_column'];
+ $source_column = $this->sanitize_column_name($args['source_column']);
+ $target_column = $this->sanitize_column_name($args['target_column']);
if(empty($source_column) || empty($target_column)){
- error_log('duplicate_column: $source_column or $target_column is empty');
+ error_log('duplicate_column: $source_column or $target_column is empty or invalid');
return $response;
}
@@ -278,11 +277,11 @@
}
// Create New Column
- $query = "ALTER TABLE $table_name ADD $target_column TEXT NOT NULL";
+ $query = "ALTER TABLE `$table_name` ADD `$target_column` TEXT NOT NULL";
$response['new_column_created'] = $wpdb->query($query);
// Copy Data from Source Column to Target Column
- $query = "UPDATE $table_name SET $target_column = $source_column";
+ $query = "UPDATE `$table_name` SET `$target_column` = `$source_column`";
// error_log('$query : ' . $query);
$response['copied_column_records'] = $wpdb->query($query);
@@ -294,8 +293,9 @@
if (empty($record_id)) {
return null;
}
+ $record_id = intval($record_id);
$table_name = $args['table_name'];
- $query = "select * from {$table_name} where id = {$record_id}";
+ $query = $this->wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $record_id);
$db_record = $this->wpdb->get_row($query);
if (is_wp_error($db_record)) {
error_log("get_record error:" . $db_record->get_error_message());
@@ -318,7 +318,7 @@
}
$query .= $this->orderby($args);
- $query .= " LIMIT " . $args['limit'];
+ $query .= " LIMIT " . max(1, intval($args['limit']));
$result = $wpdb->get_results($query);
@@ -333,12 +333,30 @@
{
$sql_string = " ORDER BY ";
$orderByArgs = [];
+
+ // Validate orderby column names against actual table columns
+ $table_columns = $this->get_table_columns($args['table_name']);
+ $allowed_columns = ['id', 'rank_order'];
+ foreach ($table_columns as $col) {
+ $allowed_columns[] = $col['Field'];
+ }
+
foreach ($args['orderby'] as $key => $value) {
- $orderByArgs[] = $args['table_name'] . "." . $value;
+ $value = sanitize_key($value);
+ if (in_array($value, $allowed_columns, true)) {
+ $orderByArgs[] = "`" . $args['table_name'] . "`.`" . $value . "`";
+ }
}
- // Looks like wptablesome_table_287.column_2, wptablesome_table_287.column_3 ....
+
+ if (empty($orderByArgs)) {
+ $orderByArgs[] = "`" . $args['table_name'] . "`.`id`";
+ }
+
$sql_string = $sql_string . implode(',', $orderByArgs);
- $sql_string .= " " . $args['order'];
+
+ // Whitelist order direction
+ $order = strtoupper(trim($args['order']));
+ $sql_string .= in_array($order, ['ASC', 'DESC'], true) ? " " . $order : " ASC";
return $sql_string;
}
@@ -527,7 +545,13 @@
return $sql_string;
}
- if ($operand2 == 'today') {
+ if ($operand2 == 'today_and_after') {
+ $negate = ($mysql_operator == '!=' || $mysql_operator == 'is_not');
+ $sql_string .= "DATE($operand1_query_string) " . ($negate ? '<' : '>=') . " CURDATE()";
+ } else if ($operand2 == 'today_and_before') {
+ $negate = ($mysql_operator == '!=' || $mysql_operator == 'is_not');
+ $sql_string .= "DATE($operand1_query_string) " . ($negate ? '>' : '<=') . " CURDATE()";
+ } else if ($operand2 == 'today') {
$sql_string .= "DATE($operand1_query_string) $mysql_operator CURDATE()";
} else if ($operand2 == 'tomorrow') {
$sql_string .= "DATEDIFF($operand1_query_string, CURDATE()) $mysql_operator 1";
@@ -682,6 +706,12 @@
$condition['mysql_operator'] = $validated_operator;
}
+ // SECURITY FIX: Cast operand_2 to numeric to prevent SQL injection
+ // For number conditions, operand_2 is interpolated directly into SQL
+ if ($condition['data_type'] == 'number') {
+ $condition['operand_2'] = floatval($condition['operand_2']);
+ }
+
return $condition;
}
@@ -727,19 +757,93 @@
}
}
- //
- // SHOULD ADD ' '
- $condition['operand_2'] = "'" . $condition['operand_2'] . "'";
+ // Escape operand_2 value to prevent SQL injection
+ $condition['operand_2'] = "'" . esc_sql($condition['operand_2']) . "'";
return $condition;
}
+ /**
+ * Sanitize a column name to prevent SQL injection.
+ * Only allows alphanumeric characters and underscores (matching the column_N pattern).
+ *
+ * @param string $column_name The column name to sanitize
+ * @return string Sanitized column name, or empty string if invalid
+ */
+ public function sanitize_column_name($column_name)
+ {
+ if (empty($column_name) || !is_string($column_name)) {
+ return '';
+ }
+
+ // Column names must match pattern: word characters only (letters, digits, underscores)
+ if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $column_name)) {
+ error_log('sanitize_column_name: rejected invalid column name: ' . substr($column_name, 0, 100));
+ return '';
+ }
+
+ return $column_name;
+ }
+
+ /**
+ * Sanitize a column type for ALTER TABLE statements.
+ * Only allows known MySQL data types.
+ *
+ * @param string $column_type The column type to sanitize
+ * @return string Sanitized column type, or empty string if invalid
+ */
+ public function sanitize_column_type($column_type)
+ {
+ if (empty($column_type) || !is_string($column_type)) {
+ return '';
+ }
+
+ $allowed_types = [
+ 'text', 'TEXT',
+ 'varchar', 'VARCHAR',
+ 'int', 'INT',
+ 'integer', 'INTEGER',
+ 'bigint', 'BIGINT',
+ 'float', 'FLOAT',
+ 'double', 'DOUBLE',
+ 'decimal', 'DECIMAL',
+ 'date', 'DATE',
+ 'datetime', 'DATETIME',
+ 'timestamp', 'TIMESTAMP',
+ 'boolean', 'BOOLEAN',
+ 'tinyint', 'TINYINT',
+ 'smallint', 'SMALLINT',
+ 'mediumint', 'MEDIUMINT',
+ 'mediumtext', 'MEDIUMTEXT',
+ 'longtext', 'LONGTEXT',
+ 'json', 'JSON',
+ ];
+
+ $type_upper = strtoupper(trim($column_type));
+
+ // Handle types with length specifiers like VARCHAR(255)
+ if (preg_match('/^([A-Z]+)((d+))?$/', $type_upper, $matches)) {
+ if (in_array($matches[1], array_map('strtoupper', $allowed_types), true)) {
+ return $type_upper;
+ }
+ }
+
+ // Check for exact match
+ if (in_array($column_type, $allowed_types, true)) {
+ return $column_type;
+ }
+
+ error_log('sanitize_column_type: rejected invalid column type: ' . substr($column_type, 0, 100));
+ return '';
+ }
+
public function delete_table($table_id)
{
global $wpdb;
+ $table_id = intval($table_id);
$table_name = TABLESOME_TABLE_NAME . '_' . $table_id;
$table_name = $wpdb->prefix . $table_name;
- $query = "DROP TABLE IF EXISTS $table_name";
+ $query = "DROP TABLE IF EXISTS `$table_name`";
$result = $wpdb->query($query);
// error_log('delete_table $result : ' . $result);
return $result;
@@ -748,9 +852,10 @@
public function empty_the_table($table_id)
{
global $wpdb;
+ $table_id = intval($table_id);
$table_name = TABLESOME_TABLE_NAME . '_' . $table_id;
$table_name = $wpdb->prefix . $table_name;
- $query = "DELETE FROM $table_name";
+ $query = "DELETE FROM `$table_name`";
$result = $wpdb->query($query);
return $result;
}
--- a/tablesome/includes/modules/tablesomedb-rest-api/tablesomedb-rest-api.php
+++ b/tablesome/includes/modules/tablesomedb-rest-api/tablesomedb-rest-api.php
@@ -983,6 +983,10 @@
$query_args = isset($params['query_args']) && is_array($params['query_args']) ? $params['query_args'] : [];
+ // Whitelist allowed query_args keys to prevent parameter injection
+ $allowed_query_keys = ['where', 'orderby', 'order', 'limit', 'table_meta', 'collection', 'number'];
+ $query_args = array_intersect_key($query_args, array_flip($allowed_query_keys));
+
$post = get_post($table_id);
$access_info = $this->check_table_access($post);
@@ -991,30 +995,27 @@
return new WP_Error($access_info['error_code'], $access_info['message']);
}
- // if (empty($post) || $post->post_type != TABLESOME_CPT) {
- // $error_code = "INVALID_POST";
- // return new WP_Error($error_code, $this->get_error_message($error_code));
- // }
$table_meta = get_tablesome_data($post->ID);
$tablesome_db = new TablesomeIncludesModulesTablesomeDBTablesomeDB();
$table = $tablesome_db->create_table_instance($post->ID);
+ // Merge with safe defaults — table_id and table_name cannot be overridden
+ // since they're not in the allowed query_args keys whitelist
$args = array_merge(
+ $query_args,
array(
'table_id' => $post->ID,
'table_name' => $table->name,
- ), $query_args
+ 'table_meta' => $table_meta,
+ 'collection' => isset($query_args['collection']) ? $query_args['collection'] : [],
+ )
);
+ // Use get_rows which returns already-formatted rows
$records = $tablesome_db->get_rows($args);
- // $query = $tablesome_db->query($query_args);
-
- // // TODO: Return the formatted data if need. don't send the actual db data
- // $records = isset($query->items) ? $query->items : [];
-
$response_data = array(
- 'records' => $tablesome_db->get_formatted_rows($records, $table_meta, []),
+ 'records' => $records,
'message' => 'Get records successfully',
'status' => 'success',
);
--- a/tablesome/includes/modules/workflow/abstract-trigger.php
+++ b/tablesome/includes/modules/workflow/abstract-trigger.php
@@ -44,9 +44,6 @@
return;
}
- // delete redirection data in DB before loop the trigger instances
- delete_option('workflow_redirection_data');
-
// TODO: Will be removed in the future. It Will be replaced with the $workflow_data variable.
$placeholders = $this->getPlaceholders($trigger_source_data);
@@ -57,9 +54,17 @@
}
- // store the redirection data in DB if any redirection action has configured
+ // Store redirection data keyed by the per-page-view redirect token
+ // to prevent race conditions between concurrent form submissions
if (isset($workflow_redirection_data) && count($workflow_redirection_data) > 0) {
- update_option('workflow_redirection_data', $workflow_redirection_data);
+ $redirect_token = isset($_COOKIE['tablesome_redirect_token'])
+ ? sanitize_text_field($_COOKIE['tablesome_redirect_token'])
+ : '';
+
+ if (!empty($redirect_token)) {
+ $transient_key = 'tablesome_redir_' . substr(md5($redirect_token), 0, 20);
+ set_transient($transient_key, $workflow_redirection_data, 60);
+ }
}
}
--- a/tablesome/includes/modules/workflow/traits/placeholder.php
+++ b/tablesome/includes/modules/workflow/traits/placeholder.php
@@ -47,6 +47,9 @@
public function applyPlaceholders($placeholders, $content)
{
// error_log('applyPlaceholders() $placeholders : ' . print_r($placeholders, true));
+ if (!is_string($content)) {
+ return '';
+ }
foreach ($placeholders as $name => $value) {
$content = str_replace($name, $value, $content);
}
--- a/tablesome/tablesome.php
+++ b/tablesome/tablesome.php
@@ -5,7 +5,7 @@
Plugin URI: https://tablesomewp.com/
Description: Powerful Tables + Form Automations. Save, Edit, Display (frontend) & Export Form entries, integrate with Notion, Redirection for Forms. Supports Contact Form 7, WPForms, Gravity Forms, Fluent, Elementor and more
Author: Pauple
-Version: 1.2.2
+Version: 1.2.4
Author URI: http://pauple.com
Network: False
Text Domain: tablesome
@@ -46,7 +46,7 @@
{
$constants = [
- 'TABLESOME_VERSION' => '1.2.2',
+ 'TABLESOME_VERSION' => '1.2.4',
'TABLESOME_DOMAIN' => 'tablesome',
'TABLESOME_CPT' => 'tablesome_cpt',
'TABLESOME__FILE__' => __FILE__,
--- a/tablesome/vendor/composer/autoload_classmap.php
+++ b/tablesome/vendor/composer/autoload_classmap.php
@@ -158,6 +158,7 @@
'Tablesome\Workflow_Library\Actions\Tablesome_Filter_Table' => $baseDir . '/workflow-library/actions/tablesome-filter-table.php',
'Tablesome\Workflow_Library\Actions\Tablesome_Generate_Pdf' => $baseDir . '/workflow-library/actions/tablesome-generate-pdf.php',
'Tablesome\Workflow_Library\Actions\Tablesome_Load_WP_Query_Content' => $baseDir . '/workflow-library/actions/tablesome-load-wp-query-content.php',
+ 'Tablesome\Workflow_Library\Actions\Tablesome_PDF_HTML' => $baseDir . '/workflow-library/actions/tablesome-pdf-html.php',
'Tablesome\Workflow_Library\Actions\WP_Post_Creation' => $baseDir . '/workflow-library/actions/wp-post-creation.php',
'Tablesome\Workflow_Library\Actions\WP_Redirection' => $baseDir . '/workflow-library/actions/wp-redirection.php',
'Tablesome\Workflow_Library\Actions\WP_Send_Mail' => $baseDir . '/workflow-library/actions/wp-send-mail.php',
--- a/tablesome/vendor/composer/autoload_static.php
+++ b/tablesome/vendor/composer/autoload_static.php
@@ -181,6 +181,7 @@
'Tablesome\Workflow_Library\Actions\Tablesome_Filter_Table' => __DIR__ . '/../..' . '/workflow-library/actions/tablesome-filter-table.php',
'Tablesome\Workflow_Library\Actions\Tablesome_Generate_Pdf' => __DIR__ . '/../..' . '/workflow-library/actions/tablesome-generate-pdf.php',
'Tablesome\Workflow_Library\Actions\Tablesome_Load_WP_Query_Content' => __DIR__ . '/../..' . '/workflow-library/actions/tablesome-load-wp-query-content.php',
+ 'Tablesome\Workflow_Library\Actions\Tablesome_PDF_HTML' => __DIR__ . '/../..' . '/workflow-library/actions/tablesome-pdf-html.php',
'Tablesome\Workflow_Library\Actions\WP_Post_Creation' => __DIR__ . '/../..' . '/workflow-library/actions/wp-post-creation.php',
'Tablesome\Workflow_Library\Actions\WP_Redirection' => __DIR__ . '/../..' . '/workflow-library/actions/wp-redirection.php',
'Tablesome\Workflow_Library\Actions\WP_Send_Mail' => __DIR__ . '/../..' . '/workflow-library/actions/wp-send-mail.php',
--- a/tablesome/vendor/composer/installed.php
+++ b/tablesome/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'pauple/tablesome',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
- 'reference' => '10c5099e3934381946b136e1b9a4081621d77102',
+ 'reference' => 'f2a0c2ac1945d58d6553193cc39b1e1623431bad',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -31,7 +31,7 @@
'pauple/tablesome' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
- 'reference' => '10c5099e3934381946b136e1b9a4081621d77102',
+ 'reference' => 'f2a0c2ac1945d58d6553193cc39b1e1623431bad',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
--- a/tablesome/workflow-library/actions/tablesome-generate-pdf.php
+++ b/tablesome/workflow-library/actions/tablesome-generate-pdf.php
@@ -69,7 +69,7 @@
$field_name = $key;
$fields_to_use[$field_name] = [];
// Apply regular placeholders first
- $content = $this->applyPlaceholders( $this->placeholders, $field['content'] );
+ $content = $this->applyPlaceholders( $this->placeholders, ( isset( $field['content'] ) ? $field['content'] : '' ) );
// Then apply signature placeholders (converts URLs to img tags)
$content = $this->applyPlaceholders( $signature_placeholders, $content );
$fields_to_use[$field_name]['value'] = $content;
@@ -104,27 +104,55 @@
if ( !empty( $file_url ) ) {
// Create the placeholder name
$placeholder_name = $this->getPlaceholderName( $field_name );
- // Convert URL to img tag with compact dimensions suitable for signatures
- // Using width=120 height=50 for a more compact signature
+ // Width-only constraint preserves aspect ratio
+ // 300px at 96 DPI = ~79mm, reasonable for signature on A4
+ // Get width from action meta or use default
+ $signature_width = ( isset( $this->action_meta['signature_width'] ) ? absint( $this->action_meta['signature_width'] ) : 300 );
+ // Apply filter for developer customization
+ $signature_width = apply_filters( 'tablesome_pdf_signature_width', $signature_width );
// Adding <br> tags to ensure it appears on its own line, left-aligned
- $img_tag = sprintf( '<br><img src="%s" width="120" height="50"><br>', esc_url( $file_url ) );
+ // datasig attribute marks this as a signature so the PDF renderer
+ // uses this explicit width instead of the global image width setting.
+ $img_tag = sprintf( '<br><img src="%s" width="%d" datasig="1"><br>', esc_url( $file_url ), $signature_width );
// Store the replacement - the placeholder with the URL will be replaced with img tag
$placeholders[$file_url] = $img_tag;
- error_log( "PDF Signature: Converting {$field_name} URL to img tag (120x50 with line breaks)" );
}
}
}
return $placeholders;
}
+ /**
+ * Strip width/height attributes from non-signature <img> tags.
+ * Signature images (marked with datasig attribute) keep their
+ * explicit width so the PDF renderer respects signature_width.
+ */
+ protected function strip_image_dimensions( $html ) {
+ return preg_replace_callback( '/<img\b([^>]*)>/i', function ( $m ) {
+ $attrs = $m[1];
+ // Keep dimensions on signature images
+ if ( stripos( $attrs, 'datasig' ) !== false ) {
+ return $m[0];
+ }
+ $attrs = preg_replace( '/\s*width=["'][^"']*["']/i', '', $attrs );
+ $attrs = preg_replace( '/\s*height=["'][^"']*["']/i', '', $attrs );
+ return '<img' . $attrs . '>';
+ }, $html );
+ }
+
public function create_pdf( $fields ) {
- require_once TABLESOME_PATH . 'includes/lib/fpdf/pdf-html.php';
- $pdf = new PDF_HTML(
+ require_once __DIR__ . '/tablesome-pdf-html.php';
+ $pdf = new Tablesome_PDF_HTML(
'P',
'mm',
'A4',
$fields
);
+ // Set configurable image width (default 300px)
+ $image_width = ( isset( $this->action_meta['image_width'] ) ? absint( $this->action_meta['image_width'] ) : 300 );
+ $image_width = apply_filters( 'tablesome_pdf_image_width', $image_width );
+ $pdf->setImageWidth( $image_width );
+ $pdf->setUploadDir( wp_upload_dir() );
$pdf->AddPage();
$title = $fields['title']['value'];
$pdf->SetFont( 'Arial', 'B', 24 );
@@ -141,13 +169,13 @@
$pdf->SetFont( 'Arial', '', 10 );
// Fix space issue
$body = " " . $body;
+ // Strip width/height from non-signature images so the PDF renderer
+ // uses the configured image_width setting instead of WordPress's
+ // thumbnail dimensions (e.g. width="150").
+ $body = $this->strip_image_dimensions( $body );
$pdf->WriteHTML( $body );
/* Footer */
// $this->footer($pdf);
- /* Output */
- $pdf_output = $pdf->Output( 'S' );
- // error_log('$pdf_output: ' . $pdf_output);
- // $pdf->Output('F', TABLESOME_PATH . '/report.pdf');
$file_info = $this->save_file( $pdf );
return $file_info;
}
@@ -160,6 +188,7 @@
$file_path = $base_path . $file_name;
$this->wp_media_file_handler->maybe_create_dir( $base_path );
$pdf->Output( 'F', $file_path );
+ $pdf->freeMemory();
$url = $upload_dir['baseurl'] . '/' . $this->tmp_direrctory_name . '/' . $file_name;
// Upload file to media library
$attachment_id = $this->wp_media_file_handler->upload_file_from_url( $url, [
--- a/tablesome/workflow-library/actions/tablesome-pdf-html.php
+++ b/tablesome/workflow-library/actions/tablesome-pdf-html.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace TablesomeWorkflow_LibraryActions;
+
+if (!defined('ABSPATH')) {
+ exit;
+}
+
+require_once TABLESOME_PATH . 'includes/lib/fpdf/pdf-html.php';
+
+/**
+ * Extends the third-party PDF_HTML class with WordPress-specific
+ * image handling: local file resolution, quality upgrade, configurable
+ * width, aspect-ratio preservation, and proper page flow.
+ */
+class Tablesome_PDF_HTML extends PDF_HTML
+{
+ protected $imageWidth = 300;
+ protected $uploadBaseUrl = '';
+ protected $uploadBaseUrlHttps = '';
+ protected $uploadBaseDir = '';
+
+ public function setImageWidth($px)
+ {
+ $this->imageWidth = max(50, min(700, intval($px)));
+ }
+
+ public function setUploadDir($upload_dir)
+ {
+ $this->uploadBaseUrl = set_url_scheme($upload_dir['baseurl'], 'http');
+ $this->uploadBaseUrlHttps = set_url_scheme($upload_dir['baseurl'], 'https');
+ $this->uploadBaseDir = $upload_dir['basedir'];
+ }
+
+ public function OpenTag($tag, $attr)
+ {
+ if ($tag !== 'IMG') {
+ parent::OpenTag($tag, $attr);
+ return;
+ }
+
+ if (!isset($attr['SRC'])) {
+ return;
+ }
+
+ $hasWidth = isset($attr['WIDTH']) && $attr['WIDTH'] > 0;
+ $maxWidth = $this->GetPageWidth() - $this->lMargin - $this->rMargin;
+
+ $src = $this->resolveImageSrc($attr['SRC']);
+ $imageSize = @getimagesize($src);
+
+ // Determine target width in px:
+ // - If width attribute set (e.g. signatures): use it
+ // - Otherwise: use the configured default
+ $targetPx = $hasWidth ? intval($attr['WIDTH']) : $this->imageWidth;
+
+ // 96 DPI conversion (px to mm)
+ $width = $targetPx * 25.4 / 96;
+
+ // Constrain to printable page width
+ if ($width > $maxWidth) {
+ $width = $maxWidth;
+ }
+
+ // Calculate height from native aspect ratio
+ if ($imageSize !== false && $imageSize[0] > 0 && $imageSize[1] > 0) {
+ $nativeRatio = $imageSize[0] / $imageSize[1];
+ $height = $width / $nativeRatio;
+ } else {
+ // Cannot read dimensions: let FPDF auto-calculate height
+ $height = 0;
+ }
+
+ // Place image on its own line at left margin
+ $this->Ln(5);
+
+ // Use y=null to enable FPDF's flowing mode (automatic page breaks)
+ $this->Image($src, $this->lMargin, null, $width, $height);
+
+ // Advance past the image
+ $this->Ln(5);
+ $this->SetX($this->lMargin);
+ }
+
+ public function freeMemory()
+ {
+ $this->buffer = '';
+ $this->pages = [];
+ $this->images = [];
+ $this->PageInfo = [];
+ }
+
+ protected function resolveImageSrc($url)
+ {
+ if (empty($this->uploadBaseDir)) {
+ return $url;
+ }
+ $local = str_replace(
+ [$this->uploadBaseUrlHttps, $this->uploadBaseUrl],
+ $this->uploadBaseDir,
+ $url
+ );
+ if ($local === $url || !file_exists($local)) {
+ return $url;
+ }
+ return $this->upgradeToBestQuality($local);
+ }
+
+ protected function upgradeToBestQuality($path)
+ {
+ // Max file size to keep memory safe (5 MB)
+ $maxSize = 5 * 1024 * 1024;
+
+ if (!preg_match('/-d+xd+(.w+)$/', $path)) {
+ return $path;
+ }
+
+ $original = preg_replace('/-d+xd+(.w+)$/', '$1', $path);
+ $ext = pathinfo($original, PATHINFO_EXTENSION);
+ $scaled = preg_replace('/.' . preg_quote($ext, '/') . '$/', '-scaled.' . $ext, $original);
+
+ if (file_exists($scaled) && filesize($scaled) <= $maxSize) {
+ return $scaled;
+ }
+
+ if (file_exists($original) && filesize($original) <= $maxSize) {
+ return $original;
+ }
+
+ return $path;
+ }
+}
--- a/tablesome/workflow-library/triggers/cf7.php
+++ b/tablesome/workflow-library/triggers/cf7.php
@@ -17,6 +17,7 @@
*/
public $unsupported_formats = array(
'submit',
+ 'group',
);
public $trigger_source_id = 0;