<?php
/**
 * General plugin functions
 *
 * @package safe-redirect-manager
 */

/**
 * Get redirects from the database
 *
 * @since 1.8
 * @param array $args Any arguments to filter by
 * @param bool  $hard Force cache refresh
 * @return array $redirects An array of redirects
 */
function srm_get_redirects( $args = array(), $hard = false ) {
	$default_max_redirects = srm_get_max_redirects();
	$transient_key         = '_srm_redirects_' . $default_max_redirects;
	$redirects             = get_transient( $transient_key );

	if ( $hard || false === $redirects ) {

		$redirects = array();

		$posts_per_page = 100;

		$i = 1;

		while ( true ) {
			if ( count( $redirects ) >= $default_max_redirects ) {
				break;
			}

			$defaults = array(
				'posts_per_page' => $posts_per_page,
				'post_status'    => 'publish',
				'paged'          => $i,
				'fields'         => 'ids',
				'orderby'        => 'menu_order ID',
				'order'          => 'ASC',
			);

			$query_args = array_merge( $defaults, $args );

			// Some arguments that don't need to be configurable
			$query_args['post_type']         = 'redirect_rule';
			$query_args['update_term_cache'] = false;

			$redirect_query = new WP_Query( $query_args );

			if ( ! $redirect_query->have_posts() ) {
				break;
			}

			// Check whether include post_status in result
			$include_post_status_in_result = is_array( $query_args['post_status'] ) || 'any' === $query_args['post_status'];

			foreach ( $redirect_query->posts as $redirect_id ) {
				if ( count( $redirects ) >= $default_max_redirects ) {
					break 2;
				}

				$redirect_data = array(
					'ID'            => $redirect_id,
					'redirect_from' => get_post_meta( $redirect_id, '_redirect_rule_from', true ),
					'redirect_to'   => get_post_meta( $redirect_id, '_redirect_rule_to', true ),
					'status_code'   => (int) get_post_meta( $redirect_id, '_redirect_rule_status_code', true ),
					'message'       => get_post_meta( $redirect_id, '_redirect_rule_message', true ),
					'enable_regex'  => (bool) get_post_meta( $redirect_id, '_redirect_rule_from_regex', true ),
					'force_https'   => get_post_meta( $redirect_id, '_force_https', true ),
				);

				if ( $include_post_status_in_result ) {
					$redirect_data['post_status'] = get_post_status( $redirect_id );
				}

				$redirects[] = $redirect_data;
			}

			$i++;

		}

		// Set transient to 30 days to remove old transients if the max redirects changes.
		set_transient( $transient_key, $redirects, 30 * DAY_IN_SECONDS );
	}

	return $redirects;
}

/**
 * Returns true if max redirects have been reached
 *
 * @since 1.8
 * @return bool
 */
function srm_max_redirects_reached() {
	$default_max_redirects = srm_get_max_redirects();

	$redirects = srm_get_redirects();

	return ( count( $redirects ) >= $default_max_redirects );
}

/**
 * Get valid HTTP status codes
 *
 * @since  1.8
 * @return array
 */
function srm_get_valid_status_codes() {
	/**
	 * Valid status codes to redirect with.
	 *
	 * @hook srm_valid_status_codes
	 * @param {array} $status_codes Valid status codes to redirect with. Default array( 301, 302, 303, 307, 403, 404, 410 ) and other codes returned by `srm_additional_status_codes` filter hook.
	 * @returns {array} Filtered valid status codes.
	 */
	return apply_filters( 'srm_valid_status_codes', array_keys( srm_get_valid_status_codes_data() ) );
}

/**
 * Get valid HTTP status codes and their labels.
 *
 * @since  2.0.0
 * @return array
 */
function srm_get_valid_status_codes_data() {
	$status_codes = array(
		301 => esc_html__( 'Moved Permanently', 'safe-redirect-manager' ),
		302 => esc_html__( 'Found', 'safe-redirect-manager' ),
		303 => esc_html__( 'See Other', 'safe-redirect-manager' ),
		307 => esc_html__( 'Temporary Redirect', 'safe-redirect-manager' ),
		403 => esc_html__( 'Forbidden', 'safe-redirect-manager' ),
		404 => esc_html__( 'Not Found', 'safe-redirect-manager' ),
		410 => esc_html__( 'Gone', 'safe-redirect-manager' ),
	);

	/**
	 * Include additional status codes as valid to redirect with.
	 *
	 * @hook srm_additional_status_codes
	 * @param {array} $status_codes Status codes to add in valid array. Default is empty array.
	 * @returns {array} Status codes.
	 */
	$additional_status_codes = apply_filters(
		'srm_additional_status_codes',
		array()
	);

	if ( empty( $additional_status_codes ) ) {
		return $status_codes;
	}

	$status_code_array = $status_codes + $additional_status_codes;

	ksort( $status_code_array, SORT_NUMERIC );

	return $status_code_array;
}

