PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.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 / Staging / Sites.php
wp-staging / Staging Last commit date
Ajax 1 week ago Dto 1 week ago Interfaces 2 months ago Jobs 1 week ago Renderer 1 week ago Service 1 day ago Tasks 1 day ago Traits 1 week ago CloneOptions.php 5 months ago FirstRun.php 4 months ago Sites.php 1 week ago StagingServiceProvider.php 1 week ago
Sites.php
414 lines
1 <?php
2
3 namespace WPStaging\Staging;
4
5 use Exception;
6 use WPStaging\Framework\Exceptions\WPStagingException;
7 use WPStaging\Staging\Dto\StagingSiteDto;
8
9 /**
10 * Class Sites
11 *
12 * This is used to manage settings on the staging site
13 *
14 * @package WPStaging\Staging
15 *
16 * @todo Manage staging sites option CRUD here?
17 */
18 class Sites
19 {
20 /**
21 * The option that stores the staging sites
22 */
23 const STAGING_SITES_OPTION = 'wpstg_staging_sites';
24
25 /**
26 * The option that stores login link settings
27 */
28 const STAGING_LOGIN_LINK_SETTINGS = 'wpstg_login_link_settings';
29
30 /**
31 * The old option that was used to store the staging sites
32 * @deprecated 4.0.5
33 */
34 const OLD_STAGING_SITES_OPTION = 'wpstg_existing_clones_beta';
35
36 /**
37 * Before upgrading structure, backup old staging site options
38 * @since 4.0.6
39 */
40 const BACKUP_STAGING_SITES_OPTION = 'wpstg_staging_sites_backup';
41
42 /**
43 * Missing cloneName routine executed
44 * @since 4.0.7
45 */
46 const MISSING_CLONE_NAME_ROUTINE_EXECUTED = 'wpstg_missing_cloneName_routine_executed';
47
48 /**
49 * The option that stores the excluded files from cloning process
50 */
51 const STAGING_EXCLUDED_FILES_OPTION = 'wpstg_clone_excluded_files_list';
52
53 /**
54 * The option that stores Godaddy the excluded files from cloning process
55 */
56 const STAGING_EXCLUDED_GD_FILES_OPTION = 'wpstg_clone_excluded_gd_files_list';
57
58 /**
59 * @var bool
60 */
61 const THROW_EXCEPTION = true;
62
63 /**
64 * Return list of staging sites in descending order of their creation time.
65 *
66 * @return array
67 * @throws WPStagingException
68 */
69 public function getSortedStagingSites()
70 {
71 $stagingSites = $this->tryGettingStagingSites(self::THROW_EXCEPTION);
72
73 // No need to sort if no sites or only one site
74 if (empty($stagingSites) || count($stagingSites) === 1) {
75 return $stagingSites;
76 }
77
78 // Sort staging sites in descending order
79 uasort($stagingSites, function ($site1, $site2) {
80 // If datetime is same, sort by directory name
81 // Will also work if both sites datetime are not set
82 if ($site1['datetime'] === $site2['datetime']) {
83 return strcmp($site2['directoryName'], $site1['directoryName']);
84 }
85
86 if (!isset($site1['datetime'])) {
87 return 1;
88 }
89
90 if (!isset($site2['datetime'])) {
91 return -1;
92 }
93
94 return $site2['datetime'] < $site1['datetime'] ? -1 : 1;
95 });
96
97 return $stagingSites;
98 }
99
100 /**
101 * Copy data from old staging site option wpstg_existing_clones_beta to new staging site option wpstg_staging_sites
102 *
103 * @see \WPStaging\Backend\Upgrade\Upgrade::upgrade2_8_7 (Free version)
104 * @see \WPStaging\Backend\Pro\Upgrade\Upgrade::upgrade4_0_5 (Pro version)
105 */
106 public function upgradeStagingSitesOption()
107 {
108 $newSitesOption = get_option(self::STAGING_SITES_OPTION, []);
109
110 // If its no valid array, it is broken
111 if (!is_array($newSitesOption)) {
112 $newSitesOption = [];
113 }
114
115 // Get the staging sites from old option
116 $oldSitesOption = get_option(self::OLD_STAGING_SITES_OPTION, []);
117
118 // Early bail: No sites to migrate
119 if (empty($oldSitesOption)) {
120 return;
121 }
122
123 // Convert old format to new, including when there are staging sites in both formats
124 $allStagingSites = $newSitesOption;
125
126 foreach ($oldSitesOption as $oldSiteSlug => $oldSite) {
127 // Migrate old site to new format
128 if (!array_key_exists($oldSiteSlug, $allStagingSites)) {
129 $allStagingSites[$oldSiteSlug] = $oldSite;
130 continue;
131 }
132
133 // If key exists and path matches, skip
134 if ($allStagingSites[$oldSiteSlug]['path'] === $oldSite['path']) {
135 continue;
136 }
137
138 // Migrate old site to new format when site slug exists in both options
139 $i = 0;
140
141 do {
142 $oldSiteSlug = $oldSiteSlug . '_' . $i;
143 } while (array_key_exists($oldSiteSlug, $allStagingSites));
144
145 $allStagingSites[$oldSiteSlug] = $oldSite;
146 }
147
148 if ($this->updateStagingSites($allStagingSites)) {
149 // Keep a backup just in case
150 update_option(self::BACKUP_STAGING_SITES_OPTION, $oldSitesOption, false);
151 delete_option(self::OLD_STAGING_SITES_OPTION);
152 }
153 }
154
155 /**
156 * Will try getting staging sites
157 *
158 * @param bool $throwException
159 * @return array
160 * @throws WPStagingException
161 */
162 public function tryGettingStagingSites(bool $throwException = false): array
163 {
164 $stagingSites = get_option(self::STAGING_SITES_OPTION, []);
165 if (empty($stagingSites)) {
166 return [];
167 }
168
169 if (is_array($stagingSites)) {
170 return $stagingSites;
171 }
172
173 if ($throwException) {
174 throw new WPStagingException('Staging sites option is not an array.');
175 }
176
177 return [];
178 }
179
180 /**
181 * Update staging sites option
182 *
183 * @param array $stagingSites
184 * @return bool
185 */
186 public function updateStagingSites($stagingSites)
187 {
188 return update_option(self::STAGING_SITES_OPTION, $stagingSites, false);
189 }
190
191 /**
192 * Upgrade the staging site data structure, add the missing cloneName, if not present
193 */
194 public function addMissingCloneNameUpgradeStructure()
195 {
196 $isAdded = get_option(self::MISSING_CLONE_NAME_ROUTINE_EXECUTED, false);
197 if ($isAdded) {
198 return;
199 }
200
201 // Current options
202 $sites = $this->tryGettingStagingSites();
203
204 // Early bail if no sites
205 if (empty($sites)) {
206 update_option(self::MISSING_CLONE_NAME_ROUTINE_EXECUTED, true);
207 return;
208 }
209
210 // Add missing cloneName if not exists
211 foreach ($sites as $key => $site) {
212 if (isset($sites[$key]['cloneName'])) {
213 continue;
214 }
215
216 $sites[$key]['cloneName'] = $sites[$key]['directoryName'];
217 }
218
219 $this->updateStagingSites($sites);
220 update_option(self::MISSING_CLONE_NAME_ROUTINE_EXECUTED, true);
221 }
222
223 /**
224 * Sanitize the clone name to be used as directory
225 *
226 * @param string $cloneName
227 * @return string
228 */
229 public function sanitizeDirectoryName($cloneName)
230 {
231 $cloneDirectoryName = preg_replace("#\W+#", '-', strtolower($cloneName));
232 return substr($cloneDirectoryName, 0, 16);
233 }
234
235 /**
236 * Generate an unused staging site name from the friendly default name pool.
237 *
238 * @param string $fallback
239 * @return string
240 * @throws WPStagingException
241 */
242 public function generateStagingSiteName(string $fallback): string
243 {
244 $nameList = [
245 'enterprise',
246 'voyager',
247 'defiant',
248 'discovery',
249 'excelsior',
250 'intrepid',
251 'constitution',
252 'reliant',
253 'grissom',
254 'yamato',
255 'excelsior',
256 'venture',
257 'cerritos',
258 'prometheus',
259 'bellerophon',
260 'sanpablo',
261 'sutherland',
262 'shenzhou',
263 'titan',
264 'reliant',
265 'stargazer',
266 'franklin',
267 'protostar',
268 ];
269
270 shuffle($nameList);
271
272 // Fetch the registered staging names once; the fallback loop below can run up to 10,000 times.
273 $existingDirectoryNames = wp_list_pluck($this->tryGettingStagingSites(), 'directoryName');
274
275 foreach ($nameList as $name) {
276 $name = $this->sanitizeDirectoryName(sanitize_text_field($name));
277 if (!empty($name) && $this->isNameAvailableForNewSite($name, $existingDirectoryNames)) {
278 return $name;
279 }
280 }
281
282 $fallback = $this->sanitizeDirectoryName($fallback);
283 if (!empty($fallback) && $this->isNameAvailableForNewSite($fallback, $existingDirectoryNames)) {
284 return $fallback;
285 }
286
287 for ($i = 1; $i <= 10000; $i++) {
288 $name = $this->sanitizeDirectoryName(sprintf('staging-%d', $i));
289 if ($this->isNameAvailableForNewSite($name, $existingDirectoryNames)) {
290 return $name;
291 }
292 }
293
294 return empty($fallback) ? 'staging' : $fallback;
295 }
296
297 /**
298 * Check whether a directory name is safe to use for a new staging site.
299 * Stricter than isCloneExists: rejects any pre-existing file or directory (even
300 * empty) so the name generator never suggests a name that collides on disk.
301 *
302 * @param string $name
303 * @param string[] $existingDirectoryNames Registered staging directory names, fetched once by the caller.
304 * @return bool
305 */
306 private function isNameAvailableForNewSite(string $name, array $existingDirectoryNames): bool
307 {
308 if (file_exists(trailingslashit(get_home_path()) . $name)) {
309 return false;
310 }
311
312 return !in_array($name, $existingDirectoryNames, true);
313 }
314
315 /**
316 * Return false if site not exists else return reason behind existing
317 *
318 * @param string $directoryName
319 * @return bool|string
320 * @throws WPStagingException
321 */
322 public function isCloneExists($directoryName)
323 {
324 $cloneDirectoryPath = trailingslashit(get_home_path()) . $directoryName;
325 if (is_file($cloneDirectoryPath)) {
326 return sprintf(esc_html__("Warning: Use another site name! A file named %s already exists where the staging site would be created.", 'wp-staging'), $directoryName);
327 }
328
329 if (!wpstg_is_empty_dir($cloneDirectoryPath)) {
330 return sprintf(esc_html__("Warning: Use another site name! Clone destination directory %s already exists and is not empty. As default, WP STAGING uses the site name as subdirectory for the clone.", 'wp-staging'), $cloneDirectoryPath);
331 }
332
333 $stagingSites = $this->tryGettingStagingSites();
334 foreach ($stagingSites as $site) {
335 if ($site['directoryName'] === $directoryName) {
336 return sprintf(esc_html__("Site name %s is already in use, please choose another name for the staging site.", "wp-staging"), $directoryName);
337 }
338 }
339
340 return false;
341 }
342
343 /**
344 * @return array
345 * @throws WPStagingException
346 */
347 public function getStagingDirectories(): array
348 {
349 $stagingSites = $this->tryGettingStagingSites();
350 return wp_list_pluck($stagingSites, 'path');
351 }
352
353 /**
354 * @param string $cloneId
355 * @return StagingSiteDto
356 *
357 * @throws Exception
358 */
359 public function getStagingSiteDtoByCloneId(string $cloneId): StagingSiteDto
360 {
361 $stagingSites = $this->tryGettingStagingSites();
362 if (empty($stagingSites)) {
363 throw new Exception('No staging sites found.');
364 }
365
366 if (!array_key_exists($cloneId, $stagingSites)) {
367 throw new Exception('Staging site not found.');
368 }
369
370 $stagingSiteArray = $stagingSites[$cloneId];
371 $stagingSiteDto = new StagingSiteDto();
372 $stagingSiteDto->hydrate($stagingSiteArray);
373 $stagingSiteDto->setCloneId($cloneId);
374
375 return $stagingSiteDto;
376 }
377
378 /**
379 * @param string $cloneName
380 * @return StagingSiteDto
381 *
382 * @throws Exception
383 */
384 public function getStagingSiteDtoByCloneName(string $cloneName): StagingSiteDto
385 {
386 $stagingSites = $this->tryGettingStagingSites();
387 if (empty($stagingSites)) {
388 throw new Exception('No staging sites found.');
389 }
390
391 foreach ($stagingSites as $cloneId => $stagingSiteArray) {
392 if ($stagingSiteArray['cloneName'] === $cloneName) {
393 $stagingSiteDto = new StagingSiteDto();
394 $stagingSiteDto->hydrate($stagingSiteArray);
395 $stagingSiteDto->setCloneId($cloneId);
396
397 return $stagingSiteDto;
398 }
399 }
400
401 throw new Exception('Staging site not found.');
402 }
403
404 /**
405 * @param string $clone
406 * @return bool
407 */
408 public function isExistingClone(string $clone): bool
409 {
410 $existingClones = get_option(self::STAGING_SITES_OPTION, []);
411 return isset($existingClones[$clone]);
412 }
413 }
414