<?php

final class WPMDB_Replace {
	protected $search;
	protected $replace;
	protected $subdomain_replaces_on;
	protected $wpmdb;
	protected $intent;
	protected $base_domain;
	protected $site_domain;
	protected $site_details;
	protected $source_protocol;
	protected $destination_protocol;
	protected $destination_url;
	protected $is_protocol_mismatch = false;

	private $table;
	private $column;
	private $row;

	function __construct( $args ) {
		$keys = array( 'table', 'search', 'replace', 'intent', 'base_domain', 'site_domain', 'wpmdb', 'site_details' );

		if ( ! is_array( $args ) ) {
			throw new InvalidArgumentException( 'WPMDB_Replace constructor expects the argument to be an array' );
		}

		foreach ( $keys as $key ) {
			if ( ! isset( $args[ $key ] ) ) {
				throw new InvalidArgumentException( "WPMDB_Replace constructor expects '$key' key to be present in the array argument" );
			}
		}

		$this->table        = $args['table'];
		$this->search       = $args['search'];
		$this->replace      = $args['replace'];
		$this->intent       = $args['intent'];
		$this->base_domain  = $args['base_domain'];
		$this->site_domain  = $args['site_domain'];
		$this->wpmdb        = $args['wpmdb'];
		$this->site_details = $args['site_details'];

		// Detect a protocol mismatch between the remote and local sites involved in the migration
		$this->detect_protocol_mismatch();
	}

	/**
	 * Determine whether to apply a subdomain replace over each value in the database.
	 *
	 * @return bool
	 */
	function is_subdomain_replaces_on() {
		if ( ! isset( $this->subdomain_replaces_on ) ) {
			$this->subdomain_replaces_on = ( is_multisite() && is_subdomain_install() && ! $this->has_same_base_domain() && apply_filters( 'wpmdb_subdomain_replace', true ) );
		}

		return $this->subdomain_replaces_on;
	}


	/**
	 * Determine if the replacement has the same base domain as the search. Produces doubled replacement strings
	 * otherwise.
	 *
	 * @return bool
	 */
	function has_same_base_domain() {
		$destination_url = isset( $this->destination_url ) ? $this->destination_url : $this->site_details['local']['site_url'];
		if ( stripos( $destination_url, $this->site_domain ) ) {
			return true;
		}

		return false;
	}


	/**
	 * Automatically replace URLs for subdomain based multisite installations
	 * e.g. //site1.example.com -> //site1.example.local for site with domain example.com
	 * NB: only handles the current network site, does not work for additional networks / mapped domains
	 *
	 * @param $new
	 *
	 * @return mixed
	 */
	function subdomain_replaces( $new ) {
		if ( empty( $this->base_domain ) ) {
			return $new;
		}

		$pattern     = '|//(.*?)\\.' . preg_quote( $this->site_domain, '|' ) . '|';
		$replacement = '//$1.' . trim( $this->base_domain );
		$new         = preg_replace( $pattern, $replacement, $new );

		return $new;
	}

	/**
	 * Detect a protocol mismatch between the remote and local sites involved in the migration
	 *
	 * @return bool
	 */
	function detect_protocol_mismatch() {
		if ( ! isset( $this->site_details['remote'] ) ) {
			return false;
		}

		/**
		 * Filters the site_urls used to check if there is a protocol mismatch.
		 *
		 * @param array
		 */
		$wpmdb_home_urls = apply_filters( 'wpmdb_replace_site_urls', array(
				// TODO: rewrite unit tests that only pass site_url so that we can rely on home_url's existence
				'local'  => isset( $this->site_details['local']['home_url'] ) ? $this->site_details['local']['home_url'] : $this->site_details['local']['site_url'],
				'remote' => isset( $this->site_details['remote']['home_url'] ) ? $this->site_details['remote']['home_url'] : $this->site_details['remote']['site_url'],
			)
		);

		$local_url_is_https  = false === stripos( $wpmdb_home_urls['local'], 'https' ) ? false : true;
		$remote_url_is_https = false === stripos( $wpmdb_home_urls['remote'], 'https' ) ? false : true;
		$local_protocol      = $local_url_is_https ? 'https' : 'http';
		$remote_protocol     = $remote_url_is_https ? 'https' : 'http';

		if ( ( $local_url_is_https && ! $remote_url_is_https ) || ( ! $local_url_is_https && $remote_url_is_https ) ) {
			$this->is_protocol_mismatch = true;
		}

		if ( 'push' === $this->intent ) {
			$this->destination_protocol = $remote_protocol;
			$this->source_protocol      = $local_protocol;
			$this->destination_url      = $wpmdb_home_urls['remote'];
		} else {
			$this->destination_protocol = $local_protocol;
			$this->source_protocol      = $remote_protocol;
			$this->destination_url      = $wpmdb_home_urls['local'];
		}

		return $this->is_protocol_mismatch;
	}

