Source: includes/data-utilities.php

<?php
/**
 * Utilities for saving, deleting, and managing cart data stores.
 *
 * @category Cart
 * @package  My Tickets
 * @author   Joe Dolson
 * @license  GPLv2 or later
 * @link     https://www.joedolson.com/my-tickets/
 */

/**
 * Abstract function for saving user data (cookie or meta). Saves as option if not logged in, as user meta if is.
 *
 * @param array  $passed Data passed to save.
 * @param string $type Type of data to save.
 * @param bool   $override Whether to override this.
 *
 * @return bool
 */
function mt_save_data( $passed, $type = 'cart', $override = false ) {
	$type = sanitize_title( $type );
	// The shape of the $passed data doesn't match the saved model when updating from the cart page.
	if ( true === $override ) {
		$save  = $passed;
		$saved = mt_get_cart();
	} else {
		switch ( $type ) {
			case 'cart':
				$save              = mt_get_cart();
				$saved             = $save;
				$options           = $passed['options'];
				$event_id          = $passed['event_id'];
				$save[ $event_id ] = $options;
				break;
			case 'payment':
				$save = $passed;
				break;
			default:
				$save = $passed;
		}
	}
	if ( 'cart' === $type ) {
		$inventory_change = mt_get_inventory_change( $save, $saved );
		foreach ( $inventory_change as $ticket => $change ) {
			mt_update_inventory( $change['event_id'], $ticket, $change['count'] );
		}
	}

	$current_user = wp_get_current_user();
	$expiration   = mt_expiration_window();

	if ( is_user_logged_in() ) {
		update_user_meta( $current_user->ID, '_mt_user_init_expiration', time() + $expiration );
		update_user_meta( $current_user->ID, "_mt_user_$type", $save );

		return true;
	} else {
		$unique_id = mt_get_unique_id();
		if ( ! $unique_id ) {
			return false;
		}
		if ( mt_get_transient( 'mt_' . $unique_id . '_' . $type ) ) {
			mt_delete_transient( 'mt_' . $unique_id . '_' . $type );
		}
		mt_set_transient( 'mt_' . $unique_id . '_' . $type, $save );
		mt_set_transient( 'mt_' . $unique_id . '_expiration', time() + $expiration );

		return true;
	}
	mt_refresh_cache();
}

/**
 * Extend cart expiration time.
 *
 * @param int $amount Time in seconds to extend cart validity.
 *
 * @return bool|int
 */
function mt_extend_expiration( $amount = 300 ) {
	$amount = absint( $amount );
	$window = mt_expiration_window();
	if ( is_user_logged_in() ) {
		$current = get_user_meta( wp_get_current_user()->ID, '_mt_user_init_expiration', true );
		$new     = (int) $current + $amount;
		// If the new amount is greater than 10 times the setting or 20 hours, don't increment.
		if ( $new > ( 10 * $window || 20 * HOUR_IN_SECONDS ) ) {
			$new = $current;
		}
		update_user_meta( wp_get_current_user()->ID, '_mt_user_init_expiration', $new );
		mt_refresh_cache();

		return $new;
	} else {
		$unique_id = mt_get_unique_id();
		if ( ! $unique_id ) {
			return false;
		}
		$current = mt_get_transient( 'mt_' . $unique_id . '_expiration' );
		$new     = (int) $current + $amount;
		// If the new amount is greater than 10 times the setting or 20 hours, don't increment.
		if ( $new > ( 10 * $window || 20 * HOUR_IN_SECONDS ) ) {
			$new = $current;
		}
		$cart = mt_get_transient( 'mt_' . $unique_id . '_cart' );
		mt_set_transient( 'mt_' . $unique_id . '_cart', $cart );
		mt_set_transient( 'mt_' . $unique_id . '_expiration', $new );
		mt_refresh_cache();

		return $new;
	}

	return false;
}

/**
 * Abstract function to delete data. Defaults to delete user's shopping cart.
 *
 * @param string $data Type of data to delete.
 * @param string $unique_id Data key to delete.
 */
function mt_delete_data( $data = 'cart', $unique_id = false ) {
	if ( 'cart' === $data ) {
		// With no arguments, this removes the current cart from inventory.
		$inventory_change = mt_get_inventory_change();
		foreach ( $inventory_change as $ticket => $change ) {
			mt_update_inventory( $change['event_id'], $ticket, $change['count'] );
		}
		mt_delete_custom_field_data();
	}
	if ( is_user_logged_in() && ! $unique_id ) {
		$current_user = wp_get_current_user();
		delete_user_meta( $current_user->ID, "_mt_user_$data" );
	}
	$unique_id = ( $unique_id ) ? $unique_id : mt_get_unique_id();
	if ( $unique_id ) {
		mt_delete_transient( 'mt_' . $unique_id . '_' . $data );
	}
	mt_refresh_cache();
}

