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 / Abilities / FullSiteImportStatusAbility.php

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

207 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * `templately/full-site-import-status` — advance a Full Site Import by ONE
4 * time-slice and report normalized status (spec 042 US2, FR-002/003/004/005).
5 *
6 * Poll-driven advancement (research.md §1/§3): each call drives the next slice
7 * of the resumable pipeline and returns where it now stands. The agent's
8 * repeated calls replace the browser's SSE `continue`→next-request loop; the
9 * import only progresses while it is polled (if the agent stops, the import
10 * pauses — its state is preserved and observable on a later poll).
11 *
12 * Drive sequence per handle:
13 * 1. first call (no `progress.download_zip`): loopback JSON
14 * `create_session_and_download` (download zip + read manifest), then
15 * 2. loopback SSE `import` (one slice; the handler `exit`s at the boundary),
16 * 3. read the incremental log via JSON `import_status`,
17 * 4. normalize via FsiStatusNormalizer.
18 *
19 * @package Templately\Modules\FullSiteImport\Abilities
20 */
21
22 namespace Templately\Modules\FullSiteImport\Abilities;
23
24 use Templately\Modules\FullSiteImport\Utils\SessionData;
25 use Templately\Modules\McpCore\Registry\ToolDescriptor;
26 use Templately\Modules\McpCore\Support\AjaxLoopbackDispatcher;
27 use Templately\Modules\FullSiteImport\Abilities\Support\FsiActiveImportGuard;
28 use Templately\Modules\FullSiteImport\Abilities\Support\FsiStatusNormalizer;
29 use Templately\Modules\McpCore\Support\Permissions;
30 use WP_Error;
31
32 class FullSiteImportStatusAbility {
33
34 const ID = 'templately/full-site-import-status';
35
36 public static function descriptor(): array {
37 return [
38 'id' => self::ID,
39 'label' => __( 'Track Templately Full Site Import', 'templately' ),
40 'description' => __( 'Advance a running full-site import by ONE step (advance=true, the default) and report progress + status: running, needs_retry, failed, complete, or expired. Poll-driven — keep calling until status is "complete" or "failed" (the import only moves while polled); on "needs_retry" call templately/retry-full-site-import then continue polling. Pass advance=false to read current progress cheaply WITHOUT running a step — ideal for a monitor/watcher checking whether the import has finished. The whole advance loop can run in a background process that treats a terminal status ("complete"/"failed") as the completion signal.', 'templately' ),
41 'input_schema' => [
42 'type' => 'object',
43 'properties' => [
44 'handle' => [ 'type' => 'string', 'description' => __( 'The import handle returned by start-full-site-import.', 'templately' ) ],
45 'last_log_index' => [ 'type' => 'integer', 'default' => 0 ],
46 'advance' => [ 'type' => 'boolean', 'default' => true, 'description' => __( 'false = report only, do not run a slice.', 'templately' ) ],
47 ],
48 'required' => [ 'handle' ],
49 'additionalProperties' => false,
50 ],
51 'output_schema' => [
52 'type' => 'object',
53 'properties' => [
54 'handle' => [ 'type' => 'string' ],
55 'status' => [ 'type' => 'string', 'enum' => [ 'running', 'needs_retry', 'failed', 'complete', 'expired' ] ],
56 'progress' => [ 'type' => 'integer' ],
57 'phase' => [ 'type' => 'string' ],
58 'log' => [ 'type' => 'array' ],
59 'last_log_index' => [ 'type' => 'integer' ],
60 'message' => [ 'type' => 'string' ],
61 'summary' => [ 'type' => 'object' ],
62 ],
63 ],
64 'execute_callback' => [ self::class, 'execute' ],
65 'permission_callback' => [ Permissions::class, 'can_use_abilities' ],
66 'access_level' => ToolDescriptor::ACCESS_FULL,
67 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => false ],
68 ];
69 }
70
71 /**
72 * @param array $input
73 * @return array|WP_Error
74 */
75 public static function execute( array $input ) {
76 $handle = self::sanitize_handle( $input['handle'] ?? '' );
77 $last_log_index = (int) ( $input['last_log_index'] ?? 0 );
78 $advance = array_key_exists( 'advance', $input ) ? (bool) $input['advance'] : true;
79
80 if ( is_wp_error( $handle ) ) {
81 return $handle;
82 }
83
84 return self::advance( $handle, $last_log_index, $advance );
85 }
86
87 /**
88 * Advance one slice (when $advance) and return normalized status. Shared
89 * with RetryFullSiteImportAbility (task T016).
90 *
91 * @param string $handle
92 * @param int $last_log_index
93 * @param bool $advance
94 * @return array|WP_Error
95 */
96 public static function advance( string $handle, int $last_log_index = 0, bool $advance = true ) {
97 $session_data = SessionData::get_data( $handle );
98
99 // Unknown / expired handle (7-day cleanup or never existed).
100 if ( empty( $session_data ) || ! is_array( $session_data ) ) {
101 return [
102 'handle' => $handle,
103 'status' => FsiStatusNormalizer::STATUS_EXPIRED,
104 'progress' => 0,
105 'phase' => '',
106 'log' => [],
107 'last_log_index' => $last_log_index,
108 ];
109 }
110
111 $sse_events = [];
112
113 if ( $advance ) {
114 $progress = $session_data['progress'] ?? [];
115
116 // First slice for this handle: download the pack (JSON handler).
117 if ( empty( $progress['download_zip'] ) ) {
118 $download = AjaxLoopbackDispatcher::dispatch_json( 'create_session_and_download', [
119 'session_id' => $handle,
120 'id' => (int) ( $session_data['id'] ?? 0 ),
121 'platform' => (string) ( $session_data['platform'] ?? 'elementor' ),
122 ] );
123 if ( is_wp_error( $download ) ) {
124 return $download;
125 }
126 // A download failure (e.g. an unentitled pack) surfaces here.
127 $failure = AjaxLoopbackDispatcher::envelope_error(
128 $download,
129 __( 'The pack could not be downloaded.', 'templately' )
130 );
131 if ( null !== $failure ) {
132 // `retryable` is the envelope's own field (spec 043); the handler's
133 // old bespoke `should_retry` key no longer exists.
134 $retry = ! empty( $failure->get_error_data()['retryable'] );
135 $message = $failure->get_error_message();
136 if ( ! $retry ) {
137 FsiActiveImportGuard::clear( get_current_user_id() );
138 }
139 return [
140 'handle' => $handle,
141 'status' => $retry ? FsiStatusNormalizer::STATUS_NEEDS_RETRY : FsiStatusNormalizer::STATUS_FAILED,
142 'progress' => 0,
143 'phase' => 'download_zip',
144 'log' => [],
145 'last_log_index' => $last_log_index,
146 'message' => $message,
147 ];
148 }
149 $session_data = SessionData::get_data( $handle );
150 }
151
152 // Run one content-import slice (SSE handler; exits at the boundary).
153 $sse_result = AjaxLoopbackDispatcher::dispatch_sse( 'import', [ 'session_id' => $handle ] );
154 if ( is_wp_error( $sse_result ) ) {
155 return $sse_result;
156 }
157 $sse_events = $sse_result;
158 $session_data = SessionData::get_data( $handle );
159 }
160
161 // Incremental log (best-effort; SSE events already carry status).
162 $log = [];
163 $new_idx = $last_log_index;
164 $log_res = AjaxLoopbackDispatcher::dispatch_json( 'import_status', [
165 'session_id' => $handle,
166 'lastLogIndex' => $last_log_index,
167 ], 'GET' );
168 if ( ! is_wp_error( $log_res ) && ! empty( $log_res['log'] ) && is_array( $log_res['log'] ) ) {
169 $log = $log_res['log'];
170 $new_idx = $last_log_index + count( $log );
171 }
172
173 $normalized = FsiStatusNormalizer::normalize( $sse_events, $session_data, $log );
174
175 // Clear the one-active-import guard on a terminal outcome.
176 if ( in_array( $normalized['status'], [ FsiStatusNormalizer::STATUS_COMPLETE, FsiStatusNormalizer::STATUS_FAILED ], true ) ) {
177 FsiActiveImportGuard::clear( get_current_user_id() );
178 }
179
180 return array_merge(
181 [
182 'handle' => $handle,
183 'log' => $log,
184 'last_log_index' => $new_idx,
185 ],
186 $normalized
187 );
188 }
189
190 /**
191 * FSI session ids are uuid4 (or, for a session minted before that change,
192 * `uniqid()`-shaped); reject anything with path or control characters before
193 * it reaches SessionData / the loopback. The character class already allows
194 * the hyphens a uuid carries, so both shapes pass.
195 *
196 * @param mixed $handle
197 * @return string|WP_Error
198 */
199 public static function sanitize_handle( $handle ) {
200 $handle = is_string( $handle ) ? trim( $handle ) : '';
201 if ( '' === $handle || 1 !== preg_match( '/^[A-Za-z0-9_.-]{6,64}$/', $handle ) ) {
202 return new WP_Error( 'expired_handle', __( 'Unknown or invalid import handle.', 'templately' ) );
203 }
204 return $handle;
205 }
206 }
207