/**
 * Flush redirect cache
 *
 * @since 1.8
 */
function srm_flush_cache() {
	delete_transient( '_srm_redirects_' . srm_get_max_redirects() );
	delete_transient( '_srm_redirects_graph' );
}

/**
 * Check for potential redirect loops or chains
 *
 * @since 1.8
 * @return boolean
 */
function srm_check_for_possible_redirect_loops() {
	$redirects = srm_get_redirects();

	if ( function_exists( 'wp_parse_url' ) ) {
		$current_url = wp_parse_url( home_url() );
	} else {
		$current_url = parse_url( home_url() );
	}

	$this_host = ( is_array( $current_url ) && ! empty( $current_url['host'] ) ) ? $current_url['host'] : '';

	foreach ( $redirects as $redirect ) {
		$redirect_from = $redirect['redirect_from'];

		// check redirect from against all redirect to's
		foreach ( $redirects as $compare_redirect ) {
			$redirect_to = $compare_redirect['redirect_to'];

			if ( function_exists( 'wp_parse_url' ) ) {
				$redirect_url = wp_parse_url( $redirect_to );
			} else {
				$redirect_url = parse_url( $redirect_to );
			}

			$redirect_host = ( is_array( $redirect_url ) && ! empty( $redirect_url['host'] ) ) ? $redirect_url['host'] : '';

			// check if we are redirecting locally
			if ( empty( $redirect_host ) || $redirect_host === $this_host ) {
				$redirect_from_url = preg_replace( '/(http:\/\/|https:\/\/|www\.)/i', '', home_url() . $redirect_from );
				$redirect_to_url   = $redirect_to;
				if ( ! preg_match( '/https?:\/\//i', $redirect_to_url ) ) {
					$redirect_to_url = $this_host . $redirect_to_url;
				} else {
					$redirect_to_url = preg_replace( '/(http:\/\/|https:\/\/|www\.)/i', '', $redirect_to_url );
				}

				// possible loop/chain found
				if ( $redirect_to_url === $redirect_from_url ) {
					return true;
				}
			}
		}
	}

	return false;
}

/**
 * Creates a redirect post, this function will be useful for import/exports scripts
 *
 * @param string $redirect_from Redirect from location
 * @param string $redirect_to Redirect to location
 * @param int    $status_code Redirect status code
 * @param bool   $enable_regex Whether to enable regex or not
 * @param string $post_status Post status
 * @param int    $menu_order Menu order
 * @param string $notes Notes
 * @param int    $author_id Author ID
 * @since 1.8
 * @uses wp_insert_post, update_post_meta
 * @return int|WP_Error
 */
