PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.2
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.2
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 / Logger / BackgroundLogger.php
wp-staging / Framework / Logger Last commit date
BackgroundLogger.php 3 days ago EventLoggerConst.php 1 week ago SseEventCache.php 1 week ago
BackgroundLogger.php
382 lines
1 <?php
2
3 namespace WPStaging\Framework\Logger;
4
5 use WP_REST_Request;
6 use WPStaging\Core\Utils\Logger;
7 use WPStaging\Framework\Job\JobTransientCache;
8 use WPStaging\Framework\Rest\Rest;
9 use WPStaging\Framework\Security\Auth;
10 use WPStaging\Framework\Traits\SetTimeLimitTrait;
11
12
13
14
15
16 class BackgroundLogger
17 {
18 use SetTimeLimitTrait;
19
20
21
22
23 private $sseEventCache;
24
25
26
27
28 private $jobTransientCache;
29
30
31
32
33 private $auth;
34
35
36
37
38
39 const STALE_JOB_THRESHOLD_SECONDS = 60;
40
41
42
43
44 private $lastPercentage = 0;
45
46
47
48
49 private $lastTaskTitle = '';
50
51 public function __construct(SseEventCache $sseEventCache, JobTransientCache $jobTransientCache, Auth $auth)
52 {
53 $this->sseEventCache = $sseEventCache;
54 $this->jobTransientCache = $jobTransientCache;
55 $this->auth = $auth;
56 }
57
58
59
60
61
62
63
64
65
66
67 public function maybePrepareSseStream($result, \WP_REST_Server $server, WP_REST_Request $request)
68 {
69
70 $route = trim($request->get_route(), '/');
71 if ($route !== Rest::WPSTG_ROUTE_NAMESPACE_V1 . '/sse-logs') {
72 return $result;
73 }
74
75 $this->setHeaders();
76
77 return $result;
78 }
79
80 public function verifyRestRequest()
81 {
82 $token = isset($_GET['token']) ? sanitize_text_field($_GET['token']) : '';
83 $jobId = $this->jobTransientCache->getJobId();
84
85
86
87
88
89 if ($token === '' || $jobId === '' || !hash_equals((string)$jobId, $token)) {
90 return new \WP_Error('rest_forbidden', __('You are not allowed to access this resource.', 'wp-staging'), ['status' => 403]);
91 }
92
93 return true;
94 }
95
96
97
98
99 public function ajaxCheckJobOutcome()
100 {
101 if (!$this->auth->isAuthenticatedRequest()) {
102 wp_send_json_error(null, 401);
103 return;
104 }
105
106 $token = isset($_POST['token']) ? sanitize_text_field($_POST['token']) : '';
107 $job = $this->jobTransientCache->findJobById($token);
108 if ($job === null) {
109 wp_send_json(['status' => 'unknown']);
110 return;
111 }
112
113 $status = isset($job['status']) ? (string)$job['status'] : '';
114
115 wp_send_json([
116 'status' => $status,
117 'message' => $this->getOutcomeMessage($status, $job),
118 'severity' => isset($job['severity']) ? (string)$job['severity'] : '',
119 ]);
120 }
121
122
123
124
125
126
127 private function getOutcomeMessage(string $status, array $job): string
128 {
129 if ($status === JobTransientCache::STATUS_SUCCESS) {
130 return esc_html__('Job completed successfully', 'wp-staging');
131 }
132
133 if ($status !== JobTransientCache::STATUS_FAILED) {
134 return '';
135 }
136
137 return !empty($job['message']) ? esc_html((string)$job['message']) : esc_html__('Job failed', 'wp-staging');
138 }
139
140
141
142
143
144 public function restEventStream(WP_REST_Request $request)
145 {
146
147
148 @ini_set('zlib.output_compression', '0');
149 @ini_set('output_buffering', 'off');
150 @ini_set('implicit_flush', '1');
151 $this->setTimeLimit(0);
152 @ignore_user_abort(true);
153 if (function_exists('apache_setenv')) {
154 @apache_setenv('no-gzip', '1');
155 @apache_setenv('dont-vary', '1');
156 }
157
158 while (ob_get_level() > 0) {
159 ob_end_clean();
160 }
161
162
163 if (PHP_VERSION_ID >= 80000) {
164 // @phpstan-ignore-next-line - PHPStan stubs may expect int for compatibility
165 @ob_implicit_flush(true);
166 } else {
167 // @phpstan-ignore-next-line - PHP < 8.0 expects int, not bool
168 @ob_implicit_flush(1);
169 }
170 flush();
171
172 $this->setHeaders();
173
174
175 echo ":" . str_repeat(' ', 2048) . "\n\n"; // phpcs:ignore
176 echo "retry: 3000\n\n"; // phpcs:ignore
177 echo ": connected\n\n"; // phpcs:ignore
178 flush();
179
180 if (!$this->isJobRunning()) {
181 $this->closeStream();
182 }
183
184 $end = microtime(true) + 5;
185 $jobId = $this->jobTransientCache->getJobId();
186 if (empty($jobId)) {
187 $data = [
188 'retry' => true,
189 'message' => esc_html__('No job ID found', 'wp-staging'),
190 ];
191
192 $this->output($jobId, 'error', json_encode($data));
193 $this->closeStream();
194 }
195
196 $offset = intval($request->get_param('offset') ?? 0);
197 $exists = $this->sseEventCache->setJobId($jobId, true);
198 if (!$exists) {
199 $data = [
200 'retry' => false,
201 'error' => esc_html__('Log file not found', 'wp-staging'),
202 ];
203
204 $this->output($jobId, 'error', json_encode($data));
205 $this->closeStream();
206 }
207
208 $lastHeartbeat = microtime(true);
209 while (microtime(true) < $end) {
210 if (connection_aborted()) {
211 $this->closeStream();
212 }
213
214 if (!$this->isJobRunning()) {
215 $this->closeStream();
216 }
217
218 $this->sseEventCache->load();
219 $total = $this->sseEventCache->getCount();
220 $events = $this->sseEventCache->getEvents($offset);
221
222 foreach ($events as $event) {
223 if ($event['type'] === SseEventCache::EVENT_TYPE_TASK) {
224 $this->pushTaskProgress($jobId, $event['data']);
225 continue;
226 }
227
228 if ($event['type'] === SseEventCache::EVENT_TYPE_COMPLETE) {
229 $this->output($jobId, $event['data']['status'], json_encode($event['data']['data']));
230 continue;
231 }
232
233 if ($event['type'] === SseEventCache::EVENT_TYPE_MEMORY_EXHAUST) {
234 $this->output($jobId, SseEventCache::EVENT_TYPE_MEMORY_EXHAUST, json_encode($event['data']));
235 $this->output($jobId, '', json_encode([
236 'type' => Logger::TYPE_ERROR,
237 'date' => $event['data']['time'],
238 'message' => "Memory exceed allowed size! Allowed memory: {$event['data']['allowedMemoryLimit']} bytes. Exceeded memory: {$event['data']['exhaustedMemorySize']} bytes",
239 ]));
240 continue;
241 }
242
243 if ($event['type'] === SseEventCache::EVENT_TYPE_FATAL_ERROR) {
244 $this->output($jobId, SseEventCache::EVENT_TYPE_FATAL_ERROR, json_encode($event['data']));
245 $this->output($jobId, '', json_encode([
246 'type' => Logger::TYPE_ERROR,
247 'date' => $event['data']['time'],
248 'message' => "Job failed due to a fatal error! Error data: " . print_r($event['data'], true),
249 ]));
250 continue;
251 }
252
253 $this->output($jobId, '', json_encode($event));
254 }
255
256
257 $now = microtime(true);
258 if ($now - $lastHeartbeat >= 1.0) {
259 echo ": ping " . $now . "\n\n"; // phpcs:ignore
260 flush();
261 $lastHeartbeat = $now;
262 }
263
264 $offset = $total;
265 if (!$this->isJobRunning()) {
266 $this->closeStream();
267 }
268
269 usleep(200000);
270 }
271
272 $this->output($jobId, 'offset', $offset);
273 $this->closeStream();
274 }
275
276 protected function output(string $id, string $name, string $data)
277 {
278 echo "id: $id" . "\n"; // phpcs:ignore
279 if (!empty($name)) {
280 echo "event: $name" . "\n"; // phpcs:ignore
281 }
282
283
284 echo "data: $data" . "\n"; // phpcs:ignore
285 echo "\n";
286
287
288 while (ob_get_level() > 0) {
289 @ob_end_flush();
290 }
291
292 flush();
293 }
294
295 protected function isJobRunning(): bool
296 {
297 $status = $this->jobTransientCache->getJobStatus();
298 $jobData = $this->jobTransientCache->getJob();
299 if ($status === JobTransientCache::STATUS_RUNNING) {
300
301
302 if (!empty($jobData['preInitAt']) && (time() - $jobData['preInitAt']) > self::STALE_JOB_THRESHOLD_SECONDS) {
303 $message = esc_html__('The background process could not start. This usually means the server cannot send HTTP requests to itself (loopback). Please check your server configuration, firewall rules, and DNS settings.', 'wp-staging');
304 $this->jobTransientCache->failJob(
305 esc_html__('Background process failed to start', 'wp-staging'),
306 $message
307 );
308 $this->output($jobData['jobId'], SseEventCache::EVENT_TYPE_FATAL_ERROR, json_encode(['message' => $message]));
309 return false;
310 }
311
312 return true;
313 }
314
315 $data = [];
316
317 if ($status === JobTransientCache::STATUS_CANCELLED) {
318 $this->output($jobData['jobId'], SseEventCache::EVENT_TYPE_TASK, json_encode([
319 'percentage' => 60,
320 'title' => esc_html__('Processing...', 'wp-staging'),
321 ]));
322 $data['title'] = $jobData['title'];
323 } elseif ($status === JobTransientCache::STATUS_FAILED) {
324 $data['message'] = !empty($jobData['message']) ? esc_html((string) $jobData['message']) : esc_html__('Job failed', 'wp-staging');
325
326 if (!empty($jobData['severity'])) {
327 $data['severity'] = $jobData['severity'];
328 }
329 } elseif ($status === JobTransientCache::STATUS_SUCCESS) {
330 $data['message'] = esc_html__('Job completed successfully', 'wp-staging');
331 }
332
333 $this->output('', $status, json_encode($data));
334 return false;
335 }
336
337 protected function pushTaskProgress(string $jobId, array $taskData)
338 {
339 if ($taskData['percentage'] === $this->lastPercentage && $taskData['title'] === $this->lastTaskTitle) {
340 return;
341 }
342
343 $this->lastPercentage = $taskData['percentage'];
344 $this->lastTaskTitle = $taskData['title'];
345
346 $this->output($jobId, SseEventCache::EVENT_TYPE_TASK, json_encode($taskData));
347 }
348
349
350
351
352
353 protected function closeStream()
354 {
355 echo ": stream closed\n\n"; // phpcs:ignore
356 flush();
357 exit();
358 }
359
360 protected function setHeaders()
361 {
362 if (headers_sent()) {
363 return;
364 }
365
366 header('Content-Type: text/event-stream; charset=UTF-8');
367
368
369
370 header('Cache-Control: private, no-cache, no-store, no-transform, must-revalidate, max-age=0');
371 header('Pragma: no-cache');
372 header('Expires: 0');
373 header('CDN-Cache-Control: no-store');
374 header('Cloudflare-CDN-Cache-Control: no-store');
375 header('Surrogate-Control: no-store');
376 header('X-LiteSpeed-Cache-Control: no-cache');
377 header('X-Accel-Buffering: no');
378 header('Connection: keep-alive');
379 header('Keep-Alive: timeout=300');
380 }
381 }
382