--- a/vidshop-for-woocommerce/includes/models/class-video-event-model.php
+++ b/vidshop-for-woocommerce/includes/models/class-video-event-model.php
@@ -71,6 +71,8 @@
* @return int
*/
public static function get_total_likes( $start_date, $end_date, $video_id = null ) {
+ global $wpdb;
+
$query = static::query()->where( 'event_type', 'like' );
// Join with sessions table
@@ -79,14 +81,15 @@
$query->join_raw( "JOIN {$sessions_table} s ON {$events_table}.session_id = s.id" );
- // Add date range condition only if dates are provided
+ // Add date range condition only if dates are provided - use prepared statement
if ( $start_date && $end_date ) {
- $query->where_raw( "s.started_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 's.started_at BETWEEN %s AND %s', $start_date, $end_date ) );
}
// Add video filter if provided
if ( $video_id ) {
- $query->where( 'video_id', $video_id );
+ $query->where( 'video_id', absint( $video_id ) );
}
return $query->count();
@@ -101,6 +104,8 @@
* @return int
*/
public static function get_unique_likes( $start_date, $end_date, $video_id = null ) {
+ global $wpdb;
+
$query = static::query()->where( 'event_type', 'like' );
// Join with sessions table
@@ -109,14 +114,15 @@
$query->join_raw( "JOIN {$sessions_table} s ON {$events_table}.session_id = s.id" );
- // Add date range condition only if dates are provided
+ // Add date range condition only if dates are provided - use prepared statement
if ( $start_date && $end_date ) {
- $query->where_raw( "s.started_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 's.started_at BETWEEN %s AND %s', $start_date, $end_date ) );
}
// Add video filter if provided
if ( $video_id ) {
- $query->where( 'video_id', $video_id );
+ $query->where( 'video_id', absint( $video_id ) );
}
return $query->count_distinct( 's.visitor_id' );
--- a/vidshop-for-woocommerce/includes/models/class-video-product-stats-model.php
+++ b/vidshop-for-woocommerce/includes/models/class-video-product-stats-model.php
@@ -114,10 +114,13 @@
* @return int The total number of views.
*/
public static function get_total_views( $start_date, $end_date ) {
+ global $wpdb;
+
$query = static::query();
if ( $start_date && $end_date ) {
- $query->where_raw( "created_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 'created_at BETWEEN %s AND %s', $start_date, $end_date ) );
}
return $query->sum( 'views' );
@@ -131,20 +134,23 @@
* @return int The total number of add to cart events.
*/
public static function get_total_add_to_cart( $start_date, $end_date ) {
+ global $wpdb;
+
$query = static::query();
if ( $start_date && $end_date ) {
- $query->where_raw( "created_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 'created_at BETWEEN %s AND %s', $start_date, $end_date ) );
}
return $query->sum( 'add_to_cart_count' );
}
- /**
- * Get top 5 products by add to cart count
- *
- * @return array Array of product IDs and their add to cart counts.
- */
+ /**
+ * Get top 5 products by add to cart count
+ *
+ * @return array Array of product IDs and their add to cart counts.
+ */
public static function get_top_added_to_cart_products() {
return static::query()
->select( array( 'product_id', 'SUM(add_to_cart_count) as total_add_to_cart', 'SUM(views) as total_views' ) )
@@ -157,14 +163,22 @@
/**
* Get products for a video
*
- * @param int $video_id The video ID.
+ * @param string $start_date Start date.
+ * @param string $end_date End date.
+ * @param int $video_id The video ID.
* @return array Array of product IDs and their add to cart counts.
*/
public static function get_products( $start_date, $end_date, $video_id ) {
- return static::query()
- ->where_raw( "created_at BETWEEN '{$start_date}' AND '{$end_date}'" )
- ->where( 'video_id', '=', $video_id )
- ->get();
+ global $wpdb;
+
+ $query = static::query();
+
+ if ( $start_date && $end_date ) {
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 'created_at BETWEEN %s AND %s', $start_date, $end_date ) );
+ }
+
+ return $query->where( 'video_id', '=', absint( $video_id ) )->get();
}
/**
--- a/vidshop-for-woocommerce/includes/models/class-video-session-model.php
+++ b/vidshop-for-woocommerce/includes/models/class-video-session-model.php
@@ -129,6 +129,8 @@
* @return int
*/
public static function get_total_sessions( $start_date, $end_date, $video_id = null ) {
+ global $wpdb;
+
$query = static::query();
if ( $video_id ) {
@@ -137,18 +139,22 @@
$sessions_table = ( new static() )->get_full_table_name();
$query->join_raw( "JOIN {$events_table} e ON {$sessions_table}.id = e.session_id" );
- $query->where_raw( "e.video_id = {$video_id}" );
+ // Use prepared statement for video_id
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 'e.video_id = %d', absint( $video_id ) ) );
// Add date range condition only if dates are provided
if ( $start_date && $end_date ) {
- $query->where_raw( "{$sessions_table}.started_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( "{$sessions_table}.started_at BETWEEN %s AND %s", $start_date, $end_date ) );
}
return $query->count_distinct( "{$sessions_table}.id" );
} else {
// Add date range condition only if dates are provided
if ( $start_date && $end_date ) {
- $query->where_raw( "started_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 'started_at BETWEEN %s AND %s', $start_date, $end_date ) );
}
return $query->count();
@@ -164,6 +170,8 @@
* @return int
*/
public static function get_unique_sessions( $start_date, $end_date, $video_id = null ) {
+ global $wpdb;
+
$query = static::query();
if ( $video_id ) {
@@ -172,18 +180,22 @@
$sessions_table = ( new static() )->get_full_table_name();
$query->join_raw( "JOIN {$events_table} e ON {$sessions_table}.id = e.session_id" );
- $query->where_raw( "e.video_id = {$video_id}" );
+ // Use prepared statement for video_id
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 'e.video_id = %d', absint( $video_id ) ) );
// Add date range condition only if dates are provided
if ( $start_date && $end_date ) {
- $query->where_raw( "{$sessions_table}.started_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( "{$sessions_table}.started_at BETWEEN %s AND %s", $start_date, $end_date ) );
}
return $query->count_distinct( "{$sessions_table}.visitor_id" );
} else {
// Add date range condition only if dates are provided
if ( $start_date && $end_date ) {
- $query->where_raw( "started_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 'started_at BETWEEN %s AND %s', $start_date, $end_date ) );
}
return $query->count_distinct( 'visitor_id' );
@@ -199,6 +211,8 @@
* @return array
*/
public static function get_top_videos( $start_date, $end_date, $limit = 5 ) {
+ global $wpdb;
+
// Get video IDs with their view counts using eloquent Query Builder
$events_table = ( new Video_Event_Model() )->get_full_table_name();
$sessions_table = ( new static() )->get_full_table_name();
@@ -209,7 +223,8 @@
// Add date range condition only if dates are provided
if ( $start_date && $end_date ) {
- $query->where_raw( "s.started_at BETWEEN '{$start_date}' AND '{$end_date}'" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query->where_raw( $wpdb->prepare( 's.started_at BETWEEN %s AND %s', $start_date, $end_date ) );
}
$video_view_counts = $query
--- a/vidshop-for-woocommerce/includes/rest-api/v1/class-videos-controller.php
+++ b/vidshop-for-woocommerce/includes/rest-api/v1/class-videos-controller.php
@@ -180,6 +180,68 @@
}
/**
+ * Get allowed fields for select query (whitelist for SQL injection prevention)
+ *
+ * @return array
+ */
+ private function get_allowed_fields() {
+ return array(
+ 'id',
+ 'title',
+ 'type',
+ 'source_url',
+ 'thumbnail_id',
+ 'video_id',
+ 'settings',
+ 'status',
+ 'created_by',
+ 'created_at',
+ 'updated_at',
+ );
+ }
+
+ /**
+ * Sanitize fields parameter - whitelist validation
+ *
+ * @param string $fields Comma-separated fields.
+ * @return string Sanitized fields.
+ */
+ public function sanitize_fields_param( $fields ) {
+ if ( empty( $fields ) ) {
+ return '';
+ }
+ error_log( 'Sanitize fields parameter: ' . $fields );
+ $requested_fields = array_map( 'trim', explode( ',', $fields ) );
+ $allowed_fields = $this->get_allowed_fields();
+ $valid_fields = array_filter(
+ $requested_fields,
+ function ( $field ) use ( $allowed_fields ) {
+ return in_array( $field, $allowed_fields, true );
+ }
+ );
+ error_log( 'Valid fields: ' . implode( ',', $valid_fields ) );
+ return implode( ',', $valid_fields );
+ }
+
+ /**
+ * Sanitize ids parameter - ensure all values are integers
+ *
+ * @param string $ids Comma-separated IDs.
+ * @return string Sanitized IDs.
+ */
+ public function sanitize_ids_param( $ids ) {
+ if ( empty( $ids ) ) {
+ return '';
+ }
+
+ $id_array = array_map( 'absint', explode( ',', $ids ) );
+ $id_array = array_filter( $id_array ); // Remove zeros
+ $id_array = array_unique( $id_array );
+
+ return implode( ',', $id_array );
+ }
+
+ /**
* Get collection parameters
*/
public function get_collection_params() {
@@ -222,12 +284,14 @@
'enum' => array( 'asc', 'desc' ),
),
'fields' => array(
- 'description' => __( 'Comma-separated list of fields to include in the response.', 'vidshop-for-woocommerce' ),
- 'type' => 'string',
+ 'description' => __( 'Comma-separated list of fields to include in the response.', 'vidshop-for-woocommerce' ),
+ 'type' => 'string',
+ 'sanitize_callback' => array( $this, 'sanitize_fields_param' ),
),
'ids' => array(
- 'description' => __( 'Comma-separated list of video IDs.', 'vidshop-for-woocommerce' ),
- 'type' => 'string',
+ 'description' => __( 'Comma-separated list of video IDs.', 'vidshop-for-woocommerce' ),
+ 'type' => 'string',
+ 'sanitize_callback' => array( $this, 'sanitize_ids_param' ),
),
);
@@ -238,20 +302,27 @@
* Get items
*/
public function get_items( $request ) {
+ global $wpdb;
+
$page = $request->get_param( 'page' );
$per_page = $request->get_param( 'per_page' );
$search = $request->get_param( 'search' );
$status = $this->check_private_permission( $request ) ? $request->get_param( 'status' ) : 'published';
$orderby = $request->get_param( 'orderby' );
$order = $request->get_param( 'order' );
- $fields = $request->get_param( 'fields' );
- $ids = $request->get_param( 'ids' );
+ $fields = $request->get_param( 'fields' ); // Already sanitized via sanitize_callback
+ $ids = $request->get_param( 'ids' ); // Already sanitized via sanitize_callback
$query = Video_Model::query();
- if ( $ids ) {
- $ids = explode( ',', $ids );
- $query->where_in( 'id', $ids );
+ // Parse sanitized IDs (already validated as integers by sanitize_callback)
+ $ids_array = array();
+ if ( ! empty( $ids ) ) {
+ $ids_array = array_map( 'intval', explode( ',', $ids ) );
+ $ids_array = array_filter( $ids_array );
+ if ( ! empty( $ids_array ) ) {
+ $query->where_in( 'id', $ids_array );
+ }
}
$relations = array(
@@ -288,13 +359,18 @@
if ( $status ) {
$query->where( 'status', $status );
}
- if ( $ids && $orderby === 'id' ) {
- $query->order_by_raw( 'FIELD(id, ' . implode( ',', $ids ) . ')' );
+
+ // Order by FIELD() for custom ID ordering - use prepared statement
+ if ( ! empty( $ids_array ) && $orderby === 'id' ) {
+ $placeholders = implode( ',', array_fill( 0, count( $ids_array ), '%d' ) );
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $query->order_by_raw( $wpdb->prepare( "FIELD(id, {$placeholders})", $ids_array ) );
} elseif ( $orderby ) {
$query->order_by( $orderby, $order );
}
- if ( $fields ) {
+ // Select specific fields (already validated via whitelist in sanitize_callback)
+ if ( ! empty( $fields ) ) {
$selected_fields = explode( ',', $fields );
$query->select( $selected_fields );
}
--- a/vidshop-for-woocommerce/includes/utils/class-query-builder.php
+++ b/vidshop-for-woocommerce/includes/utils/class-query-builder.php
@@ -401,13 +401,62 @@
}
/**
+ * Validate a column name to prevent SQL injection
+ *
+ * @param string $column The column name to validate.
+ * @return bool True if valid, false otherwise.
+ */
+ protected function is_valid_column_name( $column ) {
+ // Allow * for select all
+ if ( $column === '*' ) {
+ return true;
+ }
+
+ // Allow column names with optional table alias (e.g., "id", "table.id", "t.column_name")
+ // Pattern: starts with letter/underscore, followed by alphanumeric/underscore
+ // Optionally prefixed with table alias (same pattern) and a dot
+ if ( preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*(.[a-zA-Z_][a-zA-Z0-9_]*)?$/', $column ) ) {
+ return true;
+ }
+
+ // Allow "column AS alias" syntax with safe characters
+ if ( preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*(.[a-zA-Z_][a-zA-Z0-9_]*)?s+[Aa][Ss]s+[a-zA-Z_][a-zA-Z0-9_]*$/', $column ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Sanitize columns array - remove any invalid column names
+ *
+ * @param array $columns Array of column names.
+ * @return array Sanitized array of valid column names.
+ */
+ protected function sanitize_columns( $columns ) {
+ return array_filter(
+ $columns,
+ function ( $column ) {
+ return $this->is_valid_column_name( $column );
+ }
+ );
+ }
+
+ /**
* Set the columns to be selected
*
* @param array|mixed $columns
* @return $this
*/
public function select( $columns = array( '*' ) ) {
- $this->columns = is_array( $columns ) ? $columns : func_get_args();
+ $columns = is_array( $columns ) ? $columns : func_get_args();
+ $this->columns = $this->sanitize_columns( $columns );
+
+ // If all columns were invalid, default to all
+ if ( empty( $this->columns ) ) {
+ $this->columns = array( '*' );
+ }
+
return $this;
}
--- a/vidshop-for-woocommerce/vidshop-for-woocommerce.php
+++ b/vidshop-for-woocommerce/vidshop-for-woocommerce.php
@@ -2,7 +2,7 @@
/*
Plugin Name: VidShop for WooCommerce
Description: Upload your own videos and display WooCommerce products inside them. Let users interact and add items to cart while watching. Lightweight, fast, and fully integrated with WooCommerce.
-Version: 1.1.4
+Version: 1.1.5
Author: WPCreatix
Author URI: https://wpcreatix.com/
Plugin URI: https://wpcreatix.com/
@@ -20,7 +20,7 @@
exit;
}
-define( 'VSFW_VERSION', '1.1.4' );
+define( 'VSFW_VERSION', '1.1.5' );
define( 'VSFW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'VSFW_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'VSFW_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );