<?php

namespace WPForms\Pro\AntiSpam;

/**
 * Keyword Filter class.
 *
 * @since 1.7.8
 */
class KeywordFilter {

	/**
	 * Save option name.
	 *
	 * @since 1.7.8
	 */
	const OPTION_NAME = 'wpforms_keyword_filter_keywords';

	/**
	 * List of fields to allowed to search for keywords.
	 *
	 * @since 1.7.8
	 */
	const ALLOWED_FIELDS = [
		'text',
		'textarea',
		'name',
		'email',
		'address',
		'url',
		'richtext',
	];

	/**
	 * List of default keywords.
	 *
	 * @since 1.9.2
	 */
	const DEFAULT_KEYWORDS = [
		'earn extra cash',
		'free membership',
		'search engine optimization',
		'more internet traffic',
		'click to download',
	];

	/**
	 * Init class.
	 *
	 * @since 1.7.8
	 */
	public function init() {

		$this->hooks();
	}

	/**
	 * Hooks.
	 *
	 * @since 1.7.8
	 */
	private function hooks() {

		add_filter( 'wpforms_process_after_filter', [ $this, 'process' ], 10, 3 );
		add_action( 'wp_ajax_wpforms_builder_save_keywords',  [ $this, 'ajax_save_keywords' ] );
		add_action( 'wp_ajax_wpforms_builder_load_keywords',  [ $this, 'ajax_load_keywords' ] );
		add_action( 'wpforms_builder_print_footer_scripts', [ $this, 'reformat_warning_template' ] );
		add_action( 'wpforms_admin_builder_anti_spam_panel_content', [ $this, 'get_settings' ], 20, 1 );
	}

	/**
	 * Add content to the "Spam Protection and Security" panel in the Form Builder.
	 *
	 * @since 1.7.8
	 *
	 * @param array $form_data Form data and settings.
	 */
	public function get_settings( $form_data ) {

		ob_start();

		wpforms_panel_field(
			'toggle',
			'anti_spam',
			'enable',
			$form_data,
			__( 'Enable keyword filter', 'wpforms' ),
			[
				'parent'      => 'settings',
				'subsection'  => 'keyword_filter',
				'tooltip'     => __( 'Block entries that contain one or more keywords you define.', 'wpforms' ),
				'input_class' => 'wpforms-panel-field-toggle-next-field',
			]
		);
		?>
		<div class="wpforms-panel-field wpforms-panel-field-keyword-filter-body">
			<p>
				<?php
				printf( /* translators: %s - number of phrases. */
					esc_html__( 'Your keyword filter contains %s phrase(s).', 'wpforms' ),
					'<strong class="wpforms-panel-field-keyword-filter-keywords-count">' . absint( $this->count_keywords() ) . '</strong>'
				);
				?>
				<a href="#" class="wpforms-settings-keyword-filter-toggle-list" data-collapse="<?php esc_html_e( 'Collapse keyword list.', 'wpforms' ); ?>">
					<?php esc_html_e( 'Edit keyword list.', 'wpforms' ); ?>
				</a>
			</p>

			<div class="wpforms-panel-field-keyword-filter-keywords-container">
				<div class="wpforms-panel-field wpforms-panel-field-keyword-keywords">
					<label><?php esc_html_e( 'Keyword Filter List', 'wpforms' ); ?> <i class="fa fa-question-circle-o wpforms-help-tooltip" title="<?php esc_html_e( 'Keywords that will be blocked if they are found in a form entry.', 'wpforms' ); ?>"></i></label>
					<textarea></textarea>
				</div>
				<p class="note"><?php esc_html_e( 'Each word or phrase should be on its own line.', 'wpforms' ); ?></p>

				<div class="wpforms-panel-field-keyword-filter-actions wpforms-hidden">
					<button class="wpforms-btn wpforms-btn-sm wpforms-btn-blue wpforms-settings-keyword-filter-save-changes">
						<span class="wpforms-loading-spinner wpforms-loading-white wpforms-loading-inline wpforms-hidden"></span>
						<span class="wpforms-settings-keyword-filter-save-changes-text"><?php esc_html_e( 'Save Changes', 'wpforms' ); ?></span>
					</button>
					<button class="wpforms-btn wpforms-btn-sm wpforms-btn-blue-borders wpforms-settings-keyword-filter-cancel">
						<?php esc_html_e( 'Cancel', 'wpforms' ); ?>
					</button>
				</div>
			</div>

			<?php
			wpforms_panel_field(
				'text',
				'anti_spam',
				'message',
				$form_data,
				__( 'Keyword Filter Message', 'wpforms' ),
				[
					'parent'     => 'settings',
					'subsection' => 'keyword_filter',
					'tooltip'    => __( 'Displayed if a visitor tries to submit an entry that contains a blocked keyword.', 'wpforms' ),
					'default'    => $this->get_default_error_message( $form_data ),
					'class'      => 'wpforms-panel-field-keyword-filter-message ' . Helpers::get_filtering_message_classes( $form_data ),
				]
			);
			?>
		</div>
		<?php

		wpforms_panel_fields_group( ob_get_clean() );
	}