	/**
	 *
	 * Handles replacing the protocol if the local and destination don't have matching protocols (http > https and
	 * vice-versa).
	 *
	 * Can be filtered to disable entirely.
	 *
	 * @param $new
	 *
	 * @return mixed
	 */
	function do_protocol_replace( $new ) {
		/**
		 * Filters $do_protocol_replace, return false to prevent protocol replacement.
		 *
		 * @param bool   true                   If the replace should be skipped.
		 * @param string $this->destination_url The URL of the target site.
		 */
		$do_protocol_replace = apply_filters( 'wpmdb_replace_destination_protocol', true, $this->destination_url );

		if ( true !== $do_protocol_replace ) {
			return $new;
		}

		$parsed_destination = wp_parse_url( $this->destination_url );
		unset( $parsed_destination['scheme'] );

		$protocol_search      = $this->source_protocol . '://' . implode( '', $parsed_destination );
		$protocol_replace     = $this->destination_url;
		$new                  = str_ireplace( $protocol_search, $protocol_replace, $new, $count );

		return $new;
	}

	/**
	 * Applies find/replace pairs to a given string.
	 *
	 * @param string $subject
	 *
	 * @return string
	 */
	function apply_replaces( $subject ) {
		$new = str_ireplace( $this->search, $this->replace, $subject, $count );
		if ( $this->is_subdomain_replaces_on() ) {
			$new = $this->subdomain_replaces( $new );
		}

		if ( true === $this->is_protocol_mismatch ) {
			$new = $this->do_protocol_replace( $new );
		}

		return $new;
	}

