File "Llms.php"

Full Path: /home/stylijtl/public_html/wp-content/plugins/all-in-one-seo-pack/app/Common/Llms/Llms.php
File size: 12.07 KB
MIME-type: text/x-php
Charset: utf-8

<?php
namespace AIOSEO\Plugin\Common\Llms;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handles the LLMS.txt generation.
 *
 * @since 4.8.4
 */
class Llms {
	/**
	 * Site title
	 *
	 * since 4.8.4
	 *
	 * @var string
	 */
	protected $title;

	/**
	 * Site description
	 *
	 * since 4.8.4
	 *
	 * @var string
	 */
	protected $description;

	/**
	 * Site link
	 *
	 * since 4.8.4
	 *
	 * @var string
	 */
	protected $link;

	/**
	 * Plugin version
	 *
	 * since 4.8.4
	 *
	 * @var string
	 */
	protected $version;

	/**
	 * LLMS file recurrent action name.
	 *
	 * since 4.8.8
	 *
	 * @var string
	 */
	public $llmsTxtRecurrentAction = 'aioseo_generate_llms_txt';

	/**
	 * LLMS file single action name.
	 *
	 * since 4.8.8
	 *
	 * @var string
	 */
	public $llmsTxtSingleAction = 'aioseo_generate_llms_txt_single';

	/**
	 * Class constructor.
	 *
	 * @since 4.8.8
	 *
	 * @return void
	 */
	public function __construct() {
		add_action( 'admin_init', [ $this, 'scheduleRecurrentGenerationForLlmsTxt' ] );

		add_action( 'wp_insert_post', [ $this, 'scheduleSingleGenerationForLlmsTxt' ] );
		add_action( 'edited_term', [ $this, 'scheduleSingleGenerationForLlmsTxt' ] );

		add_action( $this->llmsTxtRecurrentAction, [ $this, 'generateLlmsTxt' ] );
		add_action( $this->llmsTxtSingleAction, [ $this, 'generateLlmsTxt' ] );
	}

	/**
	 * Schedules the LLMS file generation.
	 *
	 * @since 4.8.8
	 *
	 * @return void
	 */
	public function scheduleRecurrentGenerationForLlmsTxt() {
		if (
			! aioseo()->options->sitemap->llms->enable ||
			aioseo()->actionScheduler->isScheduled( $this->llmsTxtRecurrentAction )
		) {
			return;
		}

		aioseo()->actionScheduler->scheduleRecurrent( $this->llmsTxtRecurrentAction, 10, DAY_IN_SECONDS );
	}

	/**
	 * Schedules a single LLMS file generation.
	 *
	 * @since 4.8.8
	 *
	 * @return void
	 */
	public function scheduleSingleGenerationForLlmsTxt() {
		if (
			! aioseo()->options->sitemap->llms->enable ||
			aioseo()->actionScheduler->isScheduled( $this->llmsTxtSingleAction )
		) {
			return;
		}

		aioseo()->actionScheduler->scheduleSingle( $this->llmsTxtSingleAction, 10 );
	}

	/**
	 * Sets the site info.
	 *
	 * @since 4.8.4
	 *
	 * @return void
	 */
	protected function setSiteInfo() {
		$isMultisite = is_multisite();

		// Check for LLMS custom title setting
		$llmsTitle = aioseo()->options->sitemap->llms->advancedSettings->title;
		if ( ! empty( $llmsTitle ) ) {
			// Use LLMS title with hashtag tag replacement
			$this->title = aioseo()->tags->replaceTags( $llmsTitle );
		} else {
			// Fallback to default site title
			$this->title = $isMultisite
				? get_blog_option( get_current_blog_id(), 'blogname' )
				: get_bloginfo( 'name' );
			$this->title = $this->title ?: aioseo()->meta->title->getHomePageTitle();
		}

		// Check for LLMS custom description setting
		$llmsDescription = aioseo()->options->sitemap->llms->advancedSettings->description;
		if ( ! empty( $llmsDescription ) ) {
			// Use LLMS description with hashtag tag replacement
			$this->description = aioseo()->tags->replaceTags( $llmsDescription );
		} else {
			// Fallback to default site description
			$this->description = $isMultisite
				? get_blog_option( get_current_blog_id(), 'blogdescription' )
				: get_bloginfo( 'description' );
			$this->description = $this->description ?: aioseo()->meta->description->getHomePageDescription();
		}

		$this->link = $isMultisite
			? get_blog_option( get_current_blog_id(), 'siteurl' )
			: home_url();

		$this->version = aioseo()->helpers->getAioseoVersion();
	}

