HEX
Server: Apache/2.4.65 (Debian)
System: Linux 88f31f35b0b8 6.1.0-38-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.147-1 (2025-08-02) x86_64
User: www-data (33)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/wpseo-local/classes/schema/class-organization.php
<?php
/**
 * Local SEO plugin file.
 *
 * @package WPSEO_Local\Frontend\Schema
 */

use Yoast\WP\Local\Builders\Locations_Repository_Builder;
use Yoast\WP\Local\PostType\PostType;
use Yoast\WP\Local\Repositories\Business_Types_Repository;
use Yoast\WP\SEO\Config\Schema_IDs;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;

/**
 * Class WPSEO_Local_Organization.
 *
 * Manages the Schema for an Organization.
 *
 * @property Meta_Tags_Context $context A value object with context variables.
 * @property array<string>     $options Local SEO options.
 *
 * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
 */
class WPSEO_Local_Organization extends Abstract_Schema_Piece {

	/**
	 * Whether the organization filter has been registered.
	 *
	 * @var bool
	 */
	private static $filter_registered = false;

	/**
	 * A value object with context variables.
	 *
	 * @var Meta_Tags_Context
	 */
	public $context;

	/**
	 * Stores the options for this plugin.
	 *
	 * @var array<string|int|bool>
	 */
	public $options = [];

	/**
	 * Constructor.
	 *
	 * @param Meta_Tags_Context $context A value object with context variables.
	 */
	public function __construct( Meta_Tags_Context $context ) {
		$this->context = $context;
		$this->options = get_option( 'wpseo_local' );
	}

	/**
	 * Determines whether or not this piece should be added to the graph.
	 *
	 * @return bool
	 */
	public function is_needed() {
		// For the main organization we're using the schema filter instead.
		if ( $this->should_filter_existing_organization() ) {
			if ( ! self::$filter_registered ) {
				add_filter( 'wpseo_schema_organization', [ $this, 'filter_organization_data' ] );
				self::$filter_registered = true;
			}
			return false;
		}

		return ( wpseo_has_multiple_locations() );
	}

	/**
	 * Determines whether the schema piece should filter existing schema data.
	 *
	 * @return bool Value that indicates whether or not to filter existing schema data.
	 */
	public static function should_filter_existing_organization() {
		static $result = null;
		if ( $result !== null ) {
			return $result;
		}

		$result = (
			! wpseo_has_multiple_locations()
			|| ( wpseo_may_use_multiple_locations_shared_business_info() || wpseo_may_use_multiple_locations_shared_opening_hours() )
			|| wpseo_has_primary_location()
			|| ! wpseo_multiple_location_one_organization()
			|| wpseo_has_location_acting_as_primary()
		);

		return $result;
	}

	/**
	 * Determines whether or not we should fill this piece with shared properties if we don't have any other usable data.
	 *
	 * @return bool Value indicating whether or not to fill with shared properties.
	 */
	private function should_fill_with_shared_properties() {
		return (
			wpseo_multiple_location_one_organization()
			&& ! wpseo_has_primary_location()
			&& ! wpseo_has_location_acting_as_primary()
			&& ( wpseo_may_use_multiple_locations_shared_business_info() || wpseo_may_use_multiple_locations_shared_opening_hours() )
		);
	}

	/**
	 * Determines whether the schema piece is an organization branch node.
	 *
	 * @return bool Value that indicates whether or not the schema piece is an organization branch node.
	 */
	public function is_branch() {
		return false;
	}

	/**
	 * Adds data to the Organization Schema output.
	 *
	 * @param array<string|array<string|int>> $data Organization Schema data.
	 *
	 * @return array<string|array<string|int|array<string|array<string>>>> Organization Schema data.
	 */
	public function filter_organization_data( $data ) {
		if ( ! empty( $data['@type'] ) && ! is_array( $data['@type'] ) ) {
			$data['@type'] = [ $data['@type'] ];
		}

		array_push( $data['@type'], 'Place' );

		if ( $data['@id'] === $this->context->canonical . WPSEO_Local_Schema_IDs::BRANCH_ORGANIZATION_ID ) {
			return $data;
		}

		if ( wpseo_has_multiple_locations() && ! wpseo_multiple_location_one_organization() ) {

			if ( ! $this->will_have_branch_organization() ) {
				$data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id ];
			}

			return $data;
		}

