<?php
/**
 * Manager: Alert Formatter Class
 *
 * Class file for alert formatting.
 *
 * @since 5.3.0
 * @package wsal
 */

declare(strict_types=1);

namespace WSAL\Helpers\Formatters;

use WSAL\Helpers\WP_Helper;
use WSAL\MainWP\MainWP_Helper;
use WSAL\Entities\Metadata_Entity;
use WSAL\Entities\Occurrences_Entity;
use WSAL\WP_Sensors\WP_Plugins_Themes_Sensor;

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

if ( ! class_exists( '\WSAL\Helpers\Formatters\Alert_Formatter' ) ) {
	/**
	 * Alert_Formatter class.
	 *
	 * Class for handling the formatting of alert message/UI widget in different contexts.
	 *
	 * Formatting rules are given by given formatter configuration.
	 *
	 * @package wsal
	 */
	class Alert_Formatter {

		/**
		 * Formats given meta expression.
		 *
		 * @param string   $expression    Meta expression including the surrounding percentage chars.
		 * @param string   $value         Meta value.
		 * @param array    $configuration         The configuration rules for formatting which needs to be used.
		 * @param int|null $occurrence_id Occurrence ID. Only present if the event was already written to the database.
		 * @param array    $metadata      Meta data.
		 * @param bool     $wrap          Should metadata be wrapped in highlighted markup.
		 *
		 * @return false|mixed|string|void|WP_Error
		 *
		 * @since 5.3.0
		 */
		public static function format_meta_expression( $expression, $value, array $configuration, $occurrence_id = null, $metadata = array(), $wrap = true ) {
			\add_filter( 'wsal_truncate_alert_value', array( __CLASS__, 'data_truncate' ), 10, 4 );

			$value = apply_filters( 'wsal_truncate_alert_value', $value, $expression, $configuration['max_meta_value_length'], $configuration['ellipses_sequence'] );

			switch ( true ) {
				case '%Message%' === $expression:
					return \esc_html( $value );

				case '%MetaLink%' === $expression:
					// NULL value check is here because events related to user meta fields didn't have the MetaLink meta prior to version 4.3.2.

					if ( $configuration['is_js_in_links_allowed'] && 'NULL' !== $value ) {
						$label = __( 'Exclude custom field from the monitoring', 'wp-security-audit-log' );

						$escaped_value = \esc_js( $value );

						$result = "<a href=\"#\" data-object-type='" . \esc_attr( $metadata['Object'] ) . "' data-disable-custom-nonce='" . \esc_attr( \wp_create_nonce( 'disable-custom-nonce' . $value ) ) . "' onclick=\"return WsalDisableCustom(this, '" . $escaped_value . "');\"> " . \esc_html( $label ) . '</a>';

						return self::wrap_in_hightlight_markup( $result, $configuration, true );
					}

					return '';

				case in_array( $expression, array( '%path%', '%old_path%', '%FilePath%' ), true ):
					// Concatenate directory and file paths.
					if ( $configuration['is_js_in_links_allowed'] ) {
						$result  = '<strong><span>' . \esc_html( $value ) . '</span>';
						$result .= "<a href=\"#\" data-shortened-text='" . \esc_attr( $value ) . "'>" . $configuration['ellipses_sequence'] . '</a></strong>';

						return $result;
					}

					return \esc_html( $value );

				case in_array( $expression, array( '%MetaValue%', '%MetaValueOld%', '%MetaValueNew%' ), true ):
					// Trim the meta value to the maximum length and append configured ellipses sequence.
					$result = $value;

					return self::wrap_in_hightlight_markup( $result, $configuration );

				case '%ClientIP%' === $expression:
				case '%IPAddress%' === $expression:
					if ( is_string( $value ) ) {
						$sanitized_ips = str_replace(
							array(
								'"',
								'[',
								']',
							),
							'',
							$value
						);

						return self::wrap_in_hightlight_markup( $sanitized_ips, $configuration );
					} else {
						return self::wrap_in_emphasis_markup( __( 'unknown', 'wp-security-audit-log' ), $configuration );
					}

				case '%PostUrlIfPlublished%' === $expression:
					$post_id = null;
					if ( is_array( $metadata ) && array_key_exists( 'PostID', $metadata ) ) {
						$post_id = $metadata['PostID'];
					} else {
						$post_id = self::get_occurrence_meta_item( $occurrence_id, 'PostID' );
					}

					$occurrence_data = Occurrences_Entity::load( 'id = %d', array( $occurrence_id ) );

					if ( isset( $occurrence_data ) && isset( $occurrence_data['site_id'] ) ) {
						if ( MainWP_Helper::SET_SITE_ID_NUMBER < $occurrence_data['site_id'] ) {
							if ( isset( $metadata['PostUrl'] ) ) {
								return $metadata['PostUrl'];
							} else {
								return '';
							}
						}
					}

					$occ_post = ! is_null( $post_id ) ? get_post( $post_id ) : null;
					if ( null !== $occ_post && 'publish' === $occ_post->post_status ) {
						return get_permalink( $occ_post->ID );
					}

					return '';

				case '%MenuUrl%' === $expression:
					$menu_id = null;
					if ( 0 === $occurrence_id && is_array( $metadata ) && array_key_exists( 'MenuID', $metadata ) ) {
						$menu_id = $metadata['MenuID'];
					} else {
						$menu_id = self::get_occurrence_meta_item( $occurrence_id, 'MenuID' );
					}
					if ( null !== $menu_id ) {
						return add_query_arg(
							array(
								'action' => 'edit',
								'menu'   => $menu_id,
							),
							admin_url( 'nav-menus.php' )
						);
					}

					return '';

				case '%Attempts%' === $expression: // Failed login attempts.
					$check_value = (int) $value;
					if ( 0 === $check_value ) {
						return '';
					} else {
						return $value;
					}

					// case '%Users%' === $expression: // Failed login attempts.
					// if ( isset( $metadata['Users'] ) && is_array( $metadata['Users'] ) ) {
					// if ( empty( $metadata['Users'] ) ) {
					// return 'Unknown username';
					// }
					// return $metadata['Users'][0];
					// } elseif ( isset( $metadata['Users'] ) ) {
					// return $metadata['Users'];
					// } else {
					// return 'Unknown username';
					// }
					// $check_value = (int) $value;
					// if ( 0 === $check_value ) {
					// return '';
					// } else {
					// return $metadata['Users'][0];
					// }

				case '%LogFileText%' === $expression: // Failed login file text.
					if ( $configuration['is_js_in_links_allowed'] ) {
						$result = '<a href="javascript:;" onclick="download_failed_login_log( this )" data-download-nonce="' . esc_attr( wp_create_nonce( 'wsal-download-failed-logins' ) ) . '" title="' . esc_html__( 'Download the log file.', 'wp-security-audit-log' ) . '">' . esc_html__( 'Download the log file.', 'wp-security-audit-log' ) . '</a>';

						return self::wrap_in_hightlight_markup( $result, $configuration, true );
					}

					return '';

				case in_array( $expression, array( '%PostStatus%', '%ProductStatus%' ), true ):
					$result = $value;

					return self::wrap_in_hightlight_markup( $result, $configuration );

				case '%multisite_text%' === $expression:
					if ( WP_Helper::is_multisite() && $value ) {
						$site_info = get_blog_details( $value, true );
						if ( $site_info ) {
							$site_url = $site_info->siteurl;

							return ' on site ' . self::format_link( $configuration, $expression, $site_info->blogname, $site_url );
						}
					}

					return '';

				case '%ReportText%' === $expression:
				case '%ChangeText%' === $expression:
					return '';

				case '%TableNames%' === $expression:
					$value = str_replace( ',', ', ', $value );

					return self::wrap_in_hightlight_markup( \esc_html( $value ), $configuration );

				case '%LineBreak%' === $expression:
					return $configuration['end_of_line'];

				case '%PluginFile%' === $expression:
					return self::wrap_in_hightlight_markup( dirname( $value ), $configuration );

				case '%OldVersion%' === $expression:
					$return = ( 'NULL' !== $value ) ? $value : esc_html__( 'Upgrade event prior to WP Activity Log 5.0.0.', 'wp-security-audit-log' );
					return self::wrap_in_hightlight_markup( $return, $configuration );

				case '%PostUrl%' === $expression:
					$return = $value;

					return self::wrap_in_hightlight_markup( $return, $configuration );

				/**
				 * Allow users to expand truncated previous Login Page URL. These URLs can be long, so they're truncated by default.
				 *
				 * @since 5.6.0
				 */
				case '%LoginPageURL%' === $expression:
					// If value is NULL or empty, return an empty string. This avoids displaying 'NULL' in the alert for old plugin versions before 5.6.0.
					if ( 'NULL' === $value || empty( $value ) ) {
						return '';
					}

					$max_length = $configuration['max_meta_value_length'];

					if ( mb_strlen( $value ) > $max_length ) {
						$truncated = mb_substr( $value, 0, $max_length );
						$result    = '<span class="wsal-truncated-url"><strong>' . \esc_html( $truncated ) . '...</strong></span>';

						return $result;
					}

					// If JS in links is not allowed, we just show the full URL.
					if ( mb_strlen( $value ) > $max_length ) {
						$value = \esc_html( $value );
					}

					return self::wrap_in_hightlight_markup( $value, $configuration );

				case '%EditorLinkOrder%' === $expression:
					if ( ! isset( $metadata['EditorLinkOrder'] ) ) {
						return '';
					}
					return $metadata['EditorLinkOrder'];

				default:
					/**
					 * Allows meta formatting via filter if no match was found.
					 *
					 * @param string $expression Meta expression including the surrounding percentage chars.
					 * @param string $value Meta value.
					 *
					 * @deprecated 4.3.0 Use 'wsal_format_custom_meta' instead.
					 */
					$result = apply_filters_deprecated(
						'wsal_meta_formatter_custom_formatter',
						array(
							$value,
							$expression,
						),
						'WSAL 4.3.0',
						'wsal_format_custom_meta'
					);

					/**
					 * Allows meta formatting via filter if no match was found. Runs after the legacy filter 'wsal_meta_formatter_custom_formatter' that is kept for backwards compatibility.
					 *
					 * @param string $value Meta value.
					 * @param string $expression Meta expression including the surrounding percentage chars.
					 * @param WSAL_AlertFormatter $this Alert formatter class.
					 * @param int|null $occurrence_id Occurrence ID. Only present if the event was already written to the database. Default null.
					 *
					 * @since 4.3.0
					 */
					// Ensure result is wrapped as expected.
					if ( $wrap ) {
						$result = self::wrap_in_hightlight_markup( $result, $configuration );
					}

					return apply_filters( 'wsal_format_custom_meta', $result, $expression, $configuration, $occurrence_id );
			}
		}

		/**
		 * Truncates the data to a specific number of characters.
		 *
		 * @param mixed   $value - The value to be truncated.
		 * @param string  $expression - The expression for that value.
		 * @param integer $length - Number of characters to truncate to.
		 * @param string  $ellipses_sequence - The sequence of ellipses suffix.
		 *
		 * @return string|mixed
		 *
		 * @since 5.3.0
		 */
		public static function data_truncate( $value, $expression, $length = 50, $ellipses_sequence = '...' ) {

			switch ( $expression ) {
				case '%path%':
				case '%old_path%':
				case '%FilePath%':
					if ( mb_strlen( $value ) > $length ) {
						$value = mb_substr( $value, 0, $length );
					}
					break;
				case '%MetaValue%':
				case '%MetaValueOld%':
				case '%MetaValueNew%':
					$value = mb_strlen( $value ) > $length ? ( mb_substr( $value, 0, $length ) . $ellipses_sequence ) : $value;
					break;
				default:
					break;
			}

			return $value;
		}

		/**
		 * Handles formatting of hyperlinks in the event messages.
		 *
		 * Contains:
		 * - check for empty values
		 * - check if the link is disabled
		 * - optional URL processing
		 *
		 * @param array  $configuration - The configuration rules for formatting which needs to be used.
		 * @param string $url - URL.
		 * @param string $label - Label.
		 * @param string $title - Title.
		 * @param string $target - Target attribute.
		 *
		 * @return string
		 *
		 * @since 5.3.0
		 */
		public static function format_link( array $configuration, $url, $label, $title = '', $target = '_blank' ) {
			// Check for empty values.
			if ( null === $url || empty( $url ) ) {
				return '';
			}

			$processed_url = $url;
			$result        = self::build_link_markup( $configuration, $processed_url, $label, $title, $target );

			return self::wrap_in_hightlight_markup( $result, $configuration, true );
		}

		/**
		 * Message for some events contains HTML tags for highlighting certain parts of the message.
		 *
		 * This function replaces the original HTML tags with the correct highlight tags.
		 *
		 * It also strips any additional HTML tags apart from hyperlink and an end of line to support legacy messages.
		 *
		 * @param string $message Message text.
		 * @param array  $configuration         The configuration rules for formatting which needs to be used.
		 *
		 * @return string
		 *
		 * @since 5.3.0
		 */
		public static function process_html_tags_in_message( $message, array $configuration ) {

			$result = preg_replace(
				array( '/<strong>/', '/<\/strong>/' ),
				array( $configuration['highlight_start_tag'], $configuration['highlight_end_tag'] ),
				$message
			);

			return strip_tags( $result, $configuration['tags_allowed_in_message'] );
		}

		/**
		 * Override this method in subclass to format hyperlinks differently.
		 *
		 * Default implementation returns HTML A tag. Only implementation at the moment. We used to have Slack as well, but
		 * we moved to a different implementation. Introducing another link markup would require adding link format with
		 * placeholders to the formatter configuration.
		 *
		 * @param array  $configuration - The configuration rules for formatting which needs to be used.
		 * @param string $url - URL.
		 * @param string $label - Label.
		 * @param string $title - Title.
		 * @param string $target - Target attribute.
		 *
		 * @return string
		 *
		 * @since 5.5.0 - Added $extra_css_classes parameter to allow passing custom classes for the link.
		 */
		private static function build_link_markup( array $configuration, $url, $label, $title = '', $target = '_blank', $extra_css_classes = array() ) {

			// This may be expanded in the future. For the moment, just add thickbox class when necessary.
			$extra_css_classes = WP_Plugins_Themes_Sensor::maybe_add_thickbox_class( $url );

			if ( $configuration['use_html_markup_for_links'] ) {
				$classes_attr = $extra_css_classes ? ' class="' . \esc_attr( implode( ' ', $extra_css_classes ) ) . '"' : '';
				$title_attr   = $title ? ' title="' . \esc_attr( $title ) . '"' : '';

				return '<a ' . $classes_attr . ' href="' . \esc_url( $url ) . '" ' . $title_attr . ' target="' . $target . '">' . $label . '</a>';
			}

			return $label . ': ' . \esc_url( $url );
		}

		/**
		 * Wraps given value in highlight markup.
		 *
		 * For example meta values displayed as <strong>{meta value}</strong> in the WP admin UI.
		 *
		 * @param string $value Value.
		 * @param array  $configuration         The configuration rules for formatting which needs to be used.
		 * @param bool   $no_esc - Do we need to escape the html in the message.
		 *
		 * @return string
		 */
		public static function wrap_in_hightlight_markup( $value, array $configuration, $no_esc = false ) {
			if ( ! $no_esc ) {
				$value = esc_html( $value );
			}

			return $configuration['highlight_start_tag'] . $value . $configuration['highlight_end_tag'];
		}

		/**
		 * Wraps given value in emphasis markup.
		 *
		 * For example an unknown IP address is displayed as <i>unknown</i> in the WP admin UI.
		 *
		 * @param string $value Value.
		 * @param array  $configuration         The configuration rules for formatting which needs to be used.
		 *
		 * @return string
		 *
		 * @since 5.3.0
		 */
		private static function wrap_in_emphasis_markup( $value, array $configuration ) {

			return $configuration['emphasis_start_tag'] . $value . $configuration['emphasis_end_tag'];
		}

		/**
		 * Helper function to get meta value from an occurrence.
		 *
		 * @param int    $occurrence_id Occurrence ID.
		 * @param string $meta_key      Meta key.
		 *
		 * @return mixed|null Meta value if exists. Otherwise null
		 *
		 * @since 5.3.0
		 */
		private static function get_occurrence_meta_item( $occurrence_id, $meta_key ) {
			// get values needed.
			$meta_result = Metadata_Entity::load_by_name_and_occurrence_id( $meta_key, $occurrence_id );

			return isset( $meta_result['value'] ) ? maybe_unserialize( $meta_result['value'] ) : null;
		}
	}
}
