Changeset 3475951
- Timestamp:
- 03/05/2026 09:37:16 PM (2 days ago)
- Location:
- shift8-real-estate-listings-for-treb/trunk
- Files:
-
- 9 edited
-
admin/class-shift8-treb-admin.php (modified) (5 diffs)
-
admin/css/admin.css (modified) (1 diff)
-
admin/js/admin.js (modified) (3 diffs)
-
admin/partials/settings-page.php (modified) (2 diffs)
-
includes/class-shift8-treb-ampre-service.php (modified) (2 diffs)
-
includes/class-shift8-treb-cli.php (modified) (9 diffs)
-
includes/class-shift8-treb-sync-service.php (modified) (1 diff)
-
readme.txt (modified) (5 diffs)
-
shift8-treb.php (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
shift8-real-estate-listings-for-treb/trunk/admin/class-shift8-treb-admin.php
r3452434 r3475951 40 40 add_action('wp_ajax_shift8_treb_manual_sync', array($this, 'ajax_manual_sync')); 41 41 add_action('wp_ajax_shift8_treb_reset_sync', array($this, 'ajax_reset_sync')); 42 add_action('wp_ajax_shift8_treb_get_cities', array($this, 'ajax_get_cities')); 42 43 } 43 44 … … 131 132 'google_maps_api_key' => '', 132 133 'listing_status_filter' => 'Active', 133 'city_filter' => ' Toronto',134 'city_filter' => '', 134 135 'property_type_filter' => '', 135 136 'min_price' => '', 136 137 'max_price' => '', 138 'geographic_filter_type' => '', 139 'postal_code_prefixes' => '', 137 140 'listing_template' => '' 138 141 )); … … 141 144 include SHIFT8_TREB_PLUGIN_DIR . 'admin/partials/settings-page.php'; 142 145 } 143 144 /**145 * Register settings146 *147 * Registers plugin settings with WordPress settings API for proper148 * validation and sanitization.149 *150 * @since 1.0.0151 */152 public function register_settings() {153 register_setting(154 'shift8_treb_settings',155 'shift8_treb_settings',156 array(157 'sanitize_callback' => array($this, 'sanitize_settings'),158 'default' => array(159 'bearer_token' => '',160 'sync_frequency' => 'daily',161 'max_listings_per_query' => 100,162 'debug_enabled' => '0',163 'google_maps_api_key' => '',164 'listing_status_filter' => 'Active',165 'city_filter' => 'Toronto',166 'property_type_filter' => '',167 'min_price' => '',168 'max_price' => ''169 )170 )171 );172 }173 174 /**175 * Sanitize settings176 *177 * Validates and sanitizes all plugin settings before saving.178 *179 * @since 1.0.0180 * @param array $input Raw input data181 * @return array Sanitized settings182 */183 public function sanitize_settings($input) {184 $sanitized = array();185 186 // Sanitize Bearer Token187 if (isset($input['bearer_token'])) {188 $token = trim($input['bearer_token']);189 if (!empty($token)) {190 $sanitized['bearer_token'] = shift8_treb_encrypt_data($token);191 } else {192 // Keep existing token if new one is empty193 $existing_settings = get_option('shift8_treb_settings', array());194 $sanitized['bearer_token'] = isset($existing_settings['bearer_token']) ? $existing_settings['bearer_token'] : '';195 }196 }197 198 // Sanitize sync frequency with extended options199 $allowed_frequencies = array('hourly', 'every_8_hours', 'every_12_hours', 'daily', 'weekly', 'bi_weekly', 'monthly');200 if (isset($input['sync_frequency']) && in_array($input['sync_frequency'], $allowed_frequencies, true)) {201 $sanitized['sync_frequency'] = $input['sync_frequency'];202 } else {203 $sanitized['sync_frequency'] = 'daily';204 }205 206 // Sanitize max listings per query207 if (isset($input['max_listings_per_query'])) {208 $max_listings = absint($input['max_listings_per_query']);209 $sanitized['max_listings_per_query'] = max(1, min(1000, $max_listings)); // Between 1 and 1000210 }211 212 // Sanitize debug setting213 $sanitized['debug_enabled'] = isset($input['debug_enabled']) ? '1' : '0';214 215 // Sanitize Google Maps API key216 if (isset($input['google_maps_api_key'])) {217 $sanitized['google_maps_api_key'] = sanitize_text_field(trim($input['google_maps_api_key']));218 }219 220 // Sanitize listing status filter (based on RESO StandardStatus)221 $allowed_statuses = array('Active', 'Pending', 'ActiveUnderContract', 'Sold', 'Expired', 'Withdrawn', 'All');222 if (isset($input['listing_status_filter']) && in_array($input['listing_status_filter'], $allowed_statuses, true)) {223 $sanitized['listing_status_filter'] = $input['listing_status_filter'];224 } else {225 $sanitized['listing_status_filter'] = 'Active';226 }227 228 // Sanitize city filter229 if (isset($input['city_filter'])) {230 $sanitized['city_filter'] = sanitize_text_field(trim($input['city_filter']));231 }232 233 // Sanitize property type filter (based on RESO PropertyType)234 if (isset($input['property_type_filter'])) {235 $sanitized['property_type_filter'] = sanitize_text_field(trim($input['property_type_filter']));236 }237 238 // Sanitize price filters239 if (isset($input['min_price'])) {240 $sanitized['min_price'] = $input['min_price'] !== '' ? absint($input['min_price']) : '';241 }242 243 if (isset($input['max_price'])) {244 $sanitized['max_price'] = $input['max_price'] !== '' ? absint($input['max_price']) : '';245 }246 247 shift8_treb_log('Settings saved', array(248 'bearer_token_set' => !empty($sanitized['bearer_token']),249 'sync_frequency' => $sanitized['sync_frequency'],250 'debug_enabled' => $sanitized['debug_enabled'],251 'city_filter' => $sanitized['city_filter'],252 'max_listings' => $sanitized['max_listings_per_query']253 ));254 255 // Reschedule cron if frequency changed256 $existing_settings = get_option('shift8_treb_settings', array());257 if (isset($existing_settings['sync_frequency']) && $existing_settings['sync_frequency'] !== $sanitized['sync_frequency']) {258 wp_clear_scheduled_hook('shift8_treb_sync_listings');259 wp_schedule_event(time(), $sanitized['sync_frequency'], 'shift8_treb_sync_listings');260 }261 262 return $sanitized;263 }264 265 146 266 147 /** … … 286 167 ); 287 168 169 // jQuery UI Autocomplete ships with WordPress core 170 wp_enqueue_script('jquery-ui-autocomplete'); 171 288 172 // Add plugin-specific scripts 289 173 wp_enqueue_script( 290 174 'shift8-treb-admin', 291 175 SHIFT8_TREB_PLUGIN_URL . 'admin/js/admin.js', 292 array('jquery' ),176 array('jquery', 'jquery-ui-autocomplete'), 293 177 SHIFT8_TREB_VERSION, 294 178 true … … 437 321 438 322 /** 323 * AJAX handler for fetching city lookup values 324 * 325 * Returns cached city list from AMPRE Lookup API, refreshing if stale. 326 * Accepts optional ?refresh=1 to force cache refresh. 327 * 328 * @since 1.7.0 329 */ 330 public function ajax_get_cities() { 331 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'shift8_treb_nonce') || !current_user_can('manage_options')) { 332 wp_send_json_error(array('message' => esc_html__('Security check failed.', 'shift8-real-estate-listings-for-treb'))); 333 } 334 335 $force_refresh = !empty($_POST['refresh']); 336 $transient_key = 'shift8_treb_city_lookups'; 337 338 if (!$force_refresh) { 339 $cached = get_transient($transient_key); 340 if (false !== $cached && is_array($cached)) { 341 wp_send_json_success(array('cities' => $cached, 'source' => 'cache')); 342 } 343 } 344 345 try { 346 $settings = get_option('shift8_treb_settings', array()); 347 348 if (empty($settings['bearer_token'])) { 349 wp_send_json_error(array('message' => esc_html__('Bearer token not configured. Save your API credentials first.', 'shift8-real-estate-listings-for-treb'))); 350 } 351 352 $bearer_token = shift8_treb_decrypt_data($settings['bearer_token']); 353 $settings['bearer_token'] = $bearer_token; 354 355 require_once SHIFT8_TREB_PLUGIN_DIR . 'includes/class-shift8-treb-ampre-service.php'; 356 $service = new Shift8_TREB_AMPRE_Service($settings); 357 $cities = $service->get_city_lookups(); 358 359 if (is_wp_error($cities)) { 360 wp_send_json_error(array('message' => esc_html($cities->get_error_message()))); 361 } 362 363 set_transient($transient_key, $cities, 30 * 86400); 364 365 wp_send_json_success(array('cities' => $cities, 'source' => 'api', 'count' => count($cities))); 366 367 } catch (Exception $e) { 368 wp_send_json_error(array('message' => esc_html($e->getMessage()))); 369 } 370 } 371 372 /** 439 373 * AJAX handler for resetting sync mode 440 374 * -
shift8-real-estate-listings-for-treb/trunk/admin/css/admin.css
r3385519 r3475951 429 429 flex: 1; 430 430 } 431 432 /* jQuery UI Autocomplete overrides for WP Admin */ 433 .ui-autocomplete { 434 max-height: 250px; 435 overflow-y: auto; 436 overflow-x: hidden; 437 z-index: 100100; 438 background: #fff; 439 border: 1px solid #ccd0d4; 440 box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15); 441 } 442 443 .ui-autocomplete .ui-menu-item-wrapper { 444 padding: 6px 12px; 445 font-size: 13px; 446 cursor: pointer; 447 } 448 449 .ui-autocomplete .ui-state-active { 450 background: #0073aa; 451 color: #fff; 452 border: none; 453 margin: 0; 454 } -
shift8-real-estate-listings-for-treb/trunk/admin/js/admin.js
r3385519 r3475951 190 190 } 191 191 192 // Validate geographic filter fields based on selected type 193 var geoType = $('#geographic_filter_type').val(); 194 if (geoType === 'postal_prefix') { 195 var postalVal = $('#postal_code_prefixes').val().trim(); 196 if (!postalVal) { 197 errors.push('Postal code prefix filter is selected but no prefixes specified.'); 198 isValid = false; 199 } else { 200 var invalidPrefixes = validatePostalPrefixes(postalVal); 201 if (invalidPrefixes.length > 0) { 202 errors.push('Invalid postal code prefix(es): ' + invalidPrefixes.join(', ') + '. Format must be letter-digit-letter (e.g., M5V).'); 203 isValid = false; 204 } 205 } 206 } else if (geoType === 'city') { 207 var cityVal = $('#city_filter').val().trim(); 208 if (!cityVal) { 209 errors.push('City filter is selected but no city specified.'); 210 isValid = false; 211 } else if (cityListCache && cityListCache.length > 0) { 212 var enteredCities = splitValues(cityVal).filter(function(c) { return c.trim(); }); 213 var lowered = cityListCache.map(function(c) { return c.toLowerCase(); }); 214 var unknown = []; 215 for (var i = 0; i < enteredCities.length; i++) { 216 if (lowered.indexOf(enteredCities[i].trim().toLowerCase()) === -1) { 217 unknown.push(enteredCities[i].trim()); 218 } 219 } 220 if (unknown.length > 0) { 221 errors.push('Unrecognised city name(s): ' + unknown.join(', ') + '. Use the autocomplete suggestions or click the refresh button.'); 222 isValid = false; 223 } 224 } 225 } 226 192 227 // Show errors if any 193 228 if (!isValid) { … … 196 231 197 232 return isValid; 233 } 234 235 /** 236 * Validate postal code prefixes and return array of invalid ones 237 */ 238 function validatePostalPrefixes(value) { 239 if (!value) return []; 240 var fsaPattern = /^[A-Za-z]\d[A-Za-z]$/; 241 var prefixes = value.split(','); 242 var invalid = []; 243 for (var i = 0; i < prefixes.length; i++) { 244 var p = prefixes[i].trim(); 245 if (p && !fsaPattern.test(p)) { 246 invalid.push(p); 247 } 248 } 249 return invalid; 250 } 251 252 /** 253 * Toggle geographic filter fields based on selected type 254 */ 255 function initGeographicFilterToggle() { 256 var $select = $('#geographic_filter_type'); 257 if (!$select.length) return; 258 259 function toggleRows() { 260 var type = $select.val(); 261 $('#postal_prefix_row').toggle(type === 'postal_prefix'); 262 $('#city_filter_row').toggle(type === 'city'); 263 if (type === 'city') { 264 loadCityAutocomplete(false); 265 } 266 } 267 268 $select.on('change', toggleRows); 269 toggleRows(); 270 } 271 272 var cityListCache = null; 273 274 /** 275 * Load city list and initialise jQuery UI Autocomplete (multi-value) 276 */ 277 function loadCityAutocomplete(forceRefresh) { 278 var $field = $('#city_filter'); 279 var $status = $('#city-cache-status'); 280 if (!$field.length) return; 281 282 var data = { 283 action: 'shift8_treb_get_cities', 284 nonce: shift8TREB.nonce 285 }; 286 if (forceRefresh) { 287 data.refresh = 1; 288 } 289 290 $status.text('Loading cities...').css('color', '#0073aa').show(); 291 292 $.post(shift8TREB.ajaxurl, data, function(response) { 293 if (response.success && response.data.cities) { 294 cityListCache = response.data.cities; 295 var src = response.data.source === 'cache' ? 'cached' : 'API'; 296 $status.text(cityListCache.length + ' cities loaded (' + src + ')').css('color', '#00a32a'); 297 initCityAutocomplete($field, cityListCache); 298 } else { 299 var msg = response.data && response.data.message ? response.data.message : 'Could not load cities'; 300 $status.text(msg).css('color', '#d63638'); 301 } 302 }).fail(function() { 303 $status.text('Network error loading cities').css('color', '#d63638'); 304 }); 305 } 306 307 function splitValues(val) { 308 return val.split(/,\s*/); 309 } 310 311 function extractLast(term) { 312 return splitValues(term).pop(); 313 } 314 315 function initCityAutocomplete($field, cities) { 316 if ($field.data('ui-autocomplete')) { 317 $field.autocomplete('destroy'); 318 } 319 320 $field.on('keydown.shift8autocomplete', function(event) { 321 if (event.keyCode === $.ui.keyCode.TAB && $(this).autocomplete('instance').menu.active) { 322 event.preventDefault(); 323 } 324 }); 325 326 $field.autocomplete({ 327 minLength: 1, 328 source: function(request, response) { 329 var term = extractLast(request.term).toLowerCase(); 330 var matches = $.grep(cities, function(city) { 331 return city.toLowerCase().indexOf(term) === 0; 332 }); 333 response(matches.slice(0, 15)); 334 }, 335 focus: function() { 336 return false; 337 }, 338 select: function(event, ui) { 339 var terms = splitValues(this.value); 340 terms.pop(); 341 terms.push(ui.item.value); 342 terms.push(''); 343 this.value = terms.join(', '); 344 return false; 345 } 346 }); 347 } 348 349 /** 350 * Real-time validation for postal code prefix field 351 */ 352 function initPostalPrefixValidation() { 353 var $field = $('#postal_code_prefixes'); 354 if (!$field.length) return; 355 356 $field.on('blur', function() { 357 var val = $(this).val().trim(); 358 var $feedback = $(this).next('.shift8-treb-validation-msg'); 359 if (!$feedback.length) { 360 $feedback = $('<span class="shift8-treb-validation-msg"></span>'); 361 $(this).after($feedback); 362 } 363 364 if (!val) { 365 $feedback.text('').hide(); 366 return; 367 } 368 369 var invalid = validatePostalPrefixes(val); 370 if (invalid.length > 0) { 371 $feedback.text('Invalid: ' + invalid.join(', ') + ' — expected format: A1A (e.g., M5V)') 372 .css('color', '#d63638').show(); 373 } else { 374 var count = val.split(',').filter(function(v) { return v.trim(); }).length; 375 $feedback.text(count + ' valid prefix' + (count !== 1 ? 'es' : '')) 376 .css('color', '#00a32a').show(); 377 } 378 }); 198 379 } 199 380 … … 253 434 handleFrequencyChange(); 254 435 initAutoSave(); 436 initGeographicFilterToggle(); 437 initPostalPrefixValidation(); 438 439 $('#refresh-city-list').on('click', function() { 440 loadCityAutocomplete(true); 441 }); 255 442 256 443 // Form validation on submit -
shift8-real-estate-listings-for-treb/trunk/admin/partials/settings-page.php
r3385519 r3475951 176 176 class="small-text" /> 177 177 <p class="description"><?php esc_html_e('Only sync listings modified within the last X days. This filters by ModificationTimestamp.', 'shift8-real-estate-listings-for-treb'); ?></p> 178 </td> 179 </tr> 180 </table> 181 </div> 182 183 <!-- Geographic Filter Section --> 184 <div class="card"> 185 <h2 class="title"><?php esc_html_e('Geographic Filter', 'shift8-real-estate-listings-for-treb'); ?></h2> 186 <?php 187 $geo_type = isset($settings['geographic_filter_type']) ? $settings['geographic_filter_type'] : ''; 188 $postal_prefixes = isset($settings['postal_code_prefixes']) ? $settings['postal_code_prefixes'] : ''; 189 $city_filter_val = isset($settings['city_filter']) ? $settings['city_filter'] : ''; 190 ?> 191 <table class="form-table"> 192 <tr> 193 <th scope="row"> 194 <label for="geographic_filter_type"><?php esc_html_e('Filter Type', 'shift8-real-estate-listings-for-treb'); ?></label> 195 </th> 196 <td> 197 <select id="geographic_filter_type" name="shift8_treb_settings[geographic_filter_type]"> 198 <option value="" <?php selected($geo_type, ''); ?>><?php esc_html_e('None (no geographic filter)', 'shift8-real-estate-listings-for-treb'); ?></option> 199 <option value="postal_prefix" <?php selected($geo_type, 'postal_prefix'); ?>><?php esc_html_e('By Postal Code Prefix', 'shift8-real-estate-listings-for-treb'); ?></option> 200 <option value="city" <?php selected($geo_type, 'city'); ?>><?php esc_html_e('By City', 'shift8-real-estate-listings-for-treb'); ?></option> 201 </select> 202 <p class="description"><?php esc_html_e('Optionally restrict synced listings to a geographic area. Postal prefix and city filters are mutually exclusive.', 'shift8-real-estate-listings-for-treb'); ?></p> 203 </td> 204 </tr> 205 <tr class="shift8-treb-postal-row" <?php echo ($geo_type !== 'postal_prefix') ? 'style="display:none;"' : ''; ?>> 206 <th scope="row"> 207 <label for="postal_code_prefixes"><?php esc_html_e('Postal Code Prefixes', 'shift8-real-estate-listings-for-treb'); ?></label> 208 </th> 209 <td> 210 <input type="text" 211 id="postal_code_prefixes" 212 name="shift8_treb_settings[postal_code_prefixes]" 213 value="<?php echo esc_attr($postal_prefixes); ?>" 214 class="regular-text" 215 placeholder="<?php esc_attr_e('e.g., M5V, M6H, M8X', 'shift8-real-estate-listings-for-treb'); ?>" /> 216 <p class="description"><?php esc_html_e('Comma-separated postal code prefixes (FSA format: Letter-Number-Letter).', 'shift8-real-estate-listings-for-treb'); ?></p> 217 </td> 218 </tr> 219 <tr class="shift8-treb-city-row" <?php echo ($geo_type !== 'city') ? 'style="display:none;"' : ''; ?>> 220 <th scope="row"> 221 <label for="shift8_treb_city_filter"><?php esc_html_e('City Filter', 'shift8-real-estate-listings-for-treb'); ?></label> 222 </th> 223 <td> 224 <input type="text" 225 id="shift8_treb_city_filter" 226 name="shift8_treb_settings[city_filter]" 227 value="<?php echo esc_attr($city_filter_val); ?>" 228 class="regular-text" 229 placeholder="<?php esc_attr_e('e.g., Toronto, Mississauga, Brampton', 'shift8-real-estate-listings-for-treb'); ?>" /> 230 <p class="description"><?php esc_html_e('Comma-separated city names. Start typing to see suggestions.', 'shift8-real-estate-listings-for-treb'); ?></p> 231 <p> 232 <button type="button" id="shift8-treb-refresh-cities" class="button button-secondary"> 233 <?php esc_html_e('Refresh Cities', 'shift8-real-estate-listings-for-treb'); ?> 234 </button> 235 <span id="shift8-treb-city-cache-status" style="margin-left: 10px; color: #666;"></span> 236 </p> 178 237 </td> 179 238 </tr> … … 541 600 }); 542 601 602 // Geographic filter type toggle 603 function toggleGeoFilterRows() { 604 var filterType = $('#geographic_filter_type').val(); 605 if (filterType === 'postal_prefix') { 606 $('.shift8-treb-postal-row').show(); 607 $('.shift8-treb-city-row').hide(); 608 } else if (filterType === 'city') { 609 $('.shift8-treb-postal-row').hide(); 610 $('.shift8-treb-city-row').show(); 611 } else { 612 $('.shift8-treb-postal-row').hide(); 613 $('.shift8-treb-city-row').hide(); 614 } 615 } 616 $('#geographic_filter_type').on('change', toggleGeoFilterRows); 617 toggleGeoFilterRows(); 618 619 // Refresh Cities 620 $('#shift8-treb-refresh-cities').on('click', function() { 621 var button = $(this); 622 var statusSpan = $('#shift8-treb-city-cache-status'); 623 button.prop('disabled', true).text('<?php esc_html_e('Refreshing...', 'shift8-real-estate-listings-for-treb'); ?>'); 624 statusSpan.text(''); 625 626 $.ajax({ 627 url: ajaxurl, 628 type: 'POST', 629 data: { 630 action: 'shift8_treb_get_cities', 631 nonce: '<?php echo esc_js(wp_create_nonce('shift8_treb_nonce')); ?>', 632 refresh: 1 633 }, 634 success: function(response) { 635 if (response.success) { 636 statusSpan.text('<?php esc_html_e('City list refreshed.', 'shift8-real-estate-listings-for-treb'); ?>'); 637 } else { 638 statusSpan.css('color', '#dc3232').text('<?php esc_html_e('Failed to refresh city list.', 'shift8-real-estate-listings-for-treb'); ?>'); 639 } 640 }, 641 error: function() { 642 statusSpan.css('color', '#dc3232').text('<?php esc_html_e('Request failed.', 'shift8-real-estate-listings-for-treb'); ?>'); 643 }, 644 complete: function() { 645 button.prop('disabled', false).text('<?php esc_html_e('Refresh Cities', 'shift8-real-estate-listings-for-treb'); ?>'); 646 } 647 }); 648 }); 649 650 // City autocomplete 651 if ($.ui && $.ui.autocomplete) { 652 function extractLast(term) { 653 return term.split(/,\s*/).pop(); 654 } 655 $('#shift8_treb_city_filter').on('keydown', function(event) { 656 if (event.keyCode === $.ui.keyCode.TAB && $(this).autocomplete('instance').menu.active) { 657 event.preventDefault(); 658 } 659 }).autocomplete({ 660 source: function(request, response) { 661 $.ajax({ 662 url: ajaxurl, 663 type: 'POST', 664 data: { 665 action: 'shift8_treb_get_cities', 666 nonce: '<?php echo esc_js(wp_create_nonce('shift8_treb_nonce')); ?>' 667 }, 668 success: function(data) { 669 if (data.success && data.data.cities) { 670 var term = extractLast(request.term); 671 var filtered = $.ui.autocomplete.filter(data.data.cities, term); 672 response(filtered.slice(0, 20)); 673 } else { 674 response([]); 675 } 676 }, 677 error: function() { 678 response([]); 679 } 680 }); 681 }, 682 search: function() { 683 var term = extractLast(this.value); 684 if (term.length < 2) { 685 return false; 686 } 687 }, 688 focus: function() { 689 return false; 690 }, 691 select: function(event, ui) { 692 var terms = this.value.split(/,\s*/); 693 terms.pop(); 694 terms.push(ui.item.value); 695 terms.push(''); 696 this.value = terms.join(', '); 697 return false; 698 } 699 }); 700 } 701 543 702 // Manual Sync 544 703 $('#manual-sync').on('click', function() { -
shift8-real-estate-listings-for-treb/trunk/includes/class-shift8-treb-ampre-service.php
r3387235 r3475951 237 237 } 238 238 239 // Add geographic filter based on filter type 240 $geo_type = $this->settings['geographic_filter_type'] ?? ''; 241 if ($geo_type === 'postal_prefix' && !empty($this->settings['postal_code_prefixes'])) { 242 $prefixes = array_map('trim', explode(',', $this->settings['postal_code_prefixes'])); 243 $prefix_filters = array(); 244 foreach ($prefixes as $prefix) { 245 $prefix = strtoupper(sanitize_text_field($prefix)); 246 if (preg_match('/^[A-Z][0-9][A-Z]$/', $prefix)) { 247 $prefix_filters[] = "startswith(PostalCode, '" . $prefix . "')"; 248 } 249 } 250 if (!empty($prefix_filters)) { 251 $filters[] = '(' . implode(' or ', $prefix_filters) . ')'; 252 } 253 } elseif ($geo_type === 'city' && !empty($this->settings['city_filter'])) { 254 $cities = array_map('trim', explode(',', $this->settings['city_filter'])); 255 $city_filters = array(); 256 foreach ($cities as $city) { 257 $city = sanitize_text_field($city); 258 if (!empty($city)) { 259 $city_filters[] = "City eq '" . $city . "'"; 260 } 261 } 262 if (count($city_filters) === 1) { 263 $filters[] = $city_filters[0]; 264 } elseif (count($city_filters) > 1) { 265 $filters[] = '(' . implode(' or ', $city_filters) . ')'; 266 } 267 } 268 239 269 // Combine filters 240 270 if (!empty($filters)) { … … 394 424 )); 395 425 return new WP_Error('ampre_media_error', $e->getMessage()); 426 } 427 } 428 429 /** 430 * Get city lookup values from AMPRE Lookup resource 431 * 432 * Fetches the canonical list of city names available in PropTX. 433 * 434 * @since 1.7.0 435 * @return array|WP_Error Array of city name strings or WP_Error 436 */ 437 public function get_city_lookups() { 438 try { 439 if (empty($this->bearer_token)) { 440 throw new Exception('Bearer token is required'); 441 } 442 443 $endpoint = "Lookup?\$filter=LookupName eq 'City'&\$select=LookupValue&\$top=2000"; 444 445 $response = $this->make_request($endpoint); 446 447 if (is_wp_error($response)) { 448 throw new Exception('Lookup API request failed: ' . esc_html($response->get_error_message())); 449 } 450 451 $response_code = wp_remote_retrieve_response_code($response); 452 $response_body = wp_remote_retrieve_body($response); 453 454 if ($response_code !== 200) { 455 throw new Exception('Lookup API returned status code: ' . esc_html($response_code)); 456 } 457 458 $data = json_decode($response_body, true); 459 460 if (json_last_error() !== JSON_ERROR_NONE || !isset($data['value']) || !is_array($data['value'])) { 461 throw new Exception('Invalid response from Lookup API'); 462 } 463 464 $cities = array(); 465 foreach ($data['value'] as $item) { 466 if (!empty($item['LookupValue'])) { 467 $cities[] = $item['LookupValue']; 468 } 469 } 470 471 sort($cities, SORT_STRING | SORT_FLAG_CASE); 472 473 shift8_treb_log('City lookups fetched from AMPRE', array( 474 'count' => count($cities) 475 )); 476 477 return $cities; 478 479 } catch (Exception $e) { 480 shift8_treb_log('AMPRE city lookup error', array( 481 'error' => esc_html($e->getMessage()) 482 )); 483 return new WP_Error('ampre_lookup_error', $e->getMessage()); 396 484 } 397 485 } -
shift8-real-estate-listings-for-treb/trunk/includes/class-shift8-treb-cli.php
r3385525 r3475951 47 47 * [--skip-images] 48 48 * : Skip image downloads for faster sync (stores external URLs only) 49 * 50 * [--postal-prefix=<prefixes>] 51 * : Override geographic filter with postal code prefixes (e.g., M5V,M6H) 52 * 53 * [--city=<cities>] 54 * : Override geographic filter with city names (e.g., "Toronto W08,Mississauga") 49 55 * 50 56 * [--sequential-images] … … 92 98 93 99 try { 94 // Prepare settings overrides for CLI95 $settings_overrides = array();96 97 100 // Check bearer token before proceeding 98 101 $base_settings = get_option('shift8_treb_settings', array()); … … 110 113 } 111 114 112 // Handle listing age override 113 if ($listing_age !== null) { 114 $settings_overrides['listing_age_days'] = $listing_age; 115 $settings_overrides['last_sync_timestamp'] = null; // Force age-based filtering 116 if ($verbose) { 117 WP_CLI::line("Using listing age override: {$listing_age} days"); 118 } 119 } else { 120 // For manual CLI sync, always use listing age instead of incremental sync 121 $settings_overrides['last_sync_timestamp'] = null; 122 if ($verbose) { 123 $listing_age_days = $base_settings['listing_age_days'] ?? 30; 124 WP_CLI::line("Using WordPress setting: {$listing_age_days} days (manual sync ignores incremental)"); 125 } 126 } 127 128 // Enable members-only filtering if specified 129 if ($members_only) { 130 $settings_overrides['members_only'] = true; 131 WP_CLI::line("🎯 Members-only mode: Filtering at API level for configured member IDs"); 132 133 // Validate member IDs are configured 134 if (empty($base_settings['member_id'])) { 135 WP_CLI::error('Member IDs not configured in settings. Cannot use --members-only flag.'); 136 } 137 } 115 $settings_overrides = $this->build_settings_overrides($assoc_args, $base_settings, $verbose); 138 116 139 117 // Handle image processing options … … 430 408 * default: 90 431 409 * --- 410 * 411 * [--members-only] 412 * : Only show listings from configured member IDs (filters at API level) 413 * 414 * [--postal-prefix=<prefixes>] 415 * : Override geographic filter with postal code prefixes (e.g., M5V,M6H) 416 * 417 * [--city=<cities>] 418 * : Override geographic filter with city names (e.g., "Toronto W08,Mississauga") 432 419 * 433 420 * ## EXAMPLES … … 453 440 } 454 441 455 // Get settings and initialize AMPRE service456 $settings = get_option('shift8_treb_settings', array());457 458 if (empty($settings['bearer_token'])) {459 WP_CLI::error('Bearer token not configured');460 }461 462 // Decrypt token using the plugin's decryption function463 $decrypted_token = shift8_treb_decrypt_data($settings['bearer_token']);464 465 if (empty($decrypted_token)) {466 WP_CLI::error('Failed to decrypt bearer token');467 }468 469 // Prepare settings for AMPRE service470 $ampre_settings = array_merge($settings, array(471 'bearer_token' => $decrypted_token,472 'listing_age_days' => $days473 ));474 475 // Initialize AMPRE service476 require_once SHIFT8_TREB_PLUGIN_DIR . 'includes/class-shift8-treb-ampre-service.php';477 $ampre_service = new Shift8_TREB_AMPRE_Service($ampre_settings);478 479 WP_CLI::line('Testing API connection...');480 481 442 try { 482 $connection_result = $ampre_service->test_connection(); 443 $base_settings = get_option('shift8_treb_settings', array()); 444 445 if (empty($base_settings['bearer_token'])) { 446 WP_CLI::error('Bearer token not configured'); 447 } 448 449 $overrides = $this->build_settings_overrides($assoc_args, $base_settings, false); 450 451 if (!isset($overrides['listing_age_days'])) { 452 $overrides['listing_age_days'] = $days; 453 $overrides['last_sync_timestamp'] = null; 454 } 455 456 if (isset($assoc_args['limit'])) { 457 $overrides['max_listings_per_query'] = $limit; 458 } 459 460 require_once SHIFT8_TREB_PLUGIN_DIR . 'includes/class-shift8-treb-sync-service.php'; 461 $sync_service = new Shift8_TREB_Sync_Service($overrides); 462 463 WP_CLI::line('Testing API connection...'); 464 $connection_result = $sync_service->test_connection(); 483 465 if (!$connection_result['success']) { 484 466 WP_CLI::error('API connection failed: ' . $connection_result['message']); 485 467 } 486 468 WP_CLI::success('API connection successful'); 487 } catch (Exception $e) { 488 WP_CLI::error('API connection error: ' . esc_html($e->getMessage())); 489 } 490 491 WP_CLI::line('Fetching listings data...'); 492 493 try { 494 $listings_result = $ampre_service->get_listings(); 469 470 WP_CLI::line('Fetching listings data...'); 471 472 $listings_result = $sync_service->fetch_listings(); 495 473 496 474 if (is_wp_error($listings_result)) { … … 501 479 $total_found = count($listings); 502 480 503 // Limit the listings for analysis504 481 if ($limit > 0 && $total_found > $limit) { 505 482 $listings = array_slice($listings, 0, $limit); … … 508 485 WP_CLI::success("Found {$total_found} total listings, analyzing first " . count($listings)); 509 486 510 // Analyze the data487 $settings = get_option('shift8_treb_settings', array()); 511 488 $this->analyze_listings_data($listings, $search_mls, $show_agents, $settings); 512 489 … … 942 919 943 920 /** 921 * Preview listings from API without creating posts 922 * 923 * Shows a summary of what the API would return with current settings, 924 * including price range, city breakdown, property types, and agent summary. 925 * 926 * ## OPTIONS 927 * 928 * [--limit=<number>] 929 * : Limit number of listings to fetch 930 * 931 * [--listing-age=<days>] 932 * : Override listing age in days 933 * 934 * [--members-only] 935 * : Only show listings from configured member IDs 936 * 937 * [--postal-prefix=<prefixes>] 938 * : Override geographic filter with postal code prefixes (e.g., M5V,M6H) 939 * 940 * [--city=<cities>] 941 * : Override geographic filter with city names (e.g., "Toronto W08,Mississauga") 942 * 943 * [--format=<format>] 944 * : Output format (table or json) 945 * --- 946 * default: table 947 * options: 948 * - table 949 * - json 950 * --- 951 * 952 * ## EXAMPLES 953 * 954 * wp shift8-treb preview 955 * wp shift8-treb preview --postal-prefix=M5V,M6H,M8X 956 * wp shift8-treb preview --city="Toronto W08,Mississauga" 957 * wp shift8-treb preview --limit=20 --members-only 958 * wp shift8-treb preview --format=json 959 * 960 * @when after_wp_load 961 */ 962 public function preview($args, $assoc_args) { 963 $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'table'; 964 965 WP_CLI::line('=== Shift8 TREB Listing Preview ==='); 966 WP_CLI::line('Querying API with current settings (no posts will be created)...'); 967 WP_CLI::line(''); 968 969 try { 970 $base_settings = get_option('shift8_treb_settings', array()); 971 972 if (empty($base_settings['bearer_token'])) { 973 WP_CLI::error('Bearer token not configured.'); 974 } 975 976 $overrides = $this->build_settings_overrides($assoc_args, $base_settings, true); 977 978 require_once SHIFT8_TREB_PLUGIN_DIR . 'includes/class-shift8-treb-sync-service.php'; 979 $sync_service = new Shift8_TREB_Sync_Service($overrides); 980 981 WP_CLI::line('Testing API connection...'); 982 $connection = $sync_service->test_connection(); 983 if (!$connection['success']) { 984 WP_CLI::error('API connection failed: ' . $connection['message']); 985 } 986 WP_CLI::success('API connection successful'); 987 WP_CLI::line(''); 988 989 WP_CLI::line('Fetching listings...'); 990 $listings = $sync_service->fetch_listings(); 991 992 if (is_wp_error($listings)) { 993 WP_CLI::error('API error: ' . $listings->get_error_message()); 994 } 995 996 $total = count($listings); 997 if ($total === 0) { 998 WP_CLI::warning('No listings returned from API with current filters.'); 999 return; 1000 } 1001 1002 WP_CLI::success("Retrieved {$total} listing(s)"); 1003 WP_CLI::line(''); 1004 1005 $prices = array(); 1006 $cities = array(); 1007 $property_types = array(); 1008 $agents = array(); 1009 $member_ids = !empty($base_settings['member_id']) 1010 ? array_map('trim', explode(',', $base_settings['member_id'])) 1011 : array(); 1012 1013 foreach ($listings as $listing) { 1014 if (isset($listing['ListPrice']) && $listing['ListPrice'] > 0) { 1015 $prices[] = $listing['ListPrice']; 1016 } 1017 $city = $listing['City'] ?? 'Unknown'; 1018 $cities[$city] = ($cities[$city] ?? 0) + 1; 1019 1020 $ptype = $listing['PropertyType'] ?? 'Unknown'; 1021 $property_types[$ptype] = ($property_types[$ptype] ?? 0) + 1; 1022 1023 $agent = $listing['ListAgentKey'] ?? 'Unknown'; 1024 $agents[$agent] = ($agents[$agent] ?? 0) + 1; 1025 } 1026 1027 if ($format === 'json') { 1028 $data = array( 1029 'total' => $total, 1030 'price_min' => !empty($prices) ? min($prices) : 0, 1031 'price_max' => !empty($prices) ? max($prices) : 0, 1032 'price_median' => !empty($prices) ? $prices[intval(count($prices) / 2)] : 0, 1033 'cities' => $cities, 1034 'property_types' => $property_types, 1035 'agents' => $agents, 1036 ); 1037 WP_CLI::line(wp_json_encode($data, JSON_PRETTY_PRINT)); 1038 return; 1039 } 1040 1041 if (!empty($prices)) { 1042 sort($prices); 1043 WP_CLI::line('--- Price Summary ---'); 1044 WP_CLI::line(' Min: $' . number_format(min($prices))); 1045 WP_CLI::line(' Max: $' . number_format(max($prices))); 1046 WP_CLI::line(' Median: $' . number_format($prices[intval(count($prices) / 2)])); 1047 WP_CLI::line(''); 1048 } 1049 1050 arsort($cities); 1051 WP_CLI::line('--- City Breakdown ---'); 1052 foreach ($cities as $city => $count) { 1053 WP_CLI::line(" {$city}: {$count}"); 1054 } 1055 WP_CLI::line(''); 1056 1057 arsort($property_types); 1058 WP_CLI::line('--- Property Types ---'); 1059 foreach ($property_types as $ptype => $count) { 1060 WP_CLI::line(" {$ptype}: {$count}"); 1061 } 1062 WP_CLI::line(''); 1063 1064 arsort($agents); 1065 $top_agents = array_slice($agents, 0, 10, true); 1066 WP_CLI::line('--- Top Agents (max 10) ---'); 1067 foreach ($top_agents as $agent => $count) { 1068 $tag = in_array($agent, $member_ids, true) ? ' [MEMBER]' : ''; 1069 WP_CLI::line(" {$agent}: {$count}{$tag}"); 1070 } 1071 1072 WP_CLI::line(''); 1073 WP_CLI::success("Preview complete: {$total} listings matched."); 1074 1075 } catch (Exception $e) { 1076 WP_CLI::error('Preview failed: ' . esc_html($e->getMessage())); 1077 } 1078 } 1079 1080 /** 944 1081 * Clear all TREB sync logs 945 1082 * … … 1134 1271 } 1135 1272 } 1273 1274 /** 1275 * Build settings overrides from CLI arguments 1276 * 1277 * @since 1.8.0 1278 * @param array $assoc_args CLI associative arguments 1279 * @param array $base_settings Current plugin settings 1280 * @param bool $verbose Whether to show verbose output 1281 * @return array Settings overrides 1282 */ 1283 private function build_settings_overrides($assoc_args, $base_settings, $verbose = false) { 1284 $overrides = array(); 1285 1286 if (isset($assoc_args['listing-age'])) { 1287 $overrides['listing_age_days'] = intval($assoc_args['listing-age']); 1288 $overrides['last_sync_timestamp'] = null; 1289 if ($verbose) { 1290 WP_CLI::line("Using listing age override: " . $overrides['listing_age_days'] . " days"); 1291 } 1292 } else { 1293 $overrides['last_sync_timestamp'] = null; 1294 } 1295 1296 if (isset($assoc_args['members-only'])) { 1297 if (empty($base_settings['member_id'])) { 1298 WP_CLI::error('Member IDs not configured in settings. Cannot use --members-only.'); 1299 } 1300 $overrides['members_only'] = true; 1301 WP_CLI::line("Members-only mode: Filtering at API level for configured member IDs"); 1302 } 1303 1304 if (isset($assoc_args['postal-prefix']) && isset($assoc_args['city'])) { 1305 WP_CLI::error('--postal-prefix and --city are mutually exclusive. Use one or the other.'); 1306 } 1307 1308 if (isset($assoc_args['postal-prefix'])) { 1309 $raw = strtoupper(trim($assoc_args['postal-prefix'])); 1310 $valid = array(); 1311 foreach (array_map('trim', explode(',', $raw)) as $p) { 1312 if (preg_match('/^[A-Z][0-9][A-Z]$/', $p)) { 1313 $valid[] = $p; 1314 } 1315 } 1316 if (!empty($valid)) { 1317 $overrides['geographic_filter_type'] = 'postal_prefix'; 1318 $overrides['postal_code_prefixes'] = implode(',', $valid); 1319 WP_CLI::line("Geographic filter: Postal prefix(es) " . implode(', ', $valid)); 1320 } else { 1321 WP_CLI::warning('No valid postal code prefixes provided. Expected format: A1A (e.g., M5V)'); 1322 } 1323 } elseif (isset($assoc_args['city'])) { 1324 $overrides['geographic_filter_type'] = 'city'; 1325 $overrides['city_filter'] = sanitize_text_field(trim($assoc_args['city'])); 1326 WP_CLI::line("Geographic filter: City = " . esc_html($overrides['city_filter'])); 1327 } 1328 1329 if (isset($assoc_args['limit'])) { 1330 $overrides['max_listings_per_query'] = intval($assoc_args['limit']); 1331 } 1332 1333 return $overrides; 1334 } 1136 1335 } 1137 1336 -
shift8-real-estate-listings-for-treb/trunk/includes/class-shift8-treb-sync-service.php
r3452434 r3475951 106 106 public function test_connection() { 107 107 return $this->ampre_service->test_connection(); 108 } 109 110 /** 111 * Fetch listings from API without processing them 112 * 113 * Uses the same settings, filters, and query construction as execute_sync() 114 * but returns raw listing data without creating or modifying any posts. 115 * 116 * @since 1.7.0 117 * @return array|WP_Error Array of listing data or WP_Error on failure 118 */ 119 public function fetch_listings() { 120 if (empty($this->settings['bearer_token'])) { 121 return new WP_Error('missing_token', 'Bearer token not configured'); 122 } 123 124 return $this->ampre_service->get_listings(); 108 125 } 109 126 -
shift8-real-estate-listings-for-treb/trunk/readme.txt
r3452434 r3475951 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.4 7 Stable tag: 1. 7.47 Stable tag: 1.8.0 8 8 License: GPLv3 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 26 26 * **WalkScore Integration** - Walkability scoring for properties 27 27 * **Member-Based Categorization** - Automatic categorization based on agent membership 28 * **Geographic Filtering** - Restrict listings by postal code prefix (FSA) or city name with autocomplete 28 29 * **Sold Listing Management** - Automatically updates existing listings to sold status with title prefix and tags 29 30 * **WP-CLI Support** - Full command-line interface for server management … … 44 45 * **Direct MLS Import** - Import specific listings via WP-CLI 45 46 * **API Diagnostics** - Raw API response analysis for troubleshooting 47 * **Geographic Filtering** - Filter by postal code prefix (FSA) or city name, mutually exclusive via admin dropdown 48 * **Listing Preview** - Preview API results via WP-CLI without creating posts 46 49 * **Sync Mode Management** - Control over incremental vs age-based synchronization 47 50 * **Security Focused** - All input sanitized, output escaped, encrypted credential storage … … 132 135 133 136 == Changelog == 137 138 = 1.8.0 = 139 * **Geographic Region Filtering**: Restrict listings by postal code prefix (FSA) or city name 140 * **Postal Code Prefix Filter**: Uses OData `startswith(PostalCode, 'M5V')` for precise area targeting with FSA validation 141 * **City Name Filter**: Uses `City eq 'CityName'` with autocomplete powered by AMPRE Lookup API 142 * **Mutually Exclusive Filters**: Admin dropdown selects filter type (None, Postal Prefix, City) -- one at a time 143 * **City Autocomplete**: jQuery UI Autocomplete with cached canonical city names from AMPRE Lookup API (30-day cache) 144 * **Server-Side Validation**: Cities validated against canonical list on save; invalid entries stripped with admin warning 145 * **Client-Side Validation**: Real-time form validation against loaded city list before submission 146 * **Listing Preview Command**: New `wp shift8-treb preview` shows API results without creating posts -- includes price stats, city/agent breakdown 147 * **CLI Geographic Overrides**: `--postal-prefix=M5V,M6H` and `--city="Toronto W08,Mississauga"` flags for sync, preview, and analyze commands 148 * **CLI Code Consolidation**: Extracted shared `build_settings_overrides()` helper -- sync, preview, and analyze all parse CLI flags identically 149 * **Shared Fetch Method**: Added `Sync_Service::fetch_listings()` so preview and analyze use identical query construction as real sync 150 * **Analyze Command Upgrade**: Now uses Sync_Service instead of manual token handling -- supports all geographic filters 151 * **AMPRE API Compatibility**: Removed `tolower()` from city queries (AMPRE returns HTTP 501 for OData string functions) 152 * **Comprehensive Test Coverage**: 182 tests passing with 557 assertions, including 4 new fetch_listings tests 153 * Settings sanitization consolidated into single authoritative location, eliminating duplicate registration 134 154 135 155 = 1.7.4 = … … 300 320 == Upgrade Notice == 301 321 322 = 1.8.0 = 323 New feature: Geographic region filtering allows restricting listings by postal code prefix or city name. New WP-CLI preview command for API query analysis. CLI commands consolidated to eliminate code duplication. 324 302 325 = 1.6.2 = 303 326 Critical update: Resolves three major production issues - duplicate images, geocoding failures, and duplicate posts. Enhanced reliability with comprehensive test coverage and zero-tolerance testing approach. Includes intelligent address cleaning and multi-layered duplicate detection. -
shift8-real-estate-listings-for-treb/trunk/shift8-treb.php
r3452434 r3475951 4 4 * Plugin URI: https://github.com/stardothosting/shift8-treb 5 5 * Description: Integrates Toronto Real Estate Board (TREB) listings via AMPRE API, automatically importing property listings into WordPress. Replaces the Python script with native WordPress functionality. 6 * Version: 1. 7.46 * Version: 1.8.0 7 7 * Author: Shift8 Web 8 8 * Author URI: https://shift8web.ca … … 22 22 23 23 // Plugin constants 24 define('SHIFT8_TREB_VERSION', '1. 7.4');24 define('SHIFT8_TREB_VERSION', '1.8.0'); 25 25 define('SHIFT8_TREB_PLUGIN_FILE', __FILE__); 26 26 define('SHIFT8_TREB_PLUGIN_DIR', plugin_dir_path(__FILE__)); … … 262 262 'listing_age_days' => '30', 263 263 'listing_template' => 'Property Details:\n\nAddress: %ADDRESS%\nPrice: %PRICE%\nMLS: %MLS%\nBedrooms: %BEDROOMS%\nBathrooms: %BATHROOMS%\nSquare Feet: %SQFT%\n\nDescription:\n%DESCRIPTION%', 264 'post_excerpt_template' => '%ADDRESS%\n%LISTPRICE%\nMLS : %MLSNUMBER%' 264 'post_excerpt_template' => '%ADDRESS%\n%LISTPRICE%\nMLS : %MLSNUMBER%', 265 'geographic_filter_type' => '', 266 'postal_code_prefixes' => '', 267 'city_filter' => '' 265 268 ) 266 269 ) … … 355 358 $sanitized['post_excerpt_template'] = wp_kses_post($input['post_excerpt_template']); 356 359 } 357 360 361 // Sanitize geographic filter settings 362 $allowed_geo_types = array('', 'postal_prefix', 'city'); 363 if (isset($input['geographic_filter_type']) && in_array($input['geographic_filter_type'], $allowed_geo_types, true)) { 364 $sanitized['geographic_filter_type'] = $input['geographic_filter_type']; 365 } else { 366 $sanitized['geographic_filter_type'] = ''; 367 } 368 369 if (isset($input['postal_code_prefixes'])) { 370 $raw_prefixes = sanitize_text_field(trim($input['postal_code_prefixes'])); 371 if (!empty($raw_prefixes)) { 372 $prefixes = array_map('trim', explode(',', $raw_prefixes)); 373 $valid_prefixes = array(); 374 foreach ($prefixes as $prefix) { 375 $prefix = strtoupper($prefix); 376 if (preg_match('/^[A-Z][0-9][A-Z]$/', $prefix)) { 377 $valid_prefixes[] = $prefix; 378 } 379 } 380 $sanitized['postal_code_prefixes'] = implode(',', $valid_prefixes); 381 } else { 382 $sanitized['postal_code_prefixes'] = ''; 383 } 384 } 385 386 if (isset($input['city_filter'])) { 387 $raw_cities = sanitize_text_field(trim($input['city_filter'])); 388 if (!empty($raw_cities)) { 389 $cities = array_filter(array_map('trim', explode(',', $raw_cities))); 390 $cached_cities = get_transient('shift8_treb_city_lookups'); 391 392 if (is_array($cached_cities) && !empty($cached_cities)) { 393 $lowered_cache = array_map('strtolower', $cached_cities); 394 $valid_cities = array(); 395 $invalid_cities = array(); 396 foreach ($cities as $city) { 397 $idx = array_search(strtolower($city), $lowered_cache, true); 398 if ($idx !== false) { 399 $valid_cities[] = $cached_cities[$idx]; 400 } else { 401 $invalid_cities[] = $city; 402 } 403 } 404 if (!empty($invalid_cities)) { 405 add_settings_error( 406 'shift8_treb_settings', 407 'invalid_cities', 408 sprintf( 409 esc_html__('Unrecognised city name(s) removed: %s. Use the autocomplete suggestions.', 'shift8-real-estate-listings-for-treb'), 410 esc_html(implode(', ', $invalid_cities)) 411 ), 412 'warning' 413 ); 414 } 415 $sanitized['city_filter'] = implode(', ', $valid_cities); 416 } else { 417 $sanitized['city_filter'] = implode(', ', $cities); 418 } 419 } else { 420 $sanitized['city_filter'] = ''; 421 } 422 } 423 358 424 // Add success message 359 425 add_settings_error( … … 614 680 'google_maps_api_key' => '', 615 681 'listing_status_filter' => 'Active', 616 'city_filter' => 'Toronto', 617 'max_listings_per_sync' => 100 682 'city_filter' => '', 683 'max_listings_per_sync' => 100, 684 'geographic_filter_type' => '', 685 'postal_code_prefixes' => '' 618 686 )); 619 687 }
Note: See TracChangeset
for help on using the changeset viewer.