self::MAX_RECORDS ) { $list = array_slice( $list, -1 * self::MAX_RECORDS ); } return $list; } /** * Normalize one partner row. * * @param mixed $partner Raw row. * @return array|null */ private static function normalize_partner_row( $partner ) { if ( ! is_array( $partner ) ) { return null; } $url = isset( $partner['url'] ) ? self::canonicalize_site_url( (string) $partner['url'] ) : ''; if ( '' === $url ) { return null; } $row = $partner; $row['url'] = $url; $status = isset( $row['status'] ) ? sanitize_key( (string) $row['status'] ) : 'pending'; if ( '' === $status ) { $status = 'pending'; } $row['status'] = $status; $row['last_verified'] = isset( $row['last_verified'] ) ? sanitize_text_field( (string) $row['last_verified'] ) : ''; $row['error_detail'] = isset( $row['error_detail'] ) ? sanitize_text_field( (string) $row['error_detail'] ) : ''; $row['partner_name'] = isset( $row['partner_name'] ) ? sanitize_text_field( (string) $row['partner_name'] ) : ''; $row['partner_public_id'] = isset( $row['partner_public_id'] ) ? sanitize_text_field( (string) $row['partner_public_id'] ) : ''; $row['partner_type'] = ( isset( $row['partner_type'] ) && 'Person' === sanitize_text_field( (string) $row['partner_type'] ) ) ? 'Person' : 'Organization'; $row['my_relation_to_partner'] = isset( $row['my_relation_to_partner'] ) ? self::sanitize_relation_type( $row['my_relation_to_partner'] ) : 'collaboration'; $row['partner_relation_to_me'] = isset( $row['partner_relation_to_me'] ) ? self::sanitize_relation_type( $row['partner_relation_to_me'] ) : 'collaboration'; $row['relation_note'] = isset( $row['relation_note'] ) ? substr( sanitize_textarea_field( (string) $row['relation_note'] ), 0, 240 ) : ''; $row['partner_claim'] = isset( $row['partner_claim'] ) ? substr( sanitize_textarea_field( (string) $row['partner_claim'] ), 0, 240 ) : ''; $row['request_id'] = isset( $row['request_id'] ) ? sanitize_text_field( (string) $row['request_id'] ) : ''; $row['callback_token'] = isset( $row['callback_token'] ) ? sanitize_text_field( (string) $row['callback_token'] ) : ''; $row['sync_state'] = isset( $row['sync_state'] ) ? sanitize_key( (string) $row['sync_state'] ) : ''; $row['sync_retry_count'] = isset( $row['sync_retry_count'] ) ? max( 0, intval( $row['sync_retry_count'] ) ) : 0; $row['sync_last_failed_at'] = isset( $row['sync_last_failed_at'] ) ? sanitize_text_field( (string) $row['sync_last_failed_at'] ) : ''; $row['sync_last_error'] = isset( $row['sync_last_error'] ) ? sanitize_text_field( (string) $row['sync_last_error'] ) : ''; $row['sync_last_retry_at'] = isset( $row['sync_last_retry_at'] ) ? sanitize_text_field( (string) $row['sync_last_retry_at'] ) : ''; $row['sync_next_retry_at'] = isset( $row['sync_next_retry_at'] ) ? sanitize_text_field( (string) $row['sync_next_retry_at'] ) : ''; $row['sync_next_retry_ts'] = isset( $row['sync_next_retry_ts'] ) ? max( 0, intval( $row['sync_next_retry_ts'] ) ) : 0; $row['callback_url'] = isset( $row['callback_url'] ) ? esc_url_raw( (string) $row['callback_url'] ) : ''; return $row; } /** * Canonicalize a site URL for storage. * * @param string $url Raw URL. * @return string */ private static function canonicalize_site_url( $url ) { $url = esc_url_raw( (string) $url ); if ( '' === $url ) { return ''; } $parts = wp_parse_url( $url ); if ( ! is_array( $parts ) || empty( $parts['host'] ) ) { return ''; } $scheme = isset( $parts['scheme'] ) ? strtolower( (string) $parts['scheme'] ) : 'https'; $host = strtolower( (string) $parts['host'] ); $port = isset( $parts['port'] ) ? intval( $parts['port'] ) : 0; $path = isset( $parts['path'] ) ? (string) $parts['path'] : ''; if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) { return ''; } $path = preg_replace( '#/+#', '/', $path ); $path = rtrim( (string) $path, '/' ); $canonical = $scheme . '://' . $host; if ( $port > 0 ) { $canonical .= ':' . $port; } if ( '' !== $path ) { $canonical .= $path; } return $canonical; } /** * Normalize URL/domain to domain key. * * @param string $url URL or domain. * @return string */ private static function normalize_domain( $url ) { $parsed = wp_parse_url( (string) $url ); $host = isset( $parsed['host'] ) ? strtolower( (string) $parsed['host'] ) : strtolower( (string) $url ); $host = preg_replace( '/^www\./', '', $host ); return sanitize_text_field( (string) $host ); } /** * Sanitize relation type to allowed set. * * @param string $value Raw relation value. * @return string */ private static function sanitize_relation_type( $value ) { $value = sanitize_key( (string) $value ); if ( in_array( $value, self::RELATION_TYPES, true ) ) { return $value; } return 'collaboration'; } /** * Read legacy partner list from Carbon option (if available). * * @return array */ private static function read_legacy_list() { if ( function_exists( 'carbon_get_theme_option' ) ) { $legacy = carbon_get_theme_option( self::LEGACY_THEME_OPTION_KEY ); if ( is_array( $legacy ) ) { return $legacy; } } return array(); } /** * Mirror canonical list to legacy Carbon option for UI compatibility. * * @param array $partners Canonical partner list. * @return void */ private static function mirror_to_legacy( $partners ) { if ( ! function_exists( 'carbon_set_theme_option' ) ) { return; } try { carbon_set_theme_option( self::LEGACY_THEME_OPTION_KEY, $partners ); } catch ( \Throwable $e ) { // Keep canonical store authoritative even if legacy mirror fails. } } /** * Check list equality in deterministic JSON form. * * @param array $left Left list. * @param array $right Right list. * @return bool */ private static function lists_equal( $left, $right ) { $left_json = function_exists( 'wp_json_encode' ) ? wp_json_encode( $left ) : json_encode( $left ); $right_json = function_exists( 'wp_json_encode' ) ? wp_json_encode( $right ) : json_encode( $right ); return (string) $left_json === (string) $right_json; } }