	/**
	 * Take a serialized array and unserialize it replacing elements as needed and
	 * unserialising any subordinate arrays and performing the replace on those too.
	 *
	 * Mostly from https://github.com/interconnectit/Search-Replace-DB
	 *
	 * @param mixed $data Used to pass any subordinate arrays back to in.
	 * @param bool $serialized Does the array passed via $data need serialising.
	 * @param bool $parent_serialized Passes whether the original data passed in was serialized
	 * @param bool $filtered Should we apply before and after filters successively
	 *
	 * @return mixed    The original array with all elements replaced as needed.
	 */
	function recursive_unserialize_replace( $data, $serialized = false, $parent_serialized = false, $filtered = true ) {
		$pre = apply_filters( 'wpmdb_pre_recursive_unserialize_replace', false, $data, $this );
		if ( false !== $pre ) {
			return $pre;
		}

		$is_json           = false;
		$before_fired      = false;
		$successive_filter = $filtered;

		if ( true === $filtered ) {
			list( $data, $before_fired, $successive_filter ) = apply_filters( 'wpmdb_before_replace_custom_data', array( $data, $before_fired, $successive_filter ), $this );
		}

		// some unserialized data cannot be re-serialized eg. SimpleXMLElements
		try {
			if ( is_string( $data ) && ( $unserialized = WPMDB_Utils::unserialize( $data, __METHOD__ ) ) !== false ) {
				// PHP currently has a bug that doesn't allow you to clone the DateInterval / DatePeriod classes.
				// We skip them here as they probably won't need data to be replaced anyway
				if ( is_object( $unserialized ) ) {
					if ( $unserialized instanceof DateInterval || $unserialized instanceof DatePeriod ) {
						return $data;
					}
				}
				$data = $this->recursive_unserialize_replace( $unserialized, true, true, $successive_filter );
			} elseif ( is_array( $data ) ) {
				$_tmp = array();
				foreach ( $data as $key => $value ) {
					$_tmp[ $key ] = $this->recursive_unserialize_replace( $value, false, $parent_serialized, $successive_filter );
				}

				$data = $_tmp;
				unset( $_tmp );
			} elseif ( is_object( $data ) ) { // Submitted by Tina Matter
				$_tmp = clone $data;
				foreach ( $data as $key => $value ) {
					// Integer properties are crazy and the best thing we can do is to just ignore them.
					// see http://stackoverflow.com/a/10333200 and https://github.com/deliciousbrains/wp-migrate-db-pro/issues/853
					if ( is_int( $key ) ) {
						continue;
					}
					$_tmp->$key = $this->recursive_unserialize_replace( $value, false, $parent_serialized, $successive_filter );
				}

				$data = $_tmp;
				unset( $_tmp );
			} elseif ( $this->wpmdb->is_json( $data, true ) ) {
				$_tmp = array();
				$data = json_decode( $data, true );

				foreach ( $data as $key => $value ) {
					$_tmp[ $key ] = $this->recursive_unserialize_replace( $value, false, $parent_serialized, $successive_filter );
				}

				$data = $_tmp;
				unset( $_tmp );
				$is_json = true;
			} elseif ( is_string( $data ) ) {
				list( $data, $do_replace ) = apply_filters( 'wpmdb_replace_custom_data', array( $data, true ), $this );

				if ( $do_replace ) {
					$data = $this->apply_replaces( $data );
				}
			}

			if ( $is_json ) {
				$data = json_encode( $data );
			}

			if ( $serialized ) {
				$data = serialize( $data );
			}
		} catch ( Exception $error ) {
			$error_msg     = __( 'Failed attempting to do the recursive unserialize replace. Please contact support.', 'wp-migrate-db' );
			$error_details = $error->getMessage() . "\n\n";
			$error_details .= var_export( $data, true );
			$this->wpmdb->log_error( $error_msg, $error_details );
		}

		if ( true === $filtered ) {
			$data = apply_filters( 'wpmdb_after_replace_custom_data', $data, $before_fired, $this );
		}

		return $data;
	}

	/**
	 * Getter for the $table class property.
	 *
	 * @return string Name of the table currently being processed in the migration.
	 */
	public function get_table() {
		return $this->table;
	}

	/**
	 * Getter for the $column class property.
	 *
	 * @return string Name of the column currently being processed in the migration.
	 */
	public function get_column() {
		return $this->column;
	}

	/**
	 * Getter for the $row class property.
	 *
	 * @return string Name of the row currently being processed in the migration.
	 */
	public function get_row() {
		return $this->row;
	}

	/**
	 * Setter for the $column class property.
	 *
	 * @param string $column Name of the column currently being processed in the migration.
	 */
	public function set_column( $column ) {
		$this->column = $column;
	}

	/**
	 * Setter for the $row class property.
	 *
	 * @param string $row Name of the row currently being processed in the migration.
	 */
	public function set_row( $row ) {
		$this->row = $row;
	}

	/**
	 * Multsite safe way of comparing the table currently being processed in the migration against a desired table.
	 *
	 * The table prefix should be omitted, example:
	 *
	 * $is_posts = $this->table_is( 'posts' );
	 *
	 * @param  string $desired_table Name of the desired table, table prefix omitted.
	 *
	 * @return boolean                Whether or not the desired table is the table currently being processed.
	 */
	public function table_is( $desired_table ) {
		return $this->wpmdb->table_is( $desired_table, $this->table );
	}

	/**
	 * Intent of the current replace migration.
	 *
	 * Helpful for hookers who need to know what intent they are working on.
	 *
	 * @return string Intent of the current migration
	 */
	public function get_intent() {
		return $this->intent;
	}
}
