PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 2.1.6
Backup Migration v2.1.6
2.1.6 2.1.5.2 trunk 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.6.1 1.4.7 1.4.8 1.4.9 1.4.9.1 2.0.0 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.5.1
backup-backup / includes / database / search-replace-stack-based.php
backup-backup / includes / database Last commit date
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 }