<?php

/**
 * Modifications by: Mikołaj `iClyde` Chodorowski
 * Modified by e-mail contact: kontakt@iclyde.pl
 * Package: Safe Search and Replace on Database with Serialized Data v2.0.1
 *
 * Please contact above e-mail if you see any credits problem.
 */

// Namespace
namespace BMI\Plugin\Database;

// Use
use BMI\Plugin\BMI_Logger AS Logger;
use BMI\Plugin\Backup_Migration_Plugin as BMP;
use BMI\Plugin\Progress\BMI_ZipProgress AS Progress;

// Exit on direct access
if (!defined('ABSPATH')) exit;

/**
 * Database Search and Replace for Enginge v3
 */
class BMI_Search_Replace_Engine {

  function __construct($tables, $currentPage = 0, $totalPages = 0, $isStaging = false) {

    $this->totalPages = $totalPages;
    $this->currentPage = $currentPage;
  	$this->all_tables = $tables;
    $this->isStaging = $isStaging;

  }

  public function perform($search, $replace, $limitColumns = false) {

    return $this->search_replace($search, $replace, $this->all_tables, $limitColumns);

  }

  private function recursive_unserialize_replace($from = '', $to = '', $data = '', $serialised = false, $visited = []) {

    if (is_array($data) || is_object($data)) {
      if (in_array($data, $visited, true)) {
        return $data;
      }
      $visited[] = $data;
    }
  	try {

  		if (is_string($data) && is_serialized($data) && ($unserialized = @unserialize($data, ['allowed_classes' => ['stdClass']])) !== false) {

  			$data = $this->recursive_unserialize_replace($from, $to, $unserialized, true);

  		} else if (is_array($data)) {

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

        $data = $_tmp;
        unset($_tmp);
      } else if (is_object($data) && !is_a($data, '__PHP_Incomplete_Class')) {
        $tmp = $data;
        $props = get_object_vars($data);
        foreach ($props as $key => $value) {
          $tmp->$key = $this->recursive_unserialize_replace($from, $to, $value, false, $visited);
        }
        $data = $tmp;
        unset($tmp);
      } else if (is_string($data) && (null !== ($_tmp = json_decode($data, true))) && is_array($_tmp)) {
        foreach ($_tmp as $key => $value) {
          $_tmp[$key] = $this->recursive_unserialize_replace($from, $to, $value, false, $visited);
        }
        $data = json_encode($_tmp);
        unset($_tmp);
      } else if (is_string($data)) {
        $data = str_replace($from, $to, $data);
      }

  		if ($serialised) return serialize($data);

  	}
    catch (\Exception $error) {}
    catch (\Throwable $error) {}

  	return $data;

  }

