helpers->isWpmlActive() ) { self::$wpml = [ 'defaultLanguage' => apply_filters( 'wpml_default_language', null ), 'activeLanguages' => apply_filters( 'wpml_active_languages', null ) ]; add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWpml' ], 10, 4 ); add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWpml' ], 10, 4 ); } if ( aioseo()->helpers->isPluginActive( 'weglot' ) ) { add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWeglot' ], 10, 4 ); add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWeglot' ], 10, 4 ); add_filter( 'aioseo_sitemap_author_entry', [ $this, 'localizeWeglot' ], 10, 4 ); add_filter( 'aioseo_sitemap_archive_entry', [ $this, 'localizeWeglot' ], 10, 4 ); add_filter( 'aioseo_sitemap_date_entry', [ $this, 'localizeWeglot' ], 10, 4 ); add_filter( 'aioseo_sitemap_product_attributes', [ $this, 'localizeWeglot' ], 10, 4 ); } if ( aioseo()->helpers->isPluginActive( 'universally' ) && function_exists( 'universally_get_all_languages' ) ) { add_filter( 'aioseo_sitemap_term', [ $this, 'localizeUniversally' ], 10, 4 ); add_filter( 'aioseo_sitemap_post', [ $this, 'localizeUniversally' ], 10, 4 ); add_filter( 'aioseo_sitemap_author_entry', [ $this, 'localizeUniversally' ], 10, 4 ); add_filter( 'aioseo_sitemap_archive_entry', [ $this, 'localizeUniversally' ], 10, 4 ); add_filter( 'aioseo_sitemap_date_entry', [ $this, 'localizeUniversally' ], 10, 4 ); add_filter( 'aioseo_sitemap_product_attributes', [ $this, 'localizeUniversally' ], 10, 4 ); } } /** * Localize the entries for Weglot. * * @since 4.8.3 * * @param array $entry The entry. * @param mixed $entryId The object ID, null or a date object. * @param string $objectName The post type, taxonomy name or date type ('year' or 'month'). * @param string|null $entryType Whether the entry represents a post, term, author, archive or date. * @return array The entry. */ public function localizeWeglot( $entry, $entryId, $objectName, $entryType = null ) { try { $originalLang = function_exists( 'weglot_get_original_language' ) ? weglot_get_original_language() : ''; $translations = function_exists( 'weglot_get_destination_languages' ) ? weglot_get_destination_languages() : []; if ( empty( $originalLang ) || empty( $translations ) ) { return $entry; } switch ( $entryType ) { case 'post': $permalink = get_permalink( $entryId ); break; case 'term': $permalink = get_term_link( $entryId, $objectName ); break; case 'author': $permalink = get_author_posts_url( $entryId, $objectName ); break; case 'archive': $permalink = get_post_type_archive_link( $objectName ); break; case 'date': $permalink = 'year' === $objectName ? get_year_link( $entryId->year ) : get_month_link( $entryId->year, $entryId->month ); break; default: $permalink = ''; } $entry['languages'] = []; foreach ( $translations as $translation ) { // If the translation is not public we skip it. if ( empty( $translation['public'] ) ) { continue; } $l10nPermalink = $this->weglotGetLocalizedUrl( $permalink, $translation['language_to'] ); if ( ! empty( $l10nPermalink ) ) { $entry['languages'][] = [ 'language' => $translation['language_to'], 'location' => $l10nPermalink ]; } } // Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language. if ( ! empty( $entry['languages'] ) ) { $entry['languages'][] = [ 'language' => $originalLang, 'location' => aioseo()->helpers->decodeUrl( $entry['loc'] ) ]; } else { unset( $entry['languages'] ); } return $this->validateSubentries( $entry ); } catch ( \Exception $e ) { // Do nothing. It only exists because some "weglot" functions above throw exceptions. } return $entry; } /** * Localize the entries for Universally. * * @link https://developers.google.com/search/docs/specialty/international/localized-versions#sitemap * * @since 4.9.9 * * @param array $entry The entry. * @param mixed $entryId The object ID, null or a date object. * @param string $objectName The post type, taxonomy name or date type ('year' or 'month'). * @param string|null $entryType Whether the entry represents a post, term, author, archive or date. * @return array The entry. */ public function localizeUniversally( $entry, $entryId, $objectName, $entryType = null ) { if ( ! function_exists( 'universally_get_all_languages' ) ) { return $entry; } $context = $this->universallyGetContext(); if ( false === $context ) { return $entry; } switch ( $entryType ) { case 'post': $permalink = get_permalink( $entryId ); break; case 'term': $permalink = get_term_link( $entryId, $objectName ); break; case 'author': $permalink = get_author_posts_url( $entryId, $objectName ); break; case 'archive': $permalink = get_post_type_archive_link( $objectName ); break; case 'date': $permalink = 'year' === $objectName ? get_year_link( $entryId->year ) : get_month_link( $entryId->year, $entryId->month ); break; default: $permalink = ''; } if ( empty( $permalink ) || is_wp_error( $permalink ) ) { return $entry; } $path = wp_parse_url( $permalink, PHP_URL_PATH ); if ( empty( $path ) ) { return $entry; } $basePath = $this->universallyStripLanguagePrefixFromPath( $path, $context['validPrefixes'] ); // Strip the WP install's subdirectory so `$homeUrl . $basePath` doesn't repeat it (e.g. `/blog/fr/blog/post`). if ( '' !== $context['homePath'] && 0 === strpos( $basePath, $context['homePath'] . '/' ) ) { $basePath = substr( $basePath, strlen( $context['homePath'] ) ); } elseif ( '' !== $context['homePath'] && $basePath === $context['homePath'] ) { $basePath = '/'; } $entry['languages'] = []; foreach ( $context['languages'] as $lang ) { if ( ! is_array( $lang ) ) { continue; } if ( ! empty( $lang['isSource'] ) ) { continue; } if ( ! empty( $lang['isDisabled'] ) ) { continue; } $urlPrefix = ! empty( $lang['urlPrefix'] ) ? (string) $lang['urlPrefix'] : ''; if ( '' === $urlPrefix ) { continue; } $hreflang = ! empty( $lang['region'] ) ? $lang['region'] : ( $lang['variant'] ?? '' ); if ( '' === $hreflang ) { continue; } $l10nPermalink = aioseo()->helpers->decodeUrl( $context['homeUrl'] . '/' . $urlPrefix . $basePath ); if ( ! empty( $l10nPermalink ) ) { $entry['languages'][] = [ 'language' => $hreflang, 'location' => $l10nPermalink ]; } } if ( ! empty( $entry['languages'] ) ) { $entry['languages'][] = [ 'language' => $context['sourceHreflang'], 'location' => aioseo()->helpers->decodeUrl( $entry['loc'] ) ]; } else { unset( $entry['languages'] ); } return $this->validateSubentries( $entry ); } /** * Localize the entries for WPML. * * @since 4.0.0 * @version 4.8.3 Rename from localizeEntry to localizeWpml. * * @param array $entry The entry. * @param int $entryId The post/term ID. * @param string $objectName The post type or taxonomy name. * @param string $objectType Whether the entry is a post or term. * @return array The entry. */ public function localizeWpml( $entry, $entryId, $objectName, $objectType ) { $elementId = $entryId; $elementType = 'post_' . $objectName; if ( 'term' === $objectType ) { $term = aioseo()->helpers->getTerm( $entryId, $objectName ); $elementId = $term->term_taxonomy_id; $elementType = 'tax_' . $objectName; } $translationGroupId = apply_filters( 'wpml_element_trid', null, $elementId, $elementType ); $translations = apply_filters( 'wpml_get_element_translations', null, $translationGroupId, $elementType ); if ( empty( $translations ) ) { return $entry; } $entry['languages'] = []; $hiddenLanguages = apply_filters( 'wpml_setting', [], 'hidden_languages' ); foreach ( $translations as $translation ) { if ( empty( $translation->element_id ) || ! isset( self::$wpml['activeLanguages'][ $translation->language_code ] ) || in_array( $translation->language_code, $hiddenLanguages, true ) ) { continue; } $currentLanguage = ! empty( self::$wpml['activeLanguages'][ $translation->language_code ] ) ? self::$wpml['activeLanguages'][ $translation->language_code ] : null; $languageCode = ! empty( $currentLanguage['tag'] ) ? $currentLanguage['tag'] : $translation->language_code; if ( (int) $elementId === (int) $translation->element_id ) { $entry['language'] = $languageCode; continue; } $translatedObjectId = apply_filters( 'wpml_object_id', $entryId, $objectName, false, $translation->language_code ); if ( ( 'post' === $objectType && $this->isExcludedPost( $translatedObjectId ) ) || ( 'term' === $objectType && $this->isExcludedTerm( $translatedObjectId ) ) ) { continue; } if ( 'post' === $objectType ) { $permalink = get_permalink( $translatedObjectId ); // Special treatment for the home page translations. if ( 'page' === get_option( 'show_on_front' ) && aioseo()->helpers->wpmlIsHomePage( $entryId ) ) { $permalink = aioseo()->helpers->wpmlHomeUrl( $translation->language_code ); } } else { $permalink = get_term_link( $translatedObjectId, $objectName ); } if ( ! empty( $languageCode ) && ! empty( $permalink ) ) { $entry['languages'][] = [ 'language' => $languageCode, 'location' => aioseo()->helpers->decodeUrl( $permalink ) ]; } } // Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language. if ( ! empty( $entry['language'] ) && ! empty( $entry['languages'] ) ) { $entry['languages'][] = [ 'language' => $entry['language'], 'location' => aioseo()->helpers->decodeUrl( $entry['loc'] ) ]; } else { unset( $entry['languages'] ); } return $this->validateSubentries( $entry ); } /** * Validates the subentries with translated variants to ensure all required values are set. * * @since 4.2.3 * * @param array $entry The entry. * @return array The validated entry. */ private function validateSubentries( $entry ) { if ( ! isset( $entry['languages'] ) ) { return $entry; } foreach ( $entry['languages'] as $index => $subentry ) { if ( empty( $subentry['language'] ) || empty( $subentry['location'] ) ) { unset( $entry['languages'][ $index ] ); } } return $entry; } /** * Checks whether the given post should be excluded. * * @since 4.2.4 * * @param int $postId The post ID. * @return bool Whether the post should be excluded. */ private function isExcludedPost( $postId ) { static $excludedPostIds = null; if ( null === $excludedPostIds ) { $excludedPostIds = explode( ', ', aioseo()->sitemap->helpers->excludedPosts() ); $excludedPostIds = array_map( function ( $postId ) { return (int) $postId; }, $excludedPostIds ); } if ( in_array( $postId, $excludedPostIds, true ) ) { return true; } // Let's also check if the post is published and not password-protected. $post = get_post( $postId ); if ( ! is_a( $post, 'WP_Post' ) ) { return true; } if ( ! empty( $post->post_password ) || 'publish' !== $post->post_status ) { return true; } // Now, we must also check for noindex. $metaData = aioseo()->meta->metaData->getMetaData( $post ); if ( ! empty( $metaData->robots_noindex ) ) { return true; } return false; } /** * Checks whether the given term should be excluded. * * @since 4.2.4 * * @param int $termId The term ID. * @return bool Whether the term should be excluded. */ private function isExcludedTerm( $termId ) { static $excludedTermIds = null; if ( null === $excludedTermIds ) { $excludedTermIds = explode( ', ', aioseo()->sitemap->helpers->excludedTerms() ); $excludedTermIds = array_map( function ( $termId ) { return (int) $termId; }, $excludedTermIds ); } if ( in_array( $termId, $excludedTermIds, true ) ) { return true; } // Now, we must also check for noindex. $term = aioseo()->helpers->getTerm( $termId ); if ( ! is_a( $term, 'WP_Term' ) ) { return true; } // At least one post must be assigned to the term. $posts = aioseo()->core->db->start( 'term_relationships' ) ->select( 'object_id' ) ->where( 'term_taxonomy_id =', $term->term_taxonomy_id ) ->limit( 1 ) ->run() ->result(); if ( empty( $posts ) ) { return true; } $metaData = aioseo()->meta->metaData->getMetaData( $term ); if ( ! empty( $metaData->robots_noindex ) ) { return true; } return false; } /** * Retrieves the localized URL. * * @since 4.8.3 * * @param string $url The page URL to localize. * @param string $code The language code (e.g. 'br', 'en'). * @return string|false The localized URL or false if it fails. */ private function weglotGetLocalizedUrl( $url, $code ) { try { if ( ! $url || ! function_exists( 'weglot_get_service' ) ) { return false; } $languageService = weglot_get_service( 'Language_Service_Weglot' ); $requestUrlService = weglot_get_service( 'Request_Url_Service_Weglot' ); $wgUrl = $requestUrlService->create_url_object( $url ); $language = $languageService->get_language_from_internal( $code ); return $wgUrl->getForLanguage( $language ); } catch ( \Exception $e ) { // Do nothing. It only exists because some "weglot" functions above throw exceptions. } return false; } /** * Strips a leading Universally URL prefix from a path (same rules as universally_get_switcher_urls). * * @since 4.9.9 * * @param string $path URL path (e.g. from wp_parse_url). * @param array $validPrefixes Non-empty language URL prefixes. * @return string Path with at most one prefix removed. */ private function universallyStripLanguagePrefixFromPath( $path, $validPrefixes ) { foreach ( $validPrefixes as $prefix ) { // Replacement collapses the prefix + optional trailing slash to a single '/' so we never emit '//base'. $pattern = '#^/' . preg_quote( $prefix, '#' ) . '(/|$)#'; if ( preg_match( $pattern, $path ) ) { return preg_replace( '#^/' . preg_quote( $prefix, '#' ) . '/?#', '/', $path ); } } return $path; } /** * Returns the per-request Universally context (languages, source hreflang, valid prefixes, home URL). * * Cached on the instance so sitemap renders with thousands of entries don't re-read the transient * or rebuild the prefix list for each entry. * * @since 4.9.9 * * @return array|false The context array, or false when Universally returned no usable data. */ private function universallyGetContext() { if ( null !== $this->universallyContext ) { return $this->universallyContext; } $languages = universally_get_all_languages(); if ( empty( $languages ) || ! is_array( $languages ) ) { $this->universallyContext = false; return false; } $sourceHreflang = ''; $validPrefixes = []; foreach ( $languages as $lang ) { if ( ! is_array( $lang ) ) { continue; } if ( ! empty( $lang['isSource'] ) ) { $sourceHreflang = ! empty( $lang['region'] ) ? $lang['region'] : ( $lang['variant'] ?? '' ); } if ( ! empty( $lang['urlPrefix'] ) ) { $validPrefixes[] = (string) $lang['urlPrefix']; } } if ( '' === $sourceHreflang ) { $this->universallyContext = false; return false; } $homeUrl = home_url(); $homePath = wp_parse_url( $homeUrl, PHP_URL_PATH ); $homePath = is_string( $homePath ) ? rtrim( $homePath, '/' ) : ''; $this->universallyContext = [ 'languages' => $languages, 'sourceHreflang' => $sourceHreflang, 'validPrefixes' => $validPrefixes, 'homeUrl' => $homeUrl, 'homePath' => $homePath ]; return $this->universallyContext; } }