HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.32
Disabled: pcntl_fork
Upload Files
File: /wordpress/plugins/wp-cloud-client/1.1.2/src/BusinessProfile/FieldRenderer.php
<?php

declare(strict_types=1);

namespace VPlugins\WPCloudClient\BusinessProfile;

/**
 * Pure rendering logic for business profile fields.
 */
final class FieldRenderer {

	private const ADDRESS_COMPONENTS = [ 'address', 'city', 'state', 'zip', 'country' ];

	private const SOCIAL_PLATFORMS = [
		'facebook_url'   => 'facebook',
		'twitter_url'    => 'twitter',
		'instagram_url'  => 'instagram',
		'linkedin_url'   => 'linkedin',
		'pinterest_url'  => 'pinterest',
		'foursquare_url' => 'foursquare',
		'youtube_url'    => 'youtube',
		'rss_url'        => 'rss',
	];

	/**
	 * Render a single profile field by attribute name.
	 *
	 * @param array<string, mixed> $profile The business profile data.
	 * @param string               $attr    The attribute key to render.
	 * @return string Rendered HTML output.
	 */
	public function renderField( array $profile, string $attr ): string {
		if ( 'full_address' === $attr ) {
			return $this->renderFullAddress( $profile );
		}

		if ( 'images' === $attr || 'logo' === $attr ) {
			return $this->renderLogo( $profile );
		}

		if ( 'primary' === $attr ) {
			return $this->renderPrimary( $profile );
		}

		if ( 'services_offered' === $attr || 'services' === $attr ) {
			return $this->renderServices( $profile );
		}

		if ( 'hours_of_operation' === $attr ) {
			return $this->renderHoursOfOperation( $profile );
		}

		if ( 'booking_url' === $attr ) {
			return $this->renderBookingUrl( $profile );
		}

		if ( isset( self::SOCIAL_PLATFORMS[ $attr ] ) && ! empty( $profile[ $attr ] ) ) {
			return $this->renderSocialIcon( $attr, (string) $profile[ $attr ] );
		}

		if ( ! array_key_exists( $attr, $profile ) ) {
			return '';
		}

		$value = $profile[ $attr ];

		if ( is_array( $value ) ) {
			return esc_html( implode( ', ', array_map( 'strval', $value ) ) );
		}

		$stringValue = (string) $value;

		if ( $this->isUrl( $stringValue ) ) {
			return sprintf(
				'<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
				esc_url( $stringValue ),
				esc_html( $stringValue ),
			);
		}

		return esc_html( $stringValue );
	}

	/**
	 * Render the full address as a comma-separated string.
	 *
	 * @param array<string, mixed> $profile The business profile data.
	 * @return string Rendered address or empty string.
	 */
	public function renderFullAddress( array $profile ): string {
		$parts = [];

		foreach ( self::ADDRESS_COMPONENTS as $component ) {
			if ( ! empty( $profile[ $component ] ) ) {
				$parts[] = esc_html( (string) $profile[ $component ] );
			}
		}

		if ( empty( $parts ) ) {
			return '';
		}

		return implode( ', ', $parts );
	}

	/**
	 * Render the full address as an HTML list.
	 *
	 * @param array<string, mixed> $profile The business profile data.
	 * @param string               $prefix  CSS class prefix for styling.
	 * @return string Rendered HTML list.
	 */
	public function renderFullAddressList( array $profile, string $prefix = 'wpcc' ): string {
		$address = trim( (string) ( $profile['address'] ?? '' ) );
		$city    = trim( (string) ( $profile['city'] ?? '' ) );
		$state   = trim( (string) ( $profile['state'] ?? '' ) );
		$zip     = trim( (string) ( $profile['zip'] ?? '' ) );
		$country = trim( (string) ( $profile['country'] ?? '' ) );

		$items = [];

		if ( '' !== $address ) {
			$items[] = sprintf( '<li>%s</li>', esc_html( $address ) );
		}

		$cityLine = implode( ', ', array_filter( [ $city, trim( $state . ' ' . $zip ) ] ) );
		if ( '' !== $cityLine ) {
			$items[] = sprintf( '<li>%s</li>', esc_html( $cityLine ) );
		}

		if ( '' !== $country ) {
			$items[] = sprintf( '<li>%s</li>', esc_html( $country ) );
		}

		if ( empty( $items ) ) {
			return '';
		}

		return sprintf(
			'<ul class="%s-full-address" style="padding-left: 0; list-style: none;">%s</ul>',
			esc_attr( $prefix ),
			implode( '', $items ),
		);
	}

