<?php

namespace WPSecurityNinja\Plugin;

class wf_sn_af_fix_db_table_prefix_check extends wf_sn_af {


	/**
	 * get_label.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Sunday, October 29th, 2023.
	 * @access  static
	 * @param   mixed   $label
	 * @return  void
	 */
	static function get_label( $label ) {
		global $wpdb;
		$current_prefix  = $wpdb->prefix;
		$example_prefix  = $current_prefix . 'secure_';
		$labels          = array(
			'title'   => esc_html__( 'Change database table prefix', 'security-ninja' ),
			'fixable' => true,
			'info'    => esc_html__( 'Knowing the names of your database tables can help an attacker dump the table\'s data and get to sensitive information like password hashes. Since WP table names are predefined, the only way you can change table names is by using a unique prefix. This feature works with any current prefix, not just the default "wp_". WARNING: Make sure the wp-config.php is writable. Enter your new desired table prefix:', 'security-ninja' ) . ' ' .
						'<input type="text" name="new_table_prefix" value="" placeholder="' . esc_attr( $example_prefix ) . '" required="required" />',
			'msg_ok'  => esc_html__( 'Prefix changed successfully', 'security-ninja' ),
			'msg_bad' => esc_html__( 'Could not change table prefix', 'security-ninja' ),
		);

		if ( ! array_key_exists( $label, $labels ) ) {
			return '';
		} else {
			return $labels[ $label ];
		}
	}

