| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* remove 'action' => 'continue', |
| 5 |
* way to retry |
| 6 |
* way to skip if failed multiple times |
| 7 |
* |
| 8 |
* @todo: on runner check for timeout and retry |
| 9 |
* @todo: use ErrorException on runner to skip item when error occurs: not useful |
| 10 |
* |
| 11 |
*/ |
| 12 |
|
| 13 |
|
| 14 |
namespace Templately\Modules\FullSiteImport; |
| 15 |
|
| 16 |
use Elementor\Plugin; |
| 17 |
use Error; |
| 18 |
use Exception; |
| 19 |
use Templately\Modules\FullSiteImport\Exception\FatalErrorException; |
| 20 |
use Templately\Modules\FullSiteImport\Exception\RetryableErrorException; |
| 21 |
use Templately\Modules\FullSiteImport\Exception\UnknownErrorException; |
| 22 |
use Templately\Modules\FullSiteImport\Runners\Finalizer; |
| 23 |
use Templately\Modules\FullSiteImport\Utils\LogHandler; |
| 24 |
use Templately\Modules\FullSiteImport\Utils\Utils; |
| 25 |
use Templately\Modules\FullSiteImport\Utils\SessionData; |
| 26 |
use Templately\Modules\FullSiteImport\Utils\AIUtils; |
| 27 |
use Templately\Utils\Base; |
| 28 |
use Templately\Utils\Response\AjaxResponder; |
| 29 |
use Templately\Utils\Helper; |
| 30 |
use Templately\Utils\Installer; |
| 31 |
use Templately\Utils\Options; |
| 32 |
|
| 33 |
class FullSiteImport extends Base { |
| 34 |
use Concerns\LogHelper; |
| 35 |
use Concerns\ThrowsErrors; |
| 36 |
use Concerns\HandlesSession; |
| 37 |
use Concerns\DownloadsPack; |
| 38 |
use Concerns\RunsImport; |
| 39 |
use Concerns\ReportsStatus; |
| 40 |
use Concerns\NormalizesResult; |
| 41 |
use Concerns\CleansUp; |
| 42 |
use Concerns\TracksRevert; |
| 43 |
use Concerns\HandlesAiEndpoints; |
| 44 |
|
| 45 |
const SESSION_OPTION_KEY = 'templately_import_session'; |
| 46 |
|
| 47 |
/** |
| 48 |
* Extensions a pack carries that WordPress does not accept as an upload. |
| 49 |
* |
| 50 |
* Everything else comes from get_allowed_mime_types(), so this list only has |
| 51 |
* to name what the pack format adds on top of it: |
| 52 |
* |
| 53 |
* - `json` — every pack is built from JSON (manifest, templates, content), |
| 54 |
* and WordPress has never allowed .json uploads. |
| 55 |
* - `xml` — the WXR files under wp-content/. Also not an allowed upload: |
| 56 |
* core hands .xml to the importer plugin rather than the media library. |
| 57 |
* - `ai` — the marker segment in `{id}.ai.json`, an AI-generated page. Not |
| 58 |
* the Illustrator format. |
| 59 |
* - `svg` — packs ship SVG artwork. This plugin already allows SVG uploads |
| 60 |
* for the duration of an import (see allow_svg_upload()), but that filter is |
| 61 |
* only added in start_content_import(), which runs AFTER extraction — so at |
| 62 |
* sweep time get_allowed_mime_types() does not have it and the pack's SVGs |
| 63 |
* would be deleted. It has to be named here. |
| 64 |
* - fonts — a pack may ship webfonts, and none of these are upload types. |
| 65 |
* Passive assets; nothing here is executable by any server. |
| 66 |
* |
| 67 |
* Adding to this list is how a new pack file type gets through. It fails |
| 68 |
* closed and logs, so a missing entry shows up as a dropped file in the |
| 69 |
* import log — not as a silent hole. |
| 70 |
* |
| 71 |
* Lives on the class rather than the DownloadsPack trait that reads it: |
| 72 |
* trait constants are PHP 8.2+, and the advertised floor here is 7.2. |
| 73 |
*/ |
| 74 |
const PACK_EXTENSIONS = [ |
| 75 |
'json', 'xml', 'ai', |
| 76 |
'svg', 'svgz', |
| 77 |
'woff', 'woff2', 'ttf', 'otf', 'eot', |
| 78 |
]; |
| 79 |
public $manifest; |
| 80 |
protected $export; |
| 81 |
|
| 82 |
private $version = '1.0.0'; |
| 83 |
|
| 84 |
public $download_key; |
| 85 |
protected $dev_mode = false; |
| 86 |
protected $api_key = ''; |
| 87 |
protected $session_id = ''; |
| 88 |
protected $documents_data = []; |
| 89 |
private $is_import_status_handled = false; |
| 90 |
|
| 91 |
public $dir_path; |
| 92 |
protected $filePath; |
| 93 |
protected $tmp_dir = null; |
| 94 |
public $request_params = []; |
| 95 |
|
| 96 |
// Polling-specific property for ai_poll_template() |
| 97 |
private $polling_is_last_part = null; |
| 98 |
|
| 99 |
public function __construct() { |
| 100 |
$this->dev_mode = defined('TEMPLATELY_DEV') && TEMPLATELY_DEV; |
| 101 |
$this->api_key = Options::get_instance()->get('api_key'); |
| 102 |
|
| 103 |
// Independent endpoint handlers (registered through the same nonce/cap wrapper). |
| 104 |
// `import_revert` moved to modules/import-revert (2026-07-23) — it arrives |
| 105 |
// through the templately_fsi_ajax_handlers seam below, like the feedback shims. |
| 106 |
$asset = new Ajax\AssetController(); |
| 107 |
$pack_info = new Ajax\PackInfoController(); |
| 108 |
|
| 109 |
$this->add_ajax_action('import_settings', $this); |
| 110 |
$this->add_ajax_action('create_session_and_download', $this); |
| 111 |
$this->add_ajax_action('import_status', $this); |
| 112 |
$this->add_ajax_action('import', $this); |
| 113 |
$this->add_ajax_action('import_info', $pack_info); |
| 114 |
$this->add_ajax_action('import_global_settings', $pack_info); |
| 115 |
$this->add_ajax_action('google_font', $asset); |
| 116 |
$this->add_ajax_action('ai_get_json', $this); |
| 117 |
$this->add_ajax_action('ai_poll_template', $this); |
| 118 |
$this->add_ajax_action('merge_settings_with_template', $asset); |
| 119 |
|
| 120 |
// Cross-module AJAX handlers arrive through this seam instead of direct class |
| 121 |
// imports (e.g. post-import-feedback supplies its two deprecated shims — see |
| 122 |
// that module.php). Applied on 'init' rather than here because FSI boots before |
| 123 |
// the supplying modules (alphabetical discovery order); admin-ajax.php fires |
| 124 |
// wp_ajax_* well after 'init', so registration is never late. |
| 125 |
add_action('init', function () { |
| 126 |
$handlers = apply_filters('templately_fsi_ajax_handlers', []); |
| 127 |
foreach ($handlers as $action => $handler) { |
| 128 |
$this->add_ajax_action($action, $handler); |
| 129 |
} |
| 130 |
}); |
| 131 |
|
| 132 |
add_action('admin_init', [$this, 'admin_init']); |
| 133 |
|
| 134 |
// Cleanup scheduling moved to `modules/utilities/` (spec 052). This module |
| 135 |
// contributes tasks to the one shared sweep instead of running its own |
| 136 |
// daily event, which pruned options only and never touched the disk. |
| 137 |
|
| 138 |
if(isset($_GET['action']) && ($_GET['action'] == 'templately_pack_import' || $_GET['action'] == 'templately_pack_import_status')) { |
| 139 |
add_filter('wp_redirect', '__return_false', 999); |
| 140 |
} |
| 141 |
|
| 142 |
if ($this->dev_mode) { |
| 143 |
add_filter('http_request_host_is_external', '__return_true'); |
| 144 |
add_filter('http_request_args', function ($args) { |
| 145 |
$args['sslverify'] = false; |
| 146 |
|
| 147 |
return $args; |
| 148 |
}); |
| 149 |
} |
| 150 |
} |
| 151 |
|
| 152 |
public function add_ajax_action($action, $object) { |
| 153 |
add_action("wp_ajax_templately_pack_$action", function() use ($action, $object) { |
| 154 |
// The nonce + capability gate lives in AjaxResponder::guard() so that it |
| 155 |
// answers with the same envelope (and real machine codes) as everything |
| 156 |
// else. It sends and dies on failure. Spec 043 / PRD PHP-5. |
| 157 |
if (!AjaxResponder::guard('templately_nonce', ['install_plugins', 'install_themes'])) { |
| 158 |
return; |
| 159 |
} |
| 160 |
|
| 161 |
// Every AI/FSI step arrives through here, so this is the one place that can say |
| 162 |
// WHICH step a run reached and how the request ended. Without it a run that |
| 163 |
// stopped between two steps was indistinguishable from one that never started |
| 164 |
// the next. |
| 165 |
// |
| 166 |
// The end is reported from a shutdown handler and NOT gated on "did the handler |
| 167 |
// return", because these handlers answer through AjaxResponder, which `die()`s — |
| 168 |
// so the normal, successful path never comes back here. A flag set after |
| 169 |
// `call_user_func()` would therefore mark every success as a failure. The |
| 170 |
// shutdown handler states the facts instead and lets the reader judge: |
| 171 |
// `connection_aborted=1` means the browser hung up (ignore_user_abort is 0, so |
| 172 |
// PHP stops at the next write, silently), and a non-null `last_error` means PHP |
| 173 |
// died. |
| 174 |
$started = microtime( true ); |
| 175 |
Helper::log( sprintf( 'ajax: templately_pack_%s — start', $action ), 'fsi_ajax', 'info' ); |
| 176 |
|
| 177 |
register_shutdown_function( function () use ( $action, $started ) { |
| 178 |
Helper::log( |
| 179 |
sprintf( |
| 180 |
'ajax: templately_pack_%s — ended after %.1fs (connection_aborted=%d, http_code=%s, last_error=%s)', |
| 181 |
$action, |
| 182 |
microtime( true ) - $started, |
| 183 |
connection_aborted(), |
| 184 |
function_exists( 'http_response_code' ) ? (string) http_response_code() : '?', |
| 185 |
wp_json_encode( error_get_last() ) |
| 186 |
), |
| 187 |
'fsi_ajax', |
| 188 |
connection_aborted() || null !== error_get_last() ? 'warning' : 'info' |
| 189 |
); |
| 190 |
} ); |
| 191 |
|
| 192 |
// Call the actual handler method on the registered object (defaults to $this). |
| 193 |
call_user_func([$object, $action]); |
| 194 |
}); |
| 195 |
} |
| 196 |
|
| 197 |
|
| 198 |
|
| 199 |
|
| 200 |
|
| 201 |
|
| 202 |
|
| 203 |
|
| 204 |
|
| 205 |
|
| 206 |
|
| 207 |
|
| 208 |
|
| 209 |
|
| 210 |
|
| 211 |
|
| 212 |
|
| 213 |
|
| 214 |
|
| 215 |
|
| 216 |
|
| 217 |
|
| 218 |
|
| 219 |
private function skipped_plugin(): bool { |
| 220 |
return empty($this->request_params['plugins']) || !is_array($this->request_params['plugins']); |
| 221 |
} |
| 222 |
|
| 223 |
|
| 224 |
|
| 225 |
|
| 226 |
|
| 227 |
|
| 228 |
|
| 229 |
|
| 230 |
public function get_request_params() { |
| 231 |
return $this->request_params; |
| 232 |
} |
| 233 |
|
| 234 |
/** |
| 235 |
* Kept on FullSiteImport for backward compatibility (called as FullSiteImport::has_revert()). |
| 236 |
* The revert surface moved to modules/import-revert (2026-07-23), which answers |
| 237 |
* the `templately_has_revert` filter — no class reference in this direction. |
| 238 |
*/ |
| 239 |
public static function has_revert() { |
| 240 |
return (bool) apply_filters( 'templately_has_revert', false ); |
| 241 |
} |
| 242 |
|
| 243 |
|
| 244 |
|
| 245 |
public function allow_svg_upload($mimes) { |
| 246 |
// Allow SVG |
| 247 |
$mimes['svg'] = 'image/svg+xml'; |
| 248 |
return $mimes; |
| 249 |
} |
| 250 |
|
| 251 |
|
| 252 |
|
| 253 |
|
| 254 |
|
| 255 |
|
| 256 |
|
| 257 |
|
| 258 |
|
| 259 |
|
| 260 |
|
| 261 |
|
| 262 |
|
| 263 |
|
| 264 |
|
| 265 |
|
| 266 |
|
| 267 |
|
| 268 |
|
| 269 |
} |
| 270 |
|