function srm_create_redirect( $redirect_from, $redirect_to, $status_code = 302, $enable_regex = false, $post_status = 'publish', $menu_order = 0, $notes = '', $author_id = 1 ) {
	global $wpdb;

	$sanitized_redirect_from = srm_sanitize_redirect_from( $redirect_from );
	$sanitized_redirect_to   = srm_sanitize_redirect_to( $redirect_to );
	$sanitized_status_code   = absint( $status_code );
	$sanitized_enable_regex  = (bool) $enable_regex;
	$sanitized_post_status   = sanitize_key( $post_status );
	$sanitized_menu_order    = absint( $menu_order );
	$sanitized_notes         = sanitize_text_field( $notes );
	$sanitized_author_id     = absint( $author_id );

	// check and make sure no parameters are empty or invalid after sanitation
	if ( empty( $sanitized_redirect_from ) || empty( $sanitized_redirect_to ) ) {
		return new WP_Error( 'invalid-argument', esc_html__( 'Redirect from and/or redirect to arguments are invalid.', 'safe-redirect-manager' ) );
	}

	if ( ! in_array( $sanitized_status_code, srm_get_valid_status_codes(), true ) ) {
		return new WP_Error( 'invalid-argument', esc_html__( 'Invalid status code.', 'safe-redirect-manager' ) );
	}

	// Check if the redirect already exists.
	$existing_redirect = $wpdb->get_row(
		$wpdb->prepare(
			"SELECT
				fromMeta.post_id AS post_id,
				fromMeta.meta_value AS _redirect_rule_from,
				toMeta.meta_value AS _redirect_rule_to,
				statusCodeMeta.meta_value AS _redirect_rule_status_code,
				fromRegexMeta.meta_value AS _redirect_rule_from_regex,
				notesMeta.meta_value AS _redirect_rule_notes
			FROM
				$wpdb->postmeta AS fromMeta
			LEFT JOIN
    			$wpdb->postmeta AS toMeta ON fromMeta.post_id = toMeta.post_id AND toMeta.meta_key = %s
			LEFT JOIN
    			$wpdb->postmeta AS statusCodeMeta ON fromMeta.post_id = statusCodeMeta.post_id AND statusCodeMeta.meta_key = %s
			LEFT JOIN
    			$wpdb->postmeta AS fromRegexMeta ON fromMeta.post_id = fromRegexMeta.post_id AND fromRegexMeta.meta_key = %s
			LEFT JOIN
   				$wpdb->postmeta AS notesMeta ON fromMeta.post_id = notesMeta.post_id AND notesMeta.meta_key = %s
			WHERE
    			fromMeta.meta_key = %s AND fromMeta.meta_value = %s",
			'_redirect_rule_to',
			'_redirect_rule_status_code',
			'_redirect_rule_from_regex',
			'_redirect_rule_notes',
			'_redirect_rule_from',
			$sanitized_redirect_from
		)
	);

	// Create the post arguments.
	$post_args = array(
		'post_type'   => 'redirect_rule',
		'post_status' => $sanitized_post_status,
		'post_author' => $sanitized_author_id,
		'menu_order'  => $sanitized_menu_order,
	);

	if ( $existing_redirect ) {
		// Redirect exists, so update it.
		$post_args['ID'] = $existing_redirect->post_id;
		$post_id         = wp_update_post( $post_args );

		if ( 0 >= $post_id ) {
			return new WP_Error( 'error-updating', esc_html__( 'An error occurred updating the redirect.', 'safe-redirect-manager' ) );
		}
	} else {
		$post_id = wp_insert_post( $post_args );
		if ( 0 >= $post_id ) {
			return new WP_Error( 'error-creating', esc_html__( 'An error occurred creating the redirect.', 'safe-redirect-manager' ) );
		}
	}

	// Update the posts meta info.
	update_post_meta( $post_id, '_redirect_rule_from', wp_slash( $sanitized_redirect_from ) );
	update_post_meta( $post_id, '_redirect_rule_to', $sanitized_redirect_to );
	update_post_meta( $post_id, '_redirect_rule_status_code', $sanitized_status_code );
	update_post_meta( $post_id, '_redirect_rule_from_regex', $sanitized_enable_regex );
	update_post_meta( $post_id, '_redirect_rule_notes', $sanitized_notes );

	// We need to update the cache after creating this redirect.
	srm_flush_cache();

	return $post_id;
}

/**
 * Sanitize redirect to path
 *
 * The only difference between this function and just calling esc_url_raw is
 * esc_url_raw( 'test' ) == 'http://test', whereas sanitize_redirect_path( 'test' ) == '/test'
 *
 * @since 1.8
 * @param string $path Path to sanitize
 * @return string
 */
function srm_sanitize_redirect_to( $path ) {
	$path = trim( $path );

	if ( preg_match( '/^www\./i', $path ) ) {
		$path = 'http://' . $path;
	}

	if ( ! preg_match( '/^https?:\/\//i', $path ) ) {
		if ( strpos( $path, '/' ) !== 0 ) {
			$path = '/' . $path;
		}
	}

	return esc_url_raw( $path );
}

/**
 * Sanitize redirect from path
 *
 * @since 1.8
 * @param string  $path Path to sanitize
 * @param boolean $allow_regex Whether to allow regex
 * @return string
 */
function srm_sanitize_redirect_from( $path, $allow_regex = false ) {

	$path = trim( $path );

	if ( empty( $path ) ) {
		return '';
	}

	// dont accept paths starting with a .
	if ( ! $allow_regex && strpos( $path, '.' ) === 0 ) {
		return '';
	}

	// turn path in to absolute
	if ( preg_match( '/https?:\/\//i', $path ) ) {
		$path = preg_replace( '/^(http:\/\/|https:\/\/)(www\.)?[^\/?]+\/?(.*)/i', '/$3', $path );
	} elseif ( ! $allow_regex && strpos( $path, '/' ) !== 0 ) {
		$path = '/' . $path;
	}

	// the @ symbol will break our regex engine
	$path = str_replace( '@', '', $path );

	return $path;
}

