table_name = $wpdb->prefix . 'rwpvto_generated_images'; // Safety check: create table if it doesn't exist $this->maybe_create_table(); } /** * Check if table exists and create if needed */ private function maybe_create_table() { global $wpdb; // Table name is validated (it's our own prefix + known table name) $table_name = $wpdb->_escape( $this->table_name ); $table_exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $this->table_name ) ) === $this->table_name; if (!$table_exists) { self::create_tables(); } else { // Check if local_image_url column exists, add it if not (migration for existing installations) $this->maybe_add_local_image_url_column(); } } /** * Add local_image_url column if it doesn't exist (migration for existing installations) */ private function maybe_add_local_image_url_column() { global $wpdb; $table_name_escaped = esc_sql( $this->table_name ); // Check if column exists by trying to describe the table $columns = $wpdb->get_results( "DESCRIBE `{$table_name_escaped}`" ); $column_exists = false; if ( $columns ) { foreach ( $columns as $column ) { if ( isset( $column->Field ) && $column->Field === 'local_image_url' ) { $column_exists = true; break; } } } if ( ! $column_exists ) { $wpdb->query( "ALTER TABLE `{$table_name_escaped}` ADD COLUMN local_image_url text DEFAULT NULL AFTER generated_image_url" ); } } /** * Create database tables */ public static function create_tables() { global $wpdb; $table_name = $wpdb->prefix . 'rwpvto_generated_images'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, product_id bigint(20) unsigned NOT NULL, garment_image_url text NOT NULL, model_image_url text NOT NULL, generated_image_url text NOT NULL, local_image_url text DEFAULT NULL, replicate_prediction_id varchar(255) DEFAULT NULL, model_gender varchar(50) DEFAULT NULL, model_ethnicity varchar(50) DEFAULT NULL, category varchar(50) DEFAULT 'upper_body', garment_description text DEFAULT NULL, parameters longtext DEFAULT NULL, status varchar(50) DEFAULT 'completed', created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY product_id (product_id), KEY status (status), KEY created_at (created_at) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); } /** * Insert generated image record * * @param array $data Image data * @return int|false Insert ID or false on failure */ public function insert_image( $data ) { global $wpdb; $defaults = array( 'product_id' => 0, 'garment_image_url' => '', 'model_image_url' => '', 'generated_image_url' => '', 'local_image_url' => null, 'replicate_prediction_id' => null, 'model_gender' => null, 'model_ethnicity' => null, 'category' => 'upper_body', 'garment_description' => null, 'parameters' => null, 'status' => 'completed', 'created_at' => current_time( 'mysql' ), ); $data = wp_parse_args( $data, $defaults ); // Convert parameters array to JSON if it's an array if ( is_array( $data['parameters'] ) ) { $data['parameters'] = wp_json_encode( $data['parameters'] ); } $result = $wpdb->insert( $this->table_name, $data, array( '%d', // product_id '%s', // garment_image_url '%s', // model_image_url '%s', // generated_image_url '%s', // local_image_url '%s', // replicate_prediction_id '%s', // model_gender '%s', // model_ethnicity '%s', // category '%s', // garment_description '%s', // parameters '%s', // status '%s', // created_at ) ); if ( $result ) { return $wpdb->insert_id; } return false; } /** * Get image by ID * * @param int $id Image ID * @return object|null */ public function get_image( $id ) { global $wpdb; // Escape table name for security (table names cannot be parameterized) $table_name = esc_sql( $this->table_name ); return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$table_name}` WHERE id = %d", $id ) ); } /** * Get images by product ID * * @param int $product_id Product ID * @param array $args Additional arguments * @return array */ public function get_images_by_product( $product_id, $args = array() ) { global $wpdb; $defaults = array( 'limit' => 50, 'offset' => 0, 'orderby' => 'created_at', 'order' => 'DESC', ); $args = wp_parse_args( $args, $defaults ); // Whitelist allowed orderby and order values for security $allowed_orderby = array( 'id', 'created_at', 'product_id' ); $allowed_order = array( 'ASC', 'DESC' ); $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at'; $order = in_array( strtoupper( $args['order'] ), $allowed_order, true ) ? strtoupper( $args['order'] ) : 'DESC'; // Escape table name and column names for security $table_name_escaped = esc_sql( $this->table_name ); $orderby_escaped = esc_sql( $orderby ); $order_escaped = esc_sql( $order ); // Ensure limit and offset are integers $limit = absint( $args['limit'] ); $offset = absint( $args['offset'] ); return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$table_name_escaped}` WHERE product_id = %d ORDER BY `{$orderby_escaped}` {$order_escaped} LIMIT %d OFFSET %d", $product_id, $limit, $offset ) ); } /** * Get all images * * @param array $args Query arguments * @return array */ public function get_all_images( $args = array() ) { global $wpdb; $defaults = array( 'limit' => 50, 'offset' => 0, 'orderby' => 'created_at', 'order' => 'DESC', 'status' => null, 'search' => '', 'gender' => '', 'category' => '', ); $args = wp_parse_args( $args, $defaults ); $where = '1=1'; $where_args = array(); // Whitelist allowed values for security $allowed_status = array( 'completed', 'pending', 'failed' ); $allowed_gender = array( 'male', 'female' ); $allowed_category = array( 'upper_body', 'lower_body', 'dresses' ); // Filter by status (whitelisted and validated) $validated_status = $this->validate_whitelist( $args['status'], $allowed_status ); if ( ! empty( $validated_status ) ) { $where .= ' AND status = %s'; $where_args[] = $validated_status; // Validated against whitelist, will be escaped by prepare() } // Search by product name if ( ! empty( $args['search'] ) ) { $where .= ' AND product_id IN (SELECT ID FROM ' . $wpdb->posts . ' WHERE post_title LIKE %s AND post_type = "product")'; $where_args[] = '%' . $wpdb->esc_like( $args['search'] ) . '%'; } // Filter by gender (whitelisted and validated) $validated_gender = $this->validate_whitelist( $args['gender'], $allowed_gender ); if ( ! empty( $validated_gender ) ) { $where .= ' AND model_gender = %s'; $where_args[] = $validated_gender; // Validated against whitelist, will be escaped by prepare() } // Filter by category (whitelisted and validated) $validated_category = $this->validate_whitelist( $args['category'], $allowed_category ); if ( ! empty( $validated_category ) ) { $where .= ' AND category = %s'; $where_args[] = $validated_category; // Validated against whitelist, will be escaped by prepare() } // Whitelist allowed orderby and order values for security $allowed_orderby = array( 'id', 'created_at', 'product_id' ); $allowed_order = array( 'ASC', 'DESC' ); $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at'; $order = in_array( strtoupper( $args['order'] ), $allowed_order, true ) ? strtoupper( $args['order'] ) : 'DESC'; // Escape table name and column names for security (these cannot be parameterized) $table_name_escaped = esc_sql( $this->table_name ); $orderby_escaped = esc_sql( $orderby ); $order_escaped = esc_sql( $order ); // Ensure limit and offset are integers $limit = absint( $args['limit'] ); $offset = absint( $args['offset'] ); $where_args[] = $limit; $where_args[] = $offset; // Build query with escaped table/column names, prepare will escape the values $query = "SELECT * FROM `{$table_name_escaped}` WHERE {$where} ORDER BY `{$orderby_escaped}` {$order_escaped} LIMIT %d OFFSET %d"; // Prepare and execute query in one call so checker recognizes it as prepared return $wpdb->get_results( $wpdb->prepare( $query, ...$where_args ) ); } /** * Get total count * * @param string $search Search term * @param string $gender Gender filter * @param string $category Category filter * @return int */ public function get_total_count( $search = '', $gender = '', $category = '' ) { global $wpdb; $where = '1=1'; $where_args = array(); // Whitelist allowed values for security $allowed_gender = array( 'male', 'female' ); $allowed_category = array( 'upper_body', 'lower_body', 'dresses' ); // Search by product name if ( ! empty( $search ) ) { $where .= ' AND product_id IN (SELECT ID FROM ' . $wpdb->posts . ' WHERE post_title LIKE %s AND post_type = "product")'; $where_args[] = '%' . $wpdb->esc_like( $search ) . '%'; } // Filter by gender (whitelisted and validated) $validated_gender = $this->validate_whitelist( $gender, $allowed_gender ); if ( ! empty( $validated_gender ) ) { $where .= ' AND model_gender = %s'; $where_args[] = $validated_gender; // Validated against whitelist, will be escaped by prepare() } // Filter by category (whitelisted and validated) $validated_category = $this->validate_whitelist( $category, $allowed_category ); if ( ! empty( $validated_category ) ) { $where .= ' AND category = %s'; $where_args[] = $validated_category; // Validated against whitelist, will be escaped by prepare() } // Escape table name for security (table names cannot be parameterized) $table_name_escaped = esc_sql( $this->table_name ); // Build query with escaped table name, prepare will escape the values $query = "SELECT COUNT(*) FROM `{$table_name_escaped}` WHERE {$where}"; // Prepare and execute query in one call so checker recognizes it as prepared if ( ! empty( $where_args ) ) { return (int) $wpdb->get_var( $wpdb->prepare( $query, ...$where_args ) ); } else { // No WHERE conditions with placeholders - table name already escaped, safe to use directly return (int) $wpdb->get_var( $query ); } } /** * Get count of images created after a specific date * * @param string $date_start Start date (Y-m-d H:i:s format) * @return int */ public function get_count_since_date( $date_start ) { global $wpdb; // Escape table name for security (table names cannot be parameterized) $table_name_escaped = esc_sql( $this->table_name ); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table_name_escaped}` WHERE created_at >= %s", $date_start ) ); } /** * Get count of unique products with generated images * * @return int */ public function get_unique_products_count() { global $wpdb; // Escape table name for security (table names cannot be parameterized) $table_name_escaped = esc_sql( $this->table_name ); return (int) $wpdb->get_var( "SELECT COUNT(DISTINCT product_id) FROM `{$table_name_escaped}`" ); } /** * Update image * * @param int $id Image ID * @param array $data Data to update * @return bool */ public function update_image( $id, $data ) { global $wpdb; // Convert parameters array to JSON if it's an array if ( isset( $data['parameters'] ) && is_array( $data['parameters'] ) ) { $data['parameters'] = wp_json_encode( $data['parameters'] ); } $result = $wpdb->update( $this->table_name, $data, array( 'id' => $id ), null, array( '%d' ) ); return $result !== false; } /** * Delete image * * @param int $id Image ID * @return bool */ public function delete_image( $id ) { global $wpdb; $result = $wpdb->delete( $this->table_name, array( 'id' => $id ), array( '%d' ) ); return $result !== false; } /** * Delete images by product ID * * @param int $product_id Product ID * @return bool */ public function delete_images_by_product( $product_id ) { global $wpdb; $result = $wpdb->delete( $this->table_name, array( 'product_id' => $product_id ), array( '%d' ) ); return $result !== false; } }