<?php

namespace Simple_History\Dropins;

use Simple_History\Helpers;
use Simple_History\Simple_History;
use Simple_History\Log_Query;
use Simple_History\Log_Levels;

/**
 * Dropin Name: Global RSS Feed
 * Dropin URI: http://simple-history.com/
 * Author: Pär Thernström
 */
class RSS_Dropin extends Dropin {
	/**
	 * @inheritdoc
	 */
	public function loaded() {
		// TODO: Investigate if this include is actually needed.
		// get_editable_roles() is checked but never called in this file.
		// This might be leftover code copied from class-privacy-logger.php.
		// If not needed, this include should be removed.
		if ( ! function_exists( 'get_editable_roles' ) ) {
			require_once ABSPATH . '/wp-admin/includes/user.php';
		}

		// Check the status of the RSS feed.
		$this->is_rss_enabled();

		// Generate a rss secret, if it does not exist.
		if ( ! $this->get_rss_secret() ) {
			$this->update_rss_secret();
		}

		add_action( 'init', array( $this, 'check_for_rss_feed_request' ) );

		// Add settings with priority 15 so it' added after the main Simple History settings.
		add_action( 'admin_menu', array( $this, 'add_settings' ), 15 );
	}

	/**
	 * Add settings for the RSS feed.
	 *
	 * Also regenerates the secret if requested.
	 */
	public function add_settings() {
		// Register a setting to keep track of the RSS feed status (enabled/disabled).
		register_setting(
			Simple_History::SETTINGS_GENERAL_OPTION_GROUP,
			'simple_history_enable_rss_feed',
			array(
				'sanitize_callback' => array(
					Helpers::class,
					'sanitize_checkbox_input',
				),
			)
		);

		/**
		 * Start new section for RSS feed.
		 *
		 * @var string $settings_section_rss_id ID of the section
		 */
		$settings_section_rss_id = 'simple_history_settings_section_rss';

		/**
		 * Filters the title for the feeds section headline.
		 *
		 * @var string $rss_section_title
		 */
		$rss_section_title = apply_filters(
			'simple_history/feeds/settings_section_title',
			_x( 'RSS and JSON feeds', 'feeds settings headline', 'simple-history' )
		);

		Helpers::add_settings_section(
			$settings_section_rss_id,
			[ $rss_section_title, 'rss_feed', 'simple_history_rss_section' ],
			array( $this, 'settings_section_output' ),
			Simple_History::SETTINGS_MENU_SLUG // same slug as for options menu page.
		);

		// Enable/Disable RSS feed.
		add_settings_field(
			'simple_history_enable_rss_feed',
			Helpers::get_settings_field_title_output( __( 'Enable', 'simple-history' ), 'toggle-on' ),
			array( $this, 'settings_field_rss_enable' ),
			Simple_History::SETTINGS_MENU_SLUG,
			$settings_section_rss_id
		);

		// If RSS is activated we display other fields.
		if ( $this->is_rss_enabled() ) {
			// RSS address.
			add_settings_field(
				'simple_history_rss_feed',
				Helpers::get_settings_field_title_output( __( 'Address', 'simple-history' ), 'link' ),
				array( $this, 'settings_field_rss' ),
				Simple_History::SETTINGS_MENU_SLUG,
				$settings_section_rss_id
			);

			// Link button to regenerate RSS secret.
			add_settings_field(
				'simple_history_rss_feed_regenerate_secret',
				Helpers::get_settings_field_title_output( __( 'Regenerate', 'simple-history' ), 'autorenew' ),
				array( $this, 'settings_field_rss_regenerate' ),
				Simple_History::SETTINGS_MENU_SLUG,
				$settings_section_rss_id
			);
		}

		// Create new RSS secret.
		$create_secret_nonce_name = 'simple_history_rss_secret_regenerate_nonce';
		$create_nonce_ok          = isset( $_GET[ $create_secret_nonce_name ] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ $create_secret_nonce_name ] ) ), 'simple_history_rss_update_secret' );

		if ( $create_nonce_ok ) {
			$this->update_rss_secret();

			// Add updated-message and store in transient and then redirect
			// This is the way options.php does it.
			$msg = __( 'Created new secret RSS address', 'simple-history' );
			add_settings_error( 'simple_history_rss_feed_regenerate_secret', 'simple_history_rss_feed_regenerate_secret', $msg, 'updated' );
			set_transient( 'settings_errors', get_settings_errors(), 30 );

			/**
			 * Fires after RSS secret has been updated.
			 */
			do_action( 'simple_history/rss_feed/secret_updated' );

			$goback = esc_url_raw( add_query_arg( 'settings-updated', 'true', wp_get_referer() ) );
			// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
			wp_redirect( $goback );
			exit;
		}
	}

	/**
	 * Check if RSS feed is enabled or disabled.
	 *
	 * @return bool true if enabled, false if disabled
	 */
	public function is_rss_enabled() {
		$is_enabled = false;

		// User has never used the plugin we disable RSS feed.
		if ( $this->get_rss_secret() === false && get_option( 'simple_history_enable_rss_feed' ) === false ) {
			// We disable RSS by default, we use 0/1 to prevent fake disabled with bools from functions returning false for unset.
			update_option( 'simple_history_enable_rss_feed', '0' );
		} elseif ( get_option( 'simple_history_enable_rss_feed' ) === false ) {
			// User was using the plugin before RSS feed became disabled by default.
			// We activate RSS to prevent a "breaking change".
			update_option( 'simple_history_enable_rss_feed', '1' );
			$is_enabled = true;
		} elseif ( get_option( 'simple_history_enable_rss_feed' ) === '1' ) {
			$is_enabled = true;
		}

		return $is_enabled;
	}

	/**
	 * Output for settings field that show current RSS address.
	 */
	public function settings_field_rss_enable() {
		/**
		 * Filters the text for the RSS enable checkbox.
		 *
		 * @var string $enable_rss_text
		 */
		$enable_rss_text = apply_filters(
			'simple_history/feeds/enable_feeds_checkbox_text',
			__( 'Enable feed', 'simple-history' )
		);

		?>
		<input value="1" type="checkbox" id="simple_history_enable_rss_feed" name="simple_history_enable_rss_feed" <?php checked( $this->is_rss_enabled(), 1 ); ?> />
		<label for="simple_history_enable_rss_feed"><?php echo esc_html( $enable_rss_text ); ?></label>

		<?php
		// Show premium teaser for JSON feed below the enable checkbox.
		echo wp_kses_post(
			Helpers::get_premium_feature_teaser(
				__( 'JSON Feed for Automation', 'simple-history' ),
				[
					__( 'Structured data format for easy parsing', 'simple-history' ),
					__( 'Connect to Zapier, Make, n8n, or custom scripts', 'simple-history' ),
					__( 'Real-time monitoring and alerting', 'simple-history' ),
				],
				'premium_feeds_settings',
				__( 'Enable JSON Feed', 'simple-history' )
			)
		);
	}

	/**
	 * Check if current request is a request for the RSS feed.
	 */
	public function check_for_rss_feed_request() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['simple_history_get_rss'] ) ) {
			$this->output_rss();
			exit;
		}
	}

	/**
	 * Get the RSS secret.
	 *
	 * @return bool|string RSS secret or false if not set.
	 */
	public function get_rss_secret() {
		return get_option( 'simple_history_rss_secret' );
	}

	/**
	 * Output RSS.
	 */
	public function output_rss() {
		$rss_secret_option = get_option( 'simple_history_rss_secret' );

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$rss_secret_get = sanitize_text_field( wp_unslash( $_GET['rss_secret'] ?? '' ) );

		if ( empty( $rss_secret_option ) || empty( $rss_secret_get ) ) {
			die();
		}

		/** @var bool $rss_show */
		$rss_show = true;

		/**
		 * Filter if RSS feed should be shown or not.
		 * Default is true.
		 * @since 1.3.8
		 * @param bool $rss_show
		 */
		$rss_show = apply_filters( 'simple_history/rss_feed_show', $rss_show );

		if ( ! $rss_show || ! $this->is_rss_enabled() ) {
			wp_die( 'Nothing here.' );
		}

		header( 'Content-Type: text/xml; charset=utf-8' );

		echo '<?xml version="1.0" encoding="UTF-8"?>';

		$self_link = $this->get_rss_address();

		$title = sprintf(
			/* translators: %s blog name */
			__( 'History for %s', 'simple-history' ),
			get_bloginfo( 'name' ),
		);

		$description = sprintf(
			/* translators: %s blog name */
			esc_html__( 'WordPress History for %s', 'simple-history' ),
			get_bloginfo( 'name' )
		);

		if ( $rss_secret_option === $rss_secret_get ) {
			echo PHP_EOL;

			?>
			<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
				<channel>
					<title><?php echo esc_xml( $title ); ?></title>
					<description><?php echo esc_xml( $description ); ?></description>
					<link><?php echo esc_url( get_bloginfo( 'url' ) ); ?></link>
					<atom:link href="<?php echo esc_url( $self_link ); ?>" rel="self" type="application/atom+xml" />
					<?php

					// Override capability check: if you have a valid rss_secret_key you can read it all.
					$action_tag = 'simple_history/loggers_user_can_read/can_read_single_logger';
					add_filter( $action_tag, '__return_true', 10, 0 );

					// Modify header time output so it does not show relative date or time ago-format
					// Because we don't know when a user reads the RSS feed, time ago format may be very inaccurate.
					add_filter( 'simple_history/header_just_now_max_time', '__return_zero' );
					add_filter( 'simple_history/header_time_ago_max_time', '__return_zero' );

					// Set args from query string.
					// phpcs:ignore WordPress.Security.NonceVerification.Recommended
					$args = $this->set_log_query_args_from_query_string( $_GET );

					// Default to last 7 days when no date filter is provided,
					// to avoid scanning the entire events table.
					$has_date_filter = ! empty( $args['date_from'] ) || ! empty( $args['date_to'] ) || ! empty( $args['dates'] );
					if ( ! $has_date_filter ) {
						$args['dates'] = 'lastdays:7';
					}

					// RSS feeds don't need pagination metadata.
					$args['skip_count_query'] = true;

					// Occasion grouping is enabled so subsequentOccasions
					// values are accurate in the RSS output.

					/**
					 * Filters the arguments passed to `SimpleHistoryLogQuery()` when fetching the RSS feed
					 *
					 * @example Change number of posts to retrieve in RSS feed.
					 *
					 * // This example changes the number of posts in the RSS feed to 50 from the default 10.
					 *
					 * ```php
					 *  add_filter(
					 *    'simple_history/rss_feed_args',
					 *      function( $args ) {
					 *        $args['posts_per_page'] = 50;
					 *        return $args;
					 *     }
					 * );
					 *
					 * @example Change number of posts to retrieve in RSS feed.
					 *
					 * // This example changes the number of posts in the RSS feed to 20 from the default 10.
					 *
					 * ```php
					 *  add_filter(
					 *    'simple_history/rss_feed_args',
					 *      function( $args ) {
					 *        $args['posts_per_page'] = 20;
					 *        return $args;
					 *     }
					 * );
					 *
					 * @param array $args SimpleHistoryLogQuery arguments.
					 * @return array
					 */
					$args = apply_filters( 'simple_history/rss_feed_args', $args );

					$logQuery     = new Log_Query();
					$queryResults = $logQuery->query( $args );

					// Remove capability override after query is done
					// remove_action( $action_tag, '__return_true', 10 );.
					if ( is_wp_error( $queryResults ) ) {
						$queryResults = array( 'log_rows' => array() );
					}

					foreach ( $queryResults['log_rows'] as $row ) {
						$header_output  = $this->clean_broken_links( $this->simple_history->get_log_row_header_output( $row ) );
						$text_output    = $this->clean_broken_links( $this->simple_history->get_log_row_plain_text_output( $row ) );
						$details_output = $this->clean_broken_links( (string) $this->simple_history->get_log_row_details_output( $row ) );

						// phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- URL reference.
						// See http://cyber.law.harvard.edu/rss/rss.html#ltguidgtSubelementOfLtitemgt.
						$item_guid = esc_url( add_query_arg( 'SimpleHistoryGuid', $row->id, home_url() ) );
						$item_link = esc_url( add_query_arg( 'SimpleHistoryGuid', $row->id, home_url() ) );

						/**
						 * Filter the guid/link URL used in RSS feed.
						 * Link will be esc_url'ed by simple history, so no need to do that in your filter
						 *
						 * @since 2.0.23
						 *
						 * @param string $item_guid link.
						 * @param object $row
						 */
						$item_link = apply_filters( 'simple_history/rss_item_link', $item_link, $row );
						$item_link = esc_url( $item_link );

						$item_title = sprintf(
							'%2$s',
							Log_Levels::get_log_level_translated( $row->level ),
							wp_kses( $text_output, array() )
						);

						$level_output = sprintf(
							// translators: %s is the severity level of the log.
							esc_html__( 'Severity level: %1$s', 'simple-history' ),
							Log_Levels::get_log_level_translated( $row->level )
						);

						$wp_kses_attrs = array(
							'a'      => array(
								'href'            => array(),
								'class'           => array(),
								'data-ip-address' => array(),
								'target'          => array(),
								'title'           => array(),
							),
							'em'     => array(),
							'span'   => array(
								'class'       => array(),
								'title'       => array(),
								'aria-hidden' => array(),
							),
							'time'   => array(
								'datetime' => array(),
								'class'    => array(),
							),
							'strong' => array(
								'class' => array(),
							),
							'div'    => array(
								'class'    => array(),
								'tabindex' => array(),
							),
							'p'      => array(),
							'del'    => array(),
							'ins'    => array(),
							'table'  => array(
								'class' => array(),
							),
							'tbody'  => array(),
							'tr'     => array(),
							'td'     => array(
								'class' => array(),
							),
							'col'    => array(
								'class' => array(),
							),
						);
						?>
						<item>
						<title><?php echo esc_xml( $item_title ); ?></title>
							<description><![CDATA[
								<p><?php echo wp_kses( $header_output, $wp_kses_attrs ); ?></p>
								<p><?php echo wp_kses( $text_output, $wp_kses_attrs ); ?></p>
								<div><?php echo wp_kses( $details_output, $wp_kses_attrs ); ?></div>
								<p><?php echo wp_kses( $level_output, $wp_kses_attrs ); ?></p>
								<?php
								// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
								$occasions = $row->subsequentOccasions - 1;

								if ( $occasions ) {
									echo '<p>';
									echo esc_html(
										sprintf(
											// translators: %1$s is the number of times this log has been repeated.
											_n( '+%1$s occasion', '+%1$s occasions', $occasions, 'simple-history' ),
											(int) $occasions
										)
									);
									echo '</p>';
								}
								?>
							]]></description>
							<?php
							// author must be email to validate, but the field is optional, so we skip it.
							/* <author><?php echo $row->initiator ?></author> */
							?>
							<pubDate><?php echo esc_xml( gmdate( 'D, d M Y H:i:s', strtotime( $row->date ) ) ); ?> GMT</pubDate>
							<guid isPermaLink="false"><![CDATA[<?php echo esc_xml( $item_guid ); ?>]]></guid>
							<link><![CDATA[<?php echo esc_url( $item_link ); ?>]]></link>
						</item>
						<?php
					}

					?>
				</channel>
			</rss>
			<?php
		} else {
			// RSS secret was not ok.
			echo PHP_EOL;
			?>
			<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
				<channel>
					<title><?php echo esc_xml( $title ); ?></title>
					<description><?php echo esc_xml( $description ); ?></description>
					<link><?php echo esc_url( home_url() ); ?></link>
					<item>
						<title><?php echo esc_xml( __( 'Wrong RSS secret', 'simple-history' ) ); ?></title>
						<description><?php echo esc_xml( __( 'Your RSS secret for Simple History RSS feed is wrong. Please see WordPress settings for current link to the RSS feed.', 'simple-history' ) ); ?></description>
						<pubDate><?php echo esc_xml( gmdate( 'D, d M Y H:i:s', time() ) ); ?> GMT</pubDate>
						<guid><?php echo esc_url( add_query_arg( 'SimpleHistoryGuid', 'wrong-secret', home_url() ) ); ?></guid>
					</item>
				</channel>
			</rss>
			<?php
		}
	}

	/**
	 * Clean broken links from HTML output.
	 *
	 * Finds <a> tags with unresolved {placeholder} hrefs or empty hrefs
	 * and removes the href attribute. An <a> without href is valid HTML
	 * and renders as plain text with no link behavior.
	 *
	 * @since 5.x
	 * @param string $html HTML output from a logger.
	 * @return string Cleaned HTML with broken links neutralized.
	 */
	private function clean_broken_links( $html ) {
		$processor = new \WP_HTML_Tag_Processor( $html );

		while ( $processor->next_tag( array( 'tag_name' => 'a' ) ) ) {
			$href = $processor->get_attribute( 'href' );

			$is_empty       = $href === '' || $href === null;
			$is_placeholder = is_string( $href ) && str_contains( $href, '{' );

			if ( ! $is_empty && ! $is_placeholder ) {
				continue;
			}

			$processor->remove_attribute( 'href' );
		}

		return $processor->get_updated_html();
	}

	/**
	 * Create a new RSS secret.
	 *
	 * @return string new secret
	 */
	public function update_rss_secret() {
		$rss_secret = '';

		for ( $i = 0; $i < 20; $i++ ) {
			$rss_secret .= chr( random_int( 97, 122 ) );
		}

		update_option( 'simple_history_rss_secret', $rss_secret );

		return $rss_secret;
	}

	/**
	 * Output for settings field that show current RSS address.
	 */
	public function settings_field_rss() {
		echo '<p class="simple_history_rss_feed_query_parameters">';
		echo wp_kses(
			sprintf(
				/* translators: %s is a link to the documentation */
				__( 'Query parameters can be used to control what to include in the feed. <a href="%1$s" class="sh-ExternalLink" target="_blank">View documentation</a>.', 'simple-history' ),
				esc_url( Helpers::get_tracking_url( 'https://simple-history.com/docs/feeds/', 'docs_rss_help' ) )
			),
			[
				'a' => [
					'href'   => [],
					'target' => [],
					'class'  => [],
				],
			]
		);
		echo '</p>';
		echo '<br />';

		printf(
			'
			<p>
				<strong>
					%1$s
				</strong>
			</p>
			',
			esc_html__( 'RSS feed', 'simple-history' ) // 1
		);
		
		printf(
			'<p>
				<code>
					<a id="simple_history_rss_feed_address" href="%1$s">%1$s</a>
				</code>
			</p>',
			esc_url( $this->get_rss_address() )
		);

		/**
		 * Fires after the RSS address has been output.
		 *
		 * @param RSS_Dropin $instance
		 */
		do_action( 'simple_history/feeds/after_address', $this );
	}

	/**
	 * Output for settings field that regenerates the RSS address/secret
	 */
	public function settings_field_rss_regenerate() {
		$update_link = esc_url( add_query_arg( '', '' ) );
		$update_link = wp_nonce_url( $update_link, 'simple_history_rss_update_secret', 'simple_history_rss_secret_regenerate_nonce' );

		echo '<p>';
		esc_html_e( 'You can generate a new secret for the feeds. This is useful if you think that the address has fallen into the wrong hands.', 'simple-history' );
		echo '</p>';

		echo '<p>';
		printf(
			'<a class="button" href="%1$s">%2$s</a>',
			esc_url( $update_link ), // 1
			esc_html__( 'Generate new address', 'simple-history' ) // 2
		);

		echo '</p>';
	}

	/**
	 * Get the URL to the RSS feed.
	 *
	 * @return string URL
	 */
	public function get_rss_address() {
		$rss_secret = get_option( 'simple_history_rss_secret' );

		return add_query_arg(
			array(
				'simple_history_get_rss' => '1',
				'rss_secret'             => $rss_secret,
			),
			get_bloginfo( 'url' ) . '/'
		);
	}

	/**
	 * Content for section intro. Leave it be, even if empty.
	 * Called from add_sections_setting.
	 */
	public function settings_section_output() {
		?>
		<p>
			<strong><?php esc_html_e( 'Monitor your site activity in real-time with feeds.', 'simple-history' ); ?></strong>
		</p>
		<p>
			<?php esc_html_e( 'Get updates on logins, content changes, plugin activity and more—delivered to your feed reader or monitoring tools. Perfect for staying informed without constantly checking your dashboard.', 'simple-history' ); ?>
		</p>
		<p>
			<?php esc_html_e( 'Make sure you only share the feeds with people you trust, since they can contain sensitive or confidential information.', 'simple-history' ); ?>
		</p>
		<?php

		/**
		 * Allow premium to add additional feed information.
		 *
		 * @since 4.0
		 */
		do_action( 'simple_history/feeds/settings_section_description' );
	}

	/**
	 * Update log query args from query string.
	 *
	 * @param array $args Query string from $_GET.
	 * @return array Updated log query args.
	 */
	public function set_log_query_args_from_query_string( $args ) {
		$posts_per_page = isset( $args['posts_per_page'] ) ? (int) $args['posts_per_page'] : 10;
		$paged          = isset( $args['paged'] ) ? (int) $args['paged'] : 1;
		$date_from      = isset( $args['date_from'] ) ? sanitize_text_field( $args['date_from'] ) : null;
		$date_to        = isset( $args['date_to'] ) ? sanitize_text_field( $args['date_to'] ) : null;
		$loggers        = isset( $args['loggers'] ) ? sanitize_text_field( $args['loggers'] ) : null;
		$messages       = isset( $args['messages'] ) ? sanitize_text_field( $args['messages'] ) : null;
		$loglevels      = isset( $args['loglevels'] ) ? sanitize_text_field( $args['loglevels'] ) : null;
		$dates          = isset( $args['dates'] ) ? sanitize_text_field( $args['dates'] ) : null;

		// Exclusion filters - useful for subscribing to events excluding your own actions.
		$exclude_loggers   = isset( $args['exclude_loggers'] ) ? sanitize_text_field( $args['exclude_loggers'] ) : null;
		$exclude_messages  = isset( $args['exclude_messages'] ) ? sanitize_text_field( $args['exclude_messages'] ) : null;
		$exclude_loglevels = isset( $args['exclude_loglevels'] ) ? sanitize_text_field( $args['exclude_loglevels'] ) : null;
		$exclude_user      = isset( $args['exclude_user'] ) ? (int) $args['exclude_user'] : null;
		$exclude_users     = isset( $args['exclude_users'] ) ? sanitize_text_field( $args['exclude_users'] ) : null;

		return [
			'posts_per_page'    => $posts_per_page,
			'paged'             => $paged,
			'date_from'         => $date_from,
			'date_to'           => $date_to,
			'loggers'           => $loggers,
			'messages'          => $messages,
			'loglevels'         => $loglevels,
			'dates'             => $dates,
			'exclude_loggers'   => $exclude_loggers,
			'exclude_messages'  => $exclude_messages,
			'exclude_loglevels' => $exclude_loglevels,
			'exclude_user'      => $exclude_user,
			'exclude_users'     => $exclude_users,
		];
	}
}
