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 / Runners / Dependencies.php

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

444 lines 16.7 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\Runners;
4
5 use Exception;
6 use Templately\Modules\FullSiteImport\Exception\FatalErrorException;
7 use Templately\Modules\FullSiteImport\Utils\Form;
8 use Templately\Modules\FullSiteImport\Runners\BaseRunner;
9 use Templately\Modules\FullSiteImport\Utils\Utils;
10 use Templately\Modules\FullSiteImport\Utils\SessionData;
11 use Templately\Utils\Helper;
12 use Templately\Modules\ProPluginProvisioning\Catalog as ProPluginCatalog;
13 use Templately\Modules\ProPluginProvisioning\Module as ProPluginModule;
14 use Templately\Utils\Installer;
15
16 class Dependencies extends BaseRunner {
17
18 public function get_name(): string {
19 return 'dependencies';
20 }
21
22 public function get_label(): string {
23 return __( 'Download Dependencies', 'templately' );
24 }
25
26 public function should_log(): bool {
27 return true;
28 }
29
30 public function get_action(): string {
31 return 'eventLog';
32 }
33
34 public function log_message(): string {
35 return __( 'Downloading Dependencies', 'templately' );
36 }
37
38 public function should_run( $data, $imported_data = [] ): bool {
39 return self::has_dependencies_to_install( (array) $data );
40 }
41
42 /**
43 * Whether this import carries anything for this runner to install.
44 *
45 * Static twin of {@see self::should_run()} so callers outside the runner loop
46 * can ask the same question without instantiating a runner. The two must stay
47 * in agreement, which is why `should_run()` delegates here rather than
48 * repeating the condition.
49 *
50 * @param array $data Request params.
51 * @return bool
52 */
53 public static function has_dependencies_to_install( array $data ): bool {
54 return ! empty( $data['theme'] ) || ! empty( $data['plugins'] );
55 }
56
57 /**
58 * Whether this runner still has plugins to install and activate.
59 *
60 * True only on the slice BEFORE installation happens. `import()` installs, marks
61 * `plugins_installed`, and then deliberately `exit`s with a `continue` event so
62 * the next AJAX call starts a fresh PHP request in which WordPress has actually
63 * LOADED the newly activated plugins — they are not loadable in the request that
64 * activated them.
65 *
66 * That two-phase shape is why `Import::run()`'s builder preflight must consult
67 * this before refusing. The preflight asks "is the pack's builder loaded?", and
68 * on the first slice of a fresh site the honest answer is "not yet, and this
69 * runner is about to fix that". Running it unconditionally turned every import
70 * that needed its builder INSTALLED into a hard, non-retryable stop — the
71 * dependency step could never run, so the plugin it would have installed was
72 * exactly the plugin the user was told to go and install by hand.
73 *
74 * @param array $data Request params (carrying `progress` from the session).
75 * @return bool
76 */
77 public static function installation_pending( array $data ): bool {
78 $progress = isset( $data['progress'] ) && is_array( $data['progress'] ) ? $data['progress'] : [];
79
80 return self::has_dependencies_to_install( $data ) && empty( $progress['plugins_installed'] );
81 }
82
83 public function import( $data, $imported_data ): array {
84 $dependency_data = [];
85 $progress = $data['progress'] ?? [];
86
87 /**
88 * Checking & Installing Plugin Dependencies
89 */
90 $dependency_data['theme'] = $this->install_themes($data);
91 $dependency_data['plugins'] = $this->install_plugins($data);
92
93 /**
94 * Exit current request to allow WordPress to load newly activated plugins
95 * On the next AJAX call, WordPress will have loaded all active plugins
96 */
97 if ( empty( $progress['plugins_installed'] ) ) {
98 // Mark plugins as installed
99 SessionData::mark_step_complete($this->session_id, 'plugins_installed');
100
101 // Exit current request and continue in new AJAX call where WordPress has loaded the plugins
102 $this->sse_message( [
103 'type' => 'continue',
104 'action' => 'continue',
105 'info' => 'plugins_installed',
106 'results' => __METHOD__ . '::' . __LINE__,
107 ] );
108 exit;
109 }
110
111 /**
112 * Verify that required plugins are active and their classes are loaded.
113 * Runs in the new AJAX call, after WordPress has loaded the plugins.
114 *
115 * RE-ENABLED. This was commented out in f6697b73a (2025-12-08) as a
116 * temporary bypass and never restored. With it disabled, an import whose
117 * builder plugin is missing or failed to activate ran on to the Templates
118 * runner and died with a raw PHP fatal:
119 *
120 * Uncaught Error: Class "Elementor\Core\Base\Document" not found
121 *
122 * That fatal reached real users via post-import feedback reports. This
123 * check turns it into an actionable FatalErrorException instead.
124 */
125 $this->verify_required_plugins_active( $data );
126
127 return ['dependency_data' => $dependency_data];
128 }
129
130 private function install_plugins($request_params) {
131 $progress = $request_params['progress'] ?? [];
132 $results = [];
133
134 if ( ! empty( $request_params['plugins'] ) && is_array( $request_params['plugins'] ) ) {
135 // $this->sse_log( 'plugin', 'Installing Plugins', 1 );
136 $total_plugin = count($request_params['plugins']);
137
138 // $total_plugin_installed = $total_plugin;
139 // $_installed_plugins = $request_params['dependency_data']['plugins']['_installed_plugins'] ?? 0;
140 $progress['plugin_dependency'] = $progress['plugin_dependency'] ?? [];
141
142 // $this->before_install_hook();
143
144 // Sort plugins to ensure pro plugins are activated after free versions.
145 //
146 // The Essential Blocks pair was added with spec 059: Templately can now install
147 // Essential Blocks Pro itself, so the ordering that was previously academic (the
148 // pro edition was never installed by us) became load-bearing. A pro add-on
149 // activated before its free base loads against an absent parent.
150 $plugin_order = [
151 'elementor/elementor.php',
152 'elementor-pro/elementor-pro.php',
153 'essential-addons-for-elementor-lite/essential_adons_elementor.php',
154 'essential-addons-elementor/essential_adons_elementor.php',
155 'essential-blocks/essential-blocks.php',
156 'essential-blocks-pro/essential-blocks-pro.php',
157 ];
158
159 usort($request_params['plugins'], function($a, $b) use ($plugin_order) {
160 $pos_a = array_search($a['plugin_file'], $plugin_order);
161 $pos_b = array_search($b['plugin_file'], $plugin_order);
162
163 // If both plugins are in the order array, sort by their position
164 if ($pos_a !== false && $pos_b !== false) {
165 return $pos_a - $pos_b;
166 }
167
168 // If only one plugin is in the order array, prioritize it
169 if ($pos_a !== false) {
170 return -1;
171 }
172 if ($pos_b !== false) {
173 return 1;
174 }
175
176 // If neither plugin is in the order array, maintain original order
177 return 0;
178 });
179
180 $results = $this->loop( $request_params['plugins'], function( $key, $dependency, $results ) use(&$_installed_plugins, $total_plugin) {
181 $_installed_plugins = $results['_installed_plugins'] ?? 0;
182
183 // $result = [];
184 $this->sse_log( 'plugin', 'Installing Required Plugins: ' . $dependency['name'], floor( ( 100 * $_installed_plugins / $total_plugin ) ) );
185
186 $dependency['slug'] = $dependency['plugin_original_slug'];
187 $plugin_status = Installer::get_instance()->install($dependency);
188
189 if (!$plugin_status['success']) {
190 /**
191 * A pro plugin Templately offered to obtain, and could not (spec 059).
192 *
193 * This is NOT a failed installation. It means the user's subscription does
194 * not cover the plugin — which was also true before this feature existed,
195 * when Templately never even tried and the import ran on regardless. So it
196 * must not be reported as an error and must not stop the import, even for a
197 * pack that marks the dependency required (FR-008a).
198 *
199 * The exemption is deliberately narrow: it reaches only the two plugins in
200 * the provisioning catalog, and only when the refusal is `pro_plugin`. A
201 * genuine install failure of the same plugin (no disk space, no filesystem
202 * credentials) carries a different code and still behaves as before, as does
203 * every required free dependency.
204 */
205 $is_unobtainable_pro = ( 'pro_plugin' === ( $plugin_status['code'] ?? '' ) )
206 && self::provisioning_is_live()
207 && ProPluginCatalog::is_provisionable( $dependency );
208
209 $error_message = 'Installation Failed: ' . $dependency['name'];
210 if (!empty($plugin_status['message'])) {
211 $error_message .= ' (' . $plugin_status['message'] . ')';
212 }
213
214 if ( ! $is_unobtainable_pro ) {
215 $this->sse_message([
216 'position' => 'plugin',
217 'action' => 'updateLog',
218 'status' => 'error',
219 'message' => $error_message,
220 'type' => "plugin_{$dependency['plugin_original_slug']}",
221 'progress' => 0
222 ]);
223
224 if (isset($dependency['mustHave']) && $dependency['mustHave']) {
225 $this->removeLog('plugin');
226 throw new FatalErrorException($error_message);
227 }
228 }
229
230 $results['failed'][] = [
231 'name' => $dependency['name'],
232 'slug' => $dependency['slug'],
233 'link' => $dependency['link'] ?? '',
234 'message' => $plugin_status['message'] ?? '',
235 // Lets the completion screen tell "we could not get this for you"
236 // apart from "installing this went wrong". Deliberately NOT a verdict
237 // on entitlement: the same refusal covers a timeout, an unreachable
238 // host, a missing zip extension and a plan that does not include the
239 // plugin, and nothing here can tell them apart (spec 059 FR-007).
240 'reason' => $is_unobtainable_pro ? 'not_available' : 'install_failed',
241 ];
242 } else {
243 $_installed_plugins++;
244 // $total_plugin_installed--;
245 }
246
247 $results['_installed_plugins'] = $_installed_plugins;
248
249 // Say what happened. This line used to run unconditionally, so the ONLY live
250 // log a non-entitled user saw for a refused pro plugin said it was installed.
251 if ( ! empty( $plugin_status['success'] ) ) {
252 $this->sse_log( 'plugin', 'Installed Required Plugins: ' . $dependency['name'], floor( ( 100 * $_installed_plugins / $total_plugin ) ) );
253 } else {
254 $this->sse_log( 'plugin', 'Skipped: ' . $dependency['name'], floor( ( 100 * $_installed_plugins / $total_plugin ) ) );
255 }
256
257 return $results;
258 }, null, true);
259
260 // $this->after_install_hook();
261
262 $this->sse_message([
263 'action' => 'updateLog',
264 'status' => 'complete',
265 'message' => "Installed Required Plugins ({$results['_installed_plugins']}/$total_plugin)",
266 'type' => "plugin",
267 'progress' => 100
268 ]);
269 $results['total'] = $total_plugin;
270 $results['succeed'] = $results['_installed_plugins'];
271 } else {
272 $this->removeLog('plugin');
273 }
274
275
276
277 return $results;
278 }
279
280 /**
281 * Assert that the builder the pack targets is actually LOADED in this request.
282 *
283 * Static and public because this must ALSO run when this runner is skipped.
284 * `should_run()` only returns true when the request carries `theme`/`plugins`,
285 * so an import that supplies no plugin list — one started headlessly through
286 * an agent capability, or a pack whose plugin list did not reach the request —
287 * bypasses this runner entirely and lands in the Templates runner with no
288 * builder loaded. `Import::run()` therefore calls this directly, before any
289 * runner executes.
290 *
291 * Checking `class_exists()` rather than `is_plugin_active()` is deliberate: a
292 * plugin can be flagged active in options yet not be loaded in THIS request
293 * (WordPress only reads the active-plugin list at bootstrap, so a plugin
294 * activated mid-request is unavailable until the next one). It is the class,
295 * not the option, that `Factory\TemplateFactory` needs.
296 *
297 * @param string $platform Platform slug from the manifest.
298 * @throws FatalErrorException If the builder's classes are not loaded.
299 */
300 public static function verify_platform_available( string $platform ): void {
301 if ( 'elementor' === $platform ) {
302 if ( ! class_exists( 'Elementor\Plugin' ) && ! class_exists( 'Elementor\Core\Base\Document' ) ) {
303 throw new FatalErrorException(
304 __( 'Elementor is required by this template but is not active on your site. Please install and activate Elementor, then try again.', 'templately' )
305 );
306 }
307 }
308
309 if ( 'gutenberg' === $platform ) {
310 if ( ! defined( 'ESSENTIAL_BLOCKS_FILE' ) ) {
311 throw new FatalErrorException(
312 __( 'Essential Blocks is required by this template but is not active on your site. Please install and activate Essential Blocks, then try again.', 'templately' )
313 );
314 }
315 }
316 }
317
318 /**
319 * Verify that required plugins are active and their classes are loaded
320 *
321 * This method checks if the required plugins (Elementor or Gutenberg) are properly
322 * activated and their classes are available. This is crucial because WordPress doesn't
323 * automatically load newly installed plugins until the next request.
324 *
325 * @param array $data Request parameters containing plugin information
326 * @throws FatalErrorException If required plugin classes are not loaded
327 */
328 private function verify_required_plugins_active( $data ) {
329 self::verify_platform_available( $data['manifest']['platform'] ?? '' );
330
331 // Additional verification: Check if installed plugins are actually active
332 if ( ! empty( $data['plugins'] ) && is_array( $data['plugins'] ) ) {
333 foreach ( $data['plugins'] as $plugin ) {
334 // A pro plugin Templately offered to obtain and could not is exempt here for
335 // the same reason it is exempt from the install failure above (spec 059
336 // FR-008a): not owning it is not a broken import, and before that spec
337 // Templately never installed it and never stopped for it either. Without
338 // this, the exemption granted in install_plugins() would be re-imposed two
339 // steps later, in a fresh request, with a less helpful message.
340 //
341 // Narrow on purpose, and narrower than the install-loop check: a refusal
342 // leaves NOTHING on disk, so a catalog plugin that IS on disk yet inactive
343 // did not fail provisioning — it failed activation, which is a real
344 // failure and still stops the import.
345 if (
346 self::provisioning_is_live()
347 && ProPluginCatalog::is_provisionable( $plugin )
348 && ! is_dir( WP_PLUGIN_DIR . '/' . dirname( (string) ( $plugin['plugin_file'] ?? '' ) ) )
349 ) {
350 continue;
351 }
352
353 // Only check mustHave plugins
354 if ( isset( $plugin['mustHave'] ) && $plugin['mustHave'] ) {
355 if ( ! is_plugin_active( $plugin['plugin_file'] ) ) {
356 $error_message = sprintf(
357 __( 'Required plugin "%s" is not active. Please refresh the page and try again.', 'templately' ),
358 $plugin['name']
359 );
360 throw new FatalErrorException( $error_message );
361 }
362 }
363 }
364 }
365 }
366
367 /**
368 * Whether the pro-plugin-provisioning feature is actually on.
369 *
370 * `class_exists()` is not enough — the autoloader namespace is registered for
371 * DISABLED modules too. What a disabled module never does is add its listener, and
372 * the exemptions in this runner must switch off with it, or "behaves exactly as
373 * before when disabled" (spec 059 FR-025) is false in the one place it matters.
374 */
375 private static function provisioning_is_live(): bool {
376 return class_exists( ProPluginModule::class ) && ProPluginModule::is_live();
377 }
378
379 private function install_themes($request_params) {
380 $themes = [];
381
382
383 if ( ! empty( $request_params['theme'] ) && is_array( $request_params['theme'] ) ) {
384 $themes = [$request_params['theme']];
385 }
386 else {
387 $this->removeLog( 'theme' );
388 }
389 // $this->before_install_hook();
390
391 $results = $this->loop($themes, function($key, $theme, $results) {
392 if (isset($theme['stylesheet'])) {
393 $stylesheet = get_option('stylesheet');
394
395 // do_action('before_theme_activation', $theme); // Trigger action before theme activation
396 $this->sse_log('theme', 'Installing and Activating Theme: ' . $theme['name'], 0);
397 if (!get_option("__templately_stylesheet")) {
398 add_option("__templately_stylesheet", $stylesheet, '', 'no');
399 }
400
401 $plugin_status = Installer::get_instance()->install_and_activate_theme($theme['stylesheet']);
402
403 if (!$plugin_status['success']) {
404 $this->sse_message([
405 'action' => 'updateLog',
406 'status' => 'error',
407 'message' => "Failed to activate theme: " . $theme['name'],
408 'type' => "theme",
409 'progress' => 0
410 ]);
411 $results = [
412 'success' => false,
413 'name' => $theme['name'],
414 'slug' => $theme['stylesheet'],
415 'message' => $plugin_status['message'] ?? ''
416 ];
417 } else {
418 // do_action('after_theme_activation', $theme); // Trigger action after theme activation
419
420 $this->sse_message([
421 'action' => 'updateLog',
422 'status' => 'complete',
423 'message' => "Activated theme: " . $theme['name'],
424 'type' => "theme",
425 'progress' => 100
426 ]);
427 $results = [
428 'success' => true,
429 'name' => $theme['name'],
430 'slug' => $theme['stylesheet'],
431 'message' => $plugin_status['message'] ?? ''
432 ];
433 $progress['theme_dependency'] = true;
434 }
435
436 }
437 return $results;
438 });
439
440
441 return $results;
442 }
443 }
444