prefix . 'ea_usage_daily'; } public function retention_days() { $days = defined('EMBODIED_ASSISTANT_USAGE_RETENTION_DAYS') ? EMBODIED_ASSISTANT_USAGE_RETENTION_DAYS : 365; return max(1, min(365, (int) $days)); } public function cutoff($now = null) { return gmdate('Y-m-d', ($now ?? time()) - ($this->retention_days() - 1) * DAY_IN_SECONDS); } public function install() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta('CREATE TABLE ' . $this->table() . " ( usage_day date NOT NULL, sessions bigint unsigned NOT NULL DEFAULT 0, seconds bigint unsigned NOT NULL DEFAULT 0, PRIMARY KEY (usage_day) ) ENGINE=InnoDB " . $wpdb->get_charset_collate() . ';'); $this->cleanup(); } private function query($sql) { global $wpdb; // Private callers prepare all values and identifiers. Writes share the session transaction; // current usage totals must not come from an independently cached snapshot. // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared by every private caller; transactional accounting requires direct access. if (false === $wpdb->query($sql)) { throw new \RuntimeException('usage_storage_unavailable'); } } /** No event rows, session IDs, visitors, URLs, provider payloads or transcripts. */ public function add($day, $sessions, $seconds) { global $wpdb; if ($day < $this->cutoff() || $day > gmdate('Y-m-d')) { return; } $this->query($wpdb->prepare('INSERT INTO %i (usage_day,sessions,seconds) VALUES (%s,%d,%d) ON DUPLICATE KEY UPDATE sessions=sessions+VALUES(sessions),seconds=seconds+VALUES(seconds)', $this->table(), $day, max(0, (int) $sessions), max(0, (int) $seconds))); } /** Split elapsed time at UTC midnight; a session is counted only on its start day. */ public function add_duration($start, $end) { $start = max((int) $start, strtotime($this->cutoff() . ' UTC')); $end = min((int) $end, time()); while ($start < $end) { $next = min($end, (intdiv($start, DAY_IN_SECONDS) + 1) * DAY_IN_SECONDS); $this->add(gmdate('Y-m-d', $start), 0, $next - $start); $start = $next; } } public function cleanup() { global $wpdb; $this->query($wpdb->prepare('DELETE FROM %i WHERE usage_day<%s', $this->table(), $this->cutoff())); } public function daily() { global $wpdb; $this->cleanup(); $this->query($wpdb->prepare('SELECT usage_day,sessions,seconds FROM %i WHERE usage_day>=%s AND usage_day<=%s ORDER BY usage_day', $this->table(), $this->cutoff(), gmdate('Y-m-d'))); return array_map(static function ($row) { return (array) $row; }, $wpdb->last_result); } public function purge() { global $wpdb; $this->query($wpdb->prepare('DROP TABLE IF EXISTS %i', $this->table())); } }