/**
 * Imports redirects from CSV file or stream.
 *
 * @since 1.8
 *
 * @access public
 * @param string|resource $file File path, file pointer or stream to read redirects from.
 * @param array           $args The array of arguments. Includes column mapping and CSV settings.
 * @return array Returns importing statistic on success, otherwise FALSE.
 */
function srm_import_file( $file, $args ) {
	$handle       = $file;
	$close_handle = false;
	$doing_wp_cli = defined( 'WP_CLI' ) && WP_CLI;

	/**
	 * Import file arguments
	 *
	 * @hook srm_import_file_args
	 * @param {array} $file_arguments File arguments.
	 * @returns {array} Filtered file arguments.
	 */
	$args = apply_filters( 'srm_import_file_args', $args );

	// enable line endings auto detection
	if ( version_compare( PHP_VERSION, '8.1', '<' ) ) {
		@ini_set( 'auto_detect_line_endings', true ); // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.auto_detect_line_endingsDeprecated
	}

	// open file pointer if $file is not a resource
	if ( ! is_resource( $file ) ) {
		$handle = fopen( $file, 'rb' );
		if ( ! $handle ) {
			$doing_wp_cli && WP_CLI::error( sprintf( 'Error retrieving %s file', basename( $file ) ) );
			return false;
		}

		$close_handle = true;
	}

	// process all rows of the file
	$created = 0;
	$skipped = 0;
	$headers = fgetcsv( $handle );

	while ( ( $row = fgetcsv( $handle ) ) ) {
		// validate
		$rule = is_array( $row ) ? array_combine( $headers, $row ) : array();
		if ( empty( $rule ) || empty( $rule[ $args['source'] ] ) || empty( $rule[ $args['target'] ] ) ) {
			$doing_wp_cli && WP_CLI::warning( 'Skipping - redirection rule is formatted improperly.' );
			$skipped++;
			continue;
		}

		// sanitize
		$redirect_from = srm_sanitize_redirect_from( $rule[ $args['source'] ] );
		$redirect_to   = srm_sanitize_redirect_to( $rule[ $args['target'] ] );
		$status_code   = ! empty( $rule[ $args['code'] ] ) ? $rule[ $args['code'] ] : 302;
		$regex         = ! empty( $rule[ $args['regex'] ] ) ? filter_var( $rule[ $args['regex'] ], FILTER_VALIDATE_BOOLEAN ) : false;
		$menu_order    = ! empty( $rule[ $args['order'] ] ) ? $rule[ $args['order'] ] : 0;
		$notes         = ! empty( $rule[ $args['notes'] ] ) ? $rule[ $args['notes'] ] : '';

		// import
		$id = srm_create_redirect( $redirect_from, $redirect_to, $status_code, $regex, 'publish', $menu_order, $notes );

		if ( is_wp_error( $id ) ) {
			$doing_wp_cli && WP_CLI::warning( $id );
			$skipped++;
		} else {
			$doing_wp_cli && WP_CLI::line( "Success - Created redirect from '{$redirect_from}' to '{$redirect_to}'" );
			$created++;
		}
	}

	// close an open file pointer if we've opened it
	if ( $close_handle ) {
		fclose( $handle );
	}

	// return result statistic
	return array(
		'created' => $created,
		'skipped' => $skipped,
	);
}

/**
 * Tries to match a redirect given a path. Return the redirect array or false on failure.
 *
 * @param string $path The path to check redirects for.
 *
 * @return array|bool {
 *  Redirect array config.
 *
 *  @type string    $redirect_to    The redirect to url.
 *  @type int       $status_code    The redirect status code.
 *  @type bool      $enable_regex   Whether this redirect has regex enabled or not.
 * }
 */
function srm_match_redirect( $path ) {
	return SRM_Redirect::factory()->match_redirect( $path );
}

/**
 * Get maximum supported redirects.
 *
 * @since 2.0.0
 * @return int
 */
function srm_get_max_redirects() {
	/**
	 * Filter maximum supported redirects.
	 *
	 * @hook srm_max_redirects
	 * @param {int} $max_redirect Maximum supported redirects. Default is `1000`.
	 * @returns {int} Maximum supported redirects.
	 */
	return apply_filters( 'srm_max_redirects', 1000 );
}
