| @@ -2,8 +2,11 @@ | ||
| 2 | 2 | |
| 3 | 3 | namespace Templately\Utils; |
| 4 | 4 | |
| 5 | 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; | |
| 6 | 9 | use Plugin_Upgrader; |
| 7 | 10 | use Theme_Upgrader; |
| 8 | 11 | use WP_Ajax_Upgrader_Skin; |
| 9 | 12 | use WP_Filesystem_Base; |
| @@ -46,13 +49,13 @@ | ||
| 46 | 49 | $response['slug'] = $plugin['slug']; |
| 47 | 50 | return $response; |
| 48 | 51 | } |
| 49 | 52 | |
| 50 | - if ( isset( $plugin['is_pro'] ) && $plugin['is_pro'] ) { | |
| 51 | - if ( ! $is_installed ) { | |
| 52 | - $response['code'] = 'pro_plugin'; | |
| 53 | - $response['message'] = 'Pro Plugin'; | |
| 54 | - } | |
| 53 | + $is_pro = ! empty( $plugin['is_pro'] ); | |
| 54 | + | |
| 55 | + if ( $is_pro && ! $is_installed ) { | |
| 56 | + $response['code'] = 'pro_plugin'; | |
| 57 | + $response['message'] = 'Pro Plugin'; | |
| 55 | 58 | } |
| 56 | 59 | |
| 57 | 60 | if ( ! $is_installed ) { |
| 58 | 61 | if(!Helper::current_user_can( 'install_plugins' )){ |
| @@ -60,40 +63,79 @@ | ||
| 60 | 63 | $response['message'] = __( 'Sorry, you do not have permission to install a plugin.', 'templately' ); |
| 61 | 64 | return $response; |
| 62 | 65 | } |
| 63 | 66 | |
| 64 | - $plugins_api_args = [ | |
| 65 | - 'slug' => sanitize_key( wp_unslash( $plugin['slug'] ) ), | |
| 66 | - 'fields' => [ | |
| 67 | - 'sections' => false, | |
| 68 | - ], | |
| 69 | - ]; | |
| 70 | - if(isset($plugin['has_license']) && $plugin['has_license']){ | |
| 71 | - $plugins_api_args['has_license'] = $plugin['has_license']; | |
| 72 | - } | |
| 73 | 67 | /** |
| 74 | - * @var array|object $api | |
| 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 | |
| 75 | 85 | */ |
| 76 | - $api = plugins_api( 'plugin_information', $plugins_api_args); | |
| 86 | + $provisioned = $is_pro ? $this->provision_pro_archive( $plugin ) : null; | |
| 77 | 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 | + } | |
| 78 | 93 | |
| 79 | - if ( is_wp_error( $api ) ) { | |
| 80 | - $response['message'] = $api->get_error_message(); | |
| 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); | |
| 81 | 108 | |
| 82 | - return $response; | |
| 83 | - } | |
| 84 | 109 | |
| 85 | - $compatibility = $this->check_compatibility($api); | |
| 86 | - if (!$compatibility['success']) { | |
| 87 | - return $compatibility; | |
| 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']; | |
| 88 | 129 | } |
| 89 | 130 | |
| 90 | - $response['name'] = $api->name; | |
| 91 | - | |
| 92 | 131 | $skin = new WP_Ajax_Upgrader_Skin(); |
| 93 | 132 | $upgrader = new Plugin_Upgrader( $skin ); |
| 94 | - $result = $upgrader->install( $api->download_link ); | |
| 133 | + $result = $upgrader->install( null !== $provisioned ? $provisioned : $api->download_link ); | |
| 95 | 134 | |
| 135 | + // Nothing licensed survives the request that downloaded it, on either branch. | |
| 136 | + ProProvisioningArchive::discard( $provisioned ); | |
| 137 | + | |
| 96 | 138 | if ( is_wp_error( $result ) ) { |
| 97 | 139 | $response['code'] = $result->get_error_code(); |
| 98 | 140 | $response['message'] = $result->get_error_message(); |
| 99 | 141 | |
| @@ -122,18 +164,31 @@ | ||
| 122 | 164 | $response['message'] = __('Failed to install plugin', 'templately'); |
| 123 | 165 | return $response; |
| 124 | 166 | } |
| 125 | 167 | |
| 126 | - $install_status = install_plugin_install_status( $api ); | |
| 127 | - $plugin['plugin_file'] = $install_status['file']; | |
| 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. | |
| 128 | 175 | } |
| 129 | 176 | |
| 130 | - if ( !Helper::current_user_can( 'activate_plugins' ) && is_plugin_inactive( $file ) ) { | |
| 177 | + if ( !Helper::current_user_can( 'activate_plugins' ) && is_plugin_inactive( $plugin['plugin_file'] ) ) { | |
| 131 | 178 | $response['code'] = 'invalid_requirements'; |
| 132 | 179 | $response['message'] = __( 'Sorry, you do not have permission to activate a plugin.', 'templately' ); |
| 133 | 180 | return $response; |
| 134 | 181 | } |
| 135 | 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 | + | |
| 136 | 191 | $activate_status = $this->activate_plugin( $plugin['plugin_file'] ); |
| 137 | 192 | |
| 138 | 193 | if ( is_wp_error( $activate_status ) ) { |
| 139 | 194 | $response['message'] = $activate_status->get_error_message(); |
| @@ -147,8 +202,191 @@ | ||
| 147 | 202 | |
| 148 | 203 | return $response; |
| 149 | 204 | } |
| 150 | 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 | + | |
| 151 | 389 | public function install_and_activate_theme($theme_slug) { |
| 152 | 390 | require_once(ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'); |
| 153 | 391 | require_once(ABSPATH . 'wp-admin/includes/theme.php'); |
| 154 | 392 | require_once(ABSPATH . 'wp-admin/includes/theme-install.php'); |
| @@ -234,18 +472,26 @@ | ||
| 234 | 472 | |
| 235 | 473 | return ['success' => true]; |
| 236 | 474 | } |
| 237 | 475 | |
| 238 | - private function activate_plugin( $file ) { | |
| 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 ) { | |
| 239 | 483 | if ( is_plugin_active( $file ) ) { |
| 240 | 484 | return true; |
| 241 | 485 | } |
| 242 | 486 | |
| 243 | 487 | if ( Helper::current_user_can( 'activate_plugins' ) && is_plugin_inactive( $file ) ) { |
| 244 | - $result = activate_plugin( $file ); | |
| 488 | + $result = activate_plugin( $file, '', false, $silent ); | |
| 245 | 489 | if ( is_wp_error( $result ) ) { |
| 246 | 490 | return $result; |
| 247 | 491 | } else { |
| 492 | + $this->clear_known_activation_redirects(); | |
| 493 | + | |
| 248 | 494 | return true; |
| 249 | 495 | } |
| 250 | 496 | } |
| 251 | 497 | |
| @@ -251,8 +497,32 @@ | ||
| 251 | 497 | |
| 252 | 498 | return false; |
| 253 | 499 | } |
| 254 | 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 | + | |
| 255 | 525 | private function activate_theme($theme_slug) { |
| 256 | 526 | if (get_option('stylesheet') == $theme_slug) { |
| 257 | 527 | // The theme is already active |
| 258 | 528 | return true; |
| @@ -270,7 +540,105 @@ | ||
| 270 | 540 | } |
| 271 | 541 | } |
| 272 | 542 | |
| 273 | 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; | |
| 274 | 642 | } |
| 275 | 643 | |
| 276 | 644 | } |