PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / trunk
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant vtrunk
2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 All 59 releases
merchant / admin / classes / class-merchant-plugin-installer.php

class-merchant-plugin-installer.php in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant trunk, at admin/classes/class-merchant-plugin-installer.php

389 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Plugin Installer.
5 * This class is responsible for installing and activating plugins. This could be from wp.org and external sources.
6 *
7 * @package Merchant
8 */
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 if ( ! class_exists( 'Merchant_Plugin_Installer' ) ) {
16
17 class Merchant_Plugin_Installer {
18
19 /**
20 * Constructor.
21 *
22 */
23 public function __construct() {
24 add_action( 'admin_init', array( $this, 'init' ) );
25 }
26
27 /**
28 * Initialize the class.
29 *
30 * @return void
31 */
32 public function init() {
33 if ( ! is_admin() ) {
34 return;
35 }
36
37 if ( ! current_user_can( 'install_plugins' ) ) {
38 return;
39 }
40
41 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
42 add_action( 'wp_ajax_merchant_install_plugin', array( $this, 'install_plugin' ) );
43 add_action( 'wp_ajax_merchant_install_external_plugin', array( $this, 'install_external_plugin' ) );
44 }
45
46 /**
47 * Enqueue scripts.
48 *
49 * @return void
50 */
51 public function enqueue_scripts() {
52 wp_enqueue_script( 'merchant-plugin-installer', MERCHANT_URI . '/assets/js/admin/plugin-installer.min.js', array( 'jquery' ), MERCHANT_VERSION, true );
53
54 wp_localize_script( 'merchant-plugin-installer', 'merchantPluginInstallerConfig', array(
55 'ajax_url' => admin_url( 'admin-ajax.php' ),
56 'nonce' => wp_create_nonce( 'merchant_plugin_installer_nonce' ),
57 'i18n' => array(
58 'defaultText' => esc_html__( 'Install and Activate', 'merchant' ),
59 'installingText' => esc_html__( 'Installing...', 'merchant' ),
60 'activatingText' => esc_html__( 'Activating...', 'merchant' ),
61 'networkErrorText' => esc_html__( 'Installation failed. Please try again.', 'merchant' ),
62 ),
63 ) );
64 }
65
66 /**
67 * Install and activate a plugin from the WordPress.org repository.
68 * Handles three states: already active, installed but inactive, not installed.
69 *
70 * @return void
71 */
72 public function install_plugin() {
73 list( $slug, $plugin_name ) = $this->validate_wporg_install_request();
74
75 $state = $this->get_plugin_state( $plugin_name );
76
77 // Already active — nothing to do.
78 if ( 'active' === $state ) {
79 wp_send_json_success( array( 'message' => esc_html__( 'Plugin is already active.', 'merchant' ) ) );
80 }
81
82 // Not installed — resolve download URL from wp.org and install.
83 if ( 'not_installed' === $state ) {
84 $download_url = $this->get_wporg_download_url( $slug );
85
86 if ( is_wp_error( $download_url ) ) {
87 wp_send_json_error( array( 'message' => $download_url->get_error_message() ) );
88 }
89
90 $install_result = $this->install_from_url( $download_url );
91
92 if ( is_wp_error( $install_result ) ) {
93 wp_send_json_error( array( 'message' => $install_result->get_error_message() ) );
94 }
95 }
96
97 // Activate the plugin (covers both 'installed' and freshly installed).
98 $activate_result = $this->activate( $plugin_name );
99
100 if ( is_wp_error( $activate_result ) ) {
101 wp_send_json_error( array( 'message' => $activate_result->get_error_message() ) );
102 }
103
104 wp_send_json_success( array( 'message' => esc_html__( 'Plugin activated successfully.', 'merchant' ) ) );
105 }
106
107 /**
108 * Install and activate an external plugin.
109 * Handles three states: already active, installed but inactive, not installed.
110 *
111 * @return void
112 */
113 public function install_external_plugin() {
114 list( $url, $plugin_name ) = $this->validate_install_request();
115
116 $state = $this->get_plugin_state( $plugin_name );
117
118 // Already active — nothing to do.
119 if ( 'active' === $state ) {
120 wp_send_json_success( array( 'message' => esc_html__( 'Plugin is already active.', 'merchant' ) ) );
121 }
122
123 // Not installed — download and install first.
124 if ( 'not_installed' === $state ) {
125 $install_result = $this->install_from_url( $url );
126
127 if ( is_wp_error( $install_result ) ) {
128 wp_send_json_error( array( 'message' => $install_result->get_error_message() ) );
129 }
130 }
131
132 // Activate the plugin (covers both 'installed' and freshly installed).
133 $activate_result = $this->activate( $plugin_name );
134
135 if ( is_wp_error( $activate_result ) ) {
136 wp_send_json_error( array( 'message' => $activate_result->get_error_message() ) );
137 }
138
139 wp_send_json_success( array( 'message' => esc_html__( 'Plugin activated successfully.', 'merchant' ) ) );
140 }
141
142 /**
143 * Validate the install request.
144 * Checks nonce, capability, and required parameters.
145 * Sends a JSON error and dies if validation fails.
146 *
147 * @return array{0: string, 1: string} The validated URL and plugin name.
148 */
149 private function validate_install_request() {
150 check_ajax_referer( 'merchant_plugin_installer_nonce', 'nonce' );
151
152 if ( ! current_user_can( 'install_plugins' ) ) {
153 wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to install plugins.', 'merchant' ) ) );
154 }
155
156 if ( empty( $_POST['url'] ) ) {
157 wp_send_json_error( array( 'message' => esc_html__( 'Plugin URL is required.', 'merchant' ) ) );
158 }
159
160 if ( empty( $_POST['plugin_name'] ) ) {
161 wp_send_json_error( array( 'message' => esc_html__( 'Plugin name is required.', 'merchant' ) ) );
162 }
163
164 return array(
165 esc_url_raw( wp_unslash( $_POST['url'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
166 sanitize_text_field( wp_unslash( $_POST['plugin_name'] ) ),
167 );
168 }
169
170 /**
171 * Validate a wp.org install request.
172 * Checks nonce, capability, and required parameters (slug and plugin_name).
173 * Sends a JSON error and dies if validation fails.
174 *
175 * @return array{0: string, 1: string} The validated slug and plugin name.
176 */
177 private function validate_wporg_install_request() {
178 check_ajax_referer( 'merchant_plugin_installer_nonce', 'nonce' );
179
180 if ( ! current_user_can( 'install_plugins' ) ) {
181 wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to install plugins.', 'merchant' ) ) );
182 }
183
184 if ( empty( $_POST['slug'] ) ) {
185 wp_send_json_error( array( 'message' => esc_html__( 'Plugin slug is required.', 'merchant' ) ) );
186 }
187
188 if ( empty( $_POST['plugin_name'] ) ) {
189 wp_send_json_error( array( 'message' => esc_html__( 'Plugin name is required.', 'merchant' ) ) );
190 }
191
192 return array(
193 sanitize_text_field( wp_unslash( $_POST['slug'] ) ),
194 sanitize_text_field( wp_unslash( $_POST['plugin_name'] ) ),
195 );
196 }
197
198 /**
199 * Resolve the download URL for a plugin from the WordPress.org API.
200 *
201 * @param string $slug The plugin slug (e.g. 'woocommerce').
202 *
203 * @return string|WP_Error The download URL on success, WP_Error on failure.
204 */
205 private function get_wporg_download_url( $slug ) {
206 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
207
208 $api = plugins_api(
209 'plugin_information',
210 array(
211 'slug' => $slug,
212 'fields' => array( 'sections' => false ),
213 )
214 );
215
216 if ( is_wp_error( $api ) ) {
217 return new WP_Error(
218 'merchant_plugin_api_failed',
219 esc_html__( 'Could not retrieve plugin information from WordPress.org. Please try again later.', 'merchant' )
220 );
221 }
222
223 if ( empty( $api->download_link ) ) {
224 return new WP_Error(
225 'merchant_no_download_link',
226 esc_html__( 'No download link found for this plugin.', 'merchant' )
227 );
228 }
229
230 return $api->download_link;
231 }
232
233 /**
234 * Determine the current state of a plugin.
235 *
236 * @param string $plugin_name The plugin basename (e.g. 'athemes-patcher/athemes-patcher.php').
237 *
238 * @return string One of 'active', 'installed', or 'not_installed'.
239 */
240 private function get_plugin_state( $plugin_name ) {
241 if ( is_plugin_active( $plugin_name ) ) {
242 return 'active';
243 }
244
245 $plugin_file = WP_PLUGIN_DIR . '/' . $plugin_name;
246
247 if ( file_exists( $plugin_file ) ) {
248 return 'installed';
249 }
250
251 return 'not_installed';
252 }
253
254 /**
255 * Download and install a plugin from an external URL.
256 * Uses `overwrite_package` to handle leftover directories from failed installs.
257 *
258 * @param string $url The URL to the plugin ZIP file.
259 *
260 * @return true|WP_Error True on success, WP_Error on failure.
261 */
262 private function install_from_url( $url ) {
263 include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
264 require_once MERCHANT_DIR . 'admin/classes/class-merchant-silent-upgrader-skin.php';
265
266 $skin = $this->make_skin();
267 $upgrader = $this->make_upgrader( $skin );
268 $result = $upgrader->install( $url, array( 'overwrite_package' => true ) );
269
270 if ( is_wp_error( $result ) ) {
271 return new WP_Error(
272 'merchant_install_failed',
273 $this->get_user_friendly_error( $result )
274 );
275 }
276
277 if ( ! $result ) {
278 $captured = $skin->get_captured_errors();
279 $error_message = ! empty( $captured ) ? $captured[0] : '';
280
281 $error = new WP_Error( 'download_failed', $error_message );
282
283 return new WP_Error(
284 'merchant_install_failed',
285 $this->get_user_friendly_error( $error )
286 );
287 }
288
289 return true;
290 }
291
292 /**
293 * Factory: create a Plugin_Upgrader instance.
294 * Extracted as a protected method so tests can override it via a subclass.
295 *
296 * @param Merchant_Silent_Upgrader_Skin $skin The upgrader skin.
297 *
298 * @return Plugin_Upgrader
299 */
300 protected function make_upgrader( Merchant_Silent_Upgrader_Skin $skin ) {
301 return new Plugin_Upgrader( $skin );
302 }
303
304 /**
305 * Factory: create a Merchant_Silent_Upgrader_Skin instance.
306 * Extracted as a protected method so tests can override it via a subclass.
307 *
308 * @return Merchant_Silent_Upgrader_Skin
309 */
310 protected function make_skin() {
311 return new Merchant_Silent_Upgrader_Skin();
312 }
313
314 /**
315 * Get a user-friendly error message based on the error type.
316 *
317 * @param WP_Error $error The WP_Error object.
318 *
319 * @return string User-friendly error message.
320 */
321 private function get_user_friendly_error( WP_Error $error ) {
322 $error_code = $error->get_error_code();
323 $error_message = $error->get_error_message();
324 $error_data = $error->get_error_data();
325
326 // Ensure error_message is a string for strpos() calls.
327 $error_message = (string) $error_message;
328
329 // Extract HTTP status code if available
330 $http_code = null;
331 if ( is_array( $error_data ) && isset( $error_data['status'] ) && is_int( $error_data['status'] ) ) {
332 $http_code = $error_data['status'];
333 }
334
335 // Check for 403 Forbidden (server blocking the download)
336 if ( 403 === $http_code || strpos( $error_message, '403' ) !== false || strpos( $error_message, 'Forbidden' ) !== false ) {
337 return esc_html__( 'The download is currently unavailable. Please try again later or contact support for assistance.', 'merchant' );
338 }
339
340 // Check for server errors (500, 502, 503)
341 if ( in_array( $http_code, array( 500, 502, 503 ), true ) ||
342 strpos( $error_message, '500' ) !== false ||
343 strpos( $error_message, '502' ) !== false ||
344 strpos( $error_message, '503' ) !== false ||
345 strpos( $error_message, 'Internal Server Error' ) !== false ||
346 strpos( $error_message, 'Bad Gateway' ) !== false ||
347 strpos( $error_message, 'Service Unavailable' ) !== false ) {
348 return esc_html__( 'The download server is temporarily unavailable. Please try again in a few minutes.', 'merchant' );
349 }
350
351 // Check for rate limiting (429)
352 if ( 429 === $http_code || strpos( $error_message, '429' ) !== false || strpos( $error_message, 'Too Many Requests' ) !== false ) {
353 return esc_html__( 'Too many download requests. Please wait a moment and try again.', 'merchant' );
354 }
355
356 // Check for network/connectivity errors
357 if ( in_array( $error_code, array( 'http_request_failed', 'http_no_url', 'http_404' ), true ) ) {
358 return esc_html__( 'The server could not connect to the download source. Please try again or contact your hosting provider if the problem persists.', 'merchant' );
359 }
360
361 // Check for download failures
362 if ( strpos( $error_message, 'Download failed' ) !== false ) {
363 return esc_html__( 'The download could not be completed. Please try again or install the plugin manually.', 'merchant' );
364 }
365
366 // Generic fallback
367 return esc_html__( 'Installation failed. Please try again or contact support if the problem persists.', 'merchant' );
368 }
369
370 /**
371 * Activate a plugin.
372 *
373 * @param string $plugin_name The plugin basename (e.g. 'athemes-patcher/athemes-patcher.php').
374 *
375 * @return true|WP_Error True on success, WP_Error on failure.
376 */
377 private function activate( $plugin_name ) {
378 $result = activate_plugin( $plugin_name );
379
380 if ( is_wp_error( $result ) ) {
381 return $result;
382 }
383
384 return true;
385 }
386 }
387
388 new Merchant_Plugin_Installer();
389 }