Files
nolimit-discounts/includes/class-db.php

648 lines
18 KiB
PHP

<?php
/**
* Database tables and CRUD.
*
* @package WC_Email_Discount_Manager
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles custom tables and all CRUD operations.
*/
class WC_EDM_DB {
/**
* Get a fully prefixed table name.
*
* @param string $name Short table name (groups, group_categories, emails, shipping_rules).
* @return string
*/
public static function table( $name ) {
global $wpdb;
return $wpdb->prefix . 'wc_edm_' . $name;
}
/**
* Create/upgrade the custom tables via dbDelta(), then add foreign keys.
*/
public static function create_tables() {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset_collate = $wpdb->get_charset_collate();
$groups = self::table( 'groups' );
$group_categories = self::table( 'group_categories' );
$emails = self::table( 'emails' );
$shipping_rules = self::table( 'shipping_rules' );
$audit_log = self::table( 'audit_log' );
$sql = "CREATE TABLE {$groups} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
name varchar(191) NOT NULL,
discount_percent decimal(5,2) NOT NULL DEFAULT 0.00,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id)
) {$charset_collate};
CREATE TABLE {$group_categories} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
group_id bigint(20) unsigned NOT NULL,
category_id bigint(20) unsigned NOT NULL,
sort_order int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY group_category (group_id,category_id),
KEY group_id (group_id)
) {$charset_collate};
CREATE TABLE {$emails} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
email_pattern varchar(191) NOT NULL,
assignment_type enum('group','coupon','custom') NOT NULL DEFAULT 'group',
group_id bigint(20) unsigned DEFAULT NULL,
coupon_code varchar(191) DEFAULT NULL,
custom_discount decimal(5,2) DEFAULT NULL,
is_active tinyint(1) NOT NULL DEFAULT 1,
free_shipping tinyint(1) NOT NULL DEFAULT 0,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id),
KEY email_pattern (email_pattern),
KEY group_id (group_id)
) {$charset_collate};
CREATE TABLE {$shipping_rules} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
group_id bigint(20) unsigned NOT NULL,
shipping_method_id varchar(191) NOT NULL,
action enum('show','hide') NOT NULL DEFAULT 'show',
sort_order int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY group_id (group_id)
) {$charset_collate};
CREATE TABLE {$audit_log} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
created_at datetime NOT NULL,
user_id bigint(20) unsigned NOT NULL DEFAULT 0,
user_login varchar(191) DEFAULT NULL,
ip varchar(45) DEFAULT NULL,
method varchar(10) NOT NULL DEFAULT '',
route varchar(191) NOT NULL DEFAULT '',
status smallint(5) unsigned NOT NULL DEFAULT 0,
outcome enum('allowed','denied','rate_limited') NOT NULL DEFAULT 'allowed',
details text DEFAULT NULL,
PRIMARY KEY (id),
KEY created_at (created_at),
KEY user_id (user_id),
KEY outcome (outcome)
) {$charset_collate};";
dbDelta( $sql );
self::add_foreign_keys();
update_option( 'wc_edm_db_version', WC_EDM_VERSION );
}
/**
* Run create_tables() again when the plugin was updated without
* re-activation (dbDelta adds any missing columns).
*/
public static function maybe_upgrade() {
if ( get_option( 'wc_edm_db_version' ) !== WC_EDM_VERSION ) {
self::create_tables();
}
}
/**
* Add foreign key constraints (dbDelta cannot handle these).
*/
private static function add_foreign_keys() {
$groups = self::table( 'groups' );
self::maybe_add_foreign_key(
self::table( 'group_categories' ),
'fk_wc_edm_group_categories_group',
"FOREIGN KEY (group_id) REFERENCES {$groups} (id) ON DELETE CASCADE"
);
self::maybe_add_foreign_key(
self::table( 'emails' ),
'fk_wc_edm_emails_group',
"FOREIGN KEY (group_id) REFERENCES {$groups} (id) ON DELETE SET NULL"
);
self::maybe_add_foreign_key(
self::table( 'shipping_rules' ),
'fk_wc_edm_shipping_rules_group',
"FOREIGN KEY (group_id) REFERENCES {$groups} (id) ON DELETE CASCADE"
);
}
/**
* Add a foreign key if it does not already exist.
*
* @param string $table Table name.
* @param string $constraint Constraint name.
* @param string $definition FOREIGN KEY definition SQL.
*/
private static function maybe_add_foreign_key( $table, $constraint, $definition ) {
global $wpdb;
$exists = $wpdb->get_var(
$wpdb->prepare(
'SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = %s AND TABLE_NAME = %s AND CONSTRAINT_NAME = %s AND CONSTRAINT_TYPE = %s',
DB_NAME,
$table,
$constraint,
'FOREIGN KEY'
)
);
if ( ! $exists ) {
// Table/constraint names are internal, not user input.
$wpdb->query( "ALTER TABLE {$table} ADD CONSTRAINT {$constraint} {$definition}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
}
/*
|--------------------------------------------------------------------------
| Discount groups
|--------------------------------------------------------------------------
*/
/**
* Get all discount groups.
*
* @return array Array of row objects.
*/
public static function get_groups() {
global $wpdb;
$table = self::table( 'groups' );
return $wpdb->get_results( "SELECT * FROM {$table} ORDER BY name ASC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Count discount groups.
*
* @return int
*/
public static function count_groups() {
global $wpdb;
$table = self::table( 'groups' );
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Get the most recently created groups.
*
* @param int $limit Number of groups.
* @return array
*/
public static function get_recent_groups( $limit = 5 ) {
global $wpdb;
$table = self::table( 'groups' );
return $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d", $limit ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
/**
* Get a single group.
*
* @param int $id Group ID.
* @return object|null
*/
public static function get_group( $id ) {
global $wpdb;
$table = self::table( 'groups' );
return $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
/**
* Insert a group.
*
* @param array $data { name, discount_percent }.
* @return int New group ID (0 on failure).
*/
public static function insert_group( $data ) {
global $wpdb;
$now = current_time( 'mysql' );
$wpdb->insert(
self::table( 'groups' ),
array(
'name' => $data['name'],
'discount_percent' => $data['discount_percent'],
'created_at' => $now,
'updated_at' => $now,
),
array( '%s', '%f', '%s', '%s' )
);
return (int) $wpdb->insert_id;
}
/**
* Update a group.
*
* @param int $id Group ID.
* @param array $data { name, discount_percent }.
* @return bool
*/
public static function update_group( $id, $data ) {
global $wpdb;
return false !== $wpdb->update(
self::table( 'groups' ),
array(
'name' => $data['name'],
'discount_percent' => $data['discount_percent'],
'updated_at' => current_time( 'mysql' ),
),
array( 'id' => $id ),
array( '%s', '%f', '%s' ),
array( '%d' )
);
}
/**
* Delete a group and its related rows.
*
* @param int $id Group ID.
* @return bool
*/
public static function delete_group( $id ) {
global $wpdb;
// Manual cleanup in case foreign keys are unsupported (e.g. MyISAM).
$wpdb->delete( self::table( 'group_categories' ), array( 'group_id' => $id ), array( '%d' ) );
$wpdb->delete( self::table( 'shipping_rules' ), array( 'group_id' => $id ), array( '%d' ) );
$wpdb->update( self::table( 'emails' ), array( 'group_id' => null ), array( 'group_id' => $id ), array( '%d' ), array( '%d' ) );
return false !== $wpdb->delete( self::table( 'groups' ), array( 'id' => $id ), array( '%d' ) );
}
/*
|--------------------------------------------------------------------------
| Group categories (M2M)
|--------------------------------------------------------------------------
*/
/**
* Get category IDs for a group, ordered by sort_order.
*
* @param int $group_id Group ID.
* @return int[]
*/
public static function get_group_category_ids( $group_id ) {
global $wpdb;
$table = self::table( 'group_categories' );
return array_map(
'intval',
$wpdb->get_col(
$wpdb->prepare( "SELECT category_id FROM {$table} WHERE group_id = %d ORDER BY sort_order ASC, id ASC", $group_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
)
);
}
/**
* Replace the categories for a group. Sort order follows array order.
*
* @param int $group_id Group ID.
* @param int[] $category_ids Ordered category IDs.
*/
public static function set_group_categories( $group_id, $category_ids ) {
global $wpdb;
$wpdb->delete( self::table( 'group_categories' ), array( 'group_id' => $group_id ), array( '%d' ) );
$category_ids = array_values( array_unique( array_map( 'intval', $category_ids ) ) );
foreach ( $category_ids as $index => $category_id ) {
if ( $category_id <= 0 ) {
continue;
}
$wpdb->insert(
self::table( 'group_categories' ),
array(
'group_id' => $group_id,
'category_id' => $category_id,
'sort_order' => $index,
),
array( '%d', '%d', '%d' )
);
}
}
/*
|--------------------------------------------------------------------------
| Email assignments
|--------------------------------------------------------------------------
*/
/**
* Get all email assignments.
*
* @return array
*/
public static function get_emails() {
global $wpdb;
$table = self::table( 'emails' );
return $wpdb->get_results( "SELECT * FROM {$table} ORDER BY email_pattern ASC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Get all active email assignments.
*
* @return array
*/
public static function get_active_emails() {
global $wpdb;
$table = self::table( 'emails' );
return $wpdb->get_results( "SELECT * FROM {$table} WHERE is_active = 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Count email assignments.
*
* @param bool $active_only Count only active assignments.
* @return int
*/
public static function count_emails( $active_only = false ) {
global $wpdb;
$table = self::table( 'emails' );
$sql = "SELECT COUNT(*) FROM {$table}";
if ( $active_only ) {
$sql .= ' WHERE is_active = 1';
}
return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Get a single email assignment.
*
* @param int $id Assignment ID.
* @return object|null
*/
public static function get_email( $id ) {
global $wpdb;
$table = self::table( 'emails' );
return $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
/**
* Insert an email assignment.
*
* @param array $data Assignment data.
* @return int New ID (0 on failure).
*/
public static function insert_email( $data ) {
global $wpdb;
$now = current_time( 'mysql' );
$wpdb->insert(
self::table( 'emails' ),
array(
'email_pattern' => $data['email_pattern'],
'assignment_type' => $data['assignment_type'],
'group_id' => $data['group_id'],
'coupon_code' => $data['coupon_code'],
'custom_discount' => $data['custom_discount'],
'is_active' => $data['is_active'],
'free_shipping' => $data['free_shipping'],
'created_at' => $now,
'updated_at' => $now,
)
);
return (int) $wpdb->insert_id;
}
/**
* Update an email assignment.
*
* @param int $id Assignment ID.
* @param array $data Assignment data.
* @return bool
*/
public static function update_email( $id, $data ) {
global $wpdb;
return false !== $wpdb->update(
self::table( 'emails' ),
array(
'email_pattern' => $data['email_pattern'],
'assignment_type' => $data['assignment_type'],
'group_id' => $data['group_id'],
'coupon_code' => $data['coupon_code'],
'custom_discount' => $data['custom_discount'],
'is_active' => $data['is_active'],
'free_shipping' => $data['free_shipping'],
'updated_at' => current_time( 'mysql' ),
),
array( 'id' => $id )
);
}
/**
* Toggle an email assignment's active flag.
*
* @param int $id Assignment ID.
* @param int $is_active 1 or 0.
* @return bool
*/
public static function set_email_active( $id, $is_active ) {
global $wpdb;
return false !== $wpdb->update(
self::table( 'emails' ),
array(
'is_active' => $is_active ? 1 : 0,
'updated_at' => current_time( 'mysql' ),
),
array( 'id' => $id ),
array( '%d', '%s' ),
array( '%d' )
);
}
/**
* Delete an email assignment.
*
* @param int $id Assignment ID.
* @return bool
*/
public static function delete_email( $id ) {
global $wpdb;
return false !== $wpdb->delete( self::table( 'emails' ), array( 'id' => $id ), array( '%d' ) );
}
/*
|--------------------------------------------------------------------------
| Shipping rules
|--------------------------------------------------------------------------
*/
/**
* Get shipping rules for a group, ordered by sort_order.
*
* @param int $group_id Group ID.
* @return array
*/
public static function get_shipping_rules( $group_id ) {
global $wpdb;
$table = self::table( 'shipping_rules' );
return $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} WHERE group_id = %d ORDER BY sort_order ASC, id ASC", $group_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
/**
* Replace all shipping rules for a group.
*
* @param int $group_id Group ID.
* @param array $rules Array of { shipping_method_id, action, sort_order }.
*/
public static function replace_shipping_rules( $group_id, $rules ) {
global $wpdb;
$wpdb->delete( self::table( 'shipping_rules' ), array( 'group_id' => $group_id ), array( '%d' ) );
foreach ( $rules as $rule ) {
if ( empty( $rule['shipping_method_id'] ) || ! in_array( $rule['action'], array( 'show', 'hide' ), true ) ) {
continue;
}
$wpdb->insert(
self::table( 'shipping_rules' ),
array(
'group_id' => $group_id,
'shipping_method_id' => $rule['shipping_method_id'],
'action' => $rule['action'],
'sort_order' => (int) $rule['sort_order'],
),
array( '%d', '%s', '%s', '%d' )
);
}
}
/*
|--------------------------------------------------------------------------
| Audit log
|--------------------------------------------------------------------------
*/
/**
* Insert an audit log entry.
*
* @param array $data Entry data.
* @return int New entry ID (0 on failure).
*/
public static function insert_audit_entry( $data ) {
global $wpdb;
$wpdb->insert(
self::table( 'audit_log' ),
array(
'created_at' => current_time( 'mysql' ),
'user_id' => (int) $data['user_id'],
'user_login' => $data['user_login'],
'ip' => $data['ip'],
'method' => $data['method'],
'route' => $data['route'],
'status' => (int) $data['status'],
'outcome' => $data['outcome'],
'details' => $data['details'],
),
array( '%s', '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s' )
);
return (int) $wpdb->insert_id;
}
/**
* Get audit log entries, newest first.
*
* @param int $limit Max entries.
* @param int $offset Offset.
* @param string $outcome Optional outcome filter (allowed/denied/rate_limited).
* @return array
*/
public static function get_audit_entries( $limit = 50, $offset = 0, $outcome = '' ) {
global $wpdb;
$table = self::table( 'audit_log' );
if ( in_array( $outcome, array( 'allowed', 'denied', 'rate_limited' ), true ) ) {
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE outcome = %s ORDER BY id DESC LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$outcome,
$limit,
$offset
)
);
}
return $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d", $limit, $offset ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
/**
* Count audit log entries.
*
* @param string $outcome Optional outcome filter.
* @return int
*/
public static function count_audit_entries( $outcome = '' ) {
global $wpdb;
$table = self::table( 'audit_log' );
if ( in_array( $outcome, array( 'allowed', 'denied', 'rate_limited' ), true ) ) {
return (int) $wpdb->get_var(
$wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE outcome = %s", $outcome ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Delete audit entries older than the retention period.
*
* @param int $days Retention in days.
* @return int Rows deleted.
*/
public static function prune_audit_log( $days ) {
global $wpdb;
$table = self::table( 'audit_log' );
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( current_time( 'mysql' ) . ' -' . absint( $days ) . ' days' ) );
return (int) $wpdb->query(
$wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
/**
* Delete every audit log entry.
*
* @return int Rows deleted.
*/
public static function clear_audit_log() {
global $wpdb;
$table = self::table( 'audit_log' );
return (int) $wpdb->query( "DELETE FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
}