db = $db; $this->table = $table; } public static function table(Db $db, string $tag): string { return Naming::backup($db->prefix() . 'userlog', $tag); } /** A fresh journal for the job tagged $tag (an older one of the same job is replaced). */ public static function create(Db $db, string $tag): self { $table = self::table($db, $tag); $db->raw('DROP TABLE IF EXISTS ' . Db::ident($table)); $db->raw('CREATE TABLE ' . Db::ident($table) . ' ( id bigint unsigned NOT NULL AUTO_INCREMENT, action varchar(10) NOT NULL, user_id bigint unsigned NOT NULL DEFAULT 0, umeta_id bigint unsigned NOT NULL DEFAULT 0, meta_key varchar(255) NULL, meta_value longtext NULL, PRIMARY KEY (id) ) ' . $db->wpdb()->get_charset_collate()); return new self($db, $table); } public static function open(Db $db, string $table): self { return new self($db, $table); } public function name(): string { return $this->table; } /** An account this job created; undo deletes it (unless it has a role on another subsite by then). */ public function created_user(int $user_id): void { $this->log('user', $user_id, 0, null, null); } public function added_meta(int $umeta_id, int $user_id): void { $this->log('added', $user_id, $umeta_id, null, null); } public function changed_meta(int $umeta_id, int $user_id, string $key, string $old): void { $this->log('changed', $user_id, $umeta_id, $key, $old); } public function removed_meta(int $umeta_id, int $user_id, string $key, string $value): void { $this->log('removed', $user_id, $umeta_id, $key, $value); } /** Written in the same transaction as the merge, with its summary: the merge is complete exactly when this row exists. */ public function finished(array $summary): void { $this->log('done', 0, 0, null, (string) wp_json_encode($summary)); } /** The summary of a complete merge, or null when the merge did not finish. */ public static function summary(Db $db, string $table): ?array { if (!$db->table_exists($table)) { return null; } $raw = $db->var('SELECT meta_value FROM ' . Db::ident($table) . " WHERE action = 'done' LIMIT 1"); return is_string($raw) ? (array) json_decode($raw, true) : null; } /** * Undo, newest change first. An account the job created is deleted with its meta, unless it meanwhile got a * role on a subsite other than $scope's: then only what this job added goes. The journal is dropped. Safe to run * again on a journal a dying request had partly applied: every change is undone at most once. */ public function revert(Scope $scope): void { if (!$this->db->table_exists($this->table)) { return; } $wpdb = $this->db->wpdb(); $touched = []; foreach ($this->db->results('SELECT * FROM ' . Db::ident($this->table) . ' ORDER BY id DESC') as $r) { $touched[(int) $r->user_id] = true; switch ($r->action) { case 'added': $this->db->query($wpdb->prepare("DELETE FROM {$wpdb->usermeta} WHERE umeta_id = %d", (int) $r->umeta_id)); break; case 'changed': $this->db->query($wpdb->prepare("UPDATE {$wpdb->usermeta} SET meta_value = %s WHERE umeta_id = %d", (string) $r->meta_value, (int) $r->umeta_id)); break; case 'removed': $this->db->query($wpdb->prepare("INSERT IGNORE INTO {$wpdb->usermeta} (umeta_id, user_id, meta_key, meta_value) VALUES (%d, %d, %s, %s)", (int) $r->umeta_id, (int) $r->user_id, (string) $r->meta_key, (string) $r->meta_value)); break; case 'user': $this->delete_user((int) $r->user_id, $scope); break; } } foreach (array_keys($touched) as $id) { clean_user_cache($id); } $this->db->raw('DROP TABLE IF EXISTS ' . Db::ident($this->table)); } private function delete_user(int $user_id, Scope $scope): void { $wpdb = $this->db->wpdb(); foreach ((array) $wpdb->get_col($wpdb->prepare("SELECT meta_key FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key LIKE %s", $user_id, '%' . $wpdb->esc_like('capabilities'))) as $key) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off work while a site joins a network; nothing to cache if ($key !== $scope->prefix . 'capabilities') { return; // a role elsewhere on the network: the account stays } } $this->db->query($wpdb->prepare("DELETE FROM {$wpdb->usermeta} WHERE user_id = %d", $user_id)); $this->db->query($wpdb->prepare("DELETE FROM {$wpdb->users} WHERE ID = %d", $user_id)); } private function log(string $action, int $user_id, int $umeta_id, ?string $key, ?string $value): void { $this->db->insert($this->table, ['action' => $action, 'user_id' => $user_id, 'umeta_id' => $umeta_id, 'meta_key' => $key, 'meta_value' => $value]); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Relay's own journal table, not WordPress' meta tables } }