PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / Database / SearchReplace.php
wp-staging / Framework / Database Last commit date
Exporter 2 weeks ago QueryBuilder 2 years ago CustomTable.php 2 months ago DbInfo.php 7 months ago ExcludedTables.php 5 months ago OptionPreservationHandler.php 2 years ago SearchReplace.php 2 months ago SelectedTables.php 2 weeks ago TableDto.php 1 year ago TableService.php 9 months ago TablesRenamer.php 2 months ago WpDbInfo.php 9 months ago WpOptionsInfo.php 1 year ago iDbInfo.php 2 years ago
SearchReplace.php
314 lines
1 <?php
2
3 namespace WPStaging\Framework\Database;
4
5 use WPStaging\Framework\Facades\Hooks;
6 use WPStaging\Framework\Traits\DebugLogTrait;
7 use WPStaging\Framework\Traits\SerializeTrait;
8 use WPStaging\Framework\Traits\UrlTrait;
9
10 /**
11 * Handles search and replace operations on database values,
12 * including serialized data and WP Bakery base64-encoded content.
13 */
14 class SearchReplace
15 {
16 use DebugLogTrait;
17 use SerializeTrait;
18 use UrlTrait;
19
20 /**
21 * Filter applied after standard search/replace in replaceExtended().
22 * Allows custom decode/replace/encode logic for encoded database values (e.g. base64 JSON).
23 *
24 * @param string $data The column value after standard search/replace
25 * @param array $search The search strings
26 * @param array $replace The replacement strings
27 */
28 const FILTER_REPLACE_EXTENDED_DATA = 'wpstg.database.searchreplace.replace_extended_data';
29
30 /** @var array */
31 private $search;
32
33 /** @var array */
34 private $replace;
35
36 /** @var array */
37 private $exclude;
38
39 /** @var bool */
40 private $caseSensitive;
41
42 /** @var string */
43 private $currentSearch;
44
45 /** @var string */
46 private $currentReplace;
47
48 /** @var bool */
49 private $isWpBakeryActive;
50
51 protected $smallerReplacement = PHP_INT_MAX;
52
53 public function __construct(array $search = [], array $replace = [], $caseSensitive = true, array $exclude = [])
54 {
55 $this->search = $search;
56 $this->replace = $replace;
57 $this->caseSensitive = $caseSensitive;
58 $this->exclude = $exclude;
59 $this->isWpBakeryActive = false;
60 }
61
62 /**
63 * @return int
64 */
65 public function getSmallerSearchLength()
66 {
67 if ($this->smallerReplacement < PHP_INT_MAX) {
68 return $this->smallerReplacement;
69 }
70
71 foreach ($this->search as $search) {
72 if (strlen($search) < $this->smallerReplacement) {
73 $this->smallerReplacement = strlen($search);
74 }
75 }
76
77 return $this->smallerReplacement;
78 }
79
80 /**
81 * @param array|object|string $data
82 * @return array|object|string
83 */
84 public function replace($data)
85 {
86 if (defined('DISABLE_WPSTG_SEARCH_REPLACE') && (bool)DISABLE_WPSTG_SEARCH_REPLACE) {
87 return $data;
88 }
89
90 if (!$this->search || !$this->replace) {
91 return $data;
92 }
93
94 $totalSearch = count($this->search);
95 $totalReplace = count($this->replace);
96 if ($totalSearch !== $totalReplace) {
97 throw new \RuntimeException(
98 sprintf(
99 'Can not search and replace. There are %d items to search and %d items to replace',
100 $totalSearch,
101 $totalReplace
102 )
103 );
104 }
105
106 for ($i = 0; $i < $totalSearch; $i++) {
107 $this->currentSearch = (string)$this->search[$i];
108 $this->currentReplace = (string)$this->replace[$i];
109 $data = $this->walker($data);
110 }
111
112 return $data;
113 }
114
115 /**
116 * Extended replace that also handles WP Bakery base64 content
117 * and fires a filter for custom encoded-value replacement.
118 *
119 * @param string $data
120 * @return string
121 */
122 public function replaceExtended($data)
123 {
124 if (defined('DISABLE_WPSTG_SEARCH_REPLACE') && (bool)DISABLE_WPSTG_SEARCH_REPLACE) {
125 return $data;
126 }
127
128 if ($this->isWpBakeryActive) {
129 $data = preg_replace_callback('/\[vc_raw_html\](.+?)\[\/vc_raw_html\]/S', [$this, 'replaceWpBakeryValues'], $data);
130 }
131
132 $data = $this->replace($data);
133
134 if (!function_exists('has_filter') || has_filter(self::FILTER_REPLACE_EXTENDED_DATA) === false) {
135 return $data;
136 }
137
138 return Hooks::applyFilters(self::FILTER_REPLACE_EXTENDED_DATA, $data, $this->search, $this->replace);
139 }
140
141 public function replaceWpBakeryValues($matched)
142 {
143 $data = $this->base64Decode($matched[1]);
144 $data = $this->replace($data);
145 return '[vc_raw_html]' . base64_encode($data) . '[/vc_raw_html]';
146 }
147
148 public function setSearch(array $search)
149 {
150 $this->search = $search;
151 return $this;
152 }
153
154 public function setReplace(array $replace)
155 {
156 $this->replace = $replace;
157 return $this;
158 }
159
160 /**
161 * Append a single search-replace pair to the existing lists
162 *
163 * Resets the smallerReplacement cache so that the new pair
164 * is taken into account during subsequent replace operations.
165 *
166 * @param string $search The string to search for
167 * @param string $replace The replacement string
168 * @return $this
169 */
170 public function appendSearchReplacePair(string $search, string $replace)
171 {
172 $this->search[] = $search;
173 $this->replace[] = $replace;
174 $this->smallerReplacement = PHP_INT_MAX;
175 return $this;
176 }
177
178 public function setCaseSensitive($caseSensitive)
179 {
180 $this->caseSensitive = $caseSensitive;
181 return $this;
182 }
183
184 public function setExclude(array $exclude)
185 {
186 $this->exclude = $exclude;
187 return $this;
188 }
189
190 /**
191 * Set whether WP Bakery active
192 *
193 * @return self
194 */
195 public function setWpBakeryActive($isActive = true)
196 {
197 $this->isWpBakeryActive = $isActive;
198 return $this;
199 }
200
201 /**
202 * @param string|array|object $data
203 * @return string|array|object|bool|int|float|null
204 */
205 private function walker($data)
206 {
207 switch (gettype($data)) {
208 case "string":
209 return $this->replaceString($data);
210 case "array":
211 return $this->replaceArray($data);
212 case "object":
213 return $this->replaceObject($data);
214 }
215
216 return $data;
217 }
218
219 /**
220 * @param string $data
221 * @return string|array|object|bool|int|float|null
222 */
223 private function replaceString($data)
224 {
225 if (!$this->isSerialized($data)) {
226 return $this->strReplace($data);
227 }
228
229 // PDO instances can not be serialized or unserialized
230 if (strpos($data, 'O:3:"PDO":0:') !== false) {
231 return $data;
232 }
233
234 // DateTime object can not be unserialized.
235 // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
236 // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
237 if (strpos($data, 'O:8:"DateTime":0:') !== false) {
238 return $data;
239 }
240
241 // If the string has an object format, check if it has a stdClass, otherwise, return the original data.
242 // The logic here, we can't serialize unserialized data that has an instance of a class in the object.
243 // This may lead to "class not found" or will replaced with "__PHP_Incomplete_Class_Name" if the class hasn't been initialized yet.
244 if (strpos($data, 'O:') !== false && preg_match_all('@O:\d+:"([^"]+)"@', $data, $match) && !empty($match) && !empty($match[1])) {
245 foreach ($match[1] as $value) {
246 if ($value !== 'stdClass') {
247 return $data;
248 }
249 }
250
251 unset($match);
252 }
253
254 $unserialized = false;
255 try {
256 $unserialized = @unserialize($data);
257 } catch (\Throwable $e) {
258 $this->debugLog('replaceString. Can not unserialize data. Error: ' . $e->getMessage() . ' Data: ' . $data);
259 }
260
261 if ($unserialized !== false) {
262 return serialize($this->walker($unserialized));
263 }
264
265 return $data;
266 }
267
268 private function replaceArray(array $data)
269 {
270 foreach ($data as $key => $value) {
271 $data[$key] = $this->walker($value);
272 }
273
274 return $data;
275 }
276
277 private function replaceObject($data)
278 {
279 // This is not reliable as JsonSerializable and Serializable interfaces can record data into database
280 // but get_object_vars won't be able to fetch them to replace
281 // TODO use reflection to make sure even protected and private properties are searched and replaced
282 $props = get_object_vars($data);
283 if (!empty($props['__PHP_Incomplete_Class_Name'])) {
284 return $data;
285 }
286
287 foreach ($props as $key => $value) {
288 if ($key === '' || (isset($key[0]) && ord($key[0]) === 0)) {
289 continue;
290 }
291
292 $data->{$key} = $this->walker($value);
293 }
294
295 return $data;
296 }
297
298 private function strReplace($data = '')
299 {
300 $regexExclude = '';
301 foreach ($this->exclude as $excludeString) {
302 //TODO: I changed (FAIL) to (*FAIL) because that's what tutorials say is the right syntax. This may need testing
303 $regexExclude .= $excludeString . '(*SKIP)(*FAIL)|';
304 }
305
306 $pattern = '#' . $regexExclude . preg_quote($this->currentSearch, '#') . '#';
307 if (!$this->caseSensitive) {
308 $pattern .= 'i';
309 }
310
311 return preg_replace($pattern, $this->currentReplace, $data);
312 }
313 }
314