/**
 * Delete saved custom field data if necessary. Run whenever cart data is deleted.
 */
function mt_delete_custom_field_data() {
	$custom_fields = mt_get_custom_fields( 'delete' );
	if ( empty( $custom_fields ) ) {
		// If no custom fields registered, we're done.
		return;
	} else {
		$user      = false;
		$unique_id = false;
		if ( is_user_logged_in() ) {
			$id   = wp_get_current_user()->ID;
			$user = get_user_meta( $id );
		} else {
			$unique_id = mt_get_unique_id();
		}
		$user = false;
		foreach ( $custom_fields as $name => $field ) {
			if ( $user ) {
				foreach ( $user as $key => $meta ) {
					if ( false !== stripos( $key, '_mt_user_' . $name ) ) {
						delete_user_meta( $id, $key, $meta[0] );
					}
				}
			} elseif ( $unique_id ) {
				// This will delete all stored option data for the active user.
				global $wpdb;
				$like    = '%' . $wpdb->esc_like( $unique_id ) . '%';
				$results = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM ' . $wpdb->options . ' WHERE option_name LIKE %s', $like ) );
				foreach ( $results as $result ) {
					delete_option( $result->option_name, $result->option_value );
				}
			}
		}
		mt_refresh_cache();
	}
}

/**
 * Abstract function to retrieve data for current user/public user.
 *
 * @param string       $type Type of data.
 * @param bool|integer $user_ID User ID or false if not logged in.
 * @param bool|string  $unique_id Unique ID to fetch.
 *
 * @return array|mixed
 */
function mt_get_data( $type, $user_ID = false, $unique_id = false ) {
	// Get information about a specific user.
	if ( $user_ID && ! $unique_id ) {
		$data = get_user_meta( $user_ID, "_mt_user_$type", true );
	} else {
		$expired = false;
		if ( is_user_logged_in() && ! $unique_id ) {
			$current_user = wp_get_current_user();
			$data_age     = get_user_meta( $current_user->ID, '_mt_user_init_expiration', true );
			if ( $data_age && time() > $data_age ) {
				// Expire user's cart after the data ages out.
				if ( 'cart' === $type ) {
					mt_delete_data( 'cart' );
				} else {
					delete_user_meta( $current_user->ID, "_mt_user_$type" );
				}
				$expired = true;
			}
			if ( ! $data_age && ! $expired ) {
				$expiration = mt_expiration_window();
				update_user_meta( $current_user->ID, '_mt_user_init_expiration', time() + $expiration );
			}

			$data = get_user_meta( $current_user->ID, "_mt_user_$type", true );
		} else {
			$unique_id = ( ! $unique_id ) ? mt_get_unique_id() : $unique_id;
			if ( $unique_id ) {
				$data_age = mt_get_transient( 'mt_' . $unique_id . '_expiration' );
				if ( $data_age && time() > $data_age ) {
					// Expire user's cart after the data ages out.
					if ( 'cart' === $type ) {
						mt_delete_data( 'cart' );
					} else {
						mt_delete_transient( 'mt_' . $unique_id . '_' . $type );
					}
					$expired = true;
				}
				if ( ! $data_age && ! $expired ) {
					$expiration = mt_expiration_window();
					mt_set_transient( 'mt_' . $unique_id . '_expiration', time() + $expiration );
				}

				$data = mt_get_transient( 'mt_' . $unique_id . '_' . $type );
			} else {
				$data = '[]';
			}
			if ( $data ) {
				if ( '' !== $data && ! is_numeric( $data ) && ! is_array( $data ) ) {
					// Data could be JSON and needs to be decoded.
					$decoded = json_decode( $data );
					// If it was valid JSON, use the decoded value. Otherwise, use the original.
					if ( JSON_ERROR_NONE === json_last_error() ) {
						$data = $decoded;
					}
				}
			} else {
				$data = false;
			}
		}
	}

	return $data;
}


/**
 * Get saved cart data for user.
 *
 * @param bool|int    $user_ID User ID.
 * @param bool|string $cart_id Cart identifier.
 * @param bool        $data True to get data only without taking action.
 *
 * @return array|mixed
 */
