Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/funnel-builder/admin/class-wffn-admin.php
+++ b/funnel-builder/admin/class-wffn-admin.php
@@ -800,7 +800,7 @@
wp_enqueue_style( 'wffn-flex-admin', $this->get_admin_url() . '/assets/css/admin.css', array(), WFFN_VERSION_DEV );
if ( WFFN_Core()->admin->is_wffn_flex_page() ) {
- $this->load_react_app( 'main-20251128093808' ); //phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation
+ $this->load_react_app( 'main-20251209133743' ); //phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation
if ( isset( $_GET['page'] ) && $_GET['page'] === 'bwf' && method_exists( 'BWF_Admin_General_Settings', 'get_localized_bwf_data' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
wp_localize_script( 'wffn-contact-admin', 'bwfAdminGen', BWF_Admin_General_Settings::get_instance()->get_localized_bwf_data() );
--- a/funnel-builder/admin/views/contact/dist/dashboard.asset.php
+++ b/funnel-builder/admin/views/contact/dist/dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => 'c388de3a08104755531e83b1b763c305');
No newline at end of file
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => 'cc74a92c60e3217a0dd5b2f8bb2bc981');
No newline at end of file
--- a/funnel-builder/admin/views/contact/dist/main-20251128093808.asset.php
+++ b/funnel-builder/admin/views/contact/dist/main-20251128093808.asset.php
@@ -1 +0,0 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-components/build-style/style.css', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => 'e42b4be7cad6f97cfbca96170f601fb1');
No newline at end of file
--- a/funnel-builder/admin/views/contact/dist/main-20251209133743.asset.php
+++ b/funnel-builder/admin/views/contact/dist/main-20251209133743.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-components/build-style/style.css', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => 'a4ae5fab7f4cadcfc5519409b16fb540');
No newline at end of file
--- a/funnel-builder/admin/views/contact/dist/settings.asset.php
+++ b/funnel-builder/admin/views/contact/dist/settings.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '2dd68a9a67bf3c69ccef8bf8d44d10ca');
No newline at end of file
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '13c6d5675a4ea3072fcd434ae073d884');
No newline at end of file
--- a/funnel-builder/contact-analytics/class-wfacp-contacts-analytics.php
+++ b/funnel-builder/contact-analytics/class-wfacp-contacts-analytics.php
@@ -1,14 +1,15 @@
<?php
-defined( 'ABSPATH' ) || exit; //Exit if accessed directly
+defined( 'ABSPATH' ) || exit; // Exit if accessed directly
/**
* Class WFACP_Contacts_Analytics
*/
if ( ! class_exists( 'WFACP_Contacts_Analytics' ) ) {
- class WFACP_Contacts_Analytics extends WFFN_REST_Controller{
+ class WFACP_Contacts_Analytics extends WFFN_REST_Controller {
/**
* instance of class
+ *
* @var null
*/
private static $ins = null;
@@ -36,15 +37,15 @@
*
* @return array|object|null
*/
- public function get_contacts($funnel_id, $search = '') {
+ public function get_contacts( $funnel_id, $search = '' ) {
global $wpdb;
if ( ! empty( $search ) ) {
- $query = $wpdb->prepare( "SELECT contact.id as cid, contact.f_name, contact.l_name, contact.email, aero.date, aero.total_revenue, aero.wfacp_id FROM " . $wpdb->prefix . 'bwf_contact' . " AS contact JOIN " . $wpdb->prefix . 'wfacp_stats' . " AS aero ON contact.id=aero.cid WHERE aero.fid=%d", $funnel_id );
- $query .= $wpdb->prepare( " AND (contact.f_name LIKE %s OR contact.email LIKE %s) group by contact.id", "%" . $search . "%", "%" . $search . "%" );
+ $query = $wpdb->prepare( 'SELECT contact.id as cid, contact.f_name, contact.l_name, contact.email, aero.date, aero.total_revenue, aero.wfacp_id FROM ' . $wpdb->prefix . 'bwf_contact' . ' AS contact JOIN ' . $wpdb->prefix . 'wfacp_stats' . ' AS aero ON contact.id=aero.cid WHERE aero.fid=%d', $funnel_id );
+ $query .= $wpdb->prepare( ' AND (contact.f_name LIKE %s OR contact.email LIKE %s) group by contact.id', '%' . $search . '%', '%' . $search . '%' );
} else {
- $query = $wpdb->prepare( "SELECT aero.cid FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero WHERE aero.fid=%d", $funnel_id );
+ $query = $wpdb->prepare( 'SELECT aero.cid FROM ' . $wpdb->prefix . 'wfacp_stats' . ' AS aero WHERE aero.fid=%d', $funnel_id );
}
return $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
@@ -59,12 +60,17 @@
*/
public function get_all_contacts_records( $funnel_id, $cid ) {
global $wpdb;
- $item_data = [];
- $funnel_id = ! empty( $funnel_id ) ? absint( $funnel_id ) : $funnel_id;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $query = "SELECT aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name',aero.total_revenue as 'total_revenue',DATE_FORMAT(aero.date, '%Y-%m-%dT%TZ') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON aero.wfacp_id = p.id WHERE aero.fid=$funnel_id AND aero.cid=$cid order by aero.date asc";
+ $item_data = array();
+ $funnel_id = absint( $funnel_id );
+ $cid = absint( $cid );
+
+ $query = $wpdb->prepare(
+ "SELECT aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name', aero.total_revenue as 'total_revenue', DATE_FORMAT(aero.date, '%%Y-%%m-%%dT%%TZ') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats AS aero LEFT JOIN ' . $wpdb->prefix . 'posts as p ON aero.wfacp_id = p.id WHERE aero.fid = %d AND aero.cid = %d ORDER BY aero.date ASC',
+ $funnel_id,
+ $cid
+ );
- $order_data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $order_data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -77,27 +83,35 @@
$get_order_ids = wp_list_pluck( $order_data, 'order_id' );
if ( is_array( $get_order_ids ) && count( $get_order_ids ) > 0 ) {
-
- /**
- * get order all items meta by order id for showing name and quantity
- */
- $item_query = "SELECT oi.order_id as 'order_id', oi.order_item_name as 'product_name', oi.order_item_id as 'item_id', oim2.meta_key as 'item_meta', oim2.meta_value as 'meta_value' FROM " . $wpdb->prefix . "woocommerce_order_items as oi
- LEFT JOIN " . $wpdb->prefix . "woocommerce_order_itemmeta as oim ON oi.order_item_id = oim.order_item_id
- LEFT JOIN " . $wpdb->prefix . "woocommerce_order_itemmeta as oim2 ON oi.order_item_id = oim2.order_item_id
- WHERE oi.order_id IN (" . esc_sql( implode( ',', $get_order_ids ) ) . ") AND oi.order_item_type='line_item' AND oim.meta_key='_line_total' ORDER BY oi.order_id ASC";
-
- $item_data = $wpdb->get_results( $item_query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- $db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
- if ( true === $db_error['db_error'] ) {
- return $db_error;
+ // Sanitize order IDs
+ $get_order_ids = array_map( 'absint', $get_order_ids );
+ $get_order_ids = array_filter( $get_order_ids );
+
+ if ( ! empty( $get_order_ids ) ) {
+ /**
+ * get order all items meta by order id for showing name and quantity
+ */
+ $placeholders = implode( ',', array_fill( 0, count( $get_order_ids ), '%d' ) );
+ $item_query = $wpdb->prepare(
+ "SELECT oi.order_id as 'order_id', oi.order_item_name as 'product_name', oi.order_item_id as 'item_id', oim2.meta_key as 'item_meta', oim2.meta_value as 'meta_value' FROM " . $wpdb->prefix . 'woocommerce_order_items as oi
+ LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta as oim ON oi.order_item_id = oim.order_item_id
+ LEFT JOIN ' . $wpdb->prefix . "woocommerce_order_itemmeta as oim2 ON oi.order_item_id = oim2.order_item_id
+ WHERE oi.order_id IN ($placeholders) AND oi.order_item_type='line_item' AND oim.meta_key='_line_total' ORDER BY oi.order_id ASC",
+ ...$get_order_ids
+ );
+
+ $item_data = $wpdb->get_results( $item_query, ARRAY_A );
+ $db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
+ if ( true === $db_error['db_error'] ) {
+ return $db_error;
+ }
}
-
}
/**
* Exclude which items purchased by bump and upsell
*/
- $exclude_items = [];
+ $exclude_items = array();
$upstroke_purchase = array_keys( wp_list_pluck( $item_data, 'item_meta' ), '_upstroke_purchase', true );
$bump_purchase = array_keys( wp_list_pluck( $item_data, 'item_meta' ), '_bump_purchase', true );
if ( is_array( $upstroke_purchase ) && count( $upstroke_purchase ) > 0 ) {
@@ -117,12 +131,11 @@
}
}
}
-
}
}
foreach ( $order_data as &$order ) {
- $get_names = [];
+ $get_names = array();
if ( is_array( $item_data ) && count( $item_data ) > 0 ) {
foreach ( $item_data as $item_i ) {
if ( isset( $item_i['order_id'] ) && ( $order->order_id === $item_i['order_id'] ) && '_qty' === $item_i['item_meta'] ) {
@@ -152,11 +165,28 @@
public function get_contacts_revenue_records( $cid, $order_ids ) {
global $wpdb;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $order_ids = ! empty( $order_ids ) ? esc_sql( $order_ids ) : $order_ids;
- $query = "SELECT aero.fid as fid, aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name',aero.total_revenue as 'total_revenue',DATE_FORMAT(aero.date, '%Y-%m-%d %T') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON aero.wfacp_id = p.id WHERE aero.order_id IN ( $order_ids ) AND aero.cid=$cid order by aero.date asc";
+ $cid = absint( $cid );
+
+ // Handle order_ids - can be comma-separated string or array
+ if ( is_string( $order_ids ) ) {
+ $order_ids_array = array_map( 'absint', array_filter( explode( ',', $order_ids ) ) );
+ } else {
+ $order_ids_array = array_map( 'absint', (array) $order_ids );
+ }
+
+ if ( empty( $order_ids_array ) ) {
+ return array();
+ }
+
+ $placeholders = implode( ',', array_fill( 0, count( $order_ids_array ), '%d' ) );
+ $query_args = array_merge( $order_ids_array, array( $cid ) );
- $data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $query = $wpdb->prepare(
+ "SELECT aero.fid as fid, aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name', aero.total_revenue as 'total_revenue', DATE_FORMAT(aero.date, '%%Y-%%m-%%d %%T') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats AS aero LEFT JOIN ' . $wpdb->prefix . "posts as p ON aero.wfacp_id = p.id WHERE aero.order_id IN ($placeholders) AND aero.cid = %d ORDER BY aero.date ASC",
+ ...$query_args
+ );
+
+ $data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -172,22 +202,26 @@
*/
public function get_all_contact_record_by_cid( $cid ) {
global $wpdb;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $query = "SELECT aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name',aero.total_revenue as 'total_revenue',DATE_FORMAT(aero.date, '%Y-%m-%dT%TZ') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON aero.wfacp_id = p.id WHERE aero.cid=$cid order by aero.date asc";
+ $cid = absint( $cid );
+
+ $query = $wpdb->prepare(
+ "SELECT aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name', aero.total_revenue as 'total_revenue', DATE_FORMAT(aero.date, '%%Y-%%m-%%dT%%TZ') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats AS aero LEFT JOIN ' . $wpdb->prefix . 'posts as p ON aero.wfacp_id = p.id WHERE aero.cid = %d ORDER BY aero.date ASC',
+ $cid
+ );
- $data = $wpdb->get_results( $query );//phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
}
if ( ! empty( $data[0]->order_id ) ) {
- $order_products = ! empty( wc_get_order( $data[0]->order_id ) ) ? wffn_rest_funnel_modules()->get_first_item( $data[0]->order_id ) : [];
+ $order_products = ! empty( wc_get_order( $data[0]->order_id ) ) ? wffn_rest_funnel_modules()->get_first_item( $data[0]->order_id ) : array();
if ( ! empty( $order_products ) ) {
$data[0]->product_name = $order_products['title'];
$data[0]->product_qty = $order_products['more'];
}
- } else if ( ! empty( $data[0] ) ) {
+ } elseif ( ! empty( $data[0] ) ) {
$data[0]->product_name = '';
$data[0]->product_qty = '';
}
@@ -202,11 +236,14 @@
*/
public function export_aero_data_order_id( $order_id ) {
global $wpdb;
+ $order_id = absint( $order_id );
- $order_id = ! empty( $order_id ) ? absint( $order_id ) : $order_id;
- $filter = "aero.wfacp_id as 'id', p.post_title as 'checkout_name', '' as 'checkout_products', '' as 'checkout_coupon', aero.order_id as 'checkout_order_id', aero.total_revenue as 'checkout_total'";
- $query = "SELECT " . $filter . " FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON aero.wfacp_id = p.id WHERE aero.order_id={$order_id} order by aero.id asc";
- $data = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $query = $wpdb->prepare(
+ "SELECT aero.wfacp_id as 'id', p.post_title as 'checkout_name', '' as 'checkout_products', '' as 'checkout_coupon', aero.order_id as 'checkout_order_id', aero.total_revenue as 'checkout_total' FROM " . $wpdb->prefix . 'wfacp_stats AS aero LEFT JOIN ' . $wpdb->prefix . 'posts as p ON aero.wfacp_id = p.id WHERE aero.order_id = %d ORDER BY aero.id ASC',
+ $order_id
+ );
+
+ $data = $wpdb->get_results( $query, ARRAY_A );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return false;
@@ -223,14 +260,13 @@
*
* @return string
*/
- public function get_contacts_by_funnel_id($funnel_id, $start_date, $end_date, $is_interval = '') {
+ public function get_contacts_by_funnel_id( $funnel_id, $start_date, $end_date, $is_interval = '' ) {
global $wpdb;
$date = ( '' !== $start_date && '' !== $end_date ) ? " AND `date` >= '" . esc_sql( $start_date ) . "' AND `date` < '" . esc_sql( $end_date ) . "' " : '';
- $funnel_query = ( 0 === intval( $funnel_id ) ) ? " AND fid != " . esc_sql( $funnel_id ) . " " : " AND fid = " . esc_sql( $funnel_id ) . " ";
+ $funnel_query = ( 0 === intval( $funnel_id ) ) ? ' AND fid != ' . esc_sql( $funnel_id ) . ' ' : ' AND fid = ' . esc_sql( $funnel_id ) . ' ';
$interval_param = ! empty( $is_interval ) ? ', date as p_date ' : '';
-
- return "SELECT DISTINCT cid as contacts " . $interval_param . " FROM `" . $wpdb->prefix . "wfacp_stats` WHERE 1=1 " . $date . " " . $funnel_query;
+ return 'SELECT DISTINCT cid as contacts ' . $interval_param . ' FROM `' . $wpdb->prefix . 'wfacp_stats` WHERE 1=1 ' . $date . ' ' . $funnel_query;
}
/**
@@ -242,10 +278,10 @@
*
* @return array|false[]|object|stdClass|null
*/
- public function get_total_orders($funnel_id, $start_date, $end_date, $is_interval = '', $int_request = '') {
+ public function get_total_orders( $funnel_id, $start_date, $end_date, $is_interval = '', $int_request = '' ) {
global $wpdb;
- $funnel_id = ( $funnel_id !== '' ) ? " AND fid = " . esc_sql( $funnel_id ) . " " : " AND fid != 0 ";
- $date = ( '' !== $start_date && '' !== $end_date ) ? " AND `date` >= '" . esc_sql($start_date ) . "' AND `date` < '" . esc_sql( $end_date ) . "' " : '';
+ $funnel_id = ( $funnel_id !== '' ) ? ' AND fid = ' . esc_sql( $funnel_id ) . ' ' : ' AND fid != 0 ';
+ $date = ( '' !== $start_date && '' !== $end_date ) ? " AND `date` >= '" . esc_sql( $start_date ) . "' AND `date` < '" . esc_sql( $end_date ) . "' " : '';
$interval_query = '';
$group_by = '';
@@ -255,12 +291,12 @@
$get_interval = $this->get_interval_format_query( $int_request, 'date' );
$interval_query = $get_interval['interval_query'];
$interval_group = $get_interval['interval_group'];
- $group_by = " GROUP BY " . $interval_group;
+ $group_by = ' GROUP BY ' . $interval_group;
}
}
- $query = "SELECT COUNT(ID) as total_orders " . $interval_query . " FROM `" . $wpdb->prefix . "wfacp_stats` WHERE 1=1 " . $date . $funnel_id . $group_by . " ORDER BY id ASC";
+ $query = 'SELECT COUNT(ID) as total_orders ' . $interval_query . ' FROM `' . $wpdb->prefix . 'wfacp_stats` WHERE 1=1 ' . $date . $funnel_id . $group_by . ' ORDER BY id ASC';
$data = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
@@ -278,11 +314,24 @@
*
* @return string
*/
- public function get_timeline_data_query($limit, $order = "DESC", $order_by = 'date') {
+ public function get_timeline_data_query( $limit, $order = 'DESC', $order_by = 'date' ) {
global $wpdb;
- $limit = ( $limit !== '' ) ? " LIMIT " . esc_sql( $limit ) : '';
- return "SELECT stats.wfacp_id as id, stats.fid as 'fid', stats.cid as 'cid', stats.order_id as 'order_id', CONVERT( stats.total_revenue USING utf8) as 'total_revenue', 'aero' as 'type', posts.post_title as 'post_title', stats.date as date FROM " . $wpdb->prefix . "wfacp_stats as stats LEFT JOIN " . $wpdb->prefix . "posts as posts ON stats.wfacp_id = posts.ID ORDER BY " . esc_sql( $order_by ) . " " . esc_sql( $order ) . " " . $limit;
+ // Whitelist allowed ORDER BY columns to prevent SQL injection
+ $allowed_order_by = array( 'date', 'id', 'fid', 'cid', 'order_id', 'total_revenue' );
+ $order_by = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'date';
+
+ // Whitelist ORDER direction
+ $order = strtoupper( $order );
+ $order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
+
+ // Sanitize LIMIT - ensure it's a positive integer
+ $limit_clause = '';
+ if ( '' !== $limit && is_numeric( $limit ) ) {
+ $limit_clause = ' LIMIT ' . absint( $limit );
+ }
+
+ return "SELECT stats.wfacp_id as id, stats.fid as 'fid', stats.cid as 'cid', stats.order_id as 'order_id', CONVERT( stats.total_revenue USING utf8) as 'total_revenue', 'aero' as 'type', posts.post_title as 'post_title', stats.date as date FROM " . $wpdb->prefix . 'wfacp_stats as stats LEFT JOIN ' . $wpdb->prefix . 'posts as posts ON stats.wfacp_id = posts.ID ORDER BY ' . $order_by . ' ' . $order . $limit_clause;
}
/**
@@ -291,16 +340,32 @@
*
* @return array|false[]|object|stdClass[]|null
*/
- public function get_top_funnels($limit = '', $date_query = '') {
+ public function get_top_funnels( $limit = '', $date_query = '' ) {
global $wpdb;
- $limit = ( $limit !== '' ) ? " LIMIT " . esc_sql( $limit ) : '';
+
+ // Sanitize LIMIT - ensure it's a positive integer
+ $limit_clause = '';
+ if ( '' !== $limit && is_numeric( $limit ) ) {
+ $limit_clause = ' LIMIT ' . absint( $limit );
+ }
+
+ // Note: $date_query is expected to be a pre-built date condition
+ // It should only contain 'stats.date' column comparisons with date values
$date_query = str_replace( '{{COLUMN}}', 'stats.date', $date_query );
- $query = "SELECT funnel.id as fid, funnel.title as title, stats.total as total FROM " . $wpdb->prefix . "bwf_funnels AS funnel
- JOIN ( SELECT fid, SUM( total_revenue ) as total FROM " . $wpdb->prefix . "wfacp_stats as stats
- WHERE fid != 0 AND " . esc_sql( $date_query ) . " group by fid ) as stats ON funnel.id = stats.fid WHERE 1 = 1 GROUP BY funnel.id ORDER BY total DESC " . $limit;
+ // Security: Only allow safe date query patterns
+ // Expected format: "stats.date >= 'YYYY-MM-DD' AND stats.date < 'YYYY-MM-DD'"
+ if ( ! empty( $date_query ) && ! preg_match( '/^stats.dates*[<>=]+s*'[d-s:]+'(s+ANDs+stats.dates*[<>=]+s*'[d-s:]+')*$/i', $date_query ) ) {
+ $date_query = '1=1'; // Fallback to safe condition if pattern doesn't match
+ }
+
+ $date_condition = ! empty( $date_query ) ? $date_query : '1=1';
- $data = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $query = 'SELECT funnel.id as fid, funnel.title as title, stats.total as total FROM ' . $wpdb->prefix . 'bwf_funnels AS funnel
+ JOIN ( SELECT fid, SUM( total_revenue ) as total FROM ' . $wpdb->prefix . 'wfacp_stats as stats
+ WHERE fid != 0 AND ' . $date_condition . ' GROUP BY fid ) as stats ON funnel.id = stats.fid WHERE 1 = 1 GROUP BY funnel.id ORDER BY total DESC' . $limit_clause;
+
+ $data = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- date_query is validated above
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -315,16 +380,34 @@
*
* @return array|false[]|true
*/
- public function delete_contact($cids, $funnel_id = 0) {
+ public function delete_contact( $cids, $funnel_id = 0 ) {
global $wpdb;
- $cid_count = count( $cids );
- $stringPlaceholders = array_fill( 0, $cid_count, '%s' );
- $placeholdersForFavFruits = implode( ',', $stringPlaceholders );
- $funnel_query = ( absint( $funnel_id ) > 0 ) ? " AND fid = " . $funnel_id . " " : '';
- $query = "DELETE FROM " . $wpdb->prefix . "wfacp_stats WHERE cid IN( " . $placeholdersForFavFruits . " ) " . $funnel_query;
+ // Sanitize all cids as integers
+ $cids = array_map( 'absint', (array) $cids );
+ $cids = array_filter( $cids ); // Remove zeros
+ $cid_count = count( $cids );
+
+ if ( empty( $cid_count ) ) {
+ return true;
+ }
+
+ $placeholders = implode( ',', array_fill( 0, $cid_count, '%d' ) );
+ $funnel_id = absint( $funnel_id );
- $wpdb->query( $wpdb->prepare( $query, $cids ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ if ( $funnel_id > 0 ) {
+ $query = $wpdb->prepare(
+ 'DELETE FROM ' . $wpdb->prefix . "wfacp_stats WHERE cid IN ($placeholders) AND fid = %d",
+ ...array_merge( $cids, array( $funnel_id ) )
+ );
+ } else {
+ $query = $wpdb->prepare(
+ 'DELETE FROM ' . $wpdb->prefix . "wfacp_stats WHERE cid IN ($placeholders)",
+ ...$cids
+ );
+ }
+
+ $wpdb->query( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -338,8 +421,8 @@
*/
public function reset_analytics( $funnel_id ) {
global $wpdb;
- $query = $wpdb->prepare( "DELETE FROM " . $wpdb->prefix . "wfacp_stats WHERE fid = %d", $funnel_id );
+ $query = $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'wfacp_stats WHERE fid = %d', $funnel_id );
$wpdb->query( $query );
}
}
-}
No newline at end of file
+}
--- a/funnel-builder/contact-analytics/class-wffn-optin-contacts-analytics.php
+++ b/funnel-builder/contact-analytics/class-wffn-optin-contacts-analytics.php
@@ -1,5 +1,5 @@
<?php
-defined( 'ABSPATH' ) || exit; //Exit if accessed directly
+defined( 'ABSPATH' ) || exit; // Exit if accessed directly
/**
* Class WFFN_Optin_Contacts_Analytics
@@ -10,6 +10,7 @@
/**
* instance of class
+ *
* @var null
*/
private static $ins = null;
@@ -41,11 +42,11 @@
global $wpdb;
if ( ! empty( $search ) ) {
- $query = $wpdb->prepare("SELECT contact.id as cid, contact.f_name, contact.l_name, contact.email, optin.date, optin.opid FROM " . $wpdb->prefix . 'bwf_contact' . " AS contact JOIN " . $wpdb->prefix . 'bwf_optin_entries' . " AS optin ON contact.id=optin.cid WHERE optin.funnel_id=%d", $funnel_id );
- $query .= $wpdb->prepare( " AND (contact.f_name LIKE %s OR contact.email LIKE %s) group by contact.id", "%" . $search . "%", "%" . $search . "%" );
+ $query = $wpdb->prepare( 'SELECT contact.id as cid, contact.f_name, contact.l_name, contact.email, optin.date, optin.opid FROM ' . $wpdb->prefix . 'bwf_contact' . ' AS contact JOIN ' . $wpdb->prefix . 'bwf_optin_entries' . ' AS optin ON contact.id=optin.cid WHERE optin.funnel_id=%d', $funnel_id );
+ $query .= $wpdb->prepare( ' AND (contact.f_name LIKE %s OR contact.email LIKE %s) group by contact.id', '%' . $search . '%', '%' . $search . '%' );
} else {
- $query = $wpdb->prepare("SELECT optin.cid FROM " . $wpdb->prefix . 'bwf_optin_entries' . " AS optin WHERE optin.funnel_id=%d", $funnel_id );
+ $query = $wpdb->prepare( 'SELECT optin.cid FROM ' . $wpdb->prefix . 'bwf_optin_entries' . ' AS optin WHERE optin.funnel_id=%d', $funnel_id );
}
return $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
@@ -61,11 +62,15 @@
public function get_all_contacts_records( $funnel_id, $cid ) {
global $wpdb;
- $funnel_id = ! empty( $funnel_id ) ? absint( $funnel_id ) : $funnel_id;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $query = "SELECT optin.step_id as 'object_id',optin.data as 'data',DATE_FORMAT(optin.date, '%Y-%m-%dT%TZ') as 'date',p.post_title as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries' . " as optin LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON optin.step_id = p.id WHERE optin.funnel_id=$funnel_id AND optin.cid= $cid order by optin.date asc";
+ $funnel_id = absint( $funnel_id );
+ $cid = absint( $cid );
+ $query = $wpdb->prepare(
+ "SELECT optin.step_id as 'object_id', optin.data as 'data', DATE_FORMAT(optin.date, '%%Y-%%m-%%dT%%TZ') as 'date', p.post_title as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries as optin LEFT JOIN ' . $wpdb->prefix . 'posts as p ON optin.step_id = p.id WHERE optin.funnel_id = %d AND optin.cid = %d ORDER BY optin.date ASC',
+ $funnel_id,
+ $cid
+ );
- $data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -77,11 +82,28 @@
public function get_contacts_optin_records( $cid, $entry_ids ) {
global $wpdb;
- $entry_ids = ! empty( $entry_ids ) ? esc_sql( $entry_ids ) : $entry_ids;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $query = "SELECT optin.id, optin.funnel_id as fid, optin.email as email, optin.step_id as 'object_id', optin.data as 'data',DATE_FORMAT(optin.date, '%Y-%m-%d %T') as 'date', COALESCE( p.post_title, '' ) as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries' . " as optin LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON optin.step_id = p.id WHERE optin.id IN ( $entry_ids ) AND optin.cid= $cid order by optin.date asc";
+ $cid = absint( $cid );
+
+ // Handle entry_ids - can be comma-separated string or array
+ if ( is_string( $entry_ids ) ) {
+ $entry_ids_array = array_map( 'absint', array_filter( explode( ',', $entry_ids ) ) );
+ } else {
+ $entry_ids_array = array_map( 'absint', (array) $entry_ids );
+ }
+
+ if ( empty( $entry_ids_array ) ) {
+ return array();
+ }
- $data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $placeholders = implode( ',', array_fill( 0, count( $entry_ids_array ), '%d' ) );
+ $query_args = array_merge( $entry_ids_array, array( $cid ) );
+
+ $query = $wpdb->prepare(
+ "SELECT optin.id, optin.funnel_id as fid, optin.email as email, optin.step_id as 'object_id', optin.data as 'data', DATE_FORMAT(optin.date, '%%Y-%%m-%%d %%T') as 'date', COALESCE( p.post_title, '' ) as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries as optin LEFT JOIN ' . $wpdb->prefix . "posts as p ON optin.step_id = p.id WHERE optin.id IN ( $placeholders ) AND optin.cid = %d ORDER BY optin.date ASC",
+ ...$query_args
+ );
+
+ $data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -98,10 +120,13 @@
*/
public function get_all_contact_record_by_cid( $cid ) {
global $wpdb;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $query = "SELECT optin.step_id as 'object_id',optin.data as 'data',DATE_FORMAT(optin.date, '%Y-%m-%dT%TZ') as 'date',p.post_title as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries' . " as optin LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON optin.step_id = p.id WHERE optin.cid= $cid order by optin.date asc";
+ $cid = absint( $cid );
+ $query = $wpdb->prepare(
+ "SELECT optin.step_id as 'object_id', optin.data as 'data', DATE_FORMAT(optin.date, '%%Y-%%m-%%dT%%TZ') as 'date', p.post_title as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries as optin LEFT JOIN ' . $wpdb->prefix . 'posts as p ON optin.step_id = p.id WHERE optin.cid = %d ORDER BY optin.date ASC',
+ $cid
+ );
- $data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -121,15 +146,26 @@
*/
public function get_timeline_data_query( $limit, $order = 'DESC', $order_by = 'date', $can_union = true ) {
global $wpdb;
- $limit = ( $limit !== '' ) ? " LIMIT " . $limit : '';
- $limit = ! empty( $limit ) ? esc_sql( $limit ) : $limit;
- $order = ! empty( $order ) ? esc_sql( $order ) : $order;
- $order_by = ! empty( $order_by ) ? esc_sql( $order_by ) : $order_by;
+
+ // Whitelist allowed ORDER BY columns to prevent SQL injection
+ $allowed_order_by = array( 'date', 'id', 'step_id', 'funnel_id', 'cid' );
+ $order_by = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'date';
+
+ // Whitelist ORDER direction
+ $order = strtoupper( $order );
+ $order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
+
+ // Sanitize LIMIT - ensure it's a positive integer
+ $limit_clause = '';
+ if ( '' !== $limit && is_numeric( $limit ) ) {
+ $limit_clause = ' LIMIT ' . absint( $limit );
+ }
+
if ( $can_union ) {
- return "SELECT stats.step_id as id, stats.funnel_id as 'fid', stats.cid as 'cid', '0' as 'order_id', '0' as 'total_revenue', 'optin' as 'type', posts.post_title as 'post_title', stats.date as date FROM " . $wpdb->prefix . "bwf_optin_entries AS stats LEFT JOIN " . $wpdb->prefix . "posts AS posts ON stats.step_id=posts.ID ORDER BY " . $order_by . " " . $order . " " . $limit;
+ return "SELECT stats.step_id as id, stats.funnel_id as 'fid', stats.cid as 'cid', '0' as 'order_id', '0' as 'total_revenue', 'optin' as 'type', posts.post_title as 'post_title', stats.date as date FROM " . $wpdb->prefix . 'bwf_optin_entries AS stats LEFT JOIN ' . $wpdb->prefix . 'posts AS posts ON stats.step_id=posts.ID ORDER BY ' . $order_by . ' ' . $order . $limit_clause;
} else {
- return "SELECT stats.step_id as id, stats.funnel_id as 'fid', stats.cid as 'cid', '0' as 'order_id', '0' as 'total_revenue', 'optin' as 'type', posts.post_title as 'post_title',contact.f_name as f_name, contact.l_name as l_name, stats.date as date FROM " . $wpdb->prefix . "bwf_optin_entries AS stats LEFT JOIN " . $wpdb->prefix . "posts AS posts ON stats.step_id=posts.ID LEFT JOIN " . $wpdb->prefix . "bwf_contact AS contact ON contact.id=cid WHERE contact.id != '' ORDER BY " . $order_by . " " . $order . " " . $limit;
+ return "SELECT stats.step_id as id, stats.funnel_id as 'fid', stats.cid as 'cid', '0' as 'order_id', '0' as 'total_revenue', 'optin' as 'type', posts.post_title as 'post_title', contact.f_name as f_name, contact.l_name as l_name, stats.date as date FROM " . $wpdb->prefix . 'bwf_optin_entries AS stats LEFT JOIN ' . $wpdb->prefix . 'posts AS posts ON stats.step_id=posts.ID LEFT JOIN ' . $wpdb->prefix . "bwf_contact AS contact ON contact.id=cid WHERE contact.id != '' ORDER BY " . $order_by . ' ' . $order . $limit_clause;
}
@@ -143,14 +179,32 @@
*/
public function delete_contact( $cids, $funnel_id = 0 ) {
global $wpdb;
- $cid_count = count( $cids );
- $stringPlaceholders = array_fill( 0, $cid_count, '%s' );
- $placeholdersForFavFruits = implode( ',', $stringPlaceholders );
- $funnel_query = ( absint( $funnel_id ) > 0 ) ? " AND fid = " . $funnel_id . " " : '';
+ // Sanitize all cids as integers
+ $cids = array_map( 'absint', (array) $cids );
+ $cids = array_filter( $cids ); // Remove zeros
+ $cid_count = count( $cids );
+
+ if ( empty( $cid_count ) ) {
+ return true;
+ }
+
+ $placeholders = implode( ',', array_fill( 0, $cid_count, '%d' ) );
+ $funnel_id = absint( $funnel_id );
+
+ if ( $funnel_id > 0 ) {
+ $query = $wpdb->prepare(
+ 'DELETE FROM ' . $wpdb->prefix . "bwf_optin_entries WHERE cid IN ($placeholders) AND fid = %d",
+ ...array_merge( $cids, array( $funnel_id ) )
+ );
+ } else {
+ $query = $wpdb->prepare(
+ 'DELETE FROM ' . $wpdb->prefix . "bwf_optin_entries WHERE cid IN ($placeholders)",
+ ...$cids
+ );
+ }
- $e_query = "DELETE FROM " . $wpdb->prefix . "bwf_optin_entries WHERE cid IN (" . esc_sql($placeholdersForFavFruits ) . ") " . $funnel_query;
- $wpdb->query( $wpdb->prepare( $e_query, $cids ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $wpdb->query( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
@@ -172,10 +226,10 @@
public function get_contacts_by_funnel_id( $funnel_id, $start_date, $end_date, $is_interval = '' ) {
global $wpdb;
$date = ( '' !== $start_date && '' !== $end_date ) ? " AND `date` >= '" . esc_sql( $start_date ) . "' AND `date` < '" . esc_sql( $end_date ) . "' " : '';
- $funnel_query = ( 0 === intval( $funnel_id ) ) ? " AND funnel_id != " . $funnel_id . " " : " AND funnel_id = " . esc_sql( $funnel_id ) . " ";
+ $funnel_query = ( 0 === intval( $funnel_id ) ) ? ' AND funnel_id != ' . $funnel_id . ' ' : ' AND funnel_id = ' . esc_sql( $funnel_id ) . ' ';
$interval_param = ! empty( $is_interval ) ? ', date as p_date ' : '';
- return "SELECT DISTINCT cid as contacts " . $interval_param . " FROM `" . $wpdb->prefix . "bwf_optin_entries` WHERE 1=1 " . $date . " " . $funnel_query;
+ return 'SELECT DISTINCT cid as contacts ' . $interval_param . ' FROM `' . $wpdb->prefix . 'bwf_optin_entries` WHERE 1=1 ' . $date . ' ' . $funnel_query;
}
/**
@@ -183,7 +237,7 @@
*/
public function reset_analytics( $funnel_id ) {
global $wpdb;
- $query = $wpdb->prepare( "DELETE FROM " . $wpdb->prefix . "bwf_optin_entries WHERE funnel_id=%d", $funnel_id );
+ $query = $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'bwf_optin_entries WHERE funnel_id=%d', $funnel_id );
$wpdb->query( $query );
}
@@ -194,21 +248,33 @@
*/
public function delete_optin_entries( $entry_ids ) {
global $wpdb;
- $entry_ids = is_array( $entry_ids ) ? implode( ',', $entry_ids ) : $entry_ids;
- if ( empty( $entry_ids ) ) {
+
+ // Convert to array and sanitize as integers
+ if ( is_string( $entry_ids ) ) {
+ $entry_ids_array = array_map( 'absint', array_filter( explode( ',', $entry_ids ) ) );
+ } else {
+ $entry_ids_array = array_map( 'absint', array_filter( (array) $entry_ids ) );
+ }
+
+ if ( empty( $entry_ids_array ) ) {
return;
}
- $e_query = "DELETE FROM " . $wpdb->prefix . "bwf_optin_entries WHERE id IN (" . esc_sql( $entry_ids ) . ")";
- $wpdb->query( $e_query );
+
+ $placeholders = implode( ',', array_fill( 0, count( $entry_ids_array ), '%d' ) );
+ $query = $wpdb->prepare(
+ 'DELETE FROM ' . $wpdb->prefix . "bwf_optin_entries WHERE id IN ($placeholders)",
+ ...$entry_ids_array
+ );
+ $wpdb->query( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
}
- do_action( 'wffn_delete_optin_entries', $entry_ids );
+ do_action( 'wffn_delete_optin_entries', $entry_ids_array );
}
}
-}
No newline at end of file
+}
--- a/funnel-builder/contact-analytics/class-wfob-contacts-analytics.php
+++ b/funnel-builder/contact-analytics/class-wfob-contacts-analytics.php
@@ -1,5 +1,5 @@
<?php
-defined( 'ABSPATH' ) || exit; //Exit if accessed directly
+defined( 'ABSPATH' ) || exit; // Exit if accessed directly
/**
* Class WFOB_Contacts_Analytics
@@ -10,6 +10,7 @@
/**
* instance of class
+ *
* @var null
*/
private static $ins = null;
@@ -38,18 +39,39 @@
*
* @return array
*/
- public function get_top_bumps( $start_date, $end_date, $limit_str = '' ) {
+ public function get_top_bumps( $start_date, $end_date, $limit_str = '' ) {
global $wpdb;
- $where_condition = "WHERE bmp.converted = 1";
- $view_where_condition = "";
- if (!empty($start_date) && !empty($end_date)) {
- $where_condition = "WHERE date >= '$start_date' AND date < '$end_date' AND bmp.converted = 1";
- $view_where_condition = "WHERE date >= '$start_date' AND date < '$end_date'";
+ // Sanitize limit - ensure it's a positive integer
+ $limit_clause = '';
+ if ( ! empty( $limit_str ) && is_numeric( $limit_str ) ) {
+ $limit_clause = ' LIMIT ' . absint( $limit_str );
+ }
+
+ // Build query with prepared statements for dates
+ if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
+ $start_date = sanitize_text_field( $start_date );
+ $end_date = sanitize_text_field( $end_date );
+ $query = $wpdb->prepare(
+ 'SELECT v.bid as id, IFNULL(s.revenue, 0) as revenue, IFNULL(s.conversion, 0) as conversion, p.post_title as title, p.post_type as post_type, v.view_count as views
+ FROM (SELECT bid, COUNT(id) as view_count FROM `' . $wpdb->prefix . 'wfob_stats` WHERE date >= %s AND date < %s GROUP BY bid) as v
+ LEFT JOIN ' . $wpdb->prefix . 'posts as p ON p.id = v.bid
+ LEFT JOIN (SELECT bid, sum(total) as revenue, COUNT(id) as conversion FROM `' . $wpdb->prefix . 'wfob_stats` as bmp WHERE date >= %s AND date < %s AND bmp.converted = 1 GROUP BY bid) as s ON s.bid = v.bid
+ ORDER BY revenue DESC' . $limit_clause,
+ $start_date,
+ $end_date,
+ $start_date,
+ $end_date
+ );
+ } else {
+ $query = 'SELECT v.bid as id, IFNULL(s.revenue, 0) as revenue, IFNULL(s.conversion, 0) as conversion, p.post_title as title, p.post_type as post_type, v.view_count as views
+ FROM (SELECT bid, COUNT(id) as view_count FROM `' . $wpdb->prefix . 'wfob_stats` GROUP BY bid) as v
+ LEFT JOIN ' . $wpdb->prefix . 'posts as p ON p.id = v.bid
+ LEFT JOIN (SELECT bid, sum(total) as revenue, COUNT(id) as conversion FROM `' . $wpdb->prefix . 'wfob_stats` as bmp WHERE bmp.converted = 1 GROUP BY bid) as s ON s.bid = v.bid
+ ORDER BY revenue DESC' . $limit_clause;
}
- $query = "SELECT v.bid as id,IFNULL(s.revenue, 0) as revenue, IFNULL(s.conversion, 0) as conversion,p.post_title as title,p.post_type as post_type,v.view_count as views FROM (SELECT bid, COUNT(id) as view_count FROM `" . $wpdb->prefix . "wfob_stats` $view_where_condition GROUP BY bid) as v LEFT JOIN " . $wpdb->prefix . "posts as p ON p.id = v.bid LEFT JOIN (SELECT bid, sum(total) as revenue, COUNT(id) as conversion FROM `" . $wpdb->prefix . "wfob_stats` as bmp $where_condition GROUP BY bid) as s ON s.bid = v.bid ORDER BY revenue DESC " . $limit_str;
- $data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -57,11 +79,11 @@
if ( ! empty( $data ) && is_array( $data ) ) {
foreach ( $data as $key => $item ) {
$data[ $key ]->conversion_rate = $this->get_percentage( absint( $item->views ), $item->conversion );
- $data[ $key ]->type = $item->post_type;
- $funnel_id = get_post_meta( $item->id, '_bwf_in_funnel', true );
- $data[ $key ]->fid = $funnel_id;
+ $data[ $key ]->type = $item->post_type;
+ $funnel_id = get_post_meta( $item->id, '_bwf_in_funnel', true );
+ $data[ $key ]->fid = $funnel_id;
if ( $funnel_id ) {
- $funnel_name = $wpdb->get_var(
+ $funnel_name = $wpdb->get_var(
$wpdb->prepare(
"SELECT title FROM {$wpdb->prefix}bwf_funnels WHERE id = %d",
$funnel_id
@@ -85,12 +107,17 @@
*/
public function get_all_contacts_records( $funnel_id, $cid ) {
global $wpdb;
- $item_data = [];
- $funnel_id = ! empty( $funnel_id ) ? absint( $funnel_id ) : $funnel_id;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $query = "SELECT bump.oid as order_id, bump.bid as 'object_id', bump.iid as 'item_ids', bump.total as 'total_revenue',p.post_title as 'object_name', bump.converted as 'is_converted',DATE_FORMAT(bump.date, '%Y-%m-%dT%TZ') as 'date','bump' as 'type' FROM " . $wpdb->prefix . 'wfob_stats' . " AS bump LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON bump.bid = p.id WHERE bump.converted = 1 AND bump.fid=$funnel_id AND bump.cid=$cid order by bump.date asc";
+ $item_data = array();
+ $funnel_id = absint( $funnel_id );
+ $cid = absint( $cid );
+
+ $query = $wpdb->prepare(
+ "SELECT bump.oid as order_id, bump.bid as 'object_id', bump.iid as 'item_ids', bump.total as 'total_revenue', p.post_title as 'object_name', bump.converted as 'is_converted', DATE_FORMAT(bump.date, '%%Y-%%m-%%dT%%TZ') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'wfob_stats AS bump LEFT JOIN ' . $wpdb->prefix . 'posts as p ON bump.bid = p.id WHERE bump.converted = 1 AND bump.fid = %d AND bump.cid = %d ORDER BY bump.date ASC',
+ $funnel_id,
+ $cid
+ );
- $order_data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $order_data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -100,12 +127,12 @@
return $item_data;
}
- $all_item_ids = [];
+ $all_item_ids = array();
/** merge all items ids in one array */
if ( is_array( $order_data ) && count( $order_data ) > 0 ) {
foreach ( $order_data as &$i_array ) {
- $i_array->item_ids = ( isset( $i_array->item_ids ) && '' != $i_array->item_ids ) ? json_decode( $i_array->item_ids ) : [];
+ $i_array->item_ids = ( isset( $i_array->item_ids ) && '' != $i_array->item_ids ) ? json_decode( $i_array->item_ids ) : array();
if ( is_array( $i_array->item_ids ) && count( $i_array->item_ids ) > 0 ) {
$all_item_ids = array_merge( $all_item_ids, $i_array->item_ids );
}
@@ -116,26 +143,34 @@
/**
* get order item product name and quantity by items ids
*/
- $item_query = "SELECT oi.order_item_id as 'item_id', oi.order_item_name as 'product_name', oim.meta_value as 'qty' FROM " . $wpdb->prefix . "woocommerce_order_items as oi LEFT JOIN " . $wpdb->prefix . "woocommerce_order_itemmeta as oim ON oi.order_item_id = oim.order_item_id WHERE oi.order_item_id IN (" . esc_sql( implode( ',', $all_item_ids ) ) . ") AND oi.order_item_type = 'line_item' AND oim.meta_key = '_qty' GROUP BY oi.order_item_id";
- $item_data = $wpdb->get_results( $item_query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- $db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
- if ( true === $db_error['db_error'] ) {
- return $db_error;
+ $all_item_ids = array_map( 'absint', $all_item_ids );
+ $all_item_ids = array_filter( $all_item_ids );
+
+ if ( ! empty( $all_item_ids ) ) {
+ $placeholders = implode( ',', array_fill( 0, count( $all_item_ids ), '%d' ) );
+ $item_query = $wpdb->prepare(
+ "SELECT oi.order_item_id as 'item_id', oi.order_item_name as 'product_name', oim.meta_value as 'qty' FROM " . $wpdb->prefix . 'woocommerce_order_items as oi LEFT JOIN ' . $wpdb->prefix . "woocommerce_order_itemmeta as oim ON oi.order_item_id = oim.order_item_id WHERE oi.order_item_id IN ($placeholders) AND oi.order_item_type = 'line_item' AND oim.meta_key = '_qty' GROUP BY oi.order_item_id",
+ ...$all_item_ids
+ );
+ $item_data = $wpdb->get_results( $item_query );
+ $db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
+ if ( true === $db_error['db_error'] ) {
+ return $db_error;
+ }
}
}
foreach ( $order_data as &$order ) {
- $product_titles = [];
+ $product_titles = array();
$qty = 0;
if ( is_array( $order->item_ids ) && count( $order->item_ids ) > 0 && is_array( $item_data ) && count( $item_data ) > 0 ) {
foreach ( $order->item_ids as $item_id ) {
$search = array_search( intval( $item_id ), array_map( 'intval', wp_list_pluck( $item_data, 'item_id' ) ), true );
if ( false !== $search && isset( $item_data[ $search ] ) ) {
$product_titles[] = $item_data[ $search ]->product_name;
- $qty += absint( $item_data[ $search ]->qty );
+ $qty += absint( $item_data[ $search ]->qty );
}
}
-
}
unset( $order->item_ids );
$order->product_name = implode( ', ', $product_titles );
@@ -147,11 +182,28 @@
public function get_contacts_revenue_records( $cid, $order_ids ) {
global $wpdb;
- $order_ids = ! empty( $order_ids ) ? esc_sql( $order_ids ) : $order_ids;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $query = "SELECT conv.funnel_id as fid, conv.source as order_id, conv.step_id as 'object_id', conv.bump_total as 'total_revenue', p.post_title as 'object_name', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 1 ELSE 0 END as 'is_converted', DATE_FORMAT(conv.timestamp, '%Y-%m-%d %T') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'bwf_conversion_tracking' . " AS conv LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' AND conv.source IN ( $order_ids ) AND conv.contact_id=$cid order by conv.timestamp asc";
+ $cid = absint( $cid );
- $data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ // Handle order_ids - can be comma-separated string or array
+ if ( is_string( $order_ids ) ) {
+ $order_ids_array = array_map( 'absint', array_filter( explode( ',', $order_ids ) ) );
+ } else {
+ $order_ids_array = array_map( 'absint', (array) $order_ids );
+ }
+
+ if ( empty( $order_ids_array ) ) {
+ return array();
+ }
+
+ $placeholders = implode( ',', array_fill( 0, count( $order_ids_array ), '%d' ) );
+ $query_args = array_merge( $order_ids_array, array( $cid ) );
+
+ $query = $wpdb->prepare(
+ "SELECT conv.funnel_id as fid, conv.source as order_id, conv.step_id as 'object_id', conv.bump_total as 'total_revenue', p.post_title as 'object_name', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 1 ELSE 0 END as 'is_converted', DATE_FORMAT(conv.timestamp, '%%Y-%%m-%%d %%T') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'bwf_conversion_tracking AS conv LEFT JOIN ' . $wpdb->prefix . "posts as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' AND conv.source IN ($placeholders) AND conv.contact_id = %d ORDER BY conv.timestamp ASC",
+ ...$query_args
+ );
+
+ $data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -164,7 +216,7 @@
public function get_bumps_by_order_id( $order_id ) {
global $wpdb;
- $query = $wpdb->prepare( "SELECT conv.step_id as 'id', p.post_title as 'bump_name', '' as 'bump_products', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 'Yes' ELSE 'No' END as 'bump_converted', conv.source as 'bump_order_id', conv.bump_total as 'bump_total' FROM " . $wpdb->prefix . 'bwf_conversion_tracking' . " AS conv LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.source=%s order by conv.timestamp asc", $order_id );
+ $query = $wpdb->prepare( "SELECT conv.step_id as 'id', p.post_title as 'bump_name', '' as 'bump_products', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 'Yes' ELSE 'No' END as 'bump_converted', conv.source as 'bump_order_id', conv.bump_total as 'bump_total' FROM " . $wpdb->prefix . 'bwf_conversion_tracking' . ' AS conv LEFT JOIN ' . $wpdb->prefix . 'posts' . ' as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.source=%s order by conv.timestamp asc', $order_id );
$data = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
@@ -183,22 +235,26 @@
*/
public function get_all_contact_record_by_cid( $cid ) {
global $wpdb;
- $cid = ! empty( $cid ) ? absint( $cid ) : $cid;
- $query = "SELECT conv.source as order_id, conv.step_id as 'object_id', conv.bump_total as 'total_revenue', p.post_title as 'object_name', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 1 ELSE 0 END as 'is_converted', DATE_FORMAT(conv.timestamp, '%Y-%m-%dT%TZ') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'bwf_conversion_tracking' . " AS conv LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' AND conv.contact_id=$cid order by conv.timestamp asc";
+ $cid = absint( $cid );
- $data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $query = $wpdb->prepare(
+ "SELECT conv.source as order_id, conv.step_id as 'object_id', conv.bump_total as 'total_revenue', p.post_title as 'object_name', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 1 ELSE 0 END as 'is_converted', DATE_FORMAT(conv.timestamp, '%%Y-%%m-%%dT%%TZ') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'bwf_conversion_tracking AS conv LEFT JOIN ' . $wpdb->prefix . "posts as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' AND conv.contact_id = %d ORDER BY conv.timestamp ASC",
+ $cid
+ );
+
+ $data = $wpdb->get_results( $query );
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
}
if ( ! empty( $data[0]->order_id ) ) {
- $order_products = ! empty( wc_get_order( $data[0]->order_id ) ) ? wffn_rest_funnel_modules()->get_first_item( $data[0]->order_id ) : [];
+ $order_products = ! empty( wc_get_order( $data[0]->order_id ) ) ? wffn_rest_funnel_modules()->get_first_item( $data[0]->order_id ) : array();
if ( ! empty( $order_products ) ) {
$data[0]->product_name = $order_products['title'];
$data[0]->product_qty = $order_products['more'];
}
- } else if ( ! empty( $data[0] ) ) {
+ } elseif ( ! empty( $data[0] ) ) {
$data[0]->product_name = '';
$data[0]->product_qty = '';
}
@@ -217,12 +273,22 @@
*/
public function get_timeline_data_query( $limit, $order = 'DESC', $order_by = 'date' ) {
global $wpdb;
- $limit = ( $limit !== '' ) ? " LIMIT " . $limit : '';
- $limit = ! empty( $limit ) ? esc_sql( $limit ) : $limit;
- $order = ! empty( $order ) ? esc_sql( $order ) : $order;
- $order_by = ! empty( $order_by ) ? esc_sql( $order_by ) : $order_by;
- return "SELECT conv.step_id as id, conv.funnel_id as 'fid', conv.contact_id as 'cid', conv.source as 'order_id', CONVERT( conv.bump_total USING utf8) as 'total_revenue', 'bump' as 'type', posts.post_title as 'post_title', conv.timestamp as date FROM " . $wpdb->prefix . "bwf_conversion_tracking AS conv LEFT JOIN " . $wpdb->prefix . "posts AS posts ON conv.step_id=posts.ID WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' ORDER BY " . $order_by . " " . $order . " " . $limit;
+ // Whitelist allowed ORDER BY columns to prevent SQL injection
+ $allowed_order_by = array( 'date', 'id', 'fid', 'cid', 'order_id', 'total_revenue' );
+ $order_by = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'date';
+
+ // Whitelist ORDER direction
+ $order = strtoupper( $order );
+ $order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
+
+ // Sanitize LIMIT - ensure it's a positive integer
+ $limit_clause = '';
+ if ( '' !== $limit && is_numeric( $limit ) ) {
+ $limit_clause = ' LIMIT ' . absint( $limit );
+ }
+
+ return "SELECT conv.step_id as id, conv.funnel_id as 'fid', conv.contact_id as 'cid', conv.source as 'order_id', CONVERT( conv.bump_total USING utf8) as 'total_revenue', 'bump' as 'type', posts.post_title as 'post_title', conv.timestamp as date FROM " . $wpdb->prefix . 'bwf_conversion_tracking AS conv LEFT JOIN ' . $wpdb->prefix . "posts AS posts ON conv.step_id=posts.ID WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' ORDER BY " . $order_by . ' ' . $order . $limit_clause;
}
@@ -234,14 +300,30 @@
*/
public function get_top_funnels( $limit = '', $date_query = '' ) {
global $wpdb;
- $limit = ( $limit !== '' ) ? " LIMIT " . $limit : '';
- $limit = ! empty( $limit ) ? esc_sql( $limit ) : $limit;
+
+ // Sanitize LIMIT - ensure it's a positive integer
+ $limit_clause = '';
+ if ( '' !== $limit && is_numeric( $limit ) ) {
+ $limit_clause = ' LIMIT ' . absint( $limit );
+ }
+
+ // Note: $date_query is expected to be a pre-built date condition
+ // It should only contain 'conv.timestamp' column comparisons with date values
$date_query = str_replace( '{{COLUMN}}', 'conv.timestamp', $date_query );
- $query = "SELECT funnel.id as fid, funnel.title as title, stats.total as total FROM " . $wpdb->prefix . "bwf_funnels AS funnel
- JOIN ( SELECT funnel_id, SUM( bump_total ) as total FROM " . $wpdb->prefix . "bwf_conversion_tracking as conv
- WHERE funnel_id != 0 AND type = 2 AND bump_total > 0 AND bump_accepted != '' AND " . esc_sql( $date_query ) . " GROUP BY funnel_id ) as stats ON funnel.id = stats.funnel_id WHERE 1 = 1 GROUP BY funnel.id ORDER BY total DESC " . $limit;
- $data = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ // Security: Only allow safe date query patterns
+ // Expected format: "conv.timestamp >= 'YYYY-MM-DD' AND conv.timestamp < 'YYYY-MM-DD'"
+ if ( ! empty( $date_query ) && ! preg_match( '/^conv.timestamps*[<>=]+s*'[d-s:]+'(s+ANDs+conv.timestamps*[<>=]+s*'[d-s:]+')*$/i', $date_query ) ) {
+ $date_query = '1=1'; // Fallback to safe condition if pattern doesn't match
+ }
+
+ $date_condition = ! empty( $date_query ) ? $date_query : '1=1';
+
+ $query = 'SELECT funnel.id as fid, funnel.title as title, stats.total as total FROM ' . $wpdb->prefix . 'bwf_funnels AS funnel
+ JOIN ( SELECT funnel_id, SUM( bump_total ) as total FROM ' . $wpdb->prefix . "bwf_conversion_tracking as conv
+ WHERE funnel_id != 0 AND type = 2 AND bump_total > 0 AND bump_accepted != '' AND " . $date_condition . ' GROUP BY funnel_id ) as stats ON funnel.id = stats.funnel_id WHERE 1 = 1 GROUP BY funnel.id ORDER BY total DESC' . $limit_clause;
+
+ $data = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- date_query is validated above
$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
if ( true === $db_error['db_error'] ) {
return $db_error;
@@ -258,14 +340,32 @@
*/
public function delete_contact( $cids, $funnel_id = 0 ) {
global $wpdb;
- $cid_count = count( $cids );
- $stringPlaceholders = array_fill( 0, $cid_count, '%s' );
- $placeholdersForFavFruits = implode( ',', $stringPlaceholders );
- $funnel_query = ( absint( $funnel_id ) > 0 ) ? " AND fid = " . $funnel_id . " " : '';
- $query = "DELETE FROM " . $wpdb->prefix . "wfob_stats WHERE cid IN (" . esc_sql( $placeholdersForFavFruits ) . ") " . $funnel_query;
+ // Sanitize all cids as integers
+ $cids = array_map( 'absint', (array) $cids );
+ $cids = array_filter( $cids ); // Remove zeros
+ $cid_count = count( $cids );
+
+ if ( empty( $cid_count ) ) {
+ return true;
+ }
+
+ $placeholders = implode( ',', array_fill( 0, $cid_count, '%d' ) );
+ $funnel_id = absint( $funnel_id );
+
+ if ( $funnel_id > 0 ) {
+ $query = $wpdb->prepare(
+ 'DELETE FROM ' . $wpdb->prefix . "wfob_stats WHERE cid IN ($placeholders) AND fid = %d",
+ ...array_merge( $cids, array( $funnel_id ) )
+ );
+ } else {
+ $query = $wpdb->prepare(
+ 'DELETE FROM ' . $wpdb->prefix . "wfob_stats WHERE cid IN ($placeholders)",
+ ...$cids
+ );
+ }
- $wpdb->query( $wpdb->prepare( $query, $cids ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $wpdb->query( $query );