$data The decoded response. * @param string $label What to call the site in messages. */ public static function from_remote( array $data, string $label = 'the connected site' ): self { $shape = is_array( $data['shape'] ?? null ) ? $data['shape'] : []; return new self( $label, self::prefix_from( $shape, $data ), (bool) ( $shape['is_multisite'] ?? false ), self::blog_ids_from( $shape ) ); } /** * Read the shape of the site this plugin is running on. */ public static function from_local(): self { global $wpdb; $prefix = isset( $wpdb->prefix ) ? (string) $wpdb->prefix : 'wp_'; return new self( __( 'this site', 'shiftora' ), $prefix, is_multisite(), is_multisite() ? self::local_blog_ids() : [ 1 ] ); } /** * @param array $shape * @param array $data */ private static function prefix_from( array $shape, array $data ): string { foreach ( [ $shape['prefix'] ?? null, $data['prefix'] ?? null, $shape['base_prefix'] ?? null ] as $candidate ) { if ( is_string( $candidate ) && '' !== $candidate ) { return $candidate; } } // No prefix reported. Guessing `wp_` could write into the wrong tables, so an // empty prefix makes every lookup fail loudly instead. return ''; } /** * @param array $shape * @return int[] */ private static function blog_ids_from( array $shape ): array { $raw = $shape['blog_ids'] ?? []; if ( ! is_array( $raw ) ) { return []; } $ids = array_values( array_unique( array_map( 'intval', $raw ) ) ); sort( $ids ); return $ids; } /** @return int[] */ private static function local_blog_ids(): array { if ( ! function_exists( 'get_sites' ) ) { return [ 1 ]; } $sites = get_sites( [ 'number' => 0, 'fields' => 'ids', 'no_found_rows' => true, ] ); return is_array( $sites ) ? array_values( array_map( 'intval', $sites ) ) : [ 1 ]; } /** * The full table name for one blog's table: base prefix plus name for a * single site, base prefix, blog id, underscore and name on multisite. * * @param string $bare Table name without any prefix, e.g. `term_relationships`. * @param int $blog_id Blog the table belongs to. */ public function table( string $bare, int $blog_id = 1 ): string { if ( 1 === $blog_id || ! $this->is_multisite ) { return $this->base_prefix . $bare; } return $this->base_prefix . $blog_id . '_' . $bare; } /** * The bare name behind a full table name, for this site's prefix. * * Splitting on the last underscore would turn `wp_term_relationships` into * `relationships`, so the prefix is stripped whole and the remainder split only * once, at a blog id. * * @return string Empty when the table is not this site's. */ public function bare( string $full_table ): string { if ( '' === $this->base_prefix || ! str_starts_with( $full_table, $this->base_prefix ) ) { return ''; } $remainder = substr( $full_table, strlen( $this->base_prefix ) ); // A leading blog id and underscore, and only that: the underscore in // `term_relationships` is part of the name, not a separator. if ( preg_match( '/^(\d+)_(.+)$/', $remainder, $matches ) ) { return $matches[2]; } return $remainder; } /** * Which blog a table belongs to, and its bare name. * * @return array{blog_id:int,bare:string}|null Null when the table is not this site's. */ public function locate( string $full_table ): ?array { $bare = $this->bare( $full_table ); if ( '' === $bare ) { return null; } if ( preg_match( '/^(\d+)_(.+)$/', substr( $full_table, strlen( $this->base_prefix ) ), $matches ) ) { return [ 'blog_id' => (int) $matches[1], 'bare' => $matches[2] ]; } return [ 'blog_id' => 1, 'bare' => $bare ]; } /** * Every table this site has, as bare names, grouped by blog. * * @return array */ public function tables_by_blog(): array { global $wpdb; if ( ! isset( $wpdb->prefix ) ) { return []; } $like = $wpdb->esc_like( $this->base_prefix ) . ( $this->is_multisite ? '%' : '' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- server metadata, nothing to cache $names = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $like ) ); $grouped = []; foreach ( (array) $names as $full_table ) { $where = $this->locate( (string) $full_table ); if ( null === $where ) { continue; } $grouped[ $where['blog_id'] ][] = $where['bare']; } foreach ( $grouped as $blog_id => $bare_names ) { sort( $grouped[ $blog_id ] ); } ksort( $grouped ); return $grouped; } /** * The tables that exist once for the whole network. * * `blogs` is where the destination reads its subsites, so it is written before the * tables those subsites need. `users` and `usermeta` are here because there is no * `wp_2_users`: every site shares one set of accounts. Not every install has all * of them, so this is the set to look for. * * @return string[] */ public function network_tables(): array { if ( ! $this->is_multisite ) { return []; } return [ 'blogs', 'blogmeta', 'blog_versions', 'site', 'sitemeta', 'signups', 'registration_log', 'users', 'usermeta', ]; } /** * Whether a bare table name is one every blog keeps. Not every install has all * of them, so this is the set to look for, not the set always present. * * @return string[] */ public function per_blog_tables(): array { return [ 'posts', 'postmeta', 'comments', 'commentmeta', 'terms', 'termmeta', 'term_taxonomy', 'term_relationships', 'linkmeta', 'options', 'logmeta', ]; } /** * A description for a message, naming the site rather than saying "the source". */ public function describe(): string { return $this->is_multisite /* translators: 1: site label, e.g. "the connected site", 2: number of sites */ ? sprintf( __( '%1$s (a network of %2$d sites)', 'shiftora' ), $this->label, count( $this->blog_ids ) ) : $this->label; } }