<?php
/*
Plugin Name: Local WP Live Link Helper
Plugin URI: https://localwp.com
Description: Makes WordPress URL functions relative to play nicely with Live Links.
Version: 2.0
Author: Flywheel
Author URI: http://getflywheel.com
License: GPLv2 or later
*/

class LocalWP_Live_Link_Helper {
	/**
	 * host/domain parsed from 'home' option
	 *
	 * @var string|null
	 */
    public $home_domain;

	public function __construct() {
		$this->home_domain = str_replace( '/^www./', '', parse_url( get_option( 'home' ), PHP_URL_HOST ) );

		$this->add_filters();
	}

	/**
	 * @return string Host from server along with port re-added if using localhost routing mode.
	 */
	public function get_local_host() {
		$original = str_replace( '/^www./', '', $_SERVER['HTTP_HOST'] );

		/**
		 * If using localhost and the port isn't in HTTP_HOST then it needs to be re-added.
		 */
		if ( $original === 'localhost' ) {
			$original .= ':' . $_SERVER['SERVER_PORT'];
		}

		return $original;
	}

	/**
	 * Convenience method to get tunnel domain from headers.
	 *
	 * @return string
	 */
	public function get_tunnel_host() {
		return $_SERVER['HTTP_X_ORIGINAL_HOST'];
	}

	/**
	 * @return void
	 */
	public function add_filters() {
		/**
		 * Do not add any of these filters if the X-Original-Host header is missing.
		 */
		if ( empty( $_SERVER['HTTP_X_ORIGINAL_HOST'] ) ) {
			return;
		}

		add_action( 'send_headers', array( $this, 'send_private_cache_control_header' ), 9999 );
		add_action('send_headers', array($this, 'send_local_host_header'), 9999);

		remove_action( 'template_redirect', 'redirect_canonical' );

		$local_to_tunnel_filters = array(
			'get_rest_url',
			'wp_redirect',
			'bloginfo_url',
			'the_permalink',
			'wp_list_pages',
			'wp_list_categories',
			'the_content_more_link',
			'the_tags',
			'the_author_posts_link',
			'post_link',
			'post_type_link',
			'page_link',
			'attachment_link',
			'get_shortlink',
			'post_type_archive_link',
			'get_pagenum_link',
			'get_comments_pagenum_link',
			'term_link',
			'search_link',
			'day_link',
			'month_link',
			'year_link',
			'option_siteurl',
			'blog_option_siteurl',
			'option_home',
			'admin_url',
			'get_admin_url',
			'get_site_url',
			'network_admin_url',
			'home_url',
			'includes_url',
			'site_url',
			'site_option_siteurl',
			'network_home_url',
			'network_site_url',
			'get_the_author_url',
			'get_comment_link',
			'wp_get_attachment_image_src',
			'wp_get_attachment_thumb_url',
			'wp_get_attachment_url',
			'wp_login_url',
			'wp_logout_url',
			'wp_lostpassword_url',
			'get_stylesheet_uri',
			'get_locale_stylesheet_uri',
			'script_loader_src',
			'style_loader_src',
			'get_theme_root_uri',
			'theme_root_uri',
			'plugins_url',
			'stylesheet_directory_uri',
			'template_directory_uri'
		);

		foreach ( $local_to_tunnel_filters as $local_to_tunnel_filter ) {
			add_filter( $local_to_tunnel_filter, array( $this, 'make_link_tunnel' ) );
		}

		add_filter( 'pre_update_option', array( $this, 'make_link_local' ) );
		add_filter( 'wp_insert_post_data', array( $this, 'make_link_local_in_posts' ), 9999 );
	}

	/**
	 * Prevent possible cache poisoning by sending Cache-Control private header whenever the domain is replaced.
	 *
	 * @return void
	 */
	public function send_private_cache_control_header() {
		header( 'Cache-Control: private' );
	}

	/**
     * Send original domain so the tunnel server can perform a replacement with it.
     */
    public function send_local_host_header()
    {
        header('X-Local-Host: ' . $this->get_local_host());
    }

	/**
	 * Convenience method for replacing old host with new host.
	 *
	 * @param string $old Host to be replaced
	 * @param string $new Host to use as replacement
	 * @param string $subject Replacement subject
	 */
	public function replace_host( $old, $new, $subject ) {
		$subject = str_replace( 'www.' . $old, $new, $subject );
		$subject = str_replace( $old, $new, $subject );

		return $subject;
	}

	/**
	 * Generic replacement of the site's local hostname to the tunnel hostname
	 *
	 * @param string $str String provided by the filter
	 *
	 * @return string String with local hostname replaced with the tunnel hostname
	 */
	public function make_link_tunnel( $str ) {
		$local_host = $this->get_local_host();
		$tunnel_host = $this->get_tunnel_host();

		$str = $this->replace_host( $local_host, $tunnel_host, $str );
		$str = $this->replace_host( $this->home_domain, $tunnel_host, $str );

		/**
		 * Force HTTPS, but not for local dev testing setup ($tunnel_host ends with tunnel.testing:<port> )
		 */
		if (!preg_match('/^.*tunnel\.testing:\d+$/i', $tunnel_host)) {
			$str = str_replace( 'http://' . $tunnel_host, 'https://' . $tunnel_host, $str );
		}

		return $str;
	}

	/**
	 * Generic replacement of the site's tunnel hostname to the local hostname,
	 * used when saving options to the database via the pre_update_option filter hook
	 *
	 * @param mixed $option Option being saved, provided by the filter. Will
	 * 						usually be a serialized string, but may be an
	 * 						unserialized type in some cases. See:
	 * 						https://developer.wordpress.org/reference/functions/get_option/
	 */
	public function make_link_local( $option ) {
		if (gettype($option) !== 'string') {
			$old_str = serialize($option);
			$new_str = $this->replace_host( $this->get_tunnel_host(), $this->home_domain, $old_str );
			return unserialize($new_str);
		}

		return $this->replace_host( $this->get_tunnel_host(), $this->home_domain, $option );
	}

	/**
	 * Go through post properties and replace the tunnel host with the local host
	 *
	 * @param WP_Post $post
	 */
	public function make_link_local_in_posts( $post ) {
		$post['post_content'] = $this->make_link_local( $post['post_content'] );

		return $post;
	}
}

new LocalWP_Live_Link_Helper();