	/**
	 * Render the business logo image.
	 *
	 * @param array<string, mixed> $profile The business profile data.
	 * @return string Rendered HTML img tag.
	 */
	public function renderLogo( array $profile ): string {
		$src = $profile['images']['logo'] ?? '';

		if ( empty( $src ) ) {
			return '';
		}

		return sprintf(
			'<img src="%s" alt="%s" style="width: 100px; height: 100px;" />',
			esc_url( (string) $src ),
			esc_attr__( 'Business logo', 'wp-cloud-client' ),
		);
	}

	/**
	 * Render the primary business image.
	 *
	 * @param array<string, mixed> $profile The business profile data.
	 * @return string Rendered HTML img tag.
	 */
	public function renderPrimary( array $profile ): string {
		$src = $profile['images']['primary'] ?? '';

		if ( empty( $src ) ) {
			return '';
		}

		return sprintf(
			'<img src="%s" alt="%s" style="width: 100px; height: 100px;" />',
			esc_url( (string) $src ),
			esc_attr__( 'Primary business image', 'wp-cloud-client' ),
		);
	}

	/**
	 * Render the services offered as an HTML list.
	 *
	 * @param array<string, mixed> $profile The business profile data.
	 * @param string               $prefix  CSS class prefix for styling.
	 * @return string Rendered HTML list.
	 */
	public function renderServices( array $profile, string $prefix = 'wpcc' ): string {
		$services = $profile['services_offered'] ?? [];

		if ( ! is_array( $services ) || empty( $services ) ) {
			return sprintf(
				'<ul class="%s-services" style="padding-left: 0; list-style: none;">'
				. '<li>%s</li></ul>',
				esc_attr( $prefix ),
				esc_html__( 'None configured', 'wp-cloud-client' ),
			);
		}

		$items = '';
		foreach ( $services as $service ) {
			$items .= sprintf( '<li>%s</li>', esc_html( (string) $service ) );
		}

		return sprintf(
			'<ul class="%s-services" style="padding-left: 0; list-style: none;">%s</ul>',
			esc_attr( $prefix ),
			$items,
		);
	}

	/**
	 * Render hours of operation as an HTML list.
	 *
	 * @param array<string, mixed> $profile The business profile data.
	 * @param string               $prefix  CSS class prefix for styling.
	 * @return string Rendered HTML list.
	 */
	public function renderHoursOfOperation( array $profile, string $prefix = 'wpcc' ): string {
		$hours = $profile['hours_of_operation'] ?? [];

		if ( is_string( $hours ) ) {
			return esc_html( $hours );
		}

		if ( ! is_array( $hours ) || empty( $hours ) ) {
			return sprintf(
				'<ul class="%s-hours" style="padding-left: 0; list-style: none;">'
				. '<li>%s</li></ul>',
				esc_attr( $prefix ),
				esc_html__( 'No hours configured', 'wp-cloud-client' ),
			);
		}

		$dayOrder = [
			'Sunday'    => 0,
			'Monday'    => 1,
			'Tuesday'   => 2,
			'Wednesday' => 3,
			'Thursday'  => 4,
			'Friday'    => 5,
			'Saturday'  => 6,
		];

		$grouped = [];
		$plain   = [];

		foreach ( $hours as $timeBlock ) {
			if ( ! is_array( $timeBlock ) ) {
				$plain[] = esc_html( (string) $timeBlock );
				continue;
			}

			$days        = $timeBlock['day_of_week'] ?? [];
			$opens       = isset( $timeBlock['opens'] ) ? esc_html( (string) $timeBlock['opens'] ) : '';
			$closes      = isset( $timeBlock['closes'] ) ? esc_html( (string) $timeBlock['closes'] ) : '';
			$description = (string) ( $timeBlock['description'] ?? '' );

			if ( ! is_array( $days ) || empty( $days ) ) {
				continue;
			}

			$dayKey    = implode( ',', $days );
			$sortOrder = $dayOrder[ $days[0] ] ?? 99;

			if ( ! isset( $grouped[ $dayKey ] ) ) {
				$grouped[ $dayKey ] = [
					'days'   => $days,
					'ranges' => [],
					'sort'   => $sortOrder,
				];
			}

			if ( '' !== $opens && '' !== $closes ) {
				$range = "{$opens} - {$closes}";
				if ( '' !== $description ) {
					$range .= ' (' . self::translateDescription( $description ) . ')';
				}
				$grouped[ $dayKey ]['ranges'][] = $range;
			} elseif ( '' !== $description ) {
				$grouped[ $dayKey ]['ranges'][] = self::translateDescription( $description );
			}
		}

		uasort( $grouped, static fn ( array $a, array $b ): int => ( $a['sort'] ?? 99 ) <=> ( $b['sort'] ?? 99 ) );

		$items = '';

		foreach ( $plain as $text ) {
			$items .= sprintf( '<li>%s</li>', $text );
		}

		foreach ( $grouped as $entry ) {
			$daysText = implode( ', ', array_map( [ self::class, 'translateDayName' ], $entry['days'] ) );
			if ( ! empty( $entry['ranges'] ) ) {
				$items .= sprintf( '<li>%s: %s</li>', $daysText, implode( ', ', $entry['ranges'] ) );
			}
		}

		if ( '' === $items ) {
			return '';
		}

		return sprintf(
			'<ul class="%s-hours" style="padding-left: 0; list-style: none;">%s</ul>',
			esc_attr( $prefix ),
			$items,
		);
	}

