PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / full-site-import / Concerns / RunsImport.php

RunsImport.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! trunk, at modules/full-site-import/Concerns/RunsImport.php

464 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Modules\FullSiteImport\Concerns;
4
5 use Elementor\Plugin;
6 use Error;
7 use Exception;
8 use Templately\Modules\FullSiteImport\Import;
9 use Templately\Modules\FullSiteImport\Exception\FatalErrorException;
10 use Templately\Modules\FullSiteImport\Exception\RetryableErrorException;
11 use Templately\Modules\FullSiteImport\Runners\Finalizer;
12 use Templately\Modules\FullSiteImport\Utils\LogHandler;
13 use Templately\Utils\Response\AjaxResponder;
14 use Templately\Utils\Response\ErrorCode;
15 use Templately\Utils\Response\FatalGuard;
16 use Templately\Modules\FullSiteImport\Utils\ImportHelper;
17 use Templately\Modules\FullSiteImport\Utils\Utils;
18 use Templately\Modules\FullSiteImport\Utils\SessionData;
19 use Templately\Modules\FullSiteImport\Utils\AIUtils;
20 use Templately\Utils\Helper;
21 use Templately\Utils\Options;
22 use Templately\Modules\FullSiteImport\Utils\AttachmentPrefetcher;
23
24
25 /**
26 * RunsImport — extracted verbatim from FullSiteImport (behavior-preserving).
27 * Composed back into FullSiteImport via `use`; $this and method resolution unchanged.
28 */
29 trait RunsImport {
30 /**
31 * Owner token of this request's per-session run lock (null = not held).
32 * @var string|null
33 */
34 protected $run_lock_token = null;
35
36 public function admin_init() {
37 if (get_option('templately_flush_rewrite_rules', false)) {
38 flush_rewrite_rules();
39 delete_option('templately_flush_rewrite_rules');
40 }
41 }
42
43 private function finishRequestHeaders() {
44 // Survive the client going away — on EVERY server, not just LiteSpeed.
45 //
46 // The browser closes its stream the moment it receives `continue`, while
47 // this request is still shutting down. Anything echoed after that point
48 // (core spawns wp-cron on `shutdown`, and the Attachments runner's
49 // pre_http_request hook logs that loopback over SSE) is a write to a
50 // closed connection. Without this, PHP aborts on that write and DROPS the
51 // remaining shutdown functions — including register_shutdown(), which
52 // releases the run lock. The orphaned lock then turns every reconnect
53 // into `wait` until the 180s stale takeover: a three-minute freeze per
54 // affected chunk. The work stays bounded either way — runners chunk-exit
55 // on the request time budget.
56 ignore_user_abort(true);
57
58 if(Helper::should_flush()) {
59 // Disable output buffering and compression
60 @ini_set('output_buffering', 'Off');
61 @ini_set('zlib.output_compression', 'Off');
62 @ini_set('implicit_flush', 1);
63
64 // Time to run the import! Set no limit
65 set_time_limit(0);
66
67
68 // Set headers to prevent caching and buffering
69 header('Content-Type: text/event-stream, charset=UTF-8');
70 header('Cache-Control: no-cache, must-revalidate');
71 header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
72 header('Connection: Keep-Alive');
73 header('Pragma: no-cache');
74
75 if (!empty($GLOBALS['is_nginx'])) {
76 header('X-Accel-Buffering: no');
77 header('Content-Encoding: none');
78 }
79
80 flush();
81 ob_flush();
82 wp_ob_end_flush_all();
83 } else {
84 header("Cache-Control: no-store, no-cache");
85 // header( 'Content-Type: text/event-stream, charset=UTF-8' );
86 // header( "Connection: Keep-Alive" );
87
88 // Ignore user aborts and allow the script to run forever
89 // (Use with caution, consider progress updates or timeouts)
90 ignore_user_abort(true);
91
92 // Time to run the import! Set no limit
93 set_time_limit(0);
94
95
96 if (!empty($GLOBALS['is_nginx'])) {
97 header('X-Accel-Buffering: no');
98 header('Content-Encoding: none');
99 }
100
101 // Send output as soon as possible during long-running process
102 if (function_exists('fastcgi_finish_request')) {
103 fastcgi_finish_request();
104 } elseif (function_exists('litespeed_finish_request')) {
105 litespeed_finish_request();
106 } else {
107 wp_ob_end_flush_all();
108 }
109 }
110 }
111
112 public function import() {
113 if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
114 exit;
115 }
116
117 Utils::add_gd_editor_filter();
118
119
120 define('TEMPLATELY_START_TIME', microtime(true));
121
122 // delete_option( 'templately_fsi_log' );
123
124 register_shutdown_function( [ $this, 'register_shutdown' ] );
125
126 $this->finishRequestHeaders();
127
128 try {
129 $user = Options::get_instance()->get('user');
130 if (!empty($user['is_disconnected'])) {
131 $this->throw(__('Your site connection is disconnected. Please migrate your connection first.', 'templately'));
132 }
133
134 if(!empty($_GET['session_id'])){
135 // Security: Sanitize session_id from user input
136 $session_id = AIUtils::sanitize_path_component(sanitize_text_field($_GET['session_id']), 'session_id');
137 if (is_wp_error($session_id)) {
138 $this->throw($session_id->get_error_message());
139 }
140 $this->session_id = $session_id;
141 }
142 else {
143 $this->throw(__('Invalid Session ID.', 'templately'));
144 }
145
146
147 // Replay guard (spec 042 FR-032): a successfully-completed session's
148 // progress markers make every runner skip — "resuming" it would stream
149 // the OLD import's results as a fresh 'complete'. Refuse here directly
150 // (NOT via throw: the catch below would handle_import_status('failed'),
151 // overwriting the stored 'success' marker and filing a bogus failure
152 // report upstream).
153 if (SessionData::is_completed($this->session_id)) {
154 $stale_message = __('This import has already been completed. Please start a new import.', 'templately');
155 $this->sse_error(new FatalErrorException($stale_message), $stale_message);
156 return;
157 }
158
159 // Run lock: ONE import() execution per session. The watchdog's
160 // reconnect races the request it abandoned (which may still be
161 // executing — detached outright on the LiteSpeed branch); a live
162 // lock means "come back shortly", not an error: `wait` makes the
163 // client close and reconnect in 5s, by which time the holder has
164 // chunk-exited (and its shutdown released the lock) or gone stale.
165 $this->run_lock_token = SessionData::acquire_run_lock($this->session_id);
166 if (null === $this->run_lock_token) {
167 $this->sse_message([
168 'type' => 'wait',
169 'action' => 'wait',
170 ]);
171 exit;
172 }
173
174 $this->request_params = $this->get_session_data();
175 $this->initialize_props();
176 $this->add_revert_hooks();
177 $progress = $this->request_params['progress'] ?? [];
178
179 // TEST-ONLY, strictly gated: force ONE recoverable (retryable) step error to
180 // exercise the retry affordance for the 023 T071 forced-retry E2E. Fires ONLY
181 // when the test has set the `_templately_test_force_step_error` option, and
182 // self-clears immediately so the subsequent retry proceeds normally. The option
183 // is never set in production — this is a single dormant get_option() there.
184 if ( get_option( '_templately_test_force_step_error' ) ) {
185 delete_option( '_templately_test_force_step_error' );
186 throw new RetryableErrorException( __( 'Forced test error (retryable) — exercising the FSI retry affordance.', 'templately' ) );
187 }
188
189 // Trigger action hook for network admin multisite handling. Also the
190 // inversion seam for per-import platform wiring: elementor-integration
191 // hooks this to register its Pro-promotion child-type filter, so FSI
192 // no longer reaches into a platform module (mirroring the
193 // templately_fsi_* filter seams the gutenberg side already uses).
194 do_action( 'templately_fsi_before_import', $this, $this->request_params );
195
196 // Refresh progress after potential multisite creation
197 $progress = $this->request_params['progress'] ?? [];
198
199 if(empty($progress['create_log_dir'])){
200 // Create Log Directory and if fail then chose option method
201 LogHandler::create_log_dir();
202
203 SessionData::mark_step_complete($this->session_id, 'create_log_dir');
204 $this->sse_message( [
205 'type' => 'eventLog',
206 'action' => 'eventLog',
207 'info' => 'create_log_dir',
208 'results' => __METHOD__ . '::' . __LINE__,
209 ] );
210 }
211
212 $_id = isset($this->request_params['id']) ? (int) $this->request_params['id'] : null;
213
214 if ($_id === null) {
215 $this->throw(__('Invalid Pack ID.', 'templately'));
216 }
217
218 $this->sse_message( [
219 'type' => 'start',
220 'action' => 'eventLog',
221 'results' => __METHOD__ . '::' . __LINE__,
222 ] );
223
224 if(empty($progress['check_writing_permission'])){
225 /**
226 * Check Writing Permission
227 */
228 $this->check_writing_permission();
229
230 SessionData::mark_step_complete($this->session_id, 'check_writing_permission');
231 }
232
233 if(empty($progress['download_zip'])){
234
235 /**
236 * Download the zip
237 */
238 $this->download_zip( $_id );
239
240 SessionData::mark_step_complete($this->session_id, 'download_zip');
241 $this->sse_message( [
242 'type' => 'continue',
243 'action' => 'continue',
244 'info' => 'download_zip',
245 'results' => __METHOD__ . '::' . __LINE__,
246 ] );
247 exit;
248 }
249
250
251
252
253 /**
254 * Reading Manifest File
255 */
256 $this->manifest = $this->read_manifest($this->request_params['dir_path']);
257
258 /**
259 * Version Check
260 */
261 if ( ! empty( $this->manifest['version'] ) && version_compare( $this->manifest['version'], $this->version, '>' ) ) {
262 /**
263 * FIXME: The message should be re-written (by content/support team).
264 */
265 $this->throw( __( 'Please update the templately plugin.', 'templately' ) );
266 }
267
268 $platform = $this->manifest['platform'] ?? '';
269 if($platform === 'elementor') {
270 Helper::enable_elementor_container();
271 }
272
273
274
275 update_option('templately_import_platform', $platform);
276
277
278 /**
279 * Should Revert Old Data
280 */
281 // $this->revert();
282
283 /**
284 * Platform Based Templates Import
285 */
286 $this->start_content_import();
287
288 } catch ( Exception $e ) {
289 $this->handle_import_status('failed', $e->getMessage());
290
291 // Log the full trace server-side; never leak it to the browser (034 FR-003).
292 Helper::log( $e->getTraceAsString(), 'import_error_trace' );
293
294 // One producer (PRD PHP-4): derives `retry` from the exception type and
295 // persists the frame so LiteSpeed clients, which never receive the SSE
296 // stream, can still learn the import failed via import_status().
297 $this->sse_error( $e );
298 }
299
300 // if($_GET['part'] === 'import'){
301 // TODO: cleanup
302 // $this->clear_session_data();
303 // }
304 }
305
306 // Updated import_status method
307 public function import_status() {
308 $request_params = $this->get_session_data();
309
310 if (isset($request_params['log_type']) && $request_params['log_type'] == 'file') {
311 $log_index = isset($_GET['lastLogIndex']) ? (int) $_GET['lastLogIndex'] : 0;
312 $log = LogHandler::read_log_file($log_index);
313
314 // Enveloped like every other response (PRD PHP-5). The poller reads
315 // `body.data`; see eventSourcePolyfill.fetchProgress().
316 // ImportHelper, not LogHelper: the latter is a TRAIT, and PHP 8.1
317 // deprecates calling a static trait method on the trait itself. The
318 // notice would land mid-response on a polled status endpoint.
319 AjaxResponder::success(array_filter([
320 'count' => count($log),
321 'log' => $log,
322 'error' => ImportHelper::take_last_error($this->session_id ?? ''),
323 ], function ($value) { return null !== $value; }));
324 } else {
325 $log = get_option('templately_fsi_log');
326
327 if (!empty($log) && is_array($log) && isset($_GET['lastLogIndex'])) {
328 $lastLogIndex = (int) $_GET['lastLogIndex'];
329 $log = array_slice($log, $lastLogIndex);
330 }
331 // LiteSpeed buffers SSE, so these clients never see the stream and poll
332 // here instead. Without the error they polled a silent endpoint forever.
333 AjaxResponder::success(array_filter([
334 'count' => $log ? count($log) : 0,
335 'log' => $log,
336 'error' => ImportHelper::take_last_error($this->session_id ?? ''),
337 ], function ($value) { return null !== $value; }));
338 }
339 }
340
341 /**
342 * @throws Exception
343 */
344 private function start_content_import() {
345 add_filter('upload_mimes', array($this, 'allow_svg_upload'));
346 add_filter('elementor/files/allow_unfiltered_upload', '__return_true');
347
348 $request_params = $this->get_session_data();
349
350 // Remote images are fetched a few at a time, in parallel, into this session's
351 // working directory — see AttachmentPrefetcher. Booted per request; the files
352 // themselves survive a chunk boundary.
353 AttachmentPrefetcher::boot( $this->dir_path, function ( $message ) {
354 $this->sse_log( 'prefetch', $message, 1, 'eventLog' );
355 } );
356
357 $import = new Import(array_merge($request_params, [
358 'origin' => $this,
359 'manifest' => $this->manifest,
360 ]));
361 $imported_data = $import->run();
362
363 AttachmentPrefetcher::purge();
364
365 $import_status = $this->handle_import_status('success');
366
367 update_option('templately_flush_rewrite_rules', true, false);
368
369 $normalized_data = $this->normalize_imported_data($imported_data);
370 // Use timeout-aware wait handler for AI content processing
371 if(!empty($request_params['ai_page_ids']) && empty($normalized_data['ai_content']['processed']['credit_cost'])){
372 $updated_ids = AIUtils::get_processed_pages_data($request_params['process_id']);
373
374 // Use the static timeout-aware wait handler from AIUtils.
375 // ai_page_ids must be FLATTENED — the handler counts it against the
376 // number of processed pages (see AIUtils::flatten_ai_page_ids).
377 AIUtils::handle_sse_wait_with_timeout(
378 $this->session_id,
379 'ai_content_import_time',
380 $updated_ids,
381 AIUtils::flatten_ai_page_ids($request_params['ai_page_ids']),
382 [$this, 'sse_message'],
383 [
384 // Diagnostic only — it rides the `wait` payload, which the client uses
385 // solely to reconnect. `message` used to be here too and, being merged
386 // LAST, it replaced the wait row's label with the words "Missing Credit
387 // Cost": an internal condition name shown to the user as a progress step.
388 // The row is gone now, and so is the label that only existed to name it.
389 'name' => 'ai-content',
390 ],
391 null, // No specific template ID for this context
392 30
393 );
394 }
395
396 // Get skipped items if skip feature was enabled
397 $skipped_items = SessionData::get_skipped_items($this->session_id);
398 if (!empty($skipped_items)) {
399 $normalized_data['skipped_items'] = $skipped_items;
400 }
401
402 $this->sse_message([
403 'type' => 'complete',
404 'action' => 'complete',
405 'results' => $normalized_data,
406 ]);
407
408 update_user_meta(get_current_user_id(), 'templately_fsi_pack_id', $request_params["id"]);
409 if(!empty($import_status['hasFeedback'])){
410 update_user_meta(get_current_user_id(), 'templately_fsi_complete', 'done');
411 }
412 else{
413 update_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
414 }
415
416 do_action('templately_fsi_import_complete', $normalized_data);
417 $this->clear_data_file($request_params);
418 }
419
420 public function register_shutdown() {
421 // Release the run lock FIRST — every exit path (chunk-exit, completion,
422 // fatal) funnels through shutdown, and the owner-token check means a
423 // superseded request can never delete a newer execution's lock.
424 if ( ! empty( $this->run_lock_token ) && ! empty( $this->session_id ) ) {
425 SessionData::release_run_lock( $this->session_id, $this->run_lock_token );
426 $this->run_lock_token = null;
427 }
428
429 // Shared fatal detection + full-detail logging (spec 043 FR-008: the raw
430 // PHP message/file/line goes to the Templately log, NEVER to the client —
431 // the old handler streamed the first lines of the fatal over SSE with only
432 // ABSPATH masked, leaking plugin-relative server paths).
433 $last_error = FatalGuard::detect_and_log( 'full-site-import' );
434 if ( null !== $last_error ) {
435 $safe_message = ErrorCode::default_message( ErrorCode::FATAL_ERROR );
436
437 $this->handle_import_status( 'failed', $safe_message );
438
439 // A PHP fatal is not retryable — the same request will die the same
440 // way. FatalErrorException keeps the SSE frame's `retry` false.
441 $this->sse_error( new FatalErrorException( $safe_message ), $safe_message );
442 }
443
444 $this->debug_log("Shutdown:.....");
445 $this->debug_log("connection_status: " . $this->getConnectionStatusText());
446 $this->debug_log($last_error);
447 }
448
449 protected function getConnectionStatusText() {
450 $status = connection_status();
451 switch ($status) {
452 case CONNECTION_NORMAL:
453 return "Normal";
454 case CONNECTION_ABORTED:
455 return "Aborted";
456 case CONNECTION_TIMEOUT:
457 return "Timeout";
458 default:
459 return "Unknown";
460 }
461 }
462
463 }
464