function mt_get_cart( $user_ID = false, $cart_id = false, $data = false ) {
	$cart      = array();
	$unique_id = mt_get_unique_id();
	if ( $user_ID ) {
		// Logged-in user data is saved in user meta.
		$cart = get_user_meta( $user_ID, '_mt_user_cart', true );
	} elseif ( ! $user_ID && $cart_id ) {
		// Public data is saved in transients.
		$cart = mt_get_transient( 'mt_' . $cart_id . '_cart' );
	} else {
		if ( is_user_logged_in() ) {
			$current_user = wp_get_current_user();
			$data_age     = get_user_meta( $current_user->ID, '_mt_user_init_expiration', true );
			if ( $data_age && time() > $data_age && ! $data ) {
				mt_delete_data( 'cart' );
				delete_user_meta( $current_user->ID, '_mt_user_init_expiration' );
			} else {
				$cart = get_user_meta( $current_user->ID, '_mt_user_cart', true );
			}
		} else {
			if ( $unique_id ) {
				$data_age = mt_get_transient( 'mt_' . $unique_id . '_expiration' );
				if ( $data_age && time() > $data_age && ! $data ) {
					mt_delete_data( 'cart' );
					mt_delete_transient( 'mt_' . $unique_id . '_expiration' );
				} else {
					$cart = mt_get_transient( 'mt_' . $unique_id . '_cart' );
				}
			}
		}
	}
	if ( is_user_logged_in() && ! $cart ) {
		if ( $unique_id ) {
			$cart = mt_get_transient( 'mt_' . $unique_id . '_cart' );
		}
	}

	return ( $cart ) ? $cart : array();
}

add_action( 'init', 'mt_set_user_unique_id' );
/**
 * Set a cookie with a random ID for the current user.
 *
 * Note: if sitecookiepath doesn't match the site's render location, this won't work.
 * It'll also create a secondary issue where AJAX actions read the sitecookiepath cookie.
 */
function mt_set_user_unique_id() {
	if ( ! defined( 'DOING_CRON' ) ) {
		$unique_id  = mt_get_unique_id();
		$expiration = mt_expiration_window();
		if ( ! $unique_id ) {
			$unique_id = mt_generate_unique_id();
			if ( version_compare( PHP_VERSION, '7.3.0', '>' ) ) {
				// Fix syntax.
				$options = array(
					'expires'  => time() + $expiration,
					'path'     => COOKIEPATH,
					'domain'   => COOKIE_DOMAIN,
					'secure'   => false,
					'httponly' => true,
					'samesite' => 'Lax',
				);
				setcookie( 'mt_unique_id', $unique_id, $options );
			} else {
				setcookie( 'mt_unique_id', $unique_id, time() + $expiration, COOKIEPATH, COOKIE_DOMAIN, false, true );
			}
		}
	}
}

/**
 * Generate a unique ID to track the current cart process.
 *
 * @return string
 */
function mt_generate_unique_id() {
	$length     = 32;
	$characters = '0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz-';
	$string     = '';
	for ( $p = 0; $p < $length; $p++ ) {
		$string .= $characters[ wp_rand( 0, strlen( $characters ) - 1 ) ];
	}

	return $string;
}

/**
 * Fetch a unique ID if it exists.
 *
 * @return bool|string
 */
function mt_get_unique_id() {
	$unique_id = ( isset( $_COOKIE['mt_unique_id'] ) ) ? sanitize_text_field( $_COOKIE['mt_unique_id'] ) : false;

	return $unique_id;
}

/**
 * Get cart expiration time.
 *
 * @return int Number of seconds cart will last.
 */
function mt_expiration_window() {
	$options    = mt_get_settings();
	$expiration = $options['mt_expiration'];
	// Doesn't support less than 10 minutes.
	if ( ! $expiration || (int) $expiration < 600 ) {
		$expiration = WEEK_IN_SECONDS;
	}
	$return = absint( $expiration );
	/**
	 * Filter the length of time data is stored. (Shopping carts, unique IDs).
	 *
	 * @hook mt_expiration_window
	 *
	 * @param {int}    $time Number of seconds before data will expire. Default WEEK_IN_SECONDS.
	 *
	 * @return {int}
	 */
	$expiration = apply_filters( 'mt_expiration_window', $return );

	return $expiration;
}

/**
 * Get cart expiration timestamp.
 */
function mt_get_expiration() {
	$expires_at = 0;
	if ( is_user_logged_in() ) {
		$current_user = wp_get_current_user();
		$expires_at   = get_user_meta( $current_user->ID, '_mt_user_init_expiration', true );
	} else {
		$unique_id = mt_get_unique_id();
		if ( $unique_id ) {
			$expires_at = mt_get_transient( 'mt_' . $unique_id . '_expiration' );
		}
	}

	return absint( $expires_at );
}

/**
 * Check whether a user's cart or payment info is expired at init.
 *
 * @param string $cart_id Optional parameter to check a specific cart ID.
 */