	/**
	 * Render the booking URL.
	 *
	 * @param array<string, mixed> $profile The business profile data.
	 * @return string Rendered URL or empty string.
	 */
	public function renderBookingUrl( array $profile ): string {
		$url = (string) ( $profile['booking_url'] ?? '' );

		if ( '' === $url ) {
			return '';
		}

		if ( ! $this->isUrl( $url ) ) {
			return '';
		}

		return esc_url( $url );
	}

	/**
	 * Render a social media icon link using SVG.
	 *
	 * @param string $attr The attribute key (e.g. 'facebook_url').
	 * @param string $url  The social media URL.
	 * @return string Rendered HTML anchor with SVG icon.
	 */
	public function renderSocialIcon( string $attr, string $url ): string {
		$platform = self::SOCIAL_PLATFORMS[ $attr ] ?? null;

		if ( null === $platform || empty( $url ) ) {
			return '';
		}

		$iconUrl = plugins_url(
			'assets/business-profile/images/' . $platform . '.svg',
			VPLUGINS_WPCC_FILE,
		);

		return sprintf(
			'<a href="%s" target="_blank" rel="noopener nofollow noreferrer">'
			. '<img src="%s" alt="%s" style="height: 32px; width: 32px;" />'
			. '</a>',
			esc_url( $url ),
			esc_url( $iconUrl ),
			/* translators: %s: social platform name */
			esc_attr( sprintf( __( 'Link to %s', 'wp-cloud-client' ), ucfirst( $platform ) ) ),
		);
	}

	/**
	 * Get the social platform name for a given attribute key.
	 *
	 * @param string $attr The attribute key.
	 * @return string|null The platform name or null.
	 */
	public function getSocialPlatform( string $attr ): ?string {
		return self::SOCIAL_PLATFORMS[ $attr ] ?? null;
	}

	/**
	 * Translate a day-of-week description (e.g. "Closed", "Open 24 hours").
	 *
	 * @param string $description The description to translate.
	 * @return string Translated and escaped string.
	 */
	private static function translateDescription( string $description ): string {
		$map = [
			'Closed'        => __( 'Closed', 'wp-cloud-client' ),
			'Open 24 hours' => __( 'Open 24 hours', 'wp-cloud-client' ),
		];

		return esc_html( $map[ $description ] ?? $description );
	}

	/**
	 * Translate a day name using WordPress locale.
	 *
	 * @param string $day The English day name.
	 * @return string Translated and escaped day name.
	 */
	private static function translateDayName( string $day ): string {
		global $wp_locale;

		$dayIndex = [
			'Sunday'    => 0,
			'Monday'    => 1,
			'Tuesday'   => 2,
			'Wednesday' => 3,
			'Thursday'  => 4,
			'Friday'    => 5,
			'Saturday'  => 6,
		];

		if ( $wp_locale && isset( $dayIndex[ $day ] ) ) {
			return esc_html( $wp_locale->get_weekday( $dayIndex[ $day ] ) );
		}

		return esc_html( $day );
	}

	/**
	 * Check whether a string is a valid URL.
	 *
	 * @param string $value The string to check.
	 * @return bool True if the string is a valid URL.
	 */
	private function isUrl( string $value ): bool {
		return false !== filter_var( $value, FILTER_VALIDATE_URL );
	}
}