	/**
	 * fix.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Sunday, October 29th, 2023.
	 * @access  static
	 * @return  boolean
	 */
	public static function fix() {
		global $wpdb;
		// Check for nonce for security (assuming a nonce has been created and sent with the form)
		if ( ! isset( $_POST['_ajax_nonce'] ) || ! wp_verify_nonce( $_POST['_ajax_nonce'], 'wf_sn_do_fix' ) ) {
			return esc_html__( 'Security check failed.', 'security-ninja' );
			exit();
		}

		// Check if fields are set
		if ( ! isset( $_POST['fields'] ) ) {
			return esc_html__( 'No fields submitted.', 'security-ninja' );
			exit();
		}

		$fields = json_decode( stripslashes( $_POST['fields'] ), true );

		// Sanitize the new table prefix
		$new_table_prefix = isset( $fields['new_table_prefix'] ) ? sanitize_text_field( $fields['new_table_prefix'] ) : '';

		$current_prefix = $wpdb->prefix;

		if ( '' === $new_table_prefix ) {
			return esc_html__( 'Enter a table prefix.', 'security-ninja' );
		}

		// Ensure the prefix ends with an underscore
		$newprefix = rtrim( $new_table_prefix, '_' ) . '_';

		// Validate prefix format (alphanumeric + underscore only)
		if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $newprefix ) ) {
			return esc_html__( 'Invalid prefix format. Prefix can only contain letters, numbers, and underscores.', 'security-ninja' );
		}

		// Validate prefix length (max 60 characters to avoid MySQL identifier limits)
		if ( strlen( $newprefix ) > 60 ) {
			return esc_html__( 'Prefix is too long. Maximum length is 60 characters.', 'security-ninja' );
		}

		// Don't allow changing to the same prefix
		if ( $current_prefix === $newprefix ) {
			return sprintf( esc_html__( 'The new prefix must be different from the current prefix (%s).', 'security-ninja' ), esc_attr( $current_prefix ) );
		}

		// Check if file can be edited
		global $wp_filesystem;
		include_once ABSPATH . 'wp-admin/includes/file.php';
		// If for some reason the include doesn't work as expected just return false.
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			return false;
		}

		$writable = is_writable( wf_sn_af::$wp_config_path );

		if ( ! $writable ) {
			return esc_html__( 'Cannot edit wp-config.php', 'security-ninja' );
			exit();
		}

		// Get current prefix length for dynamic table name extraction
		$prefix_length = strlen( $current_prefix );

		// get a list of all tables in the database
		$tables = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM information_schema.tables WHERE table_schema = %s', DB_NAME ) );

		// filter out all tables with the current prefix
		$table_names = array();
		foreach ( $tables as $table_info ) {
			$table_name = $table_info->TABLE_NAME;
			// Check if table starts with current prefix
			if ( strpos( $table_name, $current_prefix ) === 0 ) {
				$table_names[] = $table_name;
			}
		}

		// Validate that we found tables
		if ( empty( $table_names ) ) {
			return sprintf( esc_html__( 'No tables found with the current prefix (%s).', 'security-ninja' ), esc_attr( $current_prefix ) );
		}

		// for each table with current prefix, make a copy with the new desired prefix
		$failed        = false;
		$table_mapping = array(); // Store old -> new mapping for cleanup
		foreach ( $table_names as $table ) {
			// Extract table name without prefix
			$table_suffix   = substr( $table, $prefix_length );
			$new_table_name = $newprefix . $table_suffix;

			// Store mapping for later use
			$table_mapping[ $table ] = $new_table_name;

			if ( false === $wpdb->query( 'CREATE TABLE `' . esc_sql( $new_table_name ) . '` LIKE `' . esc_sql( $table ) . '`' ) ) {
				$failed = true;
				break;
			} elseif ( false === $wpdb->query( 'INSERT INTO `' . esc_sql( $new_table_name ) . '` SELECT * FROM `' . esc_sql( $table ) . '`' ) ) {
				$failed = true;
				break;
			}
		}

		// if copying any of the tables failed abort and remove any created tables
		if ( $failed ) {
			foreach ( $table_mapping as $old_table => $new_table ) {
				$wpdb->query( 'DROP TABLE IF EXISTS `' . esc_sql( $new_table ) . '`' );
			}
			return self::get_label( 'msg_bad' );
		}

		// update prefix in _usermeta and _options
		// Only replace the table prefix at the beginning, not all occurrences
		// Use dynamic prefix length instead of hardcoded values
		$old_prefix_escaped = esc_sql( $current_prefix );
		$new_prefix_escaped = esc_sql( $newprefix );
		$substring_offset   = $prefix_length + 1; // +1 because we want to skip the prefix and the underscore

		// Update usermeta table - replace prefix in meta_key values
		$usermeta_table = esc_sql( $newprefix . 'usermeta' );
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE `{$usermeta_table}` SET meta_key = CONCAT(%s, SUBSTRING(meta_key, %d)) WHERE meta_key LIKE %s",
				$new_prefix_escaped,
				$substring_offset,
				$old_prefix_escaped . '_%'
			)
		);

		// Update options table - replace prefix in option_name values
		$options_table = esc_sql( $newprefix . 'options' );
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE `{$options_table}` SET option_name = CONCAT(%s, SUBSTRING(option_name, %d)) WHERE option_name LIKE %s",
				$new_prefix_escaped,
				$substring_offset,
				$old_prefix_escaped . '_%'
			)
		);

		// update wp_config
		$backup_timestamp = time();

		wf_sn_af::backup_file( wf_sn_af::$wp_config_path, $backup_timestamp, 'db_table_prefix_check' );
		wf_sn_af::edit_variable( wf_sn_af::$wp_config_path, 'table_prefix', '\'' . $newprefix . '\'' );

		// test if WordPress works, if not restore everything and drop created tables
		$no_wsod = wf_sn_af::test_wordpress_status();

		if ( ! $no_wsod ) {
			wf_sn_af::backup_file_restore( wf_sn_af::$wp_config_path, $backup_timestamp, 'db_table_prefix_check' );
			foreach ( $table_mapping as $old_table => $new_table ) {
				$wpdb->query( 'DROP TABLE IF EXISTS `' . esc_sql( $new_table ) . '`' );
			}
			return esc_html__( 'WordPress self-check failed; prefix change was rolled back. Check that your site can reach itself via HTTP.', 'security-ninja' );
		}

		// Success! Drop old tables
		foreach ( $table_names as $table ) {
			$wpdb->query( 'DROP TABLE IF EXISTS `' . esc_sql( $table ) . '`' );
		}
		wf_sn_af::mark_as_fixed( 'db_table_prefix_check' );

		return array(
			'message'    => sprintf(
				/* translators: %s: new database table prefix */
				esc_html__( 'Prefix changed successfully to %s', 'security-ninja' ),
				esc_html( $newprefix )
			),
			'new_prefix' => $newprefix,
		);
	}
}
