HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.32
Disabled: pcntl_fork
Upload Files
File: /wordpress/plugins/wp-cloud-client/1.0.1/src/Mail/MailTable.php
<?php

declare(strict_types=1);

namespace VPlugins\WPCloudClient\Mail;

/**
 * Manages the wpcc_mails database table.
 *
 * Schema:
 *   eid             bigint(20) unsigned  – microtime-based unique ID
 *   created         datetime             – insertion timestamp (UTC)
 *   notified        datetime             – when Portal was last notified (nullable)
 *   content         longtext             – JSON-encoded email envelope
 *   notify_attempts tinyint(3) unsigned  – consecutive notification failures (v2)
 */
final class MailTable {

	private const TABLE             = 'wpcc_mails';
	private const DB_VERSION_OPTION = 'wpcc_mail_db_version';
	private const DB_VERSION        = 2;

	private static ?self $instance = null;

	/**
	 * Private constructor — use getInstance().
	 */
	private function __construct() {}

	/**
	 * Return the singleton instance.
	 */
	public static function getInstance(): self {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Return the full prefixed table name.
	 */
	public function tableName(): string {
		global $wpdb;
		return $wpdb->prefix . self::TABLE;
	}

	/**
	 * Insert a blank row and return its eid.
	 *
	 * @param string $content JSON-encoded email envelope.
	 */
	public function register( string $content = '' ): int {
		global $wpdb;

		// Microtime-based ID (matches wsp-bootstrapper approach).
		$eid = (int) ( microtime( true ) * 10000 );

		$wpdb->insert(
			$this->tableName(),
			[
				'eid'     => $eid,
				'created' => current_time( 'mysql', true ),
				'content' => $content,
			],
			[ '%d', '%s', '%s' ]
		);

		return $eid;
	}

	/**
	 * Update the content of an existing row.
	 *
	 * @param int    $id      Row eid.
	 * @param string $content JSON-encoded email envelope.
	 */
	public function envelop( int $id, string $content ): void {
		global $wpdb;

		$wpdb->update(
			$this->tableName(),
			[ 'content' => $content ],
			[ 'eid' => $id ],
			[ '%s' ],
			[ '%d' ]
		);
	}

	/**
	 * Retrieve and JSON-decode an email envelope.
	 *
	 * @param int $id Row eid.
	 * @return array<string, mixed>|null
	 */
	public function get( int $id ): ?array {
		global $wpdb;

		$table = $this->tableName();
		$row   = $wpdb->get_row(
			$wpdb->prepare( "SELECT * FROM `{$table}` WHERE eid = %d", $id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			ARRAY_A
		);

		if ( null === $row ) {
			return null;
		}

		$content = json_decode( (string) $row['content'], true );

		return array_merge( $row, [ 'content' => is_array( $content ) ? $content : [] ] );
	}

	/**
	 * Delete a row by eid.
	 *
	 * @param int $id Row eid.
	 */
	public function remove( int $id ): void {
		global $wpdb;

		$wpdb->delete( $this->tableName(), [ 'eid' => $id ], [ '%d' ] );
	}

	/**
	 * Stamp the notified datetime for an email.
	 *
	 * @param int $id Row eid.
	 */
	public function markNotified( int $id ): void {
		global $wpdb;

		$wpdb->update(
			$this->tableName(),
			[ 'notified' => current_time( 'mysql', true ) ],
			[ 'eid' => $id ],
			[ '%s' ],
			[ '%d' ]
		);
	}

	/**
	 * Increment the notify_attempts counter for a set of rows.
	 *
	 * @param int[] $ids Row eids to increment.
	 */
	public function incrementAttempts( array $ids ): void {
		if ( empty( $ids ) ) {
			return;
		}

		global $wpdb;

		$table        = $this->tableName();
		$placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );

		$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
				"UPDATE `{$table}` SET notify_attempts = notify_attempts + 1 WHERE eid IN ({$placeholders})",
				...$ids
			)
		);
	}

	/**
	 * Retrieve emails that have not yet been notified to Portal,
	 * excluding rows that have hit or exceeded the attempt ceiling.
	 *
	 * @param int $limit       Maximum rows to return; 0 means no limit.
	 * @param int $maxAttempts Rows with notify_attempts >= this value are skipped.
	 * @return list<array<string, mixed>>
	 */
	public function listUnnotified( int $limit = 0, int $maxAttempts = PHP_INT_MAX ): array {
		global $wpdb;

		$table = $this->tableName();

		if ( $limit > 0 ) {
			$sql = $wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT * FROM `{$table}` WHERE notified IS NULL AND notify_attempts < %d ORDER BY created ASC LIMIT %d",
				$maxAttempts,
				$limit
			);
		} else {
			$sql = $wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT * FROM `{$table}` WHERE notified IS NULL AND notify_attempts < %d ORDER BY created ASC",
				$maxAttempts
			);
		}

		$rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		if ( ! is_array( $rows ) ) {
			return [];
		}

		return array_map(
			static function ( array $row ): array {
				$content = json_decode( (string) $row['content'], true );
				return array_merge( $row, [ 'content' => is_array( $content ) ? $content : [] ] );
			},
			$rows
		);
	}

	/**
	 * Create or upgrade the table schema.
	 */
	public function initialize(): void {
		if ( (int) get_option( self::DB_VERSION_OPTION, 0 ) >= self::DB_VERSION ) {
			return;
		}

		global $wpdb;

		$table   = $this->tableName();
		$charset = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE `{$table}` (
			eid bigint(20) unsigned NOT NULL,
			created datetime NOT NULL,
			notified datetime DEFAULT NULL,
			content longtext NOT NULL,
			notify_attempts tinyint(3) unsigned NOT NULL DEFAULT 0,
			PRIMARY KEY (eid),
			KEY eid (eid)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );

		update_option( self::DB_VERSION_OPTION, self::DB_VERSION, false );
	}

	/**
	 * Drop the table entirely (called on plugin uninstall).
	 */
	public function drop(): void {
		global $wpdb;

		$table = $this->tableName();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
		delete_option( self::DB_VERSION_OPTION );
	}
}