affiliateId = trim( $affiliateId ); } /** * Configured Mercari Ambassador affiliate id. */ public function affiliateId(): string { return $this->affiliateId; } /** * Stable §4 provider key. */ public function key(): string { return self::PROVIDER_KEY; } /** * Human-readable display name. */ public function label(): string { return self::LABEL; } /** * Mercari is used as a search-link target, not a normalized API provider. */ public function capabilities(): ProviderCapabilities { return new ProviderCapabilities( true, false, false, false, false, false, false, false, true, false, false ); } /** * Report whether the named feature is supported. * * @param string $feature Feature token. * * @throws InvalidArgumentException When `$feature` is out of set. */ public function supportsFeature( string $feature ): bool { return $this->capabilities()->supportsFeature( $feature ); } /** * Mercari link target is deterministic and does not need an API health probe. */ public function healthCheck(): ProviderHealthStatus { return ProviderHealthStatus::healthy( new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ) ); } /** * Return a synthetic Mercari search-link result. * * Mercari has no supported public item-search API for normalized product * rows. The editor search still needs a saveable target, so one deterministic * result points to the Mercari keyword search page. * * @param SearchQuery $query Normalized search query. */ public function searchProducts( SearchQuery $query ): SearchResult { $title = 'メルカリで「' . $query->keyword() . '」を検索'; $url = $this->buildSearchUrl( $query->keyword() ); $now = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); $id = 'search-' . substr( hash( 'sha256', $query->keyword() ), 0, 16 ); $snapshot = new ProductSnapshot( self::PROVIDER_KEY, new ProviderProductId( self::PROVIDER_KEY, $id ), $title, self::LABEL, 'Mercari の検索結果ページへ移動します。商品ごとの価格・在庫はリンク先で確認してください。', $url, $url, null, array(), null, AvailabilityInfo::unknown(), null, null, array( self::LABEL ), array( 'search_query' => $query->keyword() ), $now, array( 'synthetic_search_link' => true ) ); return new SearchResult( self::PROVIDER_KEY, array( $snapshot ), $query->page(), $query->perPage(), 1, false, array( 'mode' => 'search_link' ) ); } /** * Stamp a Mercari URL with the Ambassador `afid` when configured. * * @param ProductSnapshot $snapshot Snapshot to link. * @param AffiliateContext $context Per-request context. */ public function buildAffiliateLink( ProductSnapshot $snapshot, AffiliateContext $context ): AffiliateLinkResult { unset( $context ); $url = $this->buildAffiliateUrl( $snapshot->detailUrl() ); if ( $url === $snapshot->detailUrl() ) { return AffiliateLinkResult::passthrough( $url ); } return new AffiliateLinkResult( $url, array( 'nofollow', 'sponsored', 'noopener', 'noreferrer' ), '_blank', true, array() ); } /** * Build a Mercari search URL and stamp `afid` when configured. * * @param string $query Search query. */ public function buildSearchUrl( string $query ): string { $url = 'https://jp.mercari.com/search?keyword=' . rawurlencode( $query ); return $this->buildAffiliateUrl( $url ); } /** * Stamp the supplied Mercari URL with `afid` when configured. * * @param string $url Mercari URL. */ public function buildAffiliateUrl( string $url ): string { if ( $this->affiliateId === '' ) { return $url; } // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Provider helper is loadable in pure-unit tests without WordPress. $parts = parse_url( $url ); if ( ! is_array( $parts ) ) { return $url; } $host = isset( $parts['host'] ) ? strtolower( (string) $parts['host'] ) : ''; if ( $host !== 'jp.mercari.com' ) { return $url; } $query = array(); if ( isset( $parts['query'] ) && is_string( $parts['query'] ) ) { parse_str( $parts['query'], $query ); } $query = self::orderAmbassadorQuery( $query ); $query['afid'] = $this->affiliateId; $scheme = isset( $parts['scheme'] ) ? (string) $parts['scheme'] : 'https'; $path = isset( $parts['path'] ) ? (string) $parts['path'] : ''; $built = $scheme . '://' . $host . $path . '?' . http_build_query( $query, '', '&', PHP_QUERY_RFC3986 ); if ( isset( $parts['fragment'] ) && is_string( $parts['fragment'] ) && $parts['fragment'] !== '' ) { $built .= '#' . $parts['fragment']; } return $built; } /** * Reorder known Mercari ad parameters to match Ambassador-generated markup. * * @param array $query Query map. * * @return array */ private static function orderAmbassadorQuery( array $query ): array { $ordered = array(); foreach ( array( 'ad_auction_id', 'ad_auction_unit_id', 'ad_id' ) as $key ) { if ( array_key_exists( $key, $query ) ) { $ordered[ $key ] = $query[ $key ]; unset( $query[ $key ] ); } } foreach ( $query as $key => $value ) { $ordered[ $key ] = $value; } return $ordered; } /** * Build the Mercari Ambassador image URL for a product detail URL. * * @param string $url Mercari product URL. */ public static function ambassadorImageUrlFor( string $url ): ?string { // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Provider helper is loadable in pure-unit tests without WordPress. $parts = parse_url( $url ); if ( ! is_array( $parts ) || ! isset( $parts['path'] ) || ! is_string( $parts['path'] ) ) { return null; } $path = $parts['path']; if ( preg_match( '~/shops/product/([^/?#]+)~', $path, $matches ) === 1 ) { return self::AMBASSADOR_IMAGE_ENDPOINT . '?id=' . rawurlencode( $matches[1] ) . '&svc=ms'; } if ( preg_match( '~/item/([^/?#]+)~', $path, $matches ) === 1 ) { return self::AMBASSADOR_IMAGE_ENDPOINT . '?id=' . rawurlencode( $matches[1] ) . '&svc=m'; } return null; } }