PluginProbe
Code Snippets / 3.10.1
Code Snippets v3.10.1
3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 3.0.1 All 64 releases
code-snippets / php / Settings / Version_Switch.php

Version_Switch.php in Code Snippets 3.10.1, at php/Settings/Version_Switch.php

498 lines 14.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\Settings;
4
5 use Plugin_Upgrader;
6 use WP_Ajax_Upgrader_Skin;
7 use WP_Error;
8 use WP_Upgrader_Skin;
9 use function Code_Snippets\code_snippets;
10
11 /**
12 * Version switching functionality for the Code Snippets plugin.
13 *
14 * @package Code_Snippets
15 * @subpackage Settings
16 */
17 class Version_Switch {
18
19 /**
20 * Transient key where the available version data is cached.
21 */
22 private const CACHE_KEY = 'code_snippets_available_versions';
23
24 /**
25 * Transient key used to indicate when a version switch is currently taking place.
26 */
27 private const PROGRESS_KEY = 'code_snippets_version_switch_progress';
28
29 /**
30 * Duration of the version cache transient.
31 */
32 private const VERSION_CACHE_DURATION = HOUR_IN_SECONDS;
33
34 /**
35 * Duration of the 'in progress' transient.
36 */
37 private const PROGRESS_TIMEOUT = 5 * MINUTE_IN_SECONDS;
38
39 /**
40 * API endpoint for checking for available plugin versions.
41 */
42 private const WORDPRESS_API_ENDPOINT = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=code-snippets';
43
44 /**
45 * Initialise class.
46 *
47 * @return void
48 */
49 public static function init(): void {
50 add_action( 'wp_ajax_code_snippets_switch_version', [ __CLASS__, 'ajax_switch_version' ] );
51 add_action( 'wp_ajax_code_snippets_refresh_versions', [ __CLASS__, 'ajax_refresh_versions' ] );
52 }
53
54 /**
55 * Retrieve a list of plugin versions available for switching.
56 *
57 * @return array
58 */
59 public static function get_available_versions(): array {
60 $versions = get_transient( self::CACHE_KEY );
61
62 if ( false === $versions ) {
63 $response = wp_remote_get( self::WORDPRESS_API_ENDPOINT );
64
65 if ( is_wp_error( $response ) ) {
66 return [];
67 }
68
69 $body = wp_remote_retrieve_body( $response );
70 $data = json_decode( $body, true );
71
72 if ( ! $data || ! isset( $data['versions'] ) ) {
73 return [];
74 }
75
76 // Filter out 'trunk' and sort versions.
77 $versions = [];
78 foreach ( $data['versions'] as $version => $download_url ) {
79 if ( 'trunk' !== $version ) {
80 $versions[] = [
81 'version' => $version,
82 'url' => $download_url,
83 ];
84 }
85 }
86
87 // Sort versions in descending order.
88 usort(
89 $versions,
90 function ( $a, $b ) {
91 return version_compare( $b['version'], $a['version'] );
92 }
93 );
94
95 // Cache for configured duration.
96 set_transient( self::CACHE_KEY, $versions, self::VERSION_CACHE_DURATION );
97 }
98
99 return $versions;
100 }
101
102 /**
103 * Retrieve the current plugin version.
104 *
105 * @return string
106 */
107 public static function get_current_version(): string {
108 return defined( 'CODE_SNIPPETS_VERSION' ) ? CODE_SNIPPETS_VERSION : '0.0.0';
109 }
110
111 /**
112 * Determine if a version switch is currently taking place.
113 *
114 * @return bool
115 */
116 public static function is_version_switch_in_progress(): bool {
117 return get_transient( self::PROGRESS_KEY ) !== false;
118 }
119
120 /**
121 * Purge transient data associated with this class.
122 *
123 * @return void
124 */
125 public static function clear_version_caches(): void {
126 delete_transient( self::CACHE_KEY );
127 delete_transient( self::PROGRESS_KEY );
128 }
129
130 /**
131 * Validate that a target version is valid.
132 *
133 * @param string $target_version Target version for switching.
134 * @param array $available_versions List of available versions.
135 *
136 * @return array
137 */
138 public static function validate_target_version( string $target_version, array $available_versions ): array {
139 if ( empty( $target_version ) ) {
140 return [
141 'success' => false,
142 'message' => __( 'No target version specified.', 'code-snippets' ),
143 'download_url' => '',
144 ];
145 }
146
147 foreach ( $available_versions as $version_info ) {
148 if ( $version_info['version'] === $target_version ) {
149 return [
150 'success' => true,
151 'message' => '',
152 'download_url' => $version_info['url'],
153 ];
154 }
155 }
156
157 return [
158 'success' => false,
159 'message' => __( 'Invalid version specified.', 'code-snippets' ),
160 'download_url' => '',
161 ];
162 }
163
164 /**
165 * Create a response indicating an error occurred.
166 *
167 * @param string $message Error message.
168 * @param string $technical_details Additional details.
169 *
170 * @return array
171 *
172 * phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log
173 */
174 public static function create_error_response( string $message, string $technical_details = '' ): array {
175 if ( ! empty( $technical_details ) ) {
176 if ( function_exists( 'error_log' ) ) {
177 error_log( sprintf( 'Code Snippets version switch error: %s. Details: %s', $message, $technical_details ) );
178 }
179 }
180
181 return [
182 'success' => false,
183 'message' => $message,
184 ];
185 }
186
187 /**
188 * Install a plugin version from a URL.
189 *
190 * @param string $download_url Download URL.
191 *
192 * @return array|bool|WP_Error
193 */
194 public static function perform_version_install( string $download_url ) {
195 if ( ! function_exists( 'wp_update_plugins' ) ) {
196 require_once ABSPATH . 'wp-admin/includes/update.php';
197 }
198 if ( ! function_exists( 'show_message' ) ) {
199 require_once ABSPATH . 'wp-admin/includes/misc.php';
200 }
201 if ( ! class_exists( 'Plugin_Upgrader' ) ) {
202 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
203 }
204
205 $update_handler = new WP_Ajax_Upgrader_Skin();
206 $upgrader = new Plugin_Upgrader( $update_handler );
207
208 global $code_snippets_last_update_handler, $code_snippets_last_upgrader;
209 $code_snippets_last_update_handler = $update_handler;
210 $code_snippets_last_upgrader = $upgrader;
211
212 return $upgrader->install(
213 $download_url,
214 [
215 'overwrite_package' => true,
216 'clear_update_cache' => true,
217 ]
218 );
219 }
220
221 /**
222 * Extract error message from an upgrade handler.
223 *
224 * @param WP_Upgrader_Skin|null $update_handler Update handler.
225 * @param Plugin_Upgrader|null $upgrader Plugin upgrader.
226 *
227 * @return string
228 *
229 * phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_print_r
230 */
231 public static function extract_handler_messages( ?WP_Upgrader_Skin $update_handler, ?Plugin_Upgrader $upgrader ): string {
232 $handler_messages = '';
233
234 if ( isset( $update_handler ) ) {
235 if ( method_exists( $update_handler, 'get_errors' ) ) {
236 $errs = $update_handler->get_errors();
237 if ( $errs instanceof WP_Error && $errs->has_errors() ) {
238 $handler_messages .= implode( "\n", $errs->get_error_messages() );
239 }
240 }
241 if ( method_exists( $update_handler, 'get_error_messages' ) ) {
242 $em = $update_handler->get_error_messages();
243 if ( $em ) {
244 $handler_messages .= "\n" . $em;
245 }
246 }
247 if ( method_exists( $update_handler, 'get_upgrade_messages' ) ) {
248 $upgrade_msgs = $update_handler->get_upgrade_messages();
249 if ( is_array( $upgrade_msgs ) ) {
250 $handler_messages .= "\n" . implode( "\n", $upgrade_msgs );
251 } elseif ( $upgrade_msgs ) {
252 $handler_messages .= "\n" . $upgrade_msgs;
253 }
254 }
255 }
256
257 if ( empty( $handler_messages ) && isset( $upgrader->result ) ) {
258 if ( is_wp_error( $upgrader->result ) ) {
259 $handler_messages = implode( "\n", $upgrader->result->get_error_messages() );
260 } else {
261 $handler_messages = is_scalar( $upgrader->result )
262 ? (string) $upgrader->result
263 : print_r( $upgrader->result, true );
264 }
265 }
266
267 return trim( $handler_messages );
268 }
269
270 /**
271 * Report the failure of a version switch attempt.
272 *
273 * @param string $target_version Version number of attempted upgrade.
274 * @param mixed $result Result of upgrade.
275 * @param string $details Additional details.
276 *
277 * @return void
278 *
279 * phpcs:disable WordPress.PHP.DevelopmentFunctions
280 */
281 private static function log_version_switch_attempt( string $target_version, $result, string $details = '' ): void {
282 if ( function_exists( 'error_log' ) ) {
283 error_log( sprintf( 'Code Snippets version switch failed. target=%s, result=%s, details=%s', $target_version, var_export( $result, true ), $details ) );
284 }
285 }
286
287 /**
288 * Handle the failure to install a new version.
289 *
290 * @param string $target_version Version used for attempted installation.
291 * @param string $download_url URL used for downloading new version.
292 * @param mixed $install_result Result of installation attempt.
293 *
294 * @return array
295 */
296 private static function handle_installation_failure( string $target_version, string $download_url, $install_result ): array {
297 global $code_snippets_last_update_handler, $code_snippets_last_upgrader;
298
299 $handler_messages = self::extract_handler_messages( $code_snippets_last_update_handler, $code_snippets_last_upgrader );
300 self::log_version_switch_attempt( $target_version, $install_result, "URL: $download_url, Messages: $handler_messages" );
301
302 $fallback_message = __( 'Failed to switch versions. Please try again.', 'code-snippets' );
303
304 if ( ! empty( $handler_messages ) ) {
305 $short = wp_trim_words( wp_strip_all_tags( $handler_messages ), 40 );
306 $fallback_message = sprintf( '%s %s', $fallback_message, $short );
307 }
308
309 return [
310 'success' => false,
311 'message' => $fallback_message,
312 ];
313 }
314
315 /**
316 * Handle switching to a different plugin version.
317 *
318 * @param string $target_version Target version to switch to.
319 *
320 * @return array Result data.
321 */
322 public static function handle_version_switch( string $target_version ): array {
323 if ( ! current_user_can( 'update_plugins' ) ) {
324 return self::create_error_response( __( 'You do not have permission to update plugins.', 'code-snippets' ) );
325 }
326
327 $available_versions = self::get_available_versions();
328 $validation = self::validate_target_version( $target_version, $available_versions );
329
330 if ( ! $validation['success'] ) {
331 return self::create_error_response( $validation['message'] );
332 }
333
334 if ( self::get_current_version() === $target_version ) {
335 return self::create_error_response( __( 'Already on the specified version.', 'code-snippets' ) );
336 }
337
338 set_transient( self::PROGRESS_KEY, $target_version, self::PROGRESS_TIMEOUT );
339
340 $install_result = self::perform_version_install( $validation['download_url'] );
341
342 delete_transient( self::PROGRESS_KEY );
343
344 if ( is_wp_error( $install_result ) ) {
345 return self::create_error_response( $install_result->get_error_message() );
346 }
347
348 if ( $install_result ) {
349 delete_transient( self::CACHE_KEY );
350
351 // translators: %s: new version number.
352 $message = esc_html__( 'Successfully switched to version %s. Please refresh the page to see changes.', 'code-snippets' );
353
354 return [
355 'success' => true,
356 'message' => sprintf( $message, $target_version ),
357 ];
358 } else {
359 return self::handle_installation_failure( $target_version, $validation['download_url'], $install_result );
360 }
361 }
362
363 /**
364 * Render settings page field for the version switcher.
365 *
366 * @return void
367 */
368 public static function render_version_switch_field(): void {
369 $current_version = self::get_current_version();
370 $available_versions = self::get_available_versions();
371 $is_switching = self::is_version_switch_in_progress();
372
373 ?>
374 <div class="code-snippets-version-switch">
375 <p>
376 <strong><?php esc_html_e( 'Current Version:', 'code-snippets' ); ?></strong>
377 <span class="current-version"><?php echo esc_html( $current_version ); ?></span>
378 </p>
379
380 <?php if ( $is_switching ) : ?>
381 <div class="notice code-snippets-notice notice-info inline">
382 <p><?php esc_html_e( 'Version switch in progress. Please wait…', 'code-snippets' ); ?></p>
383 </div>
384 <?php else : ?>
385 <p>
386 <label for="target_version">
387 <?php esc_html_e( 'Switch to Version:', 'code-snippets' ); ?>
388 </label>
389 <select id="target_version" name="target_version" <?php disabled( empty( $available_versions ) ); ?>>
390 <option value=""><?php esc_html_e( 'Select a version…', 'code-snippets' ); ?></option>
391 <?php foreach ( $available_versions as $version_info ) { ?>
392 <option value="<?php echo esc_attr( $version_info['version'] ); ?>"
393 <?php selected( $version_info['version'], $current_version ); ?>>
394 <?php
395
396 echo esc_html( $version_info['version'] );
397
398 if ( $version_info['version'] === $current_version ) {
399 esc_html_e( ' (Current)', 'code-snippets' );
400 }
401
402 ?>
403 </option>
404 <?php } ?>
405 </select>
406 </p>
407
408 <p>
409 <button type="button" id="switch-version-btn" class="button button-secondary" disabled
410 <?php disabled( empty( $available_versions ) ); ?>>
411 <?php esc_html_e( 'Switch Version', 'code-snippets' ); ?>
412 </button>
413 </p>
414
415 <div id="version-switch-result" class="notice code-snippets-notice" style="display: none;"></div>
416 <?php endif; ?>
417 </div><?php
418 }
419
420 /**
421 * Handle version switching through AJAX.
422 *
423 * @return void
424 */
425 public static function ajax_switch_version(): void {
426 check_ajax_referer( 'code_snippets_version_switch', sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ) );
427
428 if ( ! current_user_can( 'update_plugins' ) ) {
429 wp_send_json_error( [ 'message' => __( 'You do not have permission to update plugins.', 'code-snippets' ) ] );
430 }
431
432 $target_version = sanitize_text_field( wp_unslash( $_POST['target_version'] ?? '' ) );
433
434 if ( empty( $target_version ) ) {
435 wp_send_json_error( [ 'message' => __( 'No target version specified.', 'code-snippets' ) ] );
436 }
437
438 $result = self::handle_version_switch( $target_version );
439
440 if ( $result['success'] ) {
441 wp_send_json_success( $result );
442 } else {
443 wp_send_json_error( $result );
444 }
445 }
446
447 /**
448 * Render settings page field for the refresh version button.
449 *
450 * @return void
451 */
452 public static function render_refresh_versions_field(): void {
453 printf(
454 '<button type="button" id="refresh-versions-btn" class="button button-secondary">%s</button>',
455 esc_html__( 'Refresh Available Versions', 'code-snippets' )
456 );
457
458 printf(
459 '<p class="description">%s</p>',
460 esc_html__( 'Check for the latest available plugin versions from WordPress.org.', 'code-snippets' )
461 );
462 }
463
464 /**
465 * AJAX handler for refreshing the installed version.
466 *
467 * @return void
468 */
469 public static function ajax_refresh_versions(): void {
470 check_ajax_referer( 'code_snippets_refresh_versions', sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ) );
471
472 if ( ! code_snippets()->current_user_can() ) {
473 wp_send_json_error( [ 'message' => __( 'You do not have permission to manage options.', 'code-snippets' ) ] );
474 }
475
476 delete_transient( self::CACHE_KEY );
477 self::get_available_versions();
478
479 wp_send_json_success( [ 'message' => __( 'Available versions updated successfully.', 'code-snippets' ) ] );
480 }
481
482 /**
483 * Render warning notice.
484 *
485 * @return void
486 */
487 public static function render_version_switch_warning(): void {
488 ?>
489 <div id="version-switch-warning" class="notice code-snippets-notice notice-warning hidden" role="region">
490 <p>
491 <strong><?php esc_html_e( 'Warning:', 'code-snippets' ); ?></strong>
492 <?php esc_html_e( 'Switching versions may cause compatibility issues. Always backup your site before switching versions.', 'code-snippets' ); ?>
493 </p>
494 </div>
495 <?php
496 }
497 }
498