export
3 weeks ago
better-backup-v3.php
3 weeks ago
better-backup.php
3 weeks ago
better-restore.php
3 weeks ago
even-better-restore-v3.php
3 weeks ago
even-better-restore-v4.php
3 weeks ago
interface-search-replace-repository.php
3 weeks ago
manager.php
3 weeks ago
search-replace-processor.php
3 weeks ago
search-replace-repository.php
3 weeks ago
search-replace-stack-based.php
3 weeks ago
search-replace-v2.php
3 weeks ago
search-replace.php
3 weeks ago
smart-sort.php
3 weeks ago
search-replace-stack-based.php
151 lines
| 1 | <?php |
| 2 | |
| 3 | namespace BMI\Plugin\Database; |
| 4 | |
| 5 | |
| 6 | class BMIStackSearchReplace |
| 7 | { |
| 8 | /** |
| 9 | * Main entry point. |
| 10 | * Detects the format (JSON, Serialized, or String) and routes to the correct parser. |
| 11 | */ |
| 12 | public function replace($search, $replace, $data) |
| 13 | { |
| 14 | // 1. Handle Arrays/Objects (Recursion Base) |
| 15 | if (is_array($data)) { |
| 16 | foreach ($data as $key => $value) { |
| 17 | $data[$key] = $this->replace($search, $replace, $value); |
| 18 | } |
| 19 | return $data; |
| 20 | } |
| 21 | |
| 22 | if (is_object($data)) { |
| 23 | foreach ($data as $key => $value) { |
| 24 | $data->$key = $this->replace($search, $replace, $value); |
| 25 | } |
| 26 | return $data; |
| 27 | } |
| 28 | |
| 29 | // 2. Handle Strings (The Parsing Logic) |
| 30 | if (is_string($data)) { |
| 31 | // A. Check for JSON first |
| 32 | // We use native json_decode because writing a robust JSON parser in pure PHP is inefficient. |
| 33 | // This handles the "Escaped Slashes" issue automatically. |
| 34 | if ($this->is_json($data)) { |
| 35 | $decoded = json_decode($data, true); |
| 36 | if (json_last_error() === JSON_ERROR_NONE) { |
| 37 | // Recurse into the JSON structure |
| 38 | $replaced_data = $this->replace($search, $replace, $decoded); |
| 39 | // Re-encode (handles escaping automatically) |
| 40 | return json_encode($replaced_data); |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // B. Check for Serialization |
| 45 | // If it looks serialized, we use our Custom Stack-Based Parser |
| 46 | if (is_serialized($data)) { |
| 47 | return $this->replace_serialized_stream($search, $replace, $data); |
| 48 | } |
| 49 | |
| 50 | // C. Raw String Replacement |
| 51 | // This is the leaf node of the recursion |
| 52 | if (is_array($search) && is_array($replace) && count($search) === count($replace)) { |
| 53 | return strtr($data, array_combine($search, $replace)); |
| 54 | } else { |
| 55 | return str_replace($search, $replace, $data); |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | // 3. Passthrough for other types (int, bool, null) |
| 60 | return $data; |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * A Stack-Based Parser for Serialized Strings. |
| 65 | * instead of unserializing (which creates objects), we scan the string tokens. |
| 66 | * When we find a string token 's:N:"..."', we isolate the content, recurse, and recalculate N. |
| 67 | */ |
| 68 | private function replace_serialized_stream($search, $replace, $data) |
| 69 | { |
| 70 | $result = ''; |
| 71 | $pos = 0; |
| 72 | $length = strlen($data); |
| 73 | |
| 74 | while ($pos < $length) { |
| 75 | // Find the next potential string token 's:' |
| 76 | $s_token_pos = strpos($data, 's:', $pos); |
| 77 | |
| 78 | // If no more tokens, append the rest and finish |
| 79 | if ($s_token_pos === false) { |
| 80 | $result .= substr($data, $pos); |
| 81 | break; |
| 82 | } |
| 83 | |
| 84 | // Append everything before the token (e.g., array keys, boolean markers 'b:1;') |
| 85 | $result .= substr($data, $pos, $s_token_pos - $pos); |
| 86 | |
| 87 | // Validate if this is truly a length marker (s:123:) |
| 88 | // We look for the colon after the number |
| 89 | $second_colon_pos = strpos($data, ':', $s_token_pos + 2); |
| 90 | |
| 91 | if ($second_colon_pos === false) { |
| 92 | // Malformed or just text that looks like 's:', skip it |
| 93 | $result .= 's:'; |
| 94 | $pos = $s_token_pos + 2; |
| 95 | continue; |
| 96 | } |
| 97 | |
| 98 | // Extract length integer |
| 99 | $len_str = substr($data, $s_token_pos + 2, $second_colon_pos - ($s_token_pos + 2)); |
| 100 | if (!is_numeric($len_str)) { |
| 101 | // Not a valid serialization token |
| 102 | $result .= substr($data, $s_token_pos, $second_colon_pos - $s_token_pos + 1); |
| 103 | $pos = $second_colon_pos + 1; |
| 104 | continue; |
| 105 | } |
| 106 | |
| 107 | $old_len = (int) $len_str; |
| 108 | |
| 109 | // The content starts after 's:N:"' (quote is at second_colon_pos + 1) |
| 110 | $content_start = $second_colon_pos + 2; |
| 111 | |
| 112 | // Check boundary safety |
| 113 | if ($content_start + $old_len > $length) { |
| 114 | // Malformed data, stop parsing |
| 115 | $result .= substr($data, $s_token_pos); |
| 116 | break; |
| 117 | } |
| 118 | |
| 119 | // Extract the Inner Content |
| 120 | $content = substr($data, $content_start, $old_len); |
| 121 | |
| 122 | // Process the inner content via the main replace routine. |
| 123 | // This may recurse further if the content is JSON or nested serialization, |
| 124 | // or it may simply perform a plain-text search/replace if it is just a string. |
| 125 | $new_content = $this->replace($search, $replace, $content); |
| 126 | |
| 127 | // Reconstruct the token |
| 128 | // If content changed, the length changes automatically here |
| 129 | $new_len = strlen($new_content); |
| 130 | $result .= "s:$new_len:\"$new_content\";"; |
| 131 | |
| 132 | // Advance cursor: old position + header + quote + old_content + quote + semicolon |
| 133 | // Header size = ($second_colon_pos - $s_token_pos + 1) |
| 134 | // Total skip = (header) + 1 (quote) + old_len + 2 (quote + semicolon) |
| 135 | $pos = $content_start + $old_len + 2; |
| 136 | } |
| 137 | |
| 138 | return $result; |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * Lightweight check to see if string MIGHT be JSON. |
| 143 | */ |
| 144 | private function is_json($string) |
| 145 | { |
| 146 | if (!is_string($string) || $string === '') |
| 147 | return false; |
| 148 | $first = $string[0]; |
| 149 | return ($first === '{' || $first === '['); |
| 150 | } |
| 151 | } |