PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
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 / includes / Utils / Installer.php

Installer.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at includes/Utils/Installer.php

644 lines 23.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Utils;
4
5 use Automatic_Upgrader_Skin;
6 use Templately\Modules\ProPluginProvisioning\Archive as ProProvisioningArchive;
7 use Templately\Modules\ProPluginProvisioning\Catalog as ProProvisioningCatalog;
8 use Templately\Modules\ProPluginProvisioning\Module as ProProvisioningModule;
9 use Plugin_Upgrader;
10 use Theme_Upgrader;
11 use WP_Ajax_Upgrader_Skin;
12 use WP_Filesystem_Base;
13 use function activate_plugin;
14 use function current_user_can;
15 use function install_plugin_install_status;
16 use function is_plugin_inactive;
17 use function is_wp_error;
18 use function plugins_api;
19 use function sanitize_key;
20 use function wp_unslash;
21
22 class Installer extends Base {
23 /**
24 * Some process take long time to execute
25 * for that need to raise the limit.
26 */
27 public static function raise_limits() {
28 wp_raise_memory_limit( 'admin' );
29 if ( wp_is_ini_value_changeable( 'max_execution_time' ) ) {
30 @ini_set( 'max_execution_time', 0 );
31 }
32 @ set_time_limit( 0 );
33 }
34
35 public function install( $plugin ): array {
36 require_once ABSPATH . 'wp-admin/includes/plugin.php';
37 require_once ABSPATH . 'wp-admin/includes/file.php';
38 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
39 include_once ABSPATH . 'wp-admin/includes/plugin-install.php';
40
41 $response = [ 'success' => false, 'message' => '' ];
42
43 $_plugins = Helper::get_plugins();
44 $is_installed = isset( $_plugins[ $plugin['plugin_file'] ] );
45
46 // Check if the plugin is already active
47 if (is_plugin_active($plugin['plugin_file'])) {
48 $response['success'] = true;
49 $response['slug'] = $plugin['slug'];
50 return $response;
51 }
52
53 $is_pro = ! empty( $plugin['is_pro'] );
54
55 if ( $is_pro && ! $is_installed ) {
56 $response['code'] = 'pro_plugin';
57 $response['message'] = 'Pro Plugin';
58 }
59
60 if ( ! $is_installed ) {
61 if(!Helper::current_user_can( 'install_plugins' )){
62 $response['code'] = 'invalid_requirements';
63 $response['message'] = __( 'Sorry, you do not have permission to install a plugin.', 'templately' );
64 return $response;
65 }
66
67 /**
68 * A pro plugin has no wordpress.org record, so `plugins_api()` cannot supply a
69 * download link for one. Before spec 059 that was the end of it: the response
70 * above stood, and the user was told to install the plugin by hand.
71 *
72 * Now we ASK first. If a registered provisioning source can supply an archive
73 * — because the user's subscription entitles them to it — we install from that
74 * path and skip `plugins_api()` entirely. If nothing answers, the `pro_plugin`
75 * response above is returned untouched, which is byte-for-byte the behaviour
76 * every non-entitled user has always had.
77 *
78 * The ask is made only for plugins that are actually obtainable, and the
79 * catalog — never the descriptor's own claim — decides which those are: this
80 * governs what gets downloaded from a third party and executed on the site, so
81 * a spoofed dependency response must not be able to widen it.
82 *
83 * @see modules/pro-plugin-provisioning/
84 * @see specs/059-pro-plugin-provisioning/contracts/provisioning-source.md
85 */
86 $provisioned = $is_pro ? $this->provision_pro_archive( $plugin ) : null;
87
88 if ( null === $provisioned && $is_pro ) {
89 // Not obtainable. Return the refusal rather than asking wordpress.org for
90 // a plugin it has never heard of.
91 return $response;
92 }
93
94 if ( null === $provisioned ) {
95 $plugins_api_args = [
96 'slug' => sanitize_key( wp_unslash( $plugin['slug'] ) ),
97 'fields' => [
98 'sections' => false,
99 ],
100 ];
101 if(isset($plugin['has_license']) && $plugin['has_license']){
102 $plugins_api_args['has_license'] = $plugin['has_license'];
103 }
104 /**
105 * @var array|object $api
106 */
107 $api = plugins_api( 'plugin_information', $plugins_api_args);
108
109
110 if ( is_wp_error( $api ) ) {
111 $response['message'] = $api->get_error_message();
112
113 return $response;
114 }
115
116 $compatibility = $this->check_compatibility($api);
117 if (!$compatibility['success']) {
118 return $compatibility;
119 }
120
121 $response['name'] = $api->name;
122 } else {
123 // A provisioned pro plugin: the refusal recorded above no longer applies.
124 // UNSET, not '' — an empty-string code survives every `?? 'fallback'` in the
125 // callers, and `new WP_Error( '', … )` early-returns with no error at all,
126 // which the REST server turns into a 500 with a null body.
127 unset( $response['code'], $response['message'] );
128 $response['name'] = $plugin['name'] ?? $plugin['slug'];
129 }
130
131 $skin = new WP_Ajax_Upgrader_Skin();
132 $upgrader = new Plugin_Upgrader( $skin );
133 $result = $upgrader->install( null !== $provisioned ? $provisioned : $api->download_link );
134
135 // Nothing licensed survives the request that downloaded it, on either branch.
136 ProProvisioningArchive::discard( $provisioned );
137
138 if ( is_wp_error( $result ) ) {
139 $response['code'] = $result->get_error_code();
140 $response['message'] = $result->get_error_message();
141
142 return $response;
143 } elseif ( is_wp_error( $skin->result ) ) {
144 $response['code'] = $skin->result->get_error_code();
145 $response['message'] = $skin->result->get_error_message();
146
147 return $response;
148 } elseif ( $skin->get_errors()->has_errors() ) {
149 $response['message'] = $skin->get_error_messages();
150
151 return $response;
152 } elseif ( is_null( $result ) ) {
153 global $wp_filesystem;
154 $response['code'] = 'unable_to_connect_to_filesystem';
155 $response['message'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
156
157 if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
158 $response['message'] = esc_html( $wp_filesystem->errors->get_error_message() );
159 }
160
161 return $response;
162 }
163 else if($result !== true){
164 $response['message'] = __('Failed to install plugin', 'templately');
165 return $response;
166 }
167
168 if ( null === $provisioned ) {
169 $install_status = install_plugin_install_status( $api );
170 $plugin['plugin_file'] = $install_status['file'];
171 }
172 // A provisioned plugin keeps the plugin_file the dependency descriptor named:
173 // `install_plugin_install_status()` resolves it from a wordpress.org API
174 // object, and a pro plugin has none.
175 }
176
177 if ( !Helper::current_user_can( 'activate_plugins' ) && is_plugin_inactive( $plugin['plugin_file'] ) ) {
178 $response['code'] = 'invalid_requirements';
179 $response['message'] = __( 'Sorry, you do not have permission to activate a plugin.', 'templately' );
180 return $response;
181 }
182
183 // Claim the install immediately before activating, so the caching plugin comes
184 // up as a host install — every feature off, page caching on only if nothing
185 // else owns it, and its own setup wizard skipped. One option write; it does
186 // the rest on its own activation, so there is nothing here to finish or undo.
187 if ( Caching::PLUGIN_FILE === $plugin['plugin_file'] ) {
188 Caching::claim_install();
189 }
190
191 $activate_status = $this->activate_plugin( $plugin['plugin_file'] );
192
193 if ( is_wp_error( $activate_status ) ) {
194 $response['message'] = $activate_status->get_error_message();
195 }
196
197 if ( $activate_status && ! is_wp_error( $activate_status ) ) {
198 $response['success'] = true;
199 }
200
201 $response['slug'] = $plugin['slug'];
202
203 return $response;
204 }
205
206 /**
207 * Update already-installed plugins to their latest available version.
208 *
209 * Exists because a pack's markup is produced by the block plugin version on the AUTHORING
210 * site: when the importing site runs an older one, blocks whose `save()` has since changed
211 * — or whose newer attributes this version does not declare — fail Gutenberg's validation
212 * and open with "Attempt recovery". Updating first is the only fix that keeps the pack's
213 * newer features; regenerating the markup locally would silently drop them.
214 *
215 * Mirrors {@see self::install()}'s result contract so callers can treat the two alike.
216 *
217 * NOTE: the updated code is NOT loaded in the request that updates it. Callers must
218 * re-check state in a FRESH request before acting on it — the same constraint the FSI
219 * dependency runner handles with its `plugins_installed` → `continue` → new-request split.
220 *
221 * @param string[] $plugin_files Plugin basenames, e.g. `essential-blocks/essential-blocks.php`.
222 * @return array{success:bool,results:array<int,array<string,mixed>>}
223 */
224 public function update( array $plugin_files ): array {
225 require_once ABSPATH . 'wp-admin/includes/plugin.php';
226 require_once ABSPATH . 'wp-admin/includes/file.php';
227 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
228 require_once ABSPATH . 'wp-admin/includes/update.php';
229
230 $results = [];
231
232 if ( ! Helper::current_user_can( 'update_plugins' ) ) {
233 return [
234 'success' => false,
235 'code' => 'invalid_permission',
236 'message' => __( 'Sorry, you do not have permission to update plugins.', 'templately' ),
237 'results' => [],
238 ];
239 }
240
241 self::raise_limits();
242
243 $installed = Helper::get_plugins();
244
245 foreach ( $plugin_files as $plugin_file ) {
246 $plugin_file = (string) $plugin_file;
247 $from = $installed[ $plugin_file ]['Version'] ?? null;
248
249 if ( ! isset( $installed[ $plugin_file ] ) ) {
250 $results[] = [
251 'plugin_file' => $plugin_file,
252 'updated' => false,
253 'code' => 'not_installed',
254 'message' => __( 'This plugin is not installed on the site.', 'templately' ),
255 ];
256 continue;
257 }
258
259 $was_active = is_plugin_active( $plugin_file );
260
261 // Core's own precondition, checked BEFORE handing the plugin over:
262 // `Plugin_Upgrader::upgrade()` bails with `up_to_date` when the plugin has no
263 // entry in the `update_plugins` transient. Asking first lets us report that as
264 // settled; letting the upgrader discover it yields an error we cannot identify,
265 // because `WP_Ajax_Upgrader_Skin::error()` files a string code under a GENERATED
266 // key (`unknown_upgrade_error_N`) — the literal 'up_to_date' never survives, so
267 // there is nothing downstream to match on.
268 $current = get_site_transient( 'update_plugins' );
269 if ( ! isset( $current->response[ $plugin_file ] ) ) {
270 $results[] = [
271 'plugin_file' => $plugin_file,
272 'updated' => true,
273 'code' => 'already_updated',
274 'from' => $from,
275 'to' => $from,
276 ];
277 continue;
278 }
279
280 $skin = new WP_Ajax_Upgrader_Skin();
281 $upgrader = new Plugin_Upgrader( $skin );
282 // `clear_update_cache => false` is REQUIRED in a loop, and core's own
283 // `bulk_upgrade()` does exactly this. Left at its default of true, `upgrade()`
284 // hooks `wp_clean_plugins_cache` onto `upgrader_process_complete`, so the FIRST
285 // plugin to upgrade successfully deletes the `update_plugins` transient — and
286 // every later plugin in this loop then re-reads that now-empty transient at the
287 // top of `upgrade()`, finds no `response[$plugin]`, and fails with `up_to_date`
288 // ("The plugin is at the latest version.") without being touched. Updating three
289 // plugins updated one and falsely reported the other two as already current.
290 // The single `wp_clean_plugins_cache( true )` after the loop does the refresh
291 // once, which is the whole point of deferring it.
292 $result = $upgrader->upgrade( $plugin_file, [ 'clear_update_cache' => false ] );
293
294 $error = null;
295 if ( is_wp_error( $result ) ) {
296 $error = $result;
297 } elseif ( is_wp_error( $skin->result ) ) {
298 $error = $skin->result;
299 } elseif ( $skin->get_errors()->has_errors() ) {
300 $error = $skin->get_errors();
301 }
302
303 if ( null !== $error ) {
304 $results[] = [
305 'plugin_file' => $plugin_file,
306 'updated' => false,
307 'code' => $error->get_error_code(),
308 'message' => $error->get_error_message(),
309 'from' => $from,
310 ];
311 continue;
312 }
313
314 if ( false === $result ) {
315 $results[] = [
316 'plugin_file' => $plugin_file,
317 'updated' => false,
318 'code' => 'update_failed',
319 'message' => __( 'The update could not be completed. Please update this plugin from the Plugins screen.', 'templately' ),
320 'from' => $from,
321 ];
322 continue;
323 }
324
325 // WP deactivates a plugin while upgrading it; put it back the way we found it.
326 //
327 // SILENTLY, and that is not a detail. `Plugin_Upgrader` deactivates with
328 // `deactivate_plugins( $plugin, true )` — hooks suppressed — because the plugin
329 // is not being turned off, it is being swapped underneath. Re-activating it
330 // loudly runs its ACTIVATION hooks, and those belong to the version now on disk
331 // while this request is still running the autoloader it booted with. WooCommerce
332 // 11.1.1's `WC_Install::install()` reaches for a class that exists only in its
333 // own build and fatals the whole request: every plugin after it in this loop
334 // goes un-updated, and the user is told a critical error occurred for all of
335 // them — for plugins that in fact updated correctly.
336 //
337 // A wrapper restoring prior state has no business running first-run setup. The
338 // plugin's own upgrade routine runs on the NEXT request, from its own code,
339 // which is where it can work.
340 if ( $was_active && ! is_plugin_active( $plugin_file ) ) {
341 try {
342 $this->activate_plugin( $plugin_file, true );
343 } catch ( \Throwable $e ) {
344 // The UPDATE succeeded; only putting it back failed. Say so, and let the
345 // rest of the run continue — aborting here would strand every plugin
346 // after this one, which is exactly what the fatal above did.
347 Helper::log(
348 sprintf( 'update: %s updated but could not be re-activated — %s', $plugin_file, $e->getMessage() ),
349 'installer_update',
350 'warning'
351 );
352 }
353 }
354
355 // Read the version off disk rather than the stale in-memory copy.
356 $to = null;
357 if ( function_exists( 'get_plugin_data' ) && file_exists( WP_PLUGIN_DIR . '/' . $plugin_file ) ) {
358 $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file, false, false );
359 $to = $data['Version'] ?? null;
360 }
361
362 $results[] = [
363 'plugin_file' => $plugin_file,
364 'updated' => true,
365 'from' => $from,
366 'to' => $to,
367 ];
368 }
369
370 // A fresh check so the next read of the update transient reflects what we just did.
371 // The argument is `$clear_update_cache`, and it MUST be true: `false` deletes only the
372 // `plugins` object cache and LEAVES the `update_plugins` transient in place, so every
373 // plugin we just updated keeps its stale "update available" entry and comes back as a
374 // recommendation on the next dependency check.
375 if ( function_exists( 'wp_clean_plugins_cache' ) ) {
376 wp_clean_plugins_cache( true );
377 }
378
379 $failed = array_filter( $results, function ( $result ) {
380 return empty( $result['updated'] );
381 } );
382
383 return [
384 'success' => empty( $failed ),
385 'results' => $results,
386 ];
387 }
388
389 public function install_and_activate_theme($theme_slug) {
390 require_once(ABSPATH . 'wp-admin/includes/class-wp-upgrader.php');
391 require_once(ABSPATH . 'wp-admin/includes/theme.php');
392 require_once(ABSPATH . 'wp-admin/includes/theme-install.php');
393
394 $response = ['success' => false];
395
396 // Check if the theme is already active
397 if ($theme_slug == get_option('stylesheet')) {
398 $response['success'] = true;
399 $response['message'] = __('Theme is already active', 'templately');
400 return $response;
401 }
402
403 if (!function_exists('themes_api')) {
404 $response['message'] = __('Function themes_api does not exist', 'templately');
405 return $response;
406 }
407
408 $api = themes_api('theme_information', [
409 'slug' => sanitize_key($theme_slug),
410 'fields' => [
411 'sections' => false,
412 ],
413 ]);
414
415 if (is_wp_error($api)) {
416 $response['message'] = $api->get_error_message();
417 return $response;
418 }
419
420 $compatibility = $this->check_compatibility($api);
421 if (!$compatibility['success']) {
422 return $compatibility;
423 }
424
425 if (!wp_get_theme($theme_slug)->exists()) {
426 $upgrader = new Theme_Upgrader(new Automatic_Upgrader_Skin());
427 $result = $upgrader->install($api->download_link);
428
429 if (is_wp_error($result)) {
430 $response['message'] = $result->get_error_message();
431 return $response;
432 }
433 else if($result !== true){
434 $response['message'] = __('Failed to install theme', 'templately');
435 return $response;
436 }
437
438 $response['install'] = 'success';
439 }
440
441 $activate_status = $this->activate_theme($theme_slug);
442
443 if ( is_wp_error( $activate_status ) ) {
444 $response['message'] = $activate_status->get_error_message();
445 }
446 else if ($activate_status) {
447 $response['success'] = true;
448 } else {
449 $response['message'] = __('Failed to activate theme', 'templately');
450 }
451
452 return $response;
453 }
454
455 public function check_compatibility($api) {
456 // Check compatibility with current PHP version
457 if (version_compare(PHP_VERSION, $api->requires_php, '<')) {
458 return [
459 'success' => false,
460 'message' => sprintf(__('The plugin requires PHP version %s or higher. You are running version %s.', 'templately'), $api->requires_php, PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION)
461 ];
462 }
463
464 // Check compatibility with current WP version
465 global $wp_version;
466 if (version_compare($wp_version, $api->requires, '<')) {
467 return [
468 'success' => false,
469 'message' => sprintf(__('The plugin requires WordPress version %s or higher. You are running version %s.', 'templately'), $api->requires, $wp_version)
470 ];
471 }
472
473 return ['success' => true];
474 }
475
476 /**
477 * @param string $file Plugin file.
478 * @param bool $silent Skip the activation hooks. Correct ONLY when the plugin was
479 * already set up and is merely being put back the way we found
480 * it — see the call in `update()`. A fresh install must run them.
481 */
482 private function activate_plugin( $file, $silent = false ) {
483 if ( is_plugin_active( $file ) ) {
484 return true;
485 }
486
487 if ( Helper::current_user_can( 'activate_plugins' ) && is_plugin_inactive( $file ) ) {
488 $result = activate_plugin( $file, '', false, $silent );
489 if ( is_wp_error( $result ) ) {
490 return $result;
491 } else {
492 $this->clear_known_activation_redirects();
493
494 return true;
495 }
496 }
497
498 return false;
499 }
500
501 /**
502 * Neutralise "getting started" redirect flags that popular dependency plugins
503 * set in their own activation hooks (e.g. Elementor's
504 * `elementor_activation_redirect` transient, Essential Addons' own
505 * `eael_do_activation_redirect` transient + `eael_setup_wizard` option).
506 *
507 * Templately activates these plugins programmatically during FSI / single-
508 * template import. Each plugin's own redirect check is guarded against
509 * `DOING_AJAX`, so it never fires DURING the (AJAX-driven) import request —
510 * but the flag survives past it and hijacks the user's NEXT ordinary
511 * (non-AJAX) admin page load into that plugin's onboarding wizard instead of
512 * wherever they actually navigated. Deleting an already-cleared flag is a
513 * harmless no-op, so this is safe to call unconditionally after every
514 * activation, regardless of which plugin was just activated.
515 */
516 private function clear_known_activation_redirects() {
517 delete_transient( 'elementor_activation_redirect' ); // Elementor (free)
518 delete_transient( 'eael_do_activation_redirect' ); // Essential Addons for Elementor
519
520 if ( 'redirect' === get_option( 'eael_setup_wizard' ) ) {
521 delete_option( 'eael_setup_wizard' );
522 }
523 }
524
525 private function activate_theme($theme_slug) {
526 if (get_option('stylesheet') == $theme_slug) {
527 // The theme is already active
528 return true;
529 }
530
531 if (Helper::current_user_can('switch_themes')) {
532 // Activate the theme
533 switch_theme($theme_slug);
534
535 // Check if the theme was successfully activated
536 if (get_option('stylesheet') == $theme_slug) {
537 return true;
538 } else {
539 return new \WP_Error('theme_activation_failed', __('Failed to activate theme', 'templately'));
540 }
541 }
542
543 return false;
544 }
545
546
547 /**
548 * Ask any registered provisioning source for an installable pro-plugin archive.
549 *
550 * Returns null — meaning "not available", the only failure this feature has — when the
551 * plugin is not one Templately may obtain, when the module supplying the seam is not
552 * loaded, when the site is not connected, when the current user may not activate what
553 * would be installed, or when no source answers.
554 *
555 * The activation capability is checked HERE as well as after installation: downloading
556 * megabytes for a user who could never activate the result is wasted work and leaves a
557 * plugin installed that nobody asked for.
558 *
559 * @param array $plugin Dependency descriptor.
560 * @return string|null Absolute path to an archive, or null.
561 */
562 private function provision_pro_archive( array &$plugin ) {
563 // `class_exists()` alone is NOT a "module enabled" test: Modules_Manager registers
564 // the autoloader namespace for every DISCOVERED module, inactive ones included, so
565 // the class loads even when the module never booted. What a disabled module does
566 // not do is add its filter listener — and the dev mock depends on it, so it is
567 // skipped too. No listener ⇒ nothing can answer ⇒ the pre-059 refusal stands
568 // (FR-025), without spending the checks below.
569 if ( ! class_exists( ProProvisioningModule::class ) || ! ProProvisioningModule::is_live() ) {
570 Helper::log(
571 sprintf( 'pro-provisioning: module not live, cannot obtain "%s"', $plugin['slug'] ?? '(no slug)' ),
572 'pro_plugin_provisioning',
573 'info'
574 );
575
576 return null;
577 }
578
579 $entry = ProProvisioningCatalog::find( $plugin );
580
581 if ( null === $entry ) {
582 // Not one of the plugins this feature can obtain. Ordinary, and the reason the
583 // catalog exists — but indistinguishable from a failure without saying so.
584 Helper::log(
585 sprintf( 'pro-provisioning: "%s" is not in the obtainable catalog', $plugin['slug'] ?? '(no slug)' ),
586 'pro_plugin_provisioning',
587 'info'
588 );
589
590 return null;
591 }
592
593 // The catalog matched — possibly by slug alone. Everything after installation
594 // (`is_plugin_inactive()`, `activate_plugin()`) keys on the plugin FILE, so it
595 // must be the catalog's, not whatever the descriptor did or did not carry.
596 $plugin['plugin_file'] = $entry['plugin_file'];
597
598 if ( ! Helper::current_user_can( 'activate_plugins' ) ) {
599 Helper::log(
600 sprintf( 'pro-provisioning[%s]: user cannot activate_plugins', $entry['slug'] ),
601 'pro_plugin_provisioning',
602 'info'
603 );
604
605 return null;
606 }
607
608 $credential = ProProvisioningModule::credential();
609
610 if ( '' === $credential ) {
611 // No stored api_key: the site is not connected to Templately, so there is
612 // nothing to present as proof of entitlement.
613 Helper::log(
614 sprintf( 'pro-provisioning[%s]: no stored credential — site not connected', $entry['slug'] ),
615 'pro_plugin_provisioning',
616 'info'
617 );
618
619 return null;
620 }
621
622 $archive = apply_filters( ProProvisioningModule::ARCHIVE_FILTER, null, [
623 'plugin_file' => $entry['plugin_file'],
624 'slug' => $entry['slug'],
625 'platform' => $entry['platform'],
626 'credential' => $credential,
627 ] );
628
629 if ( is_string( $archive ) && '' !== $archive && is_readable( $archive ) ) {
630 return $archive;
631 }
632
633 // The source has already logged WHY it could not supply one; record that the
634 // refusal is what the import will act on, so the two halves join up in the log.
635 Helper::log(
636 sprintf( 'pro-provisioning[%s]: no archive available — falling back to "install it yourself"', $entry['slug'] ),
637 'pro_plugin_provisioning',
638 'info'
639 );
640
641 return null;
642 }
643
644 }