1<?php
2/**
3 * Upgrader API: Plugin_Installer_Skin class
4 *
5 * @package WordPress
6 * @subpackage Upgrader
7 * @since 4.6.0
8 */
9
10/**
11 * Plugin Installer Skin for WordPress Plugin Installer.
12 *
13 * @since 2.8.0
14 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
15 *
16 * @see WP_Upgrader_Skin
17 */
18class Plugin_Installer_Skin extends WP_Upgrader_Skin {
19 public $api;
20 public $type;
21 public $url;
22 public $overwrite;
23
24 private $is_downgrading = false;
25
26 /**
27 * Constructor.
28 *
29 * Sets up the plugin installer skin.
30 *
31 * @since 2.8.0
32 *
33 * @param array $args
34 */
35 public function __construct( $args = array() ) {
36 $defaults = array(
37 'type' => 'web',
38 'url' => '',
39 'plugin' => '',
40 'nonce' => '',
41 'title' => '',
42 'overwrite' => '',
43 );
44 $args = wp_parse_args( $args, $defaults );
45
46 $this->type = $args['type'];
47 $this->url = $args['url'];
48 $this->api = isset( $args['api'] ) ? $args['api'] : array();
49 $this->overwrite = $args['overwrite'];
50
51 parent::__construct( $args );
52 }
53
54 /**
55 * Performs an action before installing a plugin.
56 *
57 * @since 2.8.0
58 */
59 public function before() {
60 if ( ! empty( $this->api ) ) {
61 $this->upgrader->strings['process_success'] = sprintf(
62 $this->upgrader->strings['process_success_specific'],
63 $this->api->name,
64 $this->api->version
65 );
66 }
67 }
68
69 /**
70 * Hides the `process_failed` error when updating a plugin by uploading a zip file.
71 *
72 * @since 5.5.0
73 *
74 * @param WP_Error $wp_error WP_Error object.
75 * @return bool True if the error should be hidden, false otherwise.
76 */
77 public function hide_process_failed( $wp_error ) {
78 if (
79 'upload' === $this->type &&
80 '' === $this->overwrite &&
81 $wp_error->get_error_code() === 'folder_exists'
82 ) {
83 return true;
84 }
85
86 return false;
87 }
88
89 /**
90 * Performs an action following a plugin install.
91 *
92 * @since 2.8.0
93 */
94 public function after() {
95 // Check if the plugin can be overwritten and output the HTML.
96 if ( $this->do_overwrite() ) {
97 return;
98 }
99
100 $plugin_file = $this->upgrader->plugin_info();
101
102 $install_actions = array();
103
104 $from = isset( $_GET['from'] ) ? wp_unslash( $_GET['from'] ) : 'plugins';
105
106 if ( 'import' === $from ) {
107 $install_actions['activate_plugin'] = sprintf(
108 '<a class="button button-primary" href="%s" target="_parent">%s</a>',
109 wp_nonce_url( 'plugins.php?action=activate&from=import&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ),
110 __( 'Activate Plugin & Run Importer' )
111 );
112 } elseif ( 'press-this' === $from ) {
113 $install_actions['activate_plugin'] = sprintf(
114 '<a class="button button-primary" href="%s" target="_parent">%s</a>',
115 wp_nonce_url( 'plugins.php?action=activate&from=press-this&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ),
116 __( 'Activate Plugin & Go to Press This' )
117 );
118 } else {
119 $install_actions['activate_plugin'] = sprintf(
120 '<a class="button button-primary" href="%s" target="_parent">%s</a>',
121 wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ),
122 __( 'Activate Plugin' )
123 );
124 }
125
126 if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) {
127 $install_actions['network_activate'] = sprintf(
128 '<a class="button button-primary" href="%s" target="_parent">%s</a>',
129 wp_nonce_url( 'plugins.php?action=activate&networkwide=1&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ),
130 _x( 'Network Activate', 'plugin' )
131 );
132 unset( $install_actions['activate_plugin'] );
133 }
134
135 if ( 'import' === $from ) {
136 $install_actions['importers_page'] = sprintf(
137 '<a href="%s" target="_parent">%s</a>',
138 admin_url( 'import.php' ),
139 __( 'Go to Importers' )
140 );
141 } elseif ( 'web' === $this->type ) {
142 $install_actions['plugins_page'] = sprintf(
143 '<a href="%s" target="_parent">%s</a>',
144 self_admin_url( 'plugin-install.php' ),
145 __( 'Go to Plugin Installer' )
146 );
147 } elseif ( 'upload' === $this->type && 'plugins' === $from ) {
148 $install_actions['plugins_page'] = sprintf(
149 '<a href="%s">%s</a>',
150 self_admin_url( 'plugin-install.php' ),
151 __( 'Go to Plugin Installer' )
152 );
153 } else {
154 $install_actions['plugins_page'] = sprintf(
155 '<a href="%s" target="_parent">%s</a>',
156 self_admin_url( 'plugins.php' ),
157 __( 'Go to Plugins page' )
158 );
159 }
160
161 if ( ! $this->result || is_wp_error( $this->result ) ) {
162 unset( $install_actions['activate_plugin'], $install_actions['network_activate'] );
163 } elseif ( ! current_user_can( 'activate_plugin', $plugin_file ) || is_plugin_active( $plugin_file ) ) {
164 unset( $install_actions['activate_plugin'] );
165 }
166
167 /**
168 * Filters the list of action links available following a single plugin installation.
169 *
170 * @since 2.7.0
171 *
172 * @param string[] $install_actions Array of plugin action links.
173 * @param object $api Object containing WordPress.org API plugin data. Empty
174 * for non-API installs, such as when a plugin is installed
175 * via upload.
176 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
177 */
178 $install_actions = apply_filters( 'install_plugin_complete_actions', $install_actions, $this->api, $plugin_file );
179
180 if ( ! empty( $install_actions ) ) {
181 $this->feedback( implode( ' ', (array) $install_actions ) );
182 }
183 }
184
185 /**
186 * Checks if the plugin can be overwritten and outputs the HTML for overwriting a plugin on upload.
187 *
188 * @since 5.5.0
189 *
190 * @return bool Whether the plugin can be overwritten and HTML was outputted.
191 */
192 private function do_overwrite() {
193 if ( 'upload' !== $this->type || ! is_wp_error( $this->result ) || 'folder_exists' !== $this->result->get_error_code() ) {
194 return false;
195 }
196
197 $folder = $this->result->get_error_data( 'folder_exists' );
198 $folder = ltrim( substr( $folder, strlen( WP_PLUGIN_DIR ) ), '/' );
199
200 $current_plugin_data = false;
201 $all_plugins = get_plugins();
202
203 foreach ( $all_plugins as $plugin => $plugin_data ) {
204 if ( strrpos( $plugin, $folder ) !== 0 ) {
205 continue;
206 }
207
208 $current_plugin_data = $plugin_data;
209 }
210
211 $new_plugin_data = $this->upgrader->new_plugin_data;
212
213 if ( ! $current_plugin_data || ! $new_plugin_data ) {
214 return false;
215 }
216
217 echo '<h2 class="update-from-upload-heading">' . esc_html__( 'This plugin is already installed.' ) . '</h2>';
218
219 $this->is_downgrading = version_compare( $current_plugin_data['Version'], $new_plugin_data['Version'], '>' );
220
221 $rows = array(
222 'Name' => __( 'Plugin name' ),
223 'Version' => __( 'Version' ),
224 'Author' => __( 'Author' ),
225 'RequiresWP' => __( 'Required WordPress version' ),
226 'RequiresPHP' => __( 'Required PHP version' ),
227 );
228
229 $table = '<table class="update-from-upload-comparison"><tbody>';
230 $table .= '<tr><th></th><th>' . esc_html_x( 'Current', 'plugin' ) . '</th>';
231 $table .= '<th>' . esc_html_x( 'Uploaded', 'plugin' ) . '</th></tr>';
232
233 $is_same_plugin = true; // Let's consider only these rows.
234
235 foreach ( $rows as $field => $label ) {
236 $old_value = ! empty( $current_plugin_data[ $field ] ) ? (string) $current_plugin_data[ $field ] : '-';
237 $new_value = ! empty( $new_plugin_data[ $field ] ) ? (string) $new_plugin_data[ $field ] : '-';
238
239 $is_same_plugin = $is_same_plugin && ( $old_value === $new_value );
240
241 $diff_field = ( 'Version' !== $field && $new_value !== $old_value );
242 $diff_version = ( 'Version' === $field && $this->is_downgrading );
243
244 $table .= '<tr><td class="name-label">' . $label . '</td><td>' . wp_strip_all_tags( $old_value ) . '</td>';
245 $table .= ( $diff_field || $diff_version ) ? '<td class="warning">' : '<td>';
246 $table .= wp_strip_all_tags( $new_value ) . '</td></tr>';
247 }
248
249 $table .= '</tbody></table>';
250
251 /**
252 * Filters the compare table output for overwriting a plugin package on upload.
253 *
254 * @since 5.5.0
255 *
256 * @param string $table The output table with Name, Version, Author, RequiresWP, and RequiresPHP info.
257 * @param array $current_plugin_data Array with current plugin data.
258 * @param array $new_plugin_data Array with uploaded plugin data.
259 */
260 echo apply_filters( 'install_plugin_overwrite_comparison', $table, $current_plugin_data, $new_plugin_data );
261
262 $install_actions = array();
263 $can_update = true;
264
265 $blocked_message = '<p>' . esc_html__( 'The plugin cannot be updated due to the following:' ) . '</p>';
266 $blocked_message .= '<ul class="ul-disc">';
267
268 $requires_php = isset( $new_plugin_data['RequiresPHP'] ) ? $new_plugin_data['RequiresPHP'] : null;
269 $requires_wp = isset( $new_plugin_data['RequiresWP'] ) ? $new_plugin_data['RequiresWP'] : null;
270
271 if ( ! is_php_version_compatible( $requires_php ) ) {
272 $error = sprintf(
273 /* translators: 1: Current PHP version, 2: Version required by the uploaded plugin. */
274 __( 'The PHP version on your server is %1$s, however the uploaded plugin requires %2$s.' ),
275 PHP_VERSION,
276 $requires_php
277 );
278
279 $blocked_message .= '<li>' . esc_html( $error ) . '</li>';
280 $can_update = false;
281 }
282
283 if ( ! is_wp_version_compatible( $requires_wp ) ) {
284 $error = sprintf(
285 /* translators: 1: Current WordPress version, 2: Version required by the uploaded plugin. */
286 __( 'Your WordPress version is %1$s, however the uploaded plugin requires %2$s.' ),
287 esc_html( wp_get_wp_version() ),
288 $requires_wp
289 );
290
291 $blocked_message .= '<li>' . esc_html( $error ) . '</li>';
292 $can_update = false;
293 }
294
295 $blocked_message .= '</ul>';
296
297 if ( $can_update ) {
298 if ( $this->is_downgrading ) {
299 $warning = sprintf(
300 /* translators: %s: Documentation URL. */
301 __( 'You are uploading an older version of a current plugin. You can continue to install the older version, but be sure to <a href="%s">back up your database and files</a> first.' ),
302 __( 'https://developer.wordpress.org/advanced-administration/security/backup/' )
303 );
304 } else {
305 $warning = sprintf(
306 /* translators: %s: Documentation URL. */
307 __( 'You are updating a plugin. Be sure to <a href="%s">back up your database and files</a> first.' ),
308 __( 'https://developer.wordpress.org/advanced-administration/security/backup/' )
309 );
310 }
311
312 echo '<p class="update-from-upload-notice">' . $warning . '</p>';
313
314 $overwrite = $this->is_downgrading ? 'downgrade-plugin' : 'update-plugin';
315
316 $install_actions['overwrite_plugin'] = sprintf(
317 '<a class="button button-primary update-from-upload-overwrite" href="%s" target="_parent">%s</a>',
318 wp_nonce_url( add_query_arg( 'overwrite', $overwrite, $this->url ), 'plugin-upload' ),
319 _x( 'Replace current with uploaded', 'plugin' )
320 );
321 } else {
322 echo $blocked_message;
323 }
324
325 $cancel_url = add_query_arg( 'action', 'upload-plugin-cancel-overwrite', $this->url );
326
327 $install_actions['plugins_page'] = sprintf(
328 '<a class="button" href="%s">%s</a>',
329 wp_nonce_url( $cancel_url, 'plugin-upload-cancel-overwrite' ),
330 __( 'Cancel and go back' )
331 );
332
333 /**
334 * Filters the list of action links available following a single plugin installation failure
335 * when overwriting is allowed.
336 *
337 * @since 5.5.0
338 *
339 * @param string[] $install_actions Array of plugin action links.
340 * @param object $api Object containing WordPress.org API plugin data.
341 * @param array $new_plugin_data Array with uploaded plugin data.
342 */
343 $install_actions = apply_filters( 'install_plugin_overwrite_actions', $install_actions, $this->api, $new_plugin_data );
344
345 if ( ! empty( $install_actions ) ) {
346 printf(
347 '<p class="update-from-upload-expired hidden">%s</p>',
348 __( 'The uploaded file has expired. Please go back and upload it again.' )
349 );
350 echo '<p class="update-from-upload-actions">' . implode( ' ', (array) $install_actions ) . '</p>';
351 }
352
353 return true;
354 }
355}
356