1 → AMBIGUOUS. * c. Fallback: exact-title query. 1 match → UNVERIFIED; >1 → AMBIGUOUS; 0 → DROPPED. * 3. Anything not MATCHED/UNVERIFIED is dropped (empty value) and recorded * in the warning report so the editor sees what to fix. * * @package MultiSitePostCloner * @license GPL-2.0-or-later */ namespace MSPC; if ( ! defined( 'ABSPATH' ) ) { exit; } class Reference_Remapper { /** @var Warning_Report */ private $report; /** @var array source_id => meta */ private $captures = array(); /** @var array source_id => new target ID (0 = unmatched) */ private $resolved = array(); /** @var array source_id => status (matched|unverified|ambiguous|dropped) */ private $status = array(); public function __construct( Warning_Report $report ) { $this->report = $report; } /** * Capture pre-switch metadata for a single source ID. * * @param int $source_id */ public function capture( $source_id ) { $source_id = (int) $source_id; if ( ! $source_id || isset( $this->captures[ $source_id ] ) ) { return; } $post = get_post( $source_id ); if ( ! $post ) { return; } $this->captures[ $source_id ] = array( 'slug' => (string) $post->post_name, 'post_type' => (string) $post->post_type, 'title' => (string) $post->post_title, ); } /** * Resolve a single ID against the *current* (target) blog. MUST be called * after switch_to_blog. Returns new ID or 0 if unmatched. Records to the * warning report on any non-clean outcome. * * @param int $source_id * @param array $field ACF field def (for label). * @return int */ public function resolve_id( $source_id, array $field ) { $source_id = (int) $source_id; if ( ! $source_id ) { return 0; } if ( array_key_exists( $source_id, $this->resolved ) ) { return $this->resolved[ $source_id ]; } if ( ! isset( $this->captures[ $source_id ] ) ) { $this->resolved[ $source_id ] = 0; $this->status[ $source_id ] = 'dropped'; $this->report->add_reference( $this->field_label( $field ), isset( $field['key'] ) ? (string) $field['key'] : '', 'dropped', sprintf( '#%d', $source_id ), '' ); return 0; } $cap = $this->captures[ $source_id ]; list( $new_id, $status ) = $this->resolve_against_target( $cap['slug'], $cap['post_type'], $cap['title'] ); $this->resolved[ $source_id ] = $new_id; $this->status[ $source_id ] = $status; if ( 'matched' !== $status ) { $this->report->add_reference( $this->field_label( $field ), isset( $field['key'] ) ? (string) $field['key'] : '', $status, $cap['title'], $cap['post_type'] ); } if ( 'ambiguous' === $status || 'dropped' === $status ) { return 0; } return (int) $new_id; } /** * Resolve a relationship array — element-by-element, mixed outcomes allowed. * * @param int[] $source_ids * @param array $field * @return int[] */ public function resolve_relationship( array $source_ids, array $field ) { $out = array(); foreach ( $source_ids as $sid ) { $nid = $this->resolve_id( (int) $sid, $field ); if ( $nid ) { $out[] = $nid; } } return $out; } /** * page_link can store either a numeric post ID OR a URL string. URLs that * resolved to a source-side ID at capture time go through the normal ID * resolution; URLs that didn't (external links) pass through unchanged. * * @param mixed $value Single value (int|string). * @param array $field * @return mixed */ public function resolve_page_link_single( $value, array $field ) { if ( is_numeric( $value ) ) { $new_id = $this->resolve_id( (int) $value, $field ); return $new_id ? $new_id : ''; } // Otherwise it's a URL (external or unresolvable). Pass through. return $value; } /** * @param array $field * @return string */ private function field_label( array $field ) { if ( ! empty( $field['label'] ) ) { return (string) $field['label']; } if ( ! empty( $field['name'] ) ) { return (string) $field['name']; } return ''; } /** * Public lookup helper used by callers that need slug-then-title resolution * without writing to the warning report (eg. post_parent). Returns the new * ID (or 0) and status string. * * @param string $slug * @param string $post_type * @param string $title * @return array{0:int,1:string} */ public function lookup_on_target( $slug, $post_type, $title ) { return $this->resolve_against_target( $slug, $post_type, $title ); } /** * @return array{0:int,1:string} [ new_id, status ] */ private function resolve_against_target( $slug, $post_type, $title ) { if ( '' !== $slug && '' !== $post_type ) { $by_slug = get_page_by_path( $slug, OBJECT, $post_type ); if ( $by_slug instanceof \WP_Post ) { // Confirm uniqueness: WP_Query for posts with this name + type, posts_per_page=2. $dupes = new \WP_Query( array( 'post_type' => $post_type, 'name' => $slug, 'posts_per_page' => 2, 'post_status' => 'any', 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); if ( count( $dupes->posts ) === 1 ) { return array( (int) $by_slug->ID, 'matched' ); } if ( count( $dupes->posts ) > 1 ) { return array( 0, 'ambiguous' ); } } } if ( '' !== $title && '' !== $post_type ) { $by_title = new \WP_Query( array( 'post_type' => $post_type, 'title' => $title, 'posts_per_page' => 2, 'post_status' => 'any', 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); $found = $by_title->posts; if ( count( $found ) === 1 ) { return array( (int) $found[0], 'unverified' ); } if ( count( $found ) > 1 ) { return array( 0, 'ambiguous' ); } } return array( 0, 'dropped' ); } /** * Pre-switch helper: if a page_link value is a URL on the source blog, * resolve it to a local post ID so it can be captured & remapped normally. * * @param mixed $value * @return int 0 if the URL is external / unresolvable. */ public function source_page_link_to_id( $value ) { if ( is_numeric( $value ) ) { return (int) $value; } if ( is_string( $value ) && '' !== $value ) { $id = url_to_postid( $value ); return (int) $id; } return 0; } }