  private function search_replace($search = '', $replace = '', $tables = [], $limitColumns = false) {

    global $wpdb;

    $excluded_columns = ['id', 'ID'];
  	$report = ['tables' => 0, 'rows' => 0, 'change' => 0, 'updates' => 0, 'currentPage' => 0, 'totalPages' => 0];

    if ($this->currentPage !== 0 && is_numeric($this->currentPage)) {
      $report['currentPage'] = $this->currentPage;
    }

    if ($this->totalPages !== 0 && is_numeric($this->totalPages)) {
      $report['totalPages'] = $this->totalPages;
    }

  	if (is_array($tables) && !empty($tables)) {

      $wpdb->show_errors();

  		foreach($tables as $table) {

  			$report['tables']++;
  			$columns = [];

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
		    $fields = $wpdb->get_results('DESCRIBE ' . BMP::escapeSQLIDentifier($table) . ';');
        foreach ($fields as $index => $object) {
          $columns[$object->Field] = $object->Key == 'PRI' ? true : false;
        }

        $fieldsForWhereStmt = [];

        foreach ($fields as $index => $columnInfo) {
          $type = strtolower($columnInfo->Type);
          if (strpos($type, 'char') !== false || strpos($type, 'text') !== false) {
            $column = mysqli_real_escape_string($wpdb->dbh, $columnInfo->Field);
            if (!in_array($column, $fieldsForWhereStmt)) {
              $fieldsForWhereStmt[] = $column;
            }
          }
        }

        $whereStmt = '';
        $totalColumns = sizeof($fieldsForWhereStmt);
        for ($i = 0; $i < $totalColumns; ++$i) {
          $column = $fieldsForWhereStmt[$i];
          if ($i == 0) $whereStmt .= ' WHERE ';
          // if ($this->isStaging) {
            $whereStmt .= '(' . BMP::escapeSQLIDentifier($column) . ' LIKE "' . esc_sql('%' . $wpdb->esc_like($search) . '%') . '"';
            $whereStmt .= ' AND ' . BMP::escapeSQLIDentifier($column) . ' NOT LIKE "' . esc_sql('%' . $wpdb->esc_like($replace) . '%') . '")';
          // } else {
          //   $whereStmt .= '`' . $column . '`' . ' LIKE ' . '"%' . mysqli_real_escape_string($wpdb->dbh, $search) . '%"';
          // }
          if ($i != $totalColumns - 1) $whereStmt .= ' OR ';
        }

        if ($whereStmt === '') continue;
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier and where clause are safely escaped via escapeSQLIDentifier() and esc_sql()
        $row_count = $wpdb->get_results('SELECT COUNT(*) AS num FROM ' . BMP::escapeSQLIDentifier($table) . $whereStmt . ';');
        $row_count = $row_count[0]->num;
        if ($row_count == 0) {
          $report['currentPage'] = $report['currentPage'] + 1;
          continue;
        }

  			$page_size = BMI_MAX_SEARCH_REPLACE_PAGE;
  			$pages = ceil($row_count / $page_size);
        $page = 0;

        if ($report['totalPages'] === 0) {
          $report['totalPages'] = $pages;
        }

  			for ($page; $page < $pages; $page++) {

          $report['currentPage'] = $report['currentPage'] + 1;

  				$current_row = 0;
  				$start = $page * $page_size;
  				$end = $start + $page_size;

          // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier and where clause are safely escaped via escapeSQLIDentifier() and esc_sql()
          $data = $wpdb->get_results($wpdb->prepare('SELECT * FROM ' . BMP::escapeSQLIDentifier($table) . $whereStmt . ' LIMIT %d, %d;', $start, $page_size));
          for ($i = 0; $i < sizeof($data); ++$i) {

            $row = $data[$i];
            $report['rows']++;
            $current_row++;

            $update_sql = [];
            $where_sql = [];
            $upd = false;

            foreach ($columns as $column => $primary_key) {

              if ($limitColumns != false) {
                if (!in_array($column, $limitColumns)) continue;
              }
              if (in_array($column, $excluded_columns)) continue;
              if (!in_array($column, $fieldsForWhereStmt)) continue;

              $edited_data = $data_to_fix = $row->$column;
              $edited_data = $this->recursive_unserialize_replace($search, $replace, $data_to_fix);

              if ($edited_data != $data_to_fix) {
  							$report['change']++;
  							$update_sql[] = '' . BMP::escapeSQLIDentifier($column) . ' = "' . esc_sql($edited_data) . '"';
  							$upd = true;
                $where_sql[] = '' . BMP::escapeSQLIDentifier($column) . ' = "' . esc_sql($data_to_fix) . '"';
  						}

            }

            if ($upd && !empty($where_sql)) {
              $sql = 'UPDATE ' . BMP::escapeSQLIDentifier($table) . ' SET ' . implode(', ', $update_sql) . ' WHERE ' . implode(' AND ', array_filter($where_sql)) . ';';
              // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifiers and values are safely escaped via escapeSQLIDentifier() and esc_sql()
              $wpdb->get_results($sql);

              unset($sql);

              $report['updates']++;

              if ($wpdb->last_error !== '') {
                // error_log(strval($wpdb->last_query));
                // error_log(strval($wpdb->last_result));
                // error_log(strval($wpdb->last_error));
              }
            }

            unset($row);

          }

          unset($data);
          return $report;

  			}

  		}

  	}

  	return $report;
  }

}
