| 1 |
<?php |
| 2 |
|
| 3 |
namespace Templately\Modules\FullSiteImport\Runners; |
| 4 |
|
| 5 |
use Exception; |
| 6 |
use Templately\Modules\FullSiteImport\Utils\Form; |
| 7 |
use Templately\Modules\FullSiteImport\Runners\BaseRunner; |
| 8 |
use Templately\Modules\FullSiteImport\Utils\Utils; |
| 9 |
use Templately\Modules\FullSiteImport\Utils\SessionData; |
| 10 |
use Templately\Utils\Helper; |
| 11 |
use Templately\Utils\Response\ResponseNormalizer; |
| 12 |
|
| 13 |
class DownloadZip extends BaseRunner { |
| 14 |
/** |
| 15 |
* @var string |
| 16 |
*/ |
| 17 |
private $download_key; |
| 18 |
|
| 19 |
/** |
| 20 |
* @var string |
| 21 |
*/ |
| 22 |
private $filePath; |
| 23 |
|
| 24 |
public function get_name(): string { |
| 25 |
return 'downloadzip'; |
| 26 |
} |
| 27 |
|
| 28 |
public function get_label(): string { |
| 29 |
return __( 'Download Zip', 'templately' ); |
| 30 |
} |
| 31 |
|
| 32 |
public function should_log(): bool { |
| 33 |
return true; |
| 34 |
} |
| 35 |
|
| 36 |
public function get_action(): string { |
| 37 |
return 'eventLog'; |
| 38 |
} |
| 39 |
|
| 40 |
public function log_message(): string { |
| 41 |
return __( 'Updating customizer settings.', 'templately' ); |
| 42 |
} |
| 43 |
|
| 44 |
public function should_run( $data, $imported_data = [] ): bool { |
| 45 |
$params = $this->origin->get_request_params(); |
| 46 |
return ! empty( $params['title'] ) || !empty( $params['slogan'] ); |
| 47 |
} |
| 48 |
|
| 49 |
public function import( $data, $imported_data ): array { |
| 50 |
/** |
| 51 |
* Download the zip |
| 52 |
*/ |
| 53 |
$this->download_zip( $_id ); |
| 54 |
|
| 55 |
SessionData::mark_step_complete($this->session_id, 'download_zip'); |
| 56 |
$this->sse_message( [ |
| 57 |
'type' => 'continue', |
| 58 |
'action' => 'continue', |
| 59 |
'info' => 'download_zip', |
| 60 |
'results' => __METHOD__ . '::' . __LINE__, |
| 61 |
] ); |
| 62 |
|
| 63 |
return [ 'customizer' => $customizer ]; |
| 64 |
} |
| 65 |
|
| 66 |
/** |
| 67 |
* @throws Exception |
| 68 |
*/ |
| 69 |
private function download_zip( $id ) { |
| 70 |
$this->sse_log( 'download', __( 'Downloading Template Pack', 'templately' ), 1 ); |
| 71 |
|
| 72 |
// Forward the host platform (e.g. 'ai-builder') so the cloud scopes the |
| 73 |
// pack download the same way as the REST/AJAX calls — without it this |
| 74 |
// binary download bypasses Helper::make_api_request() and the cloud |
| 75 |
// defaults to 'templately', re-gating AI Builder packs. Sourced from the |
| 76 |
// request param to match the param-only resolution in Helper. |
| 77 |
$requested_platform = isset( $_REQUEST['requested_platform'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 78 |
? sanitize_text_field( wp_unslash( $_REQUEST['requested_platform'] ) ) |
| 79 |
: 'templately'; |
| 80 |
|
| 81 |
// The download used to leave NO trace: a run that stopped here showed one |
| 82 |
// "Downloading Template Pack" line and nothing else — no size, no status, no |
| 83 |
// timing, no indication whether the request was even made. That is the state this |
| 84 |
// log exists to make readable. |
| 85 |
Helper::log( |
| 86 |
sprintf( 'download_zip: requesting pack %s (session=%s)', $id, $this->session_id ), |
| 87 |
'download_zip', |
| 88 |
'info' |
| 89 |
); |
| 90 |
|
| 91 |
// `connection_aborted()` is only truthful after output has been flushed, which the |
| 92 |
// SSE stream does constantly — so a shutdown handler can tell "the browser hung up" |
| 93 |
// apart from "PHP died", which is the whole question when a run ends in silence. |
| 94 |
$download_started = microtime( true ); |
| 95 |
$session_id = $this->session_id; |
| 96 |
register_shutdown_function( function () use ( $id, $session_id, $download_started ) { |
| 97 |
if ( defined( 'TEMPLATELY_DOWNLOAD_ZIP_DONE' ) ) { |
| 98 |
return; |
| 99 |
} |
| 100 |
|
| 101 |
Helper::log( |
| 102 |
sprintf( |
| 103 |
'download_zip: request ENDED before the pack was saved after %.1fs (pack=%s, session=%s, connection_aborted=%d, last_error=%s)', |
| 104 |
microtime( true ) - $download_started, |
| 105 |
$id, |
| 106 |
$session_id, |
| 107 |
connection_aborted(), |
| 108 |
wp_json_encode( error_get_last() ) |
| 109 |
), |
| 110 |
'download_zip', |
| 111 |
'error' |
| 112 |
); |
| 113 |
} ); |
| 114 |
|
| 115 |
$response = wp_remote_get( $this->get_api_url( "v2", "import/pack/$id" ), [ |
| 116 |
'timeout' => 30, |
| 117 |
'headers' => [ |
| 118 |
'Content-Type' => 'application/json', |
| 119 |
'Authorization' => 'Bearer ' . $this->api_key, |
| 120 |
'x-templately-ip' => Helper::get_ip(), |
| 121 |
'x-templately-url' => home_url('/'), |
| 122 |
'x-templately-version' => TEMPLATELY_VERSION, |
| 123 |
'x-templately-requested-platform' => $requested_platform, |
| 124 |
] |
| 125 |
]); |
| 126 |
|
| 127 |
// The pack is a ZIP: FR-012 raw passthrough, so the body stays bytes and |
| 128 |
// `download-key` stays readable off the raw response. The normalizer is |
| 129 |
// used only to CLASSIFY, matching Concerns\DownloadsPack — these two carry |
| 130 |
// the same download logic, and letting only one of them learn the contract |
| 131 |
// is how they drift apart again. |
| 132 |
$this->download_key = wp_remote_retrieve_header($response, 'download-key'); |
| 133 |
|
| 134 |
Helper::log( |
| 135 |
sprintf( |
| 136 |
'download_zip: pack %s answered %s in %.1fs (%s bytes, type=%s, download-key=%s)', |
| 137 |
$id, |
| 138 |
is_wp_error( $response ) ? 'WP_Error: ' . $response->get_error_message() : 'HTTP ' . (int) wp_remote_retrieve_response_code( $response ), |
| 139 |
microtime( true ) - $download_started, |
| 140 |
is_wp_error( $response ) ? '0' : strlen( (string) wp_remote_retrieve_body( $response ) ), |
| 141 |
is_wp_error( $response ) ? '-' : (string) wp_remote_retrieve_header( $response, 'content-type' ), |
| 142 |
$this->download_key ? 'present' : 'missing' |
| 143 |
), |
| 144 |
'download_zip', |
| 145 |
is_wp_error( $response ) ? 'error' : 'info' |
| 146 |
); |
| 147 |
|
| 148 |
$normalized = ResponseNormalizer::normalize($response, [ |
| 149 |
'raw' => true, |
| 150 |
'side_effects' => false, |
| 151 |
]); |
| 152 |
|
| 153 |
if ($normalized->is_error()) { |
| 154 |
$error = $normalized->error(); |
| 155 |
|
| 156 |
if ($error->is_retryable()) { |
| 157 |
$this->throw_retryable(__('Template pack download failed. ', 'templately') . $error->message()); |
| 158 |
} |
| 159 |
|
| 160 |
$support_message = ''; |
| 161 |
if (strpos($error->message(), Helper::web_url( '', [ 'support' => 'open' ] )) === false) { |
| 162 |
$support_message = sprintf(__(" Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), Helper::web_url( '', [ 'support' => 'open' ] )); |
| 163 |
} |
| 164 |
|
| 165 |
$this->throw_non_retryable($error->message() . $support_message); |
| 166 |
} |
| 167 |
|
| 168 |
$this->sse_log('download', __('Downloading Template Pack', 'templately'), 57); |
| 169 |
|
| 170 |
// $session_id = uniqid(); |
| 171 |
$this->dir_path = $this->tmp_dir . $this->session_id . DIRECTORY_SEPARATOR; |
| 172 |
$this->filePath = $this->tmp_dir . "{$this->session_id}.zip"; |
| 173 |
// $this->session_id = $session_id; |
| 174 |
|
| 175 |
SessionData::set($this->session_id, 'session_id', $this->session_id); |
| 176 |
SessionData::set($this->session_id, 'dir_path', $this->dir_path); |
| 177 |
SessionData::set($this->session_id, 'download_key', $this->download_key); |
| 178 |
|
| 179 |
$written = file_put_contents($this->filePath, $response['body']); // phpcs:ignore |
| 180 |
|
| 181 |
Helper::log( |
| 182 |
sprintf( 'download_zip: wrote %s bytes to %s', var_export( $written, true ), $this->filePath ), |
| 183 |
'download_zip', |
| 184 |
false === $written ? 'error' : 'info' |
| 185 |
); |
| 186 |
|
| 187 |
if ($written) { |
| 188 |
// The shutdown handler above only fires when the pack never made it to disk. |
| 189 |
if ( ! defined( 'TEMPLATELY_DOWNLOAD_ZIP_DONE' ) ) { |
| 190 |
define( 'TEMPLATELY_DOWNLOAD_ZIP_DONE', true ); |
| 191 |
} |
| 192 |
|
| 193 |
$this->sse_log('download', __('Downloading Template Pack', 'templately'), 100); |
| 194 |
|
| 195 |
$this->unzip(); |
| 196 |
|
| 197 |
Helper::log( sprintf( 'download_zip: unzipped into %s', $this->dir_path ), 'download_zip', 'info' ); |
| 198 |
} else { |
| 199 |
$this->throw_retryable(__('Downloading Failed. Please try again', 'templately')); |
| 200 |
} |
| 201 |
} |
| 202 |
|
| 203 |
} |