function mt_is_cart_expired( $cart_id = false ) {
	$types = mt_get_data_types();
	if ( is_user_logged_in() && ! $cart_id ) {
		$current_user = wp_get_current_user();
		$data_age     = get_user_meta( $current_user->ID, '_mt_user_init_expiration', true );
		foreach ( $types as $type ) {
			if ( time() > $data_age ) {
				// Expire user's cart after the data ages out.
				if ( 'cart' === $type ) {
					mt_delete_data( 'cart' );
				} else {
					delete_user_meta( $current_user->ID, "_mt_user_$type" );
				}
				// Since this user's data is expired, also remove their expiration window.
				delete_user_meta( $current_user->ID, '_mt_user_init_expiration' );
			}
		}
	} else {
		$unique_id = ( $cart_id ) ? $cart_id : mt_get_unique_id();
		if ( $unique_id ) {
			$expiration = mt_get_transient( 'mt_' . $unique_id . '_expiration' );
			if ( $expiration && time() > $expiration ) {
				mt_delete_transient( 'mt_' . $unique_id . '_expiration' );
				foreach ( $types as $type ) {
					mt_delete_transient( 'mt_' . $unique_id . '_' . $type );
				}
			}
		}
	}
}
add_action( 'init', 'mt_is_cart_expired' );

/**
 * Set or update a transient value for data storage.
 *
 * @param string $transient_id Option name.
 * @param mixed  $value Value to save.
 */
function mt_set_transient( $transient_id, $value ) {
	update_option( $transient_id, $value, 'no' );
}

/**
 * Get a transient value for data storage.
 *
 * @param string $transient_id Option name.
 *
 * @return mixed Option value.
 */
function mt_get_transient( $transient_id ) {
	$value = get_option( $transient_id );

	return $value;
}

/**
 * Delete a transient value for data storage.
 *
 * @param string $transient_id Option name.
 */
function mt_delete_transient( $transient_id ) {
	if ( strpos( $transient_id, '_cart' ) ) {
		// If this is a cart transient, parse out unique ID and update inventory.
		$saved            = mt_get_transient( $transient_id );
		$inventory_change = mt_get_inventory_change( array(), $saved );
		foreach ( $inventory_change as $ticket => $change ) {
			mt_update_inventory( $change['event_id'], $ticket, $change['count'] );
		}
	}
	delete_option( $transient_id );
}

/**
 * Poll transient keys. Remove any expired keys.
 *
 * Not used; may want to remove. See `mt_find_carts()`.
 */
function mt_check_transients() {
	$transients = get_option( 'mt_transient_keys', array() );
	foreach ( $transients as $key => $unique_id ) {
		$expire = mt_get_transient( 'mt_' . $unique_id . '_expiration' );
		if ( time() > $expire ) {
			// delete all transients for this unique ID.
			$types = mt_get_data_types();
			foreach ( $types as $type ) {
				mt_delete_transient( 'mt_' . $unique_id . '_' . $type );
			}
			unset( $transients[ $key ] );
		}
	}
}

/**
 * Get all standard data types. Does not fetch stored custom fields.
 *
 * @return array Types of data stored.
 */
function mt_get_data_types() {
	$types = array( 'cart', 'payment', 'offline-payment' );

	return $types;
}

/**
 * Find all options with the pattern mt_*$type.
 *
 * @param string $type Type of data to find.
 *
 * @return array
 */
function mt_find_carts( $type = 'cart' ) {
	global $wpdb;
	if ( 'cart' === $type ) {
		$results = $wpdb->get_results( 'SELECT option_name FROM ' . $wpdb->options . " WHERE option_name LIKE '%%mt_%%' AND option_name LIKE '%%_cart%%'" );
	} else {
		// Find orphaned expirations.
		$results = $wpdb->get_results( 'SELECT option_name FROM ' . $wpdb->options . " WHERE option_name LIKE '%%mt_%%' AND option_name LIKE '%%_expiration%%'" );
	}
	$keys = array();
	foreach ( $results as $result ) {
		$key   = $result->option_name;
		$parts = explode( '_', $key );
		if ( count( $parts ) === 3 ) {
			$unique_id = $parts[1];
			$keys[]    = $unique_id;
		}
	}

	return $keys;
}

/**
 * Check cart expirations for an array of IDs.
 *
 * @param array $unique_ids Array of cart IDs.
 */
function my_tickets_check_cart_expirations( $unique_ids ) {
	if ( ! empty( $unique_ids ) ) {
		foreach ( $unique_ids as $unique_id ) {
			// Check whether the cart is expired and delete it if it is.
			mt_is_cart_expired( $unique_id );
		}
	}
}