PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.0.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.0.0
4.11.2 4.11.1 4.11.0 4.10.0 4.9.5 4.9.4 4.9.3 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
QueryBuilder 2 years ago DbInfo.php 2 years ago ExcludedTables.php 4 years ago OptionPreservationHandler.php 2 years ago SearchReplace.php 1 year ago SelectedTables.php 2 years ago TableDto.php 5 years ago TableService.php 1 year ago TablesRenamer.php 1 year ago WpDbInfo.php 2 years ago WpOptionsInfo.php 1 year ago iDbInfo.php 3 years ago
SearchReplace.php
265 lines
1 <?php
2
3 namespace WPStaging\Framework\Database;
4
5 use WPStaging\Framework\Traits\DebugLogTrait;
6 use WPStaging\Framework\Traits\SerializeTrait;
7 use WPStaging\Framework\Traits\UrlTrait;
8
9 class SearchReplace
10 {
11 use DebugLogTrait;
12 use SerializeTrait;
13 use UrlTrait;
14
15 /** @var array */
16 private $search;
17
18 /** @var array */
19 private $replace;
20
21 /** @var array */
22 private $exclude;
23
24 /** @var bool */
25 private $caseSensitive;
26
27 /** @var string */
28 private $currentSearch;
29
30 /** @var string */
31 private $currentReplace;
32
33 /** @var bool */
34 private $isWpBakeryActive;
35
36 protected $smallerReplacement = PHP_INT_MAX;
37
38 public function __construct(array $search = [], array $replace = [], $caseSensitive = true, array $exclude = [])
39 {
40 $this->search = $search;
41 $this->replace = $replace;
42 $this->caseSensitive = $caseSensitive;
43 $this->exclude = $exclude;
44 $this->isWpBakeryActive = false;
45 }
46
47 /**
48 * @return int
49 */
50 public function getSmallerSearchLength()
51 {
52 if ($this->smallerReplacement < PHP_INT_MAX) {
53 return $this->smallerReplacement;
54 }
55
56 foreach ($this->search as $search) {
57 if (strlen($search) < $this->smallerReplacement) {
58 $this->smallerReplacement = strlen($search);
59 }
60 }
61
62 return $this->smallerReplacement;
63 }
64
65 /**
66 * @param array|object|string $data
67 * @return array|object|string
68 */
69 public function replace($data)
70 {
71 if (defined('DISABLE_WPSTG_SEARCH_REPLACE') && (bool)DISABLE_WPSTG_SEARCH_REPLACE) {
72 return $data;
73 }
74
75 if (!$this->search || !$this->replace) {
76 return $data;
77 }
78
79 $totalSearch = count($this->search);
80 $totalReplace = count($this->replace);
81 if ($totalSearch !== $totalReplace) {
82 throw new \RuntimeException(
83 sprintf(
84 'Can not search and replace. There are %d items to search and %d items to replace',
85 $totalSearch,
86 $totalReplace
87 )
88 );
89 }
90
91 for ($i = 0; $i < $totalSearch; $i++) {
92 $this->currentSearch = (string)$this->search[$i];
93 $this->currentReplace = (string)$this->replace[$i];
94 $data = $this->walker($data);
95 }
96
97 return $data;
98 }
99
100 // This is extended replace job which support search replace for WP Bakery
101 public function replaceExtended($data)
102 {
103 if ($this->isWpBakeryActive) {
104 $data = preg_replace_callback('/\[vc_raw_html\](.+?)\[\/vc_raw_html\]/S', [$this, 'replaceWpBakeryValues'], $data);
105 }
106
107 return $this->replace($data);
108 }
109
110 public function replaceWpBakeryValues($matched)
111 {
112 $data = $this->base64Decode($matched[1]);
113 $data = $this->replace($data);
114 return '[vc_raw_html]' . base64_encode($data) . '[/vc_raw_html]';
115 }
116
117 public function setSearch(array $search)
118 {
119 $this->search = $search;
120 return $this;
121 }
122
123 public function setReplace(array $replace)
124 {
125 $this->replace = $replace;
126 return $this;
127 }
128
129 public function setCaseSensitive($caseSensitive)
130 {
131 $this->caseSensitive = $caseSensitive;
132 return $this;
133 }
134
135 public function setExclude(array $exclude)
136 {
137 $this->exclude = $exclude;
138 return $this;
139 }
140
141 /**
142 * Set whether WP Bakery active
143 *
144 * @return self
145 */
146 public function setWpBakeryActive($isActive = true)
147 {
148 $this->isWpBakeryActive = $isActive;
149 return $this;
150 }
151
152 /**
153 * @param string|array|object $data
154 * @return string|array|object|bool|int|float|null
155 */
156 private function walker($data)
157 {
158 switch (gettype($data)) {
159 case "string":
160 return $this->replaceString($data);
161 case "array":
162 return $this->replaceArray($data);
163 case "object":
164 return $this->replaceObject($data);
165 }
166
167 return $data;
168 }
169
170 /**
171 * @param string $data
172 * @return string|array|object|bool|int|float|null
173 */
174 private function replaceString($data)
175 {
176 if (!$this->isSerialized($data)) {
177 return $this->strReplace($data);
178 }
179
180 // PDO instances can not be serialized or unserialized
181 if (strpos($data, 'O:3:"PDO":0:') !== false) {
182 return $data;
183 }
184
185 // DateTime object can not be unserialized.
186 // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
187 // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
188 if (strpos($data, 'O:8:"DateTime":0:') !== false) {
189 return $data;
190 }
191
192 // If the string has an object format, check if it has a stdClass, otherwise, return the original data.
193 // The logic here, we can't serialize unserialized data that has an instance of a class in the object.
194 // This may lead to "class not found" or will replaced with "__PHP_Incomplete_Class_Name" if the class hasn't been initialized yet.
195 if (strpos($data, 'O:') !== false && preg_match_all('@O:\d+:"([^"]+)"@', $data, $match) && !empty($match) && !empty($match[1])) {
196 foreach ($match[1] as $value) {
197 if ($value !== 'stdClass') {
198 return $data;
199 }
200 }
201
202 unset($match);
203 }
204
205 $unserialized = false;
206 try {
207 $unserialized = @unserialize($data);
208 } catch (\Throwable $e) {
209 $this->debugLog('replaceString. Can not unserialize data. Error: ' . $e->getMessage() . ' Data: ' . $data);
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