	/**
	 * Generates the LLMS.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return void
	 */
	public function generateLlmsTxt() {
		if ( isset( aioseo()->options->sitemap->llms->enable ) && ! aioseo()->options->sitemap->llms->enable ) {
			aioseo()->actionScheduler->unschedule( $this->llmsTxtSingleAction );
			aioseo()->actionScheduler->unschedule( $this->llmsTxtRecurrentAction );
			$this->deleteLlmsFile();

			return;
		}

		$fs   = aioseo()->core->fs;
		$file = $this->getFilePath();

		// Generate the full content
		$this->setSiteInfo();
		$content  = $this->getHeader();
		$content .= $this->getSiteDescription();
		$content .= $this->getSitemapUrl();
		$content .= $this->getContent();

		// Add UTF-8 BOM to help browsers recognize the encoding
		$content = "\xEF\xBB\xBF" . $content;
		$fs->putContents( $file, $content );
	}

	/**
	 * Gets the header section of the llms.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return string
	 */
	protected function getHeader( $llmsFull = false ) {
		$fileName = $llmsFull ? 'llms-full.txt' : 'llms.txt';

		$introText = sprintf(
			/* translators: 1 - The plugin name ("All in One SEO"), 2 - The version number */
			esc_html__( 'Generated by %1$s v%2$s, this is an %3$s file, used by LLMs to index the site.', 'all-in-one-seo-pack' ),
			esc_html( AIOSEO_PLUGIN_NAME ),
			esc_html( aioseo()->version ),
			esc_html( $fileName )
		);

		if ( $this->title ) {
			$introText .= esc_html( "\n\n# {$this->title}\n\n" );
		}

		return $introText;
	}

	/**
	 * Gets the site description section of the llms.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return string
	 */
	protected function getSiteDescription() {
		if ( $this->description ) {
			return "{$this->description}\n\n";
		}

		return '';
	}

	/**
	 * Gets the sitemap link section of the llms.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @return string
	 */
	protected function getSitemapUrl() {
		if ( ! aioseo()->options->sitemap->general->enable ) {
			return '';
		}

		$sitemapUrl = aioseo()->sitemap->helpers->getUrl( 'general' );

		return "## Sitemaps\n\n- [XML Sitemap]({$sitemapUrl}): Contains all public & indexable URLs for this website.\n\n";
	}

	/**
	 * Gets the recent content section of the llms.txt file.
	 *
	 * @since 4.8.4
	 *
	 * @param  bool   $llmsFull Whether to include the llms-full.txt file.
	 * @return string           The content of the llms.txt file.
	 */
	protected function getContent( $llmsFull = false ) {
		// Get LLMS post types settings
		$includeAllPostTypes   = aioseo()->options->sitemap->llms->advancedSettings->postTypes->all;
		$includedPostTypes     = aioseo()->options->sitemap->llms->advancedSettings->postTypes->included;
		$includeAllTaxonomies  = aioseo()->options->sitemap->llms->advancedSettings->taxonomies->all;
		$includedTaxonomies    = aioseo()->options->sitemap->llms->advancedSettings->taxonomies->included;

		// Determine which post types to include
		if ( $includeAllPostTypes ) {
			// Include all public post types except attachments
			$postTypes = array_filter( aioseo()->helpers->getPublicPostTypes( true ), function( $type ) {
				return 'attachment' !== $type;
			} );
		} else {
			// Only include the specifically selected post types, but still exclude attachments
			$postTypes = array_filter( $includedPostTypes, function( $type ) {
				return 'attachment' !== $type;
			} );
		}
		if ( $includeAllTaxonomies ) {
			$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
		} else {
			$taxonomies = $includedTaxonomies;
		}
		$originalSitemapType   = aioseo()->sitemap->type;
		$originalLinksPerIndex = aioseo()->sitemap->linksPerIndex;
		$originalIndexes       = aioseo()->sitemap->indexes;

		aioseo()->sitemap->type          = 'llms';
		aioseo()->sitemap->indexes       = true;
		aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->llms->advancedSettings->linksPerPostTax
			? aioseo()->options->sitemap->llms->advancedSettings->linksPerPostTax :
			20;

		$content = '';
		foreach ( $postTypes as $postType ) {
			$postTypeObject = get_post_type_object( $postType );
			if ( ! $postTypeObject ) {
				continue;
			}

			$posts = aioseo()->sitemap->query->posts( $postType );

			if ( ! empty( $posts ) ) {
				$content .= '## ' . $postTypeObject->labels->name . "\n\n";
				foreach ( $posts as $postObject ) {
					$post = get_post( $postObject->ID );
					if ( ! is_a( $post, 'WP_Post' ) ) {
						continue;
					}

					aioseo()->helpers->setWpQueryPost( $post );

					$content .= $this->getPostContent( $post, $llmsFull );

					aioseo()->helpers->restoreWpQuery();
				}

				$content .= "\n";
			}
		}

		// Initialize sitemap settings again for terms
		aioseo()->sitemap->type          = 'llms';
		aioseo()->sitemap->indexes       = true;
		aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->llms->advancedSettings->linksPerPostTax
			? aioseo()->options->sitemap->llms->advancedSettings->linksPerPostTax :
			20;

		// Get recent terms for each taxonomy using sitemap query
		foreach ( $taxonomies as $taxonomy ) {
			$taxonomyObject = get_taxonomy( $taxonomy );
			if ( ! $taxonomyObject ) {
				continue;
			}

			$terms = aioseo()->sitemap->query->terms( $taxonomy );

			if ( ! empty( $terms ) ) {
				$content .= '## ' . $taxonomyObject->labels->name . "\n\n";
				foreach ( $terms as $termObject ) {
					if ( is_object( $termObject ) && ! empty( $termObject->term_id ) ) {
						$term = get_term( $termObject->term_id, $taxonomy );
						if ( is_wp_error( $term ) ) {
							continue;
						}

						aioseo()->helpers->setWpQueryTerm( $term, $taxonomy );

						$content .= $this->getTermContent( $term, $taxonomy, $llmsFull );

						aioseo()->helpers->restoreWpQuery();
					}
				}
				$content .= "\n";
			}
		}

		// Restore original sitemap settings
		aioseo()->sitemap->type          = $originalSitemapType;
		aioseo()->sitemap->linksPerIndex = $originalLinksPerIndex;
		aioseo()->sitemap->indexes       = $originalIndexes;

		return $content;
	}

