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