		if ( $this->should_fill_with_shared_properties() ) {
			if ( wpseo_may_use_multiple_locations_shared_business_info() ) {
				$data = $this->fill_with_shared_business_info( $data );
			}
			if ( wpseo_may_use_multiple_locations_shared_opening_hours() ) {
				$data = $this->fill_with_shared_opening_hours( $data );
			}

			return $data;
		}

		$location = $this->get_related_location();
		$data     = $this->get_data( $data, $location );

		/**
		 * Filters the URL in the Organization piece in the Schema .
		 *
		 * @param string $site_url The homepage URL.
		 *
		 * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
		 */
		$data['url'] = apply_filters( 'yoast-local-seo-schema-organization-url', $this->context->site_url );

		return $data;
	}

	/**
	 * Gets the related location data object for schema generation.
	 *
	 * @return object Object containing location data.
	 */
	private function get_related_location() {
		$id = 0;

		if ( wpseo_has_primary_location() ) {
			$id = WPSEO_Options::get( 'multiple_locations_primary_location' );
		}
		elseif ( wpseo_has_location_acting_as_primary() ) {
			$locations_repository_builder = new Locations_Repository_Builder();
			$repo                         = $locations_repository_builder->get_locations_repository();
			$location                     = $repo->get( [ 'post_status' => 'publish' ], false );
			$id                           = reset( $location );
		}

		return $this->get_location_data( $id );
	}

	/**
	 * Gets the related location data object for schema generation.
	 *
	 * @param int $post_id Location post ID.
	 *
	 * @return object Object containing location data.
	 */
	private function get_location_data( $post_id ) {
		$args       = [];
		$args['id'] = $post_id;

		$locations_repository_builder = new Locations_Repository_Builder();
		$repository                   = $locations_repository_builder->get_locations_repository();
		$locations                    = $repository->get( $args );

		return (object) $locations[ $post_id ];
	}

	/**
	 * Generates JSON+LD output for a branch Organization.
	 *
	 * @return false This piece is already being generated by Yoast SEO.
	 */
	public function generate() {
		// This piece is already being generated by Yoast SEO and is skipped as determined in is_needed().

		return false;
	}

	/**
	 * Generates data object for Organization.
	 *
	 * @param array<int|string|array<string|array<string>>> $data     Data object of the current schema node.
	 * @param object                                        $location Data object of the related location.
	 *
	 * @return array<int|string|array<string|array<string>>> Array with Organization schema data.
	 */
	public function get_data( $data, $location ) {
		if ( ! empty( $location->business_type ) ) {
			array_push( $data['@type'], $location->business_type );
		}

		$data['@type'] = array_unique( $data['@type'] );

		if ( WPSEO_Local_Postal_Address::has_required_properties( $location ) ) {
			$data['address'] = [ '@id' => $this->context->canonical . $this->get_schema_address_id() ];
		}

		$data['@id'] = $this->context->site_url . $this->get_schema_id();

		if ( $this->should_output_main_entity_property() ) {
			$data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id ];
		}

		$data = $this->add_logo( $data );
		$data = $this->add_organization_attributes( $data, $location );

		return $data;
	}

	/**
	 * Determines whether or not to output the mainEntityOfPage property.
	 *
	 * @return bool Value indicating whether or not to output the property.
	 */
	private function should_output_main_entity_property() {
		$post_type_instance = new PostType();
		$post_type_instance->initialize();

		if ( get_post_type() !== $post_type_instance->get_post_type() ) {
			return false;
		}

		if ( ! wpseo_has_multiple_locations() ) {
			return true;
		}

		if ( $this->should_filter_existing_organization() ) {

			if ( $this->will_have_branch_organization() ) {
				if ( wpseo_has_primary_location() && wpseo_is_current_location_identical_to_primary() ) {
					return true;
				}
			}
			elseif ( wpseo_is_current_location_identical_to_primary() ) {
				return true;
			}
			elseif ( ! wpseo_has_primary_location() && ! wpseo_has_location_acting_as_primary() ) {
				return true;
			}
		}
		elseif ( ! wpseo_is_current_location_identical_to_primary() ) {
			return true;
		}

		return false;
	}

	/**
	 * Determines whether or not a branch organization schema node will be outputted.
	 *
	 * @return bool Value indicating whether or not a branch organisation will be outputted.
	 */
	private function will_have_branch_organization() {
		// When changing these conditions also update class-organization-branch.php is_needed().
		return wpseo_schema_will_have_branch_organization( $this->context->site_represents === 'company' );
	}

	/**
	 * Gets the desired ID of the schema node.
	 *
	 * @return string ID of the schema node.
	 */
	public function get_schema_id() {
		return Schema_IDs::ORGANIZATION_HASH;
	}

	/**
	 * Gets the desired ID of the address schema node.
	 *
	 * @return string ID of the schema node.
	 */
	public function get_schema_address_id() {
		return WPSEO_Local_Schema_IDs::MAIN_ADDRESS_ID;
	}

	/**
	 * Adds the logo for the current business.
	 *
	 * @param array<int|string|array<string|array<string>>> $data Array with Organization schema data.
	 *
	 * @return array<int|string|array<string|array<string>>> Array with Organization schema data.
	 */
	private function add_logo( $data ) {
		$schema_id = $this->context->canonical . WPSEO_Local_Schema_IDs::MAIN_ORGANIZATION_LOGO;

		if ( $this->is_branch() ) {
			$schema_id = $this->context->canonical . WPSEO_Local_Schema_IDs::BRANCH_ORGANIZATION_LOGO;
		}

		$data['logo']  = [ '@id' => $schema_id ];
		$data['image'] = [ '@id' => $schema_id ];

		return $data;
	}

	/**
	 * Adds attributes to the organization schema data.
	 *
	 * @param array<int|string|array<string|array<string>>> $data     Array with Organization schema data.
	 * @param object                                        $location Location data.
	 *
	 * @return array<int|string|array<string|array<string>>> Array with Organization schema data.
	 */
	private function add_organization_attributes( $data, $location ) {

		$organization_attributes = [
			'email'                => 'business_email',
			'faxNumber'            => 'business_fax',
			'areaServed'           => 'business_area_served',
			'vatID'                => 'business_vat',
			'taxID'                => 'business_tax',
			'url'                  => 'business_url',
			'globalLocationNumber' => 'global_location_number',
		];

		$business_types = new Business_Types_Repository();

		if ( $business_types->is_business_type_child_of( 'LocalBusiness', $location->business_type ) ) {
			$organization_attributes['priceRange']         = 'business_price_range';
			$organization_attributes['currenciesAccepted'] = 'business_currencies_accepted';
			$organization_attributes['paymentAccepted']    = 'business_payment_accepted';
		}

		// Add coordinates.
		if ( isset( $location->coords ) && ! empty( $location->coords['lat'] ) && ! empty( $location->coords['long'] ) ) {
			$data['geo'] = [
				'@type'     => 'GeoCoordinates',
				'latitude'  => $location->coords['lat'],
				'longitude' => $location->coords['long'],
			];
		}

		// Add telephone numbers.
		$phones = [];
		if ( $location->business_phone ) {
			$phones[] = $location->business_phone;
		}

		if ( $location->business_phone_2nd ) {
			$phones[] = $location->business_phone_2nd;
		}

		$data['telephone'] = $phones;

		if ( $location->business_contact_phone || $location->business_contact_email ) {
			$data['contactPoint'] = [
				'@type' => 'ContactPoint',
			];
			if ( $location->business_contact_phone ) {
				$data['contactPoint']['telephone'] = $location->business_contact_phone;
			}
			if ( $location->business_contact_email ) {
				$data['contactPoint']['email'] = $location->business_contact_email;
			}
		}

		// Add Opening Hours.
		$opening_hours                     = new WPSEO_Local_Opening_Hours( $location->post_id );
		$data['openingHoursSpecification'] = $opening_hours->generate_opening_hours();

		// Iterate over organization attributes and set corresponding data.
		foreach ( $organization_attributes as $attribute => $option_key ) {
			if ( ! empty( $location->$option_key ) ) {
				$data[ $attribute ] = $location->$option_key;
			}
		}

		return $data;
	}

	/**
	 * Returns the Organization Schema for a setup in which shared properties have been set and no primary location has been set.
	 *
	 * @param array<int|string|array<string|array<string>>> $data Organization Schema data.
	 *
	 * @return array<int|string|array<string|array<string>>> Organization Schema data.
	 */
	private function fill_with_shared_business_info( $data ) {
		$options = WPSEO_Options::get_all();

		if ( ! is_array( $data['@type'] ) ) {
			$data['@type'] = [ $data['@type'] ];
		}

		if ( ! empty( $options['business_type'] ) ) {
			array_push( $data['@type'], $options['business_type'] );
		}

		$data['@type'] = array_unique( $data['@type'] );

		if ( ( wpseo_has_primary_location() || wpseo_has_location_acting_as_primary() )
			&& WPSEO_Local_Postal_Address::has_required_properties( $this->get_related_location() )
		) {
			$data['address'] = [ '@id' => $this->context->canonical . $this->get_schema_address_id() ];
		}

		$data['@id'] = $this->context->site_url . $this->get_schema_id();

		if ( $this->should_output_main_entity_property() ) {
			$data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id ];
		}

		$organization_attributes = [
			'email'     => 'location_email',
			'faxNumber' => 'location_fax',
			'vatID'     => 'location_vat_id',
			'taxID'     => 'location_tax_id',
			'url'       => 'location_url',
		];

		if ( ( ! wpseo_has_multiple_locations() || wpseo_has_primary_location() || wpseo_has_location_acting_as_primary() ) ) {
			$organization_attributes['areaServed'] = 'location_area_served';
		}

		$business_types = new Business_Types_Repository();
		if ( $business_types->is_business_type_child_of( 'LocalBusiness', $options['business_type'] ) ) {

			if ( ( ! wpseo_has_multiple_locations() || wpseo_has_primary_location() || wpseo_has_location_acting_as_primary() ) ) {
				$organization_attributes['priceRange'] = 'location_price_range';
			}

			$organization_attributes['currenciesAccepted'] = 'location_currencies_accepted';
			$organization_attributes['paymentAccepted']    = 'location_payment_accepted';
		}

		// Add telephone numbers.
		$phones = [];
		if ( $options['location_phone'] ) {
			$phones[] = $options['location_phone'];
		}

		if ( $options['location_phone_2nd'] ) {
			$phones[] = $options['location_phone_2nd'];
		}
		$data['telephone'] = $phones;

		if ( $options['location_contact_phone'] || $options['location_contact_email'] ) {
			$data['contactPoint'] = [
				'@type' => 'ContactPoint',
			];
			if ( $options['location_contact_phone'] ) {
				$data['contactPoint']['telephone'] = $options['location_contact_phone'];
			}
			if ( $options['location_contact_email'] ) {
				$data['contactPoint']['email'] = $options['location_contact_email'];
			}
		}

		// Iterate over organization attributes and set corresponding data.
		foreach ( $organization_attributes as $attribute => $option_key ) {
			if ( ! empty( $options[ $option_key ] ) ) {
				$data[ $attribute ] = $options[ $option_key ];
			}
		}

		return $data;
	}

	/**
	 * Returns the Organization Schema for a setup in which shared properties have been set and no primary location has been set.
	 *
	 * @param array<int|string|array<string|array<string>>> $data Organization Schema data.
	 *
	 * @return array<int|string|array<string|array<string>>> Organization Schema data.
	 */
	private function fill_with_shared_opening_hours( $data ) {
		$opening_hours                     = new WPSEO_Local_Opening_Hours( 0 );
		$data['openingHoursSpecification'] = $opening_hours->generate_opening_hours();

		return $data;
	}
}