| 1 |
<?php |
| 2 |
/** |
| 3 |
* Remove-connection flow — server side. |
| 4 |
* |
| 5 |
* WHY THIS FILE EXISTS |
| 6 |
* -------------------- |
| 7 |
* The Connections tab lets an administrator remove an MLS connection (a row's |
| 8 |
* Remove action, admin/js/mlsimport-connections.js). This file owns everything |
| 9 |
* remove needs server-side, mirroring the add/edit module layout |
| 10 |
* (includes/mlsimport-connections-add.php / -edit.php): |
| 11 |
* |
| 12 |
* - the flow core: delete the connection's per-connection suffixed options, |
| 13 |
* drop its registry record, and — when the removed connection was the |
| 14 |
* CURRENT one — promote the next-highest-priority remaining connection to |
| 15 |
* current (mirroring its identity + credentials into the flat |
| 16 |
* mlsimport_admin_options, which every legacy read path resolves current |
| 17 |
* from). Removing the LAST connection leaves the install unconfigured, |
| 18 |
* - the single AJAX handler the row's Remove action posts to. |
| 19 |
* |
| 20 |
* ONE RULE (what remove means): removing a connection deletes the connection |
| 21 |
* itself — record, credentials, field mapping, gathered metadata. It never |
| 22 |
* deletes content: imported listings and import tasks stay (tasks bound to the |
| 23 |
* removed connection simply stop running — task binding fails closed, #277). |
| 24 |
* |
| 25 |
* @since 7.3.0 |
| 26 |
* @package Mlsimport |
| 27 |
*/ |
| 28 |
|
| 29 |
if ( ! defined( 'ABSPATH' ) ) { |
| 30 |
exit; |
| 31 |
} |
| 32 |
|
| 33 |
/** |
| 34 |
* Run the full remove flow: refuse / clean up / drop / promote. |
| 35 |
* |
| 36 |
* Step by step: |
| 37 |
* 1. Refuse: no id, or not a registered connection (the same "must name a |
| 38 |
* registered connection" rule the edit flow uses). |
| 39 |
* 2. Note whether the removed connection is the CURRENT one — decided BEFORE |
| 40 |
* anything is deleted. |
| 41 |
* 3. Delete the connection's suffixed per-connection options (the same five |
| 42 |
* bases the #274 migration creates: field selection, sync settings, the |
| 43 |
* two metadata blobs, the metadata-populated flag). |
| 44 |
* 4. Drop the registry record. |
| 45 |
* 5. Current-connection promotion: when the removed connection was current, |
| 46 |
* the highest-priority REMAINING connection becomes current — its identity |
| 47 |
* + credentials are mirrored into the flat mlsimport_admin_options through |
| 48 |
* the shared mlsimport_connections_flat_sync(), the provider access tokens |
| 49 |
* are dropped so the next MLS request re-authenticates as the promoted |
| 50 |
* connection, and the global connection flag follows the promoted record's |
| 51 |
* tested status. With NO connection remaining the flat identity is cleared |
| 52 |
* and the flag deleted — the install is unconfigured again. |
| 53 |
* 6. Priority drives the dedupe winner (#267/#282): removal is a removal seam, |
| 54 |
* so every currently flagged duplicate group is re-decided — a hidden |
| 55 |
* duplicate whose winner belonged to the removed connection is promoted. |
| 56 |
* |
| 57 |
* @param int $mls_id The connection being removed. |
| 58 |
* @return array{success:bool, code:string, message:string} |
| 59 |
*/ |
| 60 |
function mlsimport_connections_remove_execute( int $mls_id ): array { |
| 61 |
// Step 1: cheap refusals first — same pure rule as the edit flow. |
| 62 |
$refusal_messages = array( |
| 63 |
'no_mls' => esc_html__( 'No connection selected.', 'mlsimport' ), |
| 64 |
'not_registered' => esc_html__( 'This MLS is not one of your connections.', 'mlsimport' ), |
| 65 |
); |
| 66 |
$refusal = mlsimport_connections_edit_refusal( $mls_id, array_keys( Mlsimport_Connections::all() ) ); |
| 67 |
if ( null !== $refusal ) { |
| 68 |
return array( |
| 69 |
'success' => false, |
| 70 |
'code' => $refusal, |
| 71 |
'message' => $refusal_messages[ $refusal ], |
| 72 |
); |
| 73 |
} |
| 74 |
|
| 75 |
// Step 2: current-ness is decided before any state changes. |
| 76 |
$was_current = ( $mls_id === mlsimport_current_mls_id() ); |
| 77 |
|
| 78 |
// Step 3: the connection's large per-MLS state goes with it. |
| 79 |
foreach ( array( |
| 80 |
'mlsimport_admin_fields_select', |
| 81 |
'mlsimport_admin_mls_sync', |
| 82 |
'mlsimport_mls_metadata_mls_data', |
| 83 |
'mlsimport_mls_metadata_mls_enums', |
| 84 |
'mlsimport_mls_metadata_populated', |
| 85 |
) as $base ) { |
| 86 |
mlsimport_delete_connection_option( $base, $mls_id ); |
| 87 |
} |
| 88 |
|
| 89 |
// Step 4: the record itself. |
| 90 |
Mlsimport_Connections::remove( $mls_id ); |
| 91 |
|
| 92 |
// Step 5: promotion when the current connection was removed. |
| 93 |
if ( $was_current ) { |
| 94 |
$options = get_option( 'mlsimport_admin_options', array() ); |
| 95 |
$options = is_array( $options ) ? $options : array(); |
| 96 |
$remaining = Mlsimport_Connections::all(); |
| 97 |
|
| 98 |
if ( array() !== $remaining ) { |
| 99 |
// The registry is priority-sorted: the first record is the new |
| 100 |
// current. Mirror identity + credentials into the flat options. |
| 101 |
$promoted = reset( $remaining ); |
| 102 |
$adapter = Mlsimport_Provider_Family::adapter( (string) $promoted['provider_type'], (string) $promoted['mls_id'] ); |
| 103 |
update_option( |
| 104 |
'mlsimport_admin_options', |
| 105 |
mlsimport_connections_flat_sync( |
| 106 |
$options, |
| 107 |
$promoted, |
| 108 |
mlsimport_connections_credential_options( $promoted, $adapter->credential_fields() ) |
| 109 |
) |
| 110 |
); |
| 111 |
// The flag follows the promoted record's own tested status — a |
| 112 |
// promotion never fabricates a passed test. |
| 113 |
update_option( 'mlsimport_connection_test', 'yes' === $promoted['status'] ? 'yes' : '' ); |
| 114 |
} else { |
| 115 |
// Last connection removed: unconfigured install. Identity cleared; |
| 116 |
// other flat keys (account login, theme choice, stored provider |
| 117 |
// credential slots) stay untouched by design. |
| 118 |
$options['mlsimport_mls_name'] = ''; |
| 119 |
$options['mlsimport_mls_name_front'] = ''; |
| 120 |
update_option( 'mlsimport_admin_options', $options ); |
| 121 |
delete_option( 'mlsimport_connection_test' ); |
| 122 |
} |
| 123 |
|
| 124 |
// Either way the old current connection's tokens must not be reused. |
| 125 |
Mlsimport_Provider_Family::clear_direct_access_tokens(); |
| 126 |
} |
| 127 |
|
| 128 |
// Step 6: re-decide flagged duplicates under the shrunk connection set. |
| 129 |
mlsimport_dedupe_reevaluate_flagged(); |
| 130 |
|
| 131 |
return array( |
| 132 |
'success' => true, |
| 133 |
'code' => '', |
| 134 |
'message' => '', |
| 135 |
); |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* AJAX: a row's Remove action. |
| 140 |
* |
| 141 |
* Step by step: |
| 142 |
* 1. Nonce (the Connections screen nonce) + administrator capability. |
| 143 |
* 2. Run the flow core for the posted mls_id and answer the JS, which |
| 144 |
* reloads the page on success (table, slot strip, and current-connection |
| 145 |
* surfaces all re-render server-side). |
| 146 |
* |
| 147 |
* @return void |
| 148 |
*/ |
| 149 |
function mlsimport_ajax_connections_remove() { |
| 150 |
// Step 1: security. |
| 151 |
check_ajax_referer( 'mlsimport_connections_screen', 'security' ); |
| 152 |
if ( ! current_user_can( 'administrator' ) ) { |
| 153 |
wp_send_json_error( array( 'message' => 'Unauthorized' ) ); |
| 154 |
} |
| 155 |
|
| 156 |
// Step 2: run the flow and answer the row. |
| 157 |
$outcome = mlsimport_connections_remove_execute( isset( $_POST['mls_id'] ) ? (int) $_POST['mls_id'] : 0 ); |
| 158 |
if ( ! $outcome['success'] ) { |
| 159 |
wp_send_json_error( |
| 160 |
array( |
| 161 |
'code' => $outcome['code'], |
| 162 |
'message' => $outcome['message'], |
| 163 |
) |
| 164 |
); |
| 165 |
} |
| 166 |
wp_send_json_success(); |
| 167 |
} |
| 168 |
|
| 169 |
add_action( 'wp_ajax_mlsimport_connections_remove', 'mlsimport_ajax_connections_remove' ); |
| 170 |
|