PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.3.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.3.1
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
QueryBuilder 2 years ago DbInfo.php 2 years ago ExcludedTables.php 4 years ago OptionPreservationHandler.php 2 years ago SearchReplace.php 2 years ago SelectedTables.php 2 years ago TableDto.php 5 years ago TableService.php 2 years ago TablesRenamer.php 2 years ago WpDbInfo.php 2 years ago iDbInfo.php 2 years ago
SearchReplace.php
265 lines
1 <?php
2
3 namespace WPStaging\Framework\Database;
4
5 use RuntimeException;
6
7 use function WPStaging\functions\debug_log;
8
9 class SearchReplace
10 {
11 /** @var array */
12 private $search;
13
14 /** @var array */
15 private $replace;
16
17 /** @var array */
18 private $exclude;
19
20 /** @var bool */
21 private $caseSensitive;
22
23 /** @var string */
24 private $currentSearch;
25
26 /** @var string */
27 private $currentReplace;
28
29 /** @var bool */
30 private $isWpBakeryActive;
31
32 protected $smallerReplacement = PHP_INT_MAX;
33
34 public function __construct(array $search = [], array $replace = [], $caseSensitive = true, array $exclude = [])
35 {
36 $this->search = $search;
37 $this->replace = $replace;
38 $this->caseSensitive = $caseSensitive;
39 $this->exclude = $exclude;
40 $this->isWpBakeryActive = false;
41 }
42
43 /**
44 * @return int
45 */
46 public function getSmallerSearchLength()
47 {
48 if ($this->smallerReplacement < PHP_INT_MAX) {
49 return $this->smallerReplacement;
50 }
51
52 foreach ($this->search as $search) {
53 if (strlen($search) < $this->smallerReplacement) {
54 $this->smallerReplacement = strlen($search);
55 }
56 }
57
58 return $this->smallerReplacement;
59 }
60
61 /**
62 * @param array|object|string $data
63 * @return array|object|string
64 */
65 public function replace($data)
66 {
67 if (defined('DISABLE_WPSTG_SEARCH_REPLACE') && DISABLE_WPSTG_SEARCH_REPLACE) {
68 return $data;
69 }
70
71 if (!$this->search || !$this->replace) {
72 return $data;
73 }
74
75 $totalSearch = count($this->search);
76 $totalReplace = count($this->replace);
77 if ($totalSearch !== $totalReplace) {
78 throw new RuntimeException(
79 sprintf(
80 'Can not search and replace. There are %d items to search and %d items to replace',
81 $totalSearch,
82 $totalReplace
83 )
84 );
85 }
86
87 for ($i = 0; $i < $totalSearch; $i++) {
88 $this->currentSearch = (string)$this->search[$i];
89 $this->currentReplace = (string)$this->replace[$i];
90 $data = $this->walker($data);
91 }
92
93 return $data;
94 }
95
96 // This is extended replace job which support search replace for WP Bakery
97 public function replaceExtended($data)
98 {
99 if ($this->isWpBakeryActive) {
100 $data = preg_replace_callback('/\[vc_raw_html\](.+?)\[\/vc_raw_html\]/S', [$this, 'replaceWpBakeryValues'], $data);
101 }
102
103 return $this->replace($data);
104 }
105
106 public function replaceWpBakeryValues($matched)
107 {
108 $data = base64_decode($matched[1]);
109 $data = $this->replace($data);
110 return '[vc_raw_html]' . base64_encode($data) . '[/vc_raw_html]';
111 }
112
113 public function setSearch(array $search)
114 {
115 $this->search = $search;
116 return $this;
117 }
118
119 public function setReplace(array $replace)
120 {
121 $this->replace = $replace;
122 return $this;
123 }
124
125 public function setCaseSensitive($caseSensitive)
126 {
127 $this->caseSensitive = $caseSensitive;
128 return $this;
129 }
130
131 public function setExclude(array $exclude)
132 {
133 $this->exclude = $exclude;
134 return $this;
135 }
136
137 /**
138 * Set whether WP Bakery active
139 *
140 * @return self
141 */
142 public function setWpBakeryActive($isActive = true)
143 {
144 $this->isWpBakeryActive = $isActive;
145 return $this;
146 }
147
148 /**
149 * @param string|array|object $data
150 * @return string|array|object|bool|int|float|null
151 */
152 private function walker($data)
153 {
154 switch (gettype($data)) {
155 case "string":
156 return $this->replaceString($data);
157 case "array":
158 return $this->replaceArray($data);
159 case "object":
160 return $this->replaceObject($data);
161 }
162
163 return $data;
164 }
165
166 /**
167 * @param string $data
168 * @return string|array|object|bool|int|float|null
169 */
170 private function replaceString($data)
171 {
172 if (!is_serialized($data)) {
173 return $this->strReplace($data);
174 }
175
176 // PDO instances can not be serialized or unserialized
177 if (strpos($data, 'O:3:"PDO":0:') !== false) {
178 return $data;
179 }
180
181 // DateTime object can not be unserialized.
182 // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
183 // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
184 if (strpos($data, 'O:8:"DateTime":0:') !== false) {
185 return $data;
186 }
187
188 // If the string has an object format, check if it has a stdClass, otherwise, return the original data.
189 // The logic here, we can't serialize unserialized data that has an instance of a class in the object.
190 // This may lead to "class not found" or will replaced with "__PHP_Incomplete_Class_Name" if the class hasn't been initialized yet.
191 if (strpos($data, 'O:') !== false && preg_match_all('@O:\d+:"([^"]+)"@', $data, $match) && !empty($match) && !empty($match[1])) {
192 foreach ($match[1] as $value) {
193 if ($value !== 'stdClass') {
194 return $data;
195 }
196 }
197
198 unset($match);
199 }
200
201 // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
202 try {
203 $unserialized = @unserialize($data);
204 } catch (\Exception $e) {
205 debug_log('replaceString. Can not unserialize data. Error: ' . $e->getMessage() . ' Data: ' . $data);
206 $unserialized = false;
207 } catch (\TypeError $err) {
208 debug_log('replaceString. Can not unserialize data. Error: ' . $err->getMessage() . ' Data: ' . $data);
209 $unserialized = false;
210 }
211
212 if ($unserialized !== false) {
213 return serialize($this->walker($unserialized));
214 }
215
216 return $data;
217 }
218
219 private function replaceArray(array $data)
220 {
221 foreach ($data as $key => $value) {
222 $data[$key] = $this->walker($value);
223 }
224
225 return $data;
226 }
227
228 private function replaceObject($data)
229 {
230 // This is not reliable as JsonSerializable and Serializable interfaces can record data into database
231 // but get_object_vars won't be able to fetch them to replace
232 // TODO use reflection to make sure even protected and private properties are searched and replaced
233 $props = get_object_vars($data);
234 if (!empty($props['__PHP_Incomplete_Class_Name'])) {
235 return $data;
236 }
237
238 foreach ($props as $key => $value) {
239 if ($key === '' || (isset($key[0]) && ord($key[0]) === 0)) {
240 continue;
241 }
242
243 $data->{$key} = $this->walker($value);
244 }
245
246 return $data;
247 }
248
249 private function strReplace($data = '')
250 {
251 $regexExclude = '';
252 foreach ($this->exclude as $excludeString) {
253 //TODO: I changed (FAIL) to (*FAIL) because that's what tutorials say is the right syntax. This may need testing
254 $regexExclude .= $excludeString . '(*SKIP)(*FAIL)|';
255 }
256
257 $pattern = '#' . $regexExclude . preg_quote($this->currentSearch, null) . '#';
258 if (!$this->caseSensitive) {
259 $pattern .= 'i';
260 }
261
262 return preg_replace($pattern, $this->currentReplace, $data);
263 }
264 }
265