PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.3.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.3.0
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 / Backup / Task / FileRestoreTask.php
wp-staging / Backup / Task Last commit date
Tasks 10 months ago BackupTask.php 1 year ago FileBackupTask.php 11 months ago FileRestoreTask.php 1 year ago RestoreTask.php 1 year ago
FileRestoreTask.php
337 lines
1 <?php
2
3 namespace WPStaging\Backup\Task;
4
5 use WPStaging\Framework\Adapter\Directory;
6 use WPStaging\Framework\Filesystem\Filesystem;
7 use WPStaging\Framework\Filesystem\MissingFileException;
8 use WPStaging\Framework\Filesystem\PathIdentifier;
9 use WPStaging\Framework\Queue\FinishedQueueException;
10 use WPStaging\Framework\Queue\SeekableQueueInterface;
11 use WPStaging\Framework\Utils\Cache\Cache;
12 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
13 use WPStaging\Framework\Traits\RestoreFileExclusionTrait;
14 use WPStaging\Framework\Job\Dto\StepsDto;
15 use WPStaging\Framework\Job\Dto\TaskResponseDto;
16 use WPStaging\Backup\Entity\BackupMetadata;
17 use WPStaging\Framework\Job\Interfaces\FileTaskInterface;
18 use WPStaging\Framework\Job\Task\FileHandler\FileProcessor;
19 use WPStaging\Framework\SiteInfo;
20 use WPStaging\Vendor\Psr\Log\LoggerInterface;
21 use WPStaging\Framework\Facades\Hooks;
22
23 /**
24 * Class FileRestoreTask
25 *
26 * This is an abstract class for the filesystem-based restore actions of restoring a site,
27 * such as plugins, themes, mu-plugins and uploads files.
28 *
29 * It's main philosophy is to control the individual queue of what needs to be processed
30 * from each of the concrete restores. It delegates actual processing of the queue to a separate class.
31 *
32 * @package WPStaging\Backup\Abstracts\Task
33 */
34 abstract class FileRestoreTask extends RestoreTask implements FileTaskInterface
35 {
36 use EndOfLinePlaceholderTrait;
37 use RestoreFileExclusionTrait;
38
39 /**
40 * @var string
41 */
42 const FILTER_EXCLUDE_FILES_DURING_RESTORE = 'wpstg.backup.restore.exclude_paths';
43
44 /**
45 * Note: internal use
46 * @var string
47 */
48 const FILTER_EXCLUDE_ENQUEUE_DELETE = 'wpstg.backup.restore.exclude_enqueue_delete';
49
50 /**
51 * @var Filesystem
52 */
53 protected $filesystem;
54
55 /**
56 * @var Directory
57 */
58 protected $directory;
59
60 /**
61 * @var FileProcessor
62 */
63 private $restoreFileProcessor;
64
65 /**
66 * @var int
67 */
68 protected $processedNow;
69
70 /**
71 * @var PathIdentifier
72 */
73 protected $pathIdentifier;
74
75 /**
76 * @var bool
77 */
78 protected $isSiteHostedOnWordPressCom = false;
79
80 public function __construct(
81 LoggerInterface $logger,
82 Cache $cache,
83 StepsDto $stepsDto,
84 SeekableQueueInterface $taskQueue,
85 Filesystem $filesystem,
86 Directory $directory,
87 FileProcessor $restoreFileProcessor,
88 PathIdentifier $pathIdentifier,
89 SiteInfo $siteInfo
90 ) {
91 parent::__construct($logger, $cache, $stepsDto, $taskQueue);
92 $this->filesystem = $filesystem;
93 $this->directory = $directory;
94 $this->restoreFileProcessor = $restoreFileProcessor;
95 $this->pathIdentifier = $pathIdentifier;
96 $this->isSiteHostedOnWordPressCom = $siteInfo->isHostedOnWordPressCom();
97 }
98
99 /**
100 * @return void
101 */
102 public function prepareFileRestore()
103 {
104 if ($this->stepsDto->getTotal() === 0) {
105 $this->buildQueue();
106 $this->taskQueue->seek(0);
107
108 // Just an arbitrary number, when there are no more items in the Queue we call stepsDto->finish()
109 $this->stepsDto->setTotal(100);
110 }
111 }
112
113 /**
114 * @return TaskResponseDto
115 */
116 public function execute(): TaskResponseDto
117 {
118 if ($this->isSkipped()) {
119 $this->stepsDto->finish();
120 $this->logger->warning(sprintf(esc_html__('%s skipped by filter!', 'wp-staging'), static::getTaskTitle()));
121 return $this->generateResponse(false);
122 }
123
124 try {
125 $this->checkMissingParts();
126 } catch (MissingFileException $ex) {
127 $this->stepsDto->finish();
128 $this->logger->warning(sprintf(esc_html__('%s skipped due to missing part!', 'wp-staging'), static::getTaskTitle()));
129 return $this->generateResponse(false);
130 }
131
132 $this->prepareFileRestore();
133
134 try {
135 while (!$this->isThreshold()) {
136 $this->processNextItemInQueue();
137 $this->processedNow++;
138 }
139 } catch (FinishedQueueException $e) {
140 $this->stepsDto->finish();
141 }
142
143 $this->logger->info(sprintf(esc_html__('%s (processed %d items)', 'wp-staging'), static::getTaskTitle(), (int)$this->processedNow));
144
145 return $this->generateResponse(false);
146 }
147
148 protected function getOriginalSuffix(): string
149 {
150 return '_wpstg_tmp';
151 }
152
153 /**
154 * Concrete classes of the FileRestoreTask must build
155 * the queue once, enqueuing everything that needs
156 * to be moved or deleted, using $this->enqueueMove
157 * or $this->enqueueDelete.
158 *
159 * @return void
160 */
161 abstract protected function buildQueue();
162
163 /**
164 * Skip the task if part is missing for this task
165 *
166 * @return array
167 */
168 abstract protected function getParts(): array;
169
170 /**
171 * Skip the task if set by filter
172 *
173 * @return bool
174 */
175 abstract protected function isSkipped(): bool;
176
177 /**
178 * Skip the task if part is missing for this task
179 *
180 * @throws MissingFileException
181 * @return void
182 */
183 protected function checkMissingParts()
184 {
185 if (!$this->jobDataDto->getBackupMetadata()->getIsMultipartBackup()) {
186 return;
187 }
188
189 $parts = $this->getParts();
190
191 $backupDir = $this->directory->getBackupDirectory();
192
193 foreach ($parts as $part) {
194 $filepath = $backupDir . $part;
195 if (!file_exists($filepath)) {
196 throw new MissingFileException();
197 }
198 }
199 }
200
201 /**
202 * Executes the next item in the queue.
203 * @return void
204 */
205 protected function processNextItemInQueue()
206 {
207 $nextInQueueRaw = $this->taskQueue->dequeue();
208
209 if (is_null($nextInQueueRaw)) {
210 throw new FinishedQueueException();
211 }
212
213 // Skip blank lines
214 if ($nextInQueueRaw === '') {
215 return;
216 }
217
218 $nextInQueue = json_decode($nextInQueueRaw, true);
219
220 // Make sure we read expected data from the queue
221 if (!is_array($nextInQueue)) {
222 $this->logger->warning(sprintf(
223 __('%s: An internal error occurred that prevented this item from being restored. Skipping it... (Error Code: INVALID_QUEUE_ITEM)', 'wp-staging'),
224 static::getTaskTitle()
225 ));
226 $this->logger->debug($nextInQueueRaw);
227
228 return;
229 }
230
231 // Make sure data is in the expected format
232 array_map(function ($requiredKey) use ($nextInQueue, $nextInQueueRaw) {
233 if (!array_key_exists($requiredKey, $nextInQueue)) {
234 $this->logger->warning(sprintf(
235 __('%s: An internal error occurred that prevented this item from being restored. Skipping it... (Error Code: INVALID_QUEUE_ITEM)', 'wp-staging'),
236 static::getTaskTitle()
237 ));
238 $this->logger->debug($nextInQueueRaw);
239
240 return;
241 }
242 }, ['action', 'source', 'destination']);
243
244 $source = $nextInQueue['source'];
245
246 // Make sure destination is within WordPress
247 // @todo Test backup in Windows and restoring in Linux and vice-versa
248 $destination = $nextInQueue['destination'];
249 $destination = $this->replacePlaceholdersWithEOLs($destination);
250 $destination = wp_normalize_path($destination);
251
252 // Executes the action
253 $this->restoreFileProcessor->handle($nextInQueue['action'], $source, $destination, $this, $this->logger);
254 }
255
256 /**
257 * @param string $source Source path to move.
258 * @param string $destination Where to move source to.
259 * @return void
260 */
261 public function enqueueMove(string $source, string $destination)
262 {
263 $this->enqueue([
264 'action' => 'move',
265 'source' => wp_normalize_path($source),
266 'destination' => wp_normalize_path($destination),
267 ]);
268 }
269
270 /**
271 * @param string $path The path to delete. Can be a folder, which will be deleted recursively.
272 * @return void
273 */
274 public function enqueueDelete(string $path)
275 {
276 if ($this->isExcludeEnqueueDelete($path)) {
277 return;
278 }
279
280 $this->enqueue([
281 'action' => 'delete',
282 'source' => '',
283 'destination' => wp_normalize_path($path),
284 ]);
285 }
286
287 /**
288 * Use to retry last action in next request,
289 * if it wasn't completed in current request.
290 * @return void
291 */
292 public function retryLastActionInNextRequest()
293 {
294 $this->taskQueue->retry($dequeue = false);
295 }
296
297 /**
298 * @return bool
299 */
300 protected function isRestoreOnSubsite(): bool
301 {
302 if (!is_multisite()) {
303 return false;
304 }
305
306 return $this->jobDataDto->getBackupMetadata()->getBackupType() !== BackupMetadata::BACKUP_TYPE_MULTISITE;
307 }
308
309 /**
310 * @param array $action An array of actions to perform.
311 * @return void
312 */
313 private function enqueue(array $action)
314 {
315 $this->taskQueue->enqueue(json_encode($action));
316 }
317
318 private function isExcludeEnqueueDelete($filePath)
319 {
320 $normalizedFilePath = rtrim(wp_normalize_path($filePath), '/');
321 $excludedFiles = Hooks::applyFilters(self::FILTER_EXCLUDE_ENQUEUE_DELETE, []);
322
323 if (empty($excludedFiles)) {
324 return false;
325 }
326
327 foreach ($excludedFiles as $excludedFile) {
328 $normalizedExcludedFile = rtrim(wp_normalize_path($excludedFile), '/');
329 if (strpos($normalizedFilePath, $normalizedExcludedFile) === 0) {
330 return true;
331 }
332 }
333
334 return false;
335 }
336 }
337