	/**
	 * Gets the post content section of the llms.txt file.
	 *
	 * @since 4.8.8
	 *
	 * @param  \WP_Post $post     The post object.
	 * @param  bool     $llmsFull Whether to include the llms-full.txt file.
	 * @return string             The content of the llms.txt file.
	 */
	protected function getPostContent( $post, $llmsFull = false ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$title   = apply_filters( 'aioseo_llms_post_title', $post->post_title, $post );
		$content = '- [' . aioseo()->helpers->decodeHtmlEntities( $title ) . '](' . aioseo()->helpers->decodeUrl( get_permalink( $post ) ) . ')';

		$description = aioseo()->meta->description->getPostDescription( $post );
		$description = apply_filters( 'aioseo_llms_post_description', $description, $post );
		if ( ! empty( $description ) ) {
			$content .= ' - ' . aioseo()->helpers->decodeHtmlEntities( $description );
		}

		$content .= "\n";

		return $content;
	}

	/**
	 * Gets the term content section of the llms.txt file.
	 *
	 * @since 4.9.3
	 *
	 * @param  \WP_Term $term     The term object.
	 * @param  string   $taxonomy The taxonomy name.
	 * @param  bool     $llmsFull Whether to include the llms-full.txt file.
	 * @return string             The content of the llms.txt file.
	 */
	protected function getTermContent( $term, $taxonomy, $llmsFull = false ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$title   = apply_filters( 'aioseo_llms_term_title', $term->name, $term );
		$content = '- [' . aioseo()->helpers->decodeHtmlEntities( $title ) . '](' . aioseo()->helpers->decodeUrl( get_term_link( $term, $taxonomy ) ) . ')';

		$description = aioseo()->meta->description->getTermDescription( $term );
		$description = apply_filters( 'aioseo_llms_term_description', $description, $term );
		if ( ! empty( $description ) ) {
			$content .= ' - ' . aioseo()->helpers->decodeHtmlEntities( $description );
		}

		$content .= "\n";

		return $content;
	}

	/**
	 * Deletes the LLMS.txt file.
	 *
	 * @since 4.8.8
	 *
	 * @return void
	 */
	public function deleteLlmsFile() {
		$fs   = aioseo()->core->fs;
		$file = $this->getFilePath();
		if ( $fs->isWpfsValid() ) {
			$fs->fs->delete( $file, false, 'f' );
		}
	}

	/**
	 * Gets the file path for the llms.txt or llms-full.txt file.
	 *
	 * Uses `dirname( WP_CONTENT_DIR )` instead of `ABSPATH` to support non-standard
	 * WordPress installations where `ABSPATH` doesn't point to the web root.
	 *
	 * @since 4.9.5
	 *
	 * @param  bool   $full Whether to get the full version path.
	 * @return string       The file path.
	 */
	public function getFilePath( $full = false ) {
		$filename = $full ? 'llms-full.txt' : 'llms.txt';

		return trailingslashit( dirname( WP_CONTENT_DIR ) ) . sanitize_file_name( $filename );
	}
}