db = $db; $this->connections = $connections; $this->table = Schema::tables()['profiles']; } /** @param mixed $settings */ public function create(string $name, string $type, $settings): array { $name = trim(mb_substr(sanitize_text_field($name), 0, 100)); if ($name === '') { throw new RelayError('profiles.name_required', esc_html__('Give the profile a name.', 'siterelay')); } if (!in_array($type, self::TYPES, true)) { throw new RelayError('profiles.invalid_type', esc_html__('Profiles can be saved for exports, find & replace, pulls and pushes.', 'siterelay')); } $now = gmdate('Y-m-d H:i:s'); $row = ['id' => wp_generate_uuid4(), 'name' => $name, 'type' => $type, 'settings' => (string) wp_json_encode($this->normalize($type, $settings)), 'created_at' => $now, 'updated_at' => $now]; $this->db->quietly(fn () => $this->db->wpdb()->insert($this->table, $row)); return $this->get($row['id']); } public function get(string $id): array { $rows = $this->db->results($this->db->wpdb()->prepare('SELECT * FROM ' . Db::ident($this->table) . ' WHERE id = %s', $id), ARRAY_A); if (!$rows) { throw new RelayError('profiles.not_found', esc_html__('That profile no longer exists.', 'siterelay')); } return self::row($rows[0]); } /** @return array[] by name */ public function all(): array { return array_map([self::class, 'row'], $this->db->results('SELECT * FROM ' . Db::ident($this->table) . ' ORDER BY name ASC, created_at ASC', ARRAY_A)); } public function delete(string $id): void { $this->db->quietly(fn () => $this->db->wpdb()->delete($this->table, ['id' => $id])); } /** * The settings a profile keeps, checked like a job's: tables ("all" or names), pairs (regexes validated), * GUIDs; gzip for exports; the saved connection and file parts for pulls and pushes. * * @param mixed $input */ public function normalize(string $type, $input): array { $in = is_array($input) ? $input : []; $tables = $in['tables'] ?? 'all'; if ($tables !== 'all') { $tables = array_values(array_unique(array_filter(is_array($tables) ? $tables : [], static fn ($t) => is_string($t) && preg_match('/^[A-Za-z0-9_$-]{1,64}$/', $t)))); } $pairs = []; foreach (is_array($in['pairs'] ?? null) ? $in['pairs'] : [] as $row) { if (is_array($row) && is_string($row['find'] ?? null) && $row['find'] !== '') { $pairs[] = ['find' => $row['find'], 'replace' => (string) ($row['replace'] ?? ''), 'regex' => !empty($row['regex']), 'case_sensitive' => !empty($row['case_sensitive'])]; } } PairFactory::from_settings($pairs); $out = ['tables' => $tables, 'pairs' => $pairs, 'replace_guids' => !empty($in['replace_guids'])]; if (is_multisite() && (int) ($in['subsite'] ?? 0) > 0) { $out['subsite'] = (int) $in['subsite']; // a profile opens on its subsite (spec 2026-10-16 §7.1) } if ($type === 'export') { $out['gzip'] = !empty($in['gzip']); } if ($type === 'pull' || $type === 'push') { $out['connection_id'] = (string) $this->connections->get((string) ($in['connection_id'] ?? ''))['id']; $out['files'] = FileParts::normalize($in['files'] ?? null); if ((int) ($in['remote_subsite'] ?? 0) > 0) { $out['remote_subsite'] = (int) $in['remote_subsite']; // the subsite of the network there (spec 2026-10-16 §7.1) } } return $out; } public static function to_api(array $profile): array { return $profile; } private static function row(array $r): array { return ['id' => $r['id'], 'name' => $r['name'], 'type' => $r['type'], 'settings' => json_decode((string) $r['settings'], true) ?: [], 'created_at' => $r['created_at'], 'updated_at' => $r['updated_at']]; } }