	/**
	 * Prevent form submitting if filter fails.
	 *
	 * @since 1.7.8
	 *
	 * @param array $fields    Fields data.
	 * @param array $entry     Submitted form entry.
	 * @param array $form_data Form data and settings.
	 *
	 * @return array
	 */
	public function process( $fields, $entry, $form_data ) {

		// If the form is set to store spam entries detected by filtering, we do not need to display the error message.
		if ( Helpers::is_store_filtering_spam( $form_data ) ) {
			return $fields;
		}

		if ( ! $this->is_valid( $form_data, $fields ) ) {
			$form_id = ! empty( $form_data['id'] ) ? $form_data['id'] : 0;

			wpforms()->obj( 'process' )->errors[ $form_id ]['footer'] = $this->get_error_message( $form_data );
		}

		return $fields;
	}

	/**
	 * Check if form submission is valid.
	 *
	 * @since 1.9.2
	 *
	 * @param array $form_data Form data and settings.
	 * @param array $fields    Fields data.
	 *
	 * @return bool
	 */
	public function is_valid( array $form_data, array $fields ): bool {

		if (
			! $this->is_enabled( $form_data ) ||
			! $this->is_blocked_submission( $fields )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Get keywords count.
	 *
	 * @since 1.7.8
	 *
	 * @return int
	 */
	private function count_keywords() {

		return count( $this->get_keywords() );
	}

	/**
	 * Clean the text before searching for keywords.
	 *
	 * @since 1.7.8
	 *
	 * @param array $fields Entry fields.
	 *
	 * @return string
	 */
	protected function get_submitted_content( $fields ) {

		$filtered_fields = $this->get_filtered_fields();

		foreach ( $fields as $key => $field ) {

			// We need to process additional data from address field for filter.
			if ( $field['type'] === 'address' ) {
				$fields[ $key ] = $this->prepare_address_field( $field );
			}

			// Exclude radio fields unless it's an Other option.
			if ( $field['type'] === 'radio' && empty( $field['is_other'] ) ) {
				unset( $fields[ $key ] );
				continue;
			}

			if ( ! in_array( $field['type'], $filtered_fields, true ) ) {
				unset( $fields[ $key ] );
			}
		}

		return implode( ' ', wp_list_pluck( $fields, 'value' ) );
	}

	/**
	 * Include all available address into the filter.
	 *
	 * @since 1.7.8
	 *
	 * @param array $field Field data.
	 *
	 * @return array
	 */
	private function prepare_address_field( $field ) {

		$subfields = [ 'address1', 'address2', 'city', 'state', 'postal', 'country' ];
		$value     = '';

		foreach ( $subfields as $subfield ) {
			if ( ! empty( $field[ $subfield ] ) ) {
				$value .= $field[ $subfield ] . ' ';
			}
		}

		$field['value'] = $value;

		return $field;
	}

	/**
	 * Get fields available to filter through.
	 *
	 * @since 1.7.8
	 *
	 * @return array
	 */
	private function get_filtered_fields() {

		/**
		 * Modify the list of filterable fields.
		 *
		 * @since 1.7.8
		 *
		 * @param array $filterable_fields Array of fields.
		 */
		return apply_filters(
			'wpforms_pro_anti_spam_keyword_filter_get_filtered_fields',
			self::ALLOWED_FIELDS
		);
	}

	/**
	 * Format blocked phrases before search.
	 *
	 * @since 1.7.8
	 * @since 1.9.2 Added default keywords.
	 *
	 * @return array
	 */
	public function get_keywords(): array {

		$defaults = wp_json_encode( self::DEFAULT_KEYWORDS );
		$keywords = (array) json_decode( get_option( self::OPTION_NAME, $defaults ), true );

		/**
		 * Filter keywords list.
		 *
		 * @since 1.7.8
		 *
		 * @param array $keywords Keywords List.
		 */
		return (array) apply_filters( 'wpforms_pro_anti_spam_keyword_filter_get_keywords', $keywords );
	}

	/**
	 * Check if a blocked keyword is set in the submission.
	 *
	 * @since 1.7.8
	 *
	 * @param array $fields List of submitted fields.
	 *
	 * @return bool
	 */
	protected function is_blocked_submission( $fields ) {

		$text     = $this->get_submitted_content( $fields );
		$keywords = $this->get_keywords();

		if ( empty( $keywords ) ) {
			return false;
		}

		foreach ( $keywords as $keyword ) {
			if ( $this->match_keyword( $text, $keyword ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if text contains keywords.
	 *
	 * @since 1.7.8
	 *
	 * @param string $text    Text.
	 * @param string $keyword Keyword.
	 *
	 * @return bool
	 */
	private function match_keyword( $text, $keyword ) {

		$keyword = preg_quote( trim( $keyword ), '/' );

		/**
		 * Look for a matching keyword (case-insensitive), but not as part of another word.
		 *
		 * First group:
		 * `?<=` - Look-behind assertion;
		 * `^` - keyword is at starting position, i.e. a separate word / phrase, or
		 * `\W` - any non-word character (anything except a-zA-Z0-9 and underscore), or
		 *
		 * Second group:
		 * `?=` - Look-ahead assertion;
		 * `\W` - any non-word character, or
		 * `$` - end position, i.e. end of a separate word / phrase.
		 */
		$regex = '/(?<=^|\W)' . $keyword . '(?=\W|$)/i';

		if ( preg_match( $regex, $text ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Is Keywords filter enabled in settings?
	 *
	 * @since 1.7.8
	 *
	 * @param array $form_data Form data and settings.
	 *
	 * @return bool
	 */
	protected function is_enabled( $form_data ) {

		return ! empty( $form_data['settings']['anti_spam']['keyword_filter']['enable'] );
	}

	/**
	 * Get error message.
	 *
	 * @since 1.7.8
	 * @since 1.9.2 Changed method visibility.
	 *
	 * @param array $form_data Form data and settings.
	 *
	 * @return string
	 */
	public function get_error_message( $form_data ) {

		return ! empty( $form_data['settings']['anti_spam']['keyword_filter']['message'] ) ?
			$form_data['settings']['anti_spam']['keyword_filter']['message'] :
			$this->get_default_error_message( $form_data );
	}

	/**
	 * Return default error message.
	 *
	 * @since 1.7.8
	 *
	 * @param array $form_data Form Data.
	 *
	 * @return string
	 */
	private function get_default_error_message( $form_data ) {

		/**
		 * Modify default error message.
		 *
		 * @since 1.7.8
		 *
		 * @param string $message   Request arguments.
		 * @param array  $form_data Form Data.
		 *
		 * @return string
		 */
		return apply_filters( 'wpforms_pro_anti_spam_keyword_filter_get_default_error_message', esc_html__( 'Sorry, your message can\'t be submitted because it contains prohibited words.', 'wpforms' ), $form_data );
	}

	/**
	 * Load keywords on demand.
	 *
	 * @since 1.7.8
	 */
	public function ajax_load_keywords() {

		check_ajax_referer( 'wpforms-builder', 'nonce' );

		// Check for permissions.
		if ( ! wpforms_current_user_can( 'edit_forms' ) ) {
			wp_send_json_error();
		}

		wp_send_json_success( [ 'keywords' => $this->get_keywords() ] );
	}

	/**
	 * Save keywords list into the option table.
	 *
	 * @since 1.7.8
	 */
	public function ajax_save_keywords() {

		check_ajax_referer( 'wpforms-builder', 'nonce' );

		// Check for permissions.
		if ( ! wpforms_current_user_can( 'edit_forms' ) ) {
			wp_send_json_error();
		}

		// Check for required items.
		if ( ! isset( $_POST['keywords'] ) ) {
			wp_send_json_error();
		}

		$keywords = array_filter(
			array_map(
				'trim',
				array_map(
					'sanitize_text_field',
					// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					explode( PHP_EOL, wp_unslash( $_POST['keywords'] ) )
				)
			)
		);

		update_option( self::OPTION_NAME, wp_json_encode( array_values( $keywords ) ), 'no' );

		wp_send_json_success();
	}

	/**
	 * Keywords warning message block.
	 *
	 * @since 1.7.8
	 */
	public function reformat_warning_template() {
		?>
		<script type="text/html" id="tmpl-wpforms-settings-anti-spam-keyword-filter-reformat-warning-template">
			<?php
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo wpforms_render( 'builder/antispam/reformat-warning' );
			?>
		</script>
		<?php
	}
}
