Changeset 3442632
- Timestamp:
- 01/19/2026 02:49:27 PM (9 days ago)
- Location:
- finalpos/trunk
- Files:
-
- 11 edited
-
finalpos.php (modified) (2 diffs)
-
includes/admin/class-finalpos-admin-assets.php (modified) (2 diffs)
-
includes/admin/dashboard/class-finalpos-dashboard.php (modified) (2 diffs)
-
includes/admin/wizard/class-finalpos-wizard.php (modified) (1 diff)
-
includes/admin/wizard/views/activation-form.php (modified) (1 diff)
-
includes/admin/wizard/views/settings-form.php (modified) (1 diff)
-
includes/common-functions.php (modified) (5 diffs)
-
includes/lifecycle.php (modified) (12 diffs)
-
integrations/readonly-orders.php (modified) (1 diff)
-
readme.txt (modified) (2 diffs)
-
uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
finalpos/trunk/finalpos.php
r3420195 r3442632 5 5 * Author: FinalPOS 6 6 * Author URI: https://finalpos.com 7 * version: 1.3. 67 * version: 1.3.7 8 8 * License: GPLv2 or later 9 9 * License URI: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html … … 23 23 24 24 // Define plugin constants. 25 define( 'FINALPOS_VERSION', '1.3. 6' );25 define( 'FINALPOS_VERSION', '1.3.7' ); 26 26 define( 'FINALPOS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 27 27 define( 'FINALPOS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); -
finalpos/trunk/includes/admin/class-finalpos-admin-assets.php
r3411106 r3442632 89 89 90 90 // Load specific styles for wizard page. 91 if ( isset( $_GET['page'] ) && $_GET['page'] === FINALPOS_PAGE_SETUP ) { 91 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page parameter for asset loading. 92 $current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; 93 94 if ( $current_page === FINALPOS_PAGE_SETUP ) { 92 95 wp_enqueue_style( FINALPOS_HANDLE_WIZARD ); 93 96 … … 101 104 102 105 // Load specific styles for dashboard page. 103 if ( isset( $_GET['page'] ) && $_GET['page']=== FINALPOS_PAGE_DASHBOARD ) {106 if ( $current_page === FINALPOS_PAGE_DASHBOARD ) { 104 107 wp_enqueue_style( FINALPOS_HANDLE_DASHBOARD ); 105 108 } -
finalpos/trunk/includes/admin/dashboard/class-finalpos-dashboard.php
r3420195 r3442632 9 9 class FinalPOS_Dashboard { 10 10 public static function init() { 11 if ( isset( $_POST['action'] ) && $_POST['action'] === FINALPOS_ACTION_DISCONNECT ) { 11 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in handle_disconnect(). 12 if ( isset( $_POST['action'] ) && sanitize_text_field( wp_unslash( $_POST['action'] ) ) === FINALPOS_ACTION_DISCONNECT ) { 12 13 self::handle_disconnect(); 13 14 } … … 16 17 17 18 private static function handle_disconnect() { 18 // Verify nonce 19 if ( ! isset( $_POST['finalpos_disconnect_nonce'] ) || ! wp_verify_nonce( $_POST['finalpos_disconnect_nonce'], FINALPOS_NONCE_DISCONNECT ) ) { 19 // Verify nonce. 20 if ( ! isset( $_POST['finalpos_disconnect_nonce'] ) || 21 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['finalpos_disconnect_nonce'] ) ), FINALPOS_NONCE_DISCONNECT ) ) { 20 22 wp_die( 'Security check failed.' ); 21 23 } -
finalpos/trunk/includes/admin/wizard/class-finalpos-wizard.php
r3411106 r3442632 35 35 } 36 36 37 // Check if there's a stage parameter in the URL 38 if ( isset( $_GET['stage'] ) && in_array( $_GET['stage'], array( 'auth_key', 'settings' ) ) ) { 39 $requested_stage = sanitize_text_field( $_GET['stage'] ); 40 41 // Only allow moving to settings if we have required data 37 // Check if there's a stage parameter in the URL. 38 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Stage parameter controls navigation; sensitive operations use custom nonce below. 39 if ( isset( $_GET['stage'] ) && in_array( sanitize_text_field( wp_unslash( $_GET['stage'] ) ), array( 'auth_key', 'settings' ), true ) ) { 40 // phpcs:ignore WordPress.Security.NonceVerification.Recommended 41 $requested_stage = sanitize_text_field( wp_unslash( $_GET['stage'] ) ); 42 43 // Only allow moving to settings if we have required data. 42 44 if ( $requested_stage === 'settings' ) { 43 // If this is a redirect from WooCommerce auth with nonce, verify it 45 // If this is a redirect from WooCommerce auth with nonce, verify it. 46 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Custom nonce verification follows. 44 47 if ( isset( $_GET['finalpos_nonce'] ) ) { 45 $nonce = sanitize_text_field( $_GET['finalpos_nonce'] ); 48 // phpcs:ignore WordPress.Security.NonceVerification.Recommended 49 $nonce = sanitize_text_field( wp_unslash( $_GET['finalpos_nonce'] ) ); 46 50 $transient_name = 'finalpos_auth_nonce_' . $nonce; 47 51 -
finalpos/trunk/includes/admin/wizard/views/activation-form.php
r3420195 r3442632 92 92 <?php 93 93 $error_message = get_transient( 'finalpos_setup_error' ); 94 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only page showing redirect messages. 94 95 if ( isset( $_GET['error'] ) ) : 95 96 ?> 96 <p class="finalpos-error-message"><?php echo esc_html( wp_unslash( $_GET['error'] ) ); ?></p> 97 <p class="finalpos-error-message"><?php echo esc_html( sanitize_text_field( wp_unslash( $_GET['error'] ) ) ); ?></p> 98 <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only page showing redirect messages. ?> 97 99 <?php elseif ( isset( $_GET['message'] ) ) : ?> 98 <p class="finalpos-success-message"><?php echo esc_html( wp_unslash( $_GET['message']) ); ?></p>100 <p class="finalpos-success-message"><?php echo esc_html( sanitize_text_field( wp_unslash( $_GET['message'] ) ) ); ?></p> 99 101 <?php elseif ( $error_message ) : ?> 100 102 <p class="finalpos-error-message"><?php echo esc_html( $error_message ); ?></p> -
finalpos/trunk/includes/admin/wizard/views/settings-form.php
r3420195 r3442632 39 39 endif; 40 40 41 // Display error message if present. 41 // Display error message if present (passed via redirect from admin-post.php). 42 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a display-only page showing redirect error messages. 42 43 if ( isset( $_GET['error'] ) ) { 43 $error_message = urldecode( wp_unslash( $_GET['error'] ) ); 44 // phpcs:ignore WordPress.Security.NonceVerification.Recommended 45 $error_message = sanitize_text_field( wp_unslash( $_GET['error'] ) ); 44 46 echo '<div class="notice notice-error"><p>' . esc_html( $error_message ) . '</p></div>'; 45 47 } -
finalpos/trunk/includes/common-functions.php
r3420195 r3442632 216 216 * Disconnect the plugin by cleaning up all plugin data 217 217 * 218 * Following WordPress best practices for data cleanup: 219 * - Delete all plugin options 220 * - Delete all plugin transients 221 * - Clean up WooCommerce API keys and webhooks 222 * - Clear object cache 223 * 218 224 * @return void 219 225 */ 220 226 public static function disconnect_plugin(): void { 221 // Delete all plugin options 227 // Delete all plugin options. 222 228 $options = array( 223 229 FINALPOS_OPTION_CURRENT_STAGE, … … 230 236 FINALPOS_OPTION_LAST_NONCE, 231 237 FINALPOS_OPTION_LAST_NONCE_TIME, 238 FINALPOS_OPTION_REACTIVATION_ERROR, 232 239 ); 233 240 … … 236 243 } 237 244 245 // Delete plugin transients. 246 delete_transient( FINALPOS_TRANSIENT_ACTIVATION_REDIRECT ); 247 248 // Clean up any auth nonce transients (they use a prefix pattern). 238 249 global $wpdb; 239 250 240 // Remove API keys251 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 241 252 $wpdb->query( 242 253 $wpdb->prepare( 243 'DELETE FROM `' . $wpdb->prefix . 'woocommerce_api_keys` WHERE `description` LIKE "%s"', 254 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", 255 '_transient_' . FINALPOS_TRANSIENT_AUTH_NONCE_PREFIX . '%', 256 '_transient_timeout_' . FINALPOS_TRANSIENT_AUTH_NONCE_PREFIX . '%' 257 ) 258 ); 259 260 // Remove FinalPOS API keys from WooCommerce. 261 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 262 $wpdb->query( 263 $wpdb->prepare( 264 "DELETE FROM {$wpdb->prefix}woocommerce_api_keys WHERE description LIKE %s", 244 265 FINALPOS_API_DESCRIPTION_PATTERN 245 266 ) 246 267 ); 247 268 248 // Remove webhooks 269 // Remove FinalPOS webhooks from WooCommerce. 270 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 249 271 $wpdb->query( 250 272 $wpdb->prepare( 251 'DELETE FROM `' . $wpdb->prefix . 'wc_webhooks` WHERE `name` LIKE "%s"',273 "DELETE FROM {$wpdb->prefix}wc_webhooks WHERE name LIKE %s", 252 274 FINALPOS_WEBHOOK_NAME_PATTERN 253 275 ) 254 276 ); 277 278 // Clear object cache to ensure clean state. 279 wp_cache_flush(); 255 280 } 256 281 … … 340 365 * Check if REST API is available 341 366 * 342 * Tests internal REST API availability by making a request to the WooCommerce REST API. 367 * Tests internal REST API availability using WordPress internal REST infrastructure. 368 * This avoids external HTTP requests which can fail in tunneled environments (ngrok, DDEV share, etc.). 343 369 * 344 370 * @return array Array with 'available' boolean and 'message' string … … 346 372 public static function is_rest_api_available(): array { 347 373 348 // Try to make an internal REST API request to verify it's working. 349 $rest_url = rest_url( 'wc/v3' ); 350 $response = wp_remote_get( 351 $rest_url, 352 array( 353 'timeout' => 10, 354 'sslverify' => false, 355 'cookies' => $_COOKIE, // Pass current cookies for authentication. 356 ) 357 ); 358 359 if ( is_wp_error( $response ) ) { 374 // First check if REST API is disabled via filters. 375 if ( ! apply_filters( 'rest_enabled', true ) ) { 376 return array( 377 'available' => false, 378 'message' => __( 'REST API is disabled. Please check your WordPress configuration or plugins that might be disabling the REST API.', 'finalpos' ), 379 ); 380 } 381 382 // Check if WooCommerce REST API namespace is registered. 383 $rest_server = rest_get_server(); 384 $namespaces = $rest_server->get_namespaces(); 385 386 if ( ! in_array( 'wc/v3', $namespaces, true ) ) { 387 return array( 388 'available' => false, 389 'message' => __( 'WooCommerce REST API (wc/v3) is not available. Please ensure WooCommerce is properly installed and activated.', 'finalpos' ), 390 ); 391 } 392 393 // Use internal REST request to avoid loopback/tunnel issues (ngrok, DDEV share, etc.). 394 $request = new WP_REST_Request( 'GET', '/wc/v3' ); 395 $response = rest_do_request( $request ); 396 397 if ( $response->is_error() ) { 398 $error = $response->as_error(); 399 $status_code = $response->get_status(); 400 401 // 401 Unauthorized is OK - it means the API is working but requires authentication. 402 if ( 401 === $status_code ) { 403 return array( 404 'available' => true, 405 'message' => '', 406 ); 407 } 408 409 // 403 Forbidden might indicate a Coming Soon plugin blocking access. 410 if ( 403 === $status_code ) { 411 return array( 412 'available' => false, 413 'message' => __( 'REST API access is forbidden (403). This is usually caused by a Coming Soon plugin, maintenance mode, or security plugin blocking API access. Please disable any such plugins before connecting.', 'finalpos' ), 414 ); 415 } 416 417 // 503 Service Unavailable usually means maintenance mode. 418 if ( 503 === $status_code ) { 419 return array( 420 'available' => false, 421 'message' => __( 'REST API returned Service Unavailable (503). Your site appears to be in maintenance mode. Please disable maintenance mode before connecting.', 'finalpos' ), 422 ); 423 } 424 425 // Other errors. 360 426 return array( 361 427 'available' => false, 362 428 'message' => sprintf( 363 429 /* translators: %s: Error message */ 364 __( 'REST API is not accessible: %s. This may be caused by a Coming Soon plugin, security plugin, or server configuration.', 'finalpos' ), 365 $response->get_error_message() 366 ), 367 ); 368 } 369 370 $status_code = wp_remote_retrieve_response_code( $response ); 371 372 // 401 Unauthorized is OK - it means the API is working but requires authentication. 373 // 200 OK is also fine. 374 // 403 Forbidden might indicate a Coming Soon plugin blocking access. 375 if ( 403 === $status_code ) { 376 return array( 377 'available' => false, 378 'message' => __( 'REST API access is forbidden (403). This is usually caused by a Coming Soon plugin, maintenance mode, or security plugin blocking API access. Please disable any such plugins before connecting.', 'finalpos' ), 379 ); 380 } 381 382 // 503 Service Unavailable usually means maintenance mode. 383 if ( 503 === $status_code ) { 384 return array( 385 'available' => false, 386 'message' => __( 'REST API returned Service Unavailable (503). Your site appears to be in maintenance mode. Please disable maintenance mode before connecting.', 'finalpos' ), 387 ); 388 } 389 390 // Check for unexpected status codes. 391 if ( $status_code >= 500 ) { 392 return array( 393 'available' => false, 394 'message' => sprintf( 395 /* translators: %d: HTTP status code */ 396 __( 'REST API returned server error (%d). Please check your server configuration and error logs.', 'finalpos' ), 397 $status_code 430 __( 'REST API error: %s', 'finalpos' ), 431 $error->get_error_message() 398 432 ), 399 433 ); -
finalpos/trunk/includes/lifecycle.php
r3411106 r3442632 3 3 * FinalPOS Plugin Lifecycle Management 4 4 * 5 * Handles plugin activation, deactivation, and uninstallation 5 * Handles plugin activation, deactivation, and uninstallation. 6 * Following WordPress best practices: 7 * - Activation: Set up initial state, redirect to setup 8 * - Deactivation: Pause integrations, clean temp data, preserve user settings 9 * - Uninstallation: Full cleanup (handled in uninstall.php) 10 * 11 * @package FinalPOS 6 12 */ 7 13 … … 10 16 } 11 17 18 /** 19 * Class FinalPOS_Lifecycle 20 * 21 * Manages plugin lifecycle events. 22 */ 12 23 class FinalPOS_Lifecycle { 13 24 /** … … 34 45 */ 35 46 private function __construct() { 36 // Hook into admin notices to display reactivation errors 47 // Hook into admin notices to display reactivation errors. 37 48 add_action( 'admin_notices', array( $this, 'display_reactivation_error' ) ); 38 49 } 39 50 40 51 /** 41 * Handles plugin activation 52 * Handles plugin activation. 53 * 54 * Sets up initial state and schedules redirect to setup wizard. 55 * 56 * @return void 42 57 */ 43 58 public static function activate() { … … 46 61 47 62 /** 48 * Restore the WooCommerce API key to read-write, activate all webhooks, and reinitiate sync on reactivation 63 * Restore the WooCommerce API key to read-write, activate all webhooks, and reinitiate sync on reactivation. 64 * 65 * This is the reverse of deactivate() - restores full functionality. 66 * 67 * @return void 49 68 */ 50 69 public static function reactivate() { 51 // Only proceed if we're reactivating after a deactivation 52 // (not first-time activation) 53 70 // Only proceed if we're reactivating after a deactivation (not first-time activation). 54 71 if ( ! get_option( FINALPOS_OPTION_JWT_TOKEN ) ) { 55 72 return; … … 58 75 global $wpdb; 59 76 60 // Restore API key to read-write 61 $wpdb->query( 62 $wpdb->prepare( 63 "UPDATE `{$wpdb->prefix}woocommerce_api_keys` 64 SET `permissions` = %s 65 WHERE `description` LIKE %s", 77 // Restore API key to read-write. 78 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 79 $wpdb->query( 80 $wpdb->prepare( 81 "UPDATE {$wpdb->prefix}woocommerce_api_keys SET permissions = %s WHERE description LIKE %s", 66 82 'read_write', 67 83 FINALPOS_API_DESCRIPTION_PATTERN … … 69 85 ); 70 86 71 // Restore all webhooks to active 72 $wpdb->query( 73 $wpdb->prepare( 74 "UPDATE `{$wpdb->prefix}wc_webhooks` 75 SET `status` = %s 76 WHERE `name` LIKE %s", 87 // Restore all webhooks to active. 88 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 89 $wpdb->query( 90 $wpdb->prepare( 91 "UPDATE {$wpdb->prefix}wc_webhooks SET status = %s WHERE name LIKE %s", 77 92 'active', 78 93 FINALPOS_WEBHOOK_NAME_PATTERN … … 80 95 ); 81 96 82 // Only attempt sync if wizard was previously completed 97 // Only attempt sync if wizard was previously completed. 83 98 if ( get_option( FINALPOS_OPTION_CURRENT_STAGE ) === FINALPOS_STAGE_COMPLETE ) { 84 99 … … 115 130 } 116 131 } catch ( Exception $e ) { 117 // Store the error state 132 // Store the error state for display on next admin page load. 118 133 update_option( FINALPOS_OPTION_REACTIVATION_ERROR, true ); 119 134 } … … 123 138 /** 124 139 * Display reactivation error notice if it exists 140 * 141 * @return void 125 142 */ 126 143 public function display_reactivation_error() { … … 131 148 </div> 132 149 <?php 133 // Clear the error state after displaying 150 // Clear the error state after displaying. 134 151 delete_option( FINALPOS_OPTION_REACTIVATION_ERROR ); 135 152 } … … 137 154 138 155 /** 139 * Make the WooCommerce API key read-only and pause all webhooks on deactivation 156 * Handle plugin deactivation. 157 * 158 * Following WordPress best practices: 159 * - Pause external integrations (webhooks) 160 * - Restrict API permissions (read-only) 161 * - Clean up temporary transients 162 * - Preserve user settings (for potential reactivation) 163 * 164 * This is reversible - reactivate() restores full functionality. 165 * 166 * @return void 140 167 */ 141 168 public static function deactivate() { 142 169 global $wpdb; 143 170 144 // Make API key read-only 145 $wpdb->query( 146 $wpdb->prepare( 147 "UPDATE `{$wpdb->prefix}woocommerce_api_keys` 148 SET `permissions` = %s 149 WHERE `description` LIKE %s", 171 // Make API key read-only (prevents write operations while plugin is inactive). 172 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 173 $wpdb->query( 174 $wpdb->prepare( 175 "UPDATE {$wpdb->prefix}woocommerce_api_keys SET permissions = %s WHERE description LIKE %s", 150 176 'read', 151 177 FINALPOS_API_DESCRIPTION_PATTERN … … 153 179 ); 154 180 155 // Pause all webhooks 156 $wpdb->query( 157 $wpdb->prepare( 158 "UPDATE `{$wpdb->prefix}wc_webhooks` 159 SET `status` = %s 160 WHERE `name` LIKE %s", 181 // Pause all webhooks (stops webhook deliveries while plugin is inactive). 182 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 183 $wpdb->query( 184 $wpdb->prepare( 185 "UPDATE {$wpdb->prefix}wc_webhooks SET status = %s WHERE name LIKE %s", 161 186 'paused', 162 187 FINALPOS_WEBHOOK_NAME_PATTERN 163 188 ) 164 189 ); 190 191 // Clean up temporary transients (not needed while plugin is inactive). 192 delete_transient( FINALPOS_TRANSIENT_ACTIVATION_REDIRECT ); 193 194 // Clean up any pending auth nonce transients. 195 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 196 $wpdb->query( 197 $wpdb->prepare( 198 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", 199 '_transient_' . FINALPOS_TRANSIENT_AUTH_NONCE_PREFIX . '%', 200 '_transient_timeout_' . FINALPOS_TRANSIENT_AUTH_NONCE_PREFIX . '%' 201 ) 202 ); 203 204 // Note: We intentionally DO NOT delete user options/settings here. 205 // User data is preserved for potential reactivation. 206 // Full cleanup only happens on uninstall (see uninstall.php). 165 207 } 166 208 } -
finalpos/trunk/integrations/readonly-orders.php
r3420195 r3442632 339 339 */ 340 340 function finalpos_disable_bulk_actions_for_readonly( $actions ) { 341 // Only disable bulk actions on the edit order screen when viewing a specific read-only order 342 if ( isset( $_GET['post'] ) && is_numeric( $_GET['post'] ) && finalpos_is_order_readonly( $_GET['post'] ) ) { 341 // Only disable bulk actions on the edit order screen when viewing a specific read-only order. 342 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading URL parameter for display logic. 343 $post_id = isset( $_GET['post'] ) && is_numeric( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; 344 345 if ( $post_id && finalpos_is_order_readonly( $post_id ) ) { 343 346 if ( isset( $actions['trash'] ) ) { 344 347 unset( $actions['trash'] ); -
finalpos/trunk/readme.txt
r3420195 r3442632 7 7 Tested up to: 6.8.3 8 8 Requires PHP: 7.4 9 Stable tag: 1.3. 69 Stable tag: 1.3.7 10 10 License: GPLv2 or later 11 11 License URI: http://www.gnu.org/licenses/gpl-2.0 … … 93 93 94 94 == Changelog == 95 = 1.3.7 = 96 * Fixed critical error when deleting plugin 97 * Improved plugin lifecycle management (activation, deactivation, uninstall) 98 * Enhanced data cleanup following WordPress best practices 99 * PHPCS security compliance improvements 100 95 101 = 1.3.6 = 96 102 * Support for coming soon mode -
finalpos/trunk/uninstall.php
r3411106 r3442632 13 13 } 14 14 15 // Include constants file first (required for disconnect_plugin method). 16 require_once plugin_dir_path( __FILE__ ) . 'includes/constants.php'; 17 15 18 // Include the common functions file to access disconnect_plugin method. 16 19 require_once plugin_dir_path( __FILE__ ) . 'includes/common-functions.php';
Note: See TracChangeset
for help on using the changeset viewer.