| 1 |
<?php |
| 2 |
if ( ! defined( 'ABSPATH' ) ) { |
| 3 |
exit; // Exit if accessed directly |
| 4 |
} |
| 5 |
|
| 6 |
/* |
| 7 |
* --------------------------------------------------------------------------- |
| 8 |
* FILE ROLE: admin-side controller for the whole plugin. |
| 9 |
* --------------------------------------------------------------------------- |
| 10 |
* This ~3,470-line class (Mlsimport_Admin) is the admin monolith referenced in |
| 11 |
* CLAUDE.md. Its hooks are registered by the core Mlsimport class via the |
| 12 |
* Loader. Broadly it owns: |
| 13 |
* - Asset enqueue for wp-admin (styles, field-selector JS, standalone React |
| 14 |
* settings app, deactivation survey, searchable selects). |
| 15 |
* - Admin menu + settings pages (main options page, Import History, and the |
| 16 |
* standalone theme_id 990 "Design Settings" React page). |
| 17 |
* - Settings registration + validation callbacks (register_setting) for the |
| 18 |
* several mlsimport_admin_* option groups. |
| 19 |
* - The mlsimport_item (Import Task) metaboxes: rendering the import-parameter |
| 20 |
* form and saving its post meta. |
| 21 |
* - The MLS connection test and SaaS token/metadata retrieval. |
| 22 |
* - Building the RESO listing-request arguments from an Import Task's meta. |
| 23 |
* - The import engine: manual (AJAX), hourly cron per item, and the |
| 24 |
* background/Action Scheduler batch processors. |
| 25 |
* - The daily reconciliation sweep (delete/keep local listings vs. the MLS |
| 26 |
* feed) with a truncated-feed safety guard. |
| 27 |
* - The plugin-deactivation exit survey (~18 AJAX handlers total live here). |
| 28 |
* NOTE: the enviroment/ directory name is intentionally misspelled plugin-wide; |
| 29 |
* "env_data" is the active theme adapter, "mls_env_data" the MLS provider one. |
| 30 |
* --------------------------------------------------------------------------- |
| 31 |
*/ |
| 32 |
|
| 33 |
|
| 34 |
/** |
| 35 |
* The admin-specific functionality of the plugin. |
| 36 |
* |
| 37 |
* @link http://mlsimport.com/ |
| 38 |
* @since 1.0.0 |
| 39 |
* |
| 40 |
* @package Mlsimport |
| 41 |
* @subpackage Mlsimport/admin |
| 42 |
*/ |
| 43 |
|
| 44 |
|
| 45 |
/** |
| 46 |
* The admin-specific functionality of the plugin. |
| 47 |
* |
| 48 |
* Defines the plugin name, version, and two examples hooks for how to |
| 49 |
* enqueue the admin-specific stylesheet and JavaScript. |
| 50 |
* |
| 51 |
* @package Mlsimport |
| 52 |
* @subpackage Mlsimport/admin |
| 53 |
* @author MlsImport <office@mlsimport.com> |
| 54 |
*/ |
| 55 |
class Mlsimport_Admin { |
| 56 |
|
| 57 |
/** |
| 58 |
* The ID of this plugin. |
| 59 |
* |
| 60 |
* @since 1.0.0 |
| 61 |
* @access private |
| 62 |
* @var string $plugin_name The ID of this plugin. |
| 63 |
*/ |
| 64 |
private $plugin_name; |
| 65 |
|
| 66 |
/** |
| 67 |
* The version of this plugin. |
| 68 |
* |
| 69 |
* @since 1.0.0 |
| 70 |
* @access private |
| 71 |
* @var string $version The current version of this plugin. |
| 72 |
*/ |
| 73 |
private $version; |
| 74 |
// Back-reference to the core Mlsimport instance (set externally). |
| 75 |
public $main; |
| 76 |
// ThemeImport API client instance (OAuth + all SaaS API calls). |
| 77 |
public $theme_importer; |
| 78 |
// Active theme adapter object (e.g. ResidenceClass); stdClass when no theme. |
| 79 |
public $env_data; |
| 80 |
// Active MLS provider adapter object; stdClass when none configured. |
| 81 |
public $mls_env_data; |
| 82 |
// Reserved handle for a batch/queue processor (declared, assigned elsewhere). |
| 83 |
protected $process_all; |
| 84 |
// Field-import definition array (populated per request where used). |
| 85 |
public $field_import; |
| 86 |
// Map of supported theme_id => human name (990 standalone, 991-994 themes). |
| 87 |
public $themes; |
| 88 |
/** |
| 89 |
* Initialize the class and set its properties. |
| 90 |
* |
| 91 |
* @since 1.0.0 |
| 92 |
* @param string $plugin_name The name of this plugin. |
| 93 |
* @param string $version The version of this plugin. |
| 94 |
*/ |
| 95 |
public function __construct( $plugin_name, $version ) { |
| 96 |
|
| 97 |
// Store the plugin slug (used as the option-key prefix) and version. |
| 98 |
$this->plugin_name = $plugin_name; |
| 99 |
$this->version = $version; |
| 100 |
|
| 101 |
// RESO fields that are enum/lookup-driven and shown on the Import Task form. |
| 102 |
$this->field_import = array( |
| 103 |
'City', |
| 104 |
'CountyOrParish', |
| 105 |
'MlsStatus', |
| 106 |
'PropertySubType', |
| 107 |
'PropertyType', |
| 108 |
'StandardStatus', |
| 109 |
'InternetEntireListingDisplayYN', |
| 110 |
'InternetAddressDisplayYN', |
| 111 |
); |
| 112 |
|
| 113 |
// theme_id => adapter name. The class is derived by stripping "Wp" and |
| 114 |
// appending "Class" (e.g. WpResidence -> ResidenceClass). |
| 115 |
$this->themes = array( |
| 116 |
990 => 'Standalone', |
| 117 |
991 => 'WpResidence', |
| 118 |
992 => 'Houzez', |
| 119 |
993 => 'RealHomes', |
| 120 |
994 => 'Wpestate', |
| 121 |
); |
| 122 |
} |
| 123 |
/** |
| 124 |
* Wire up the theme and MLS provider adapter objects for this request. |
| 125 |
* |
| 126 |
* Reads the configured theme_id, resolves the theme adapter class name, and |
| 127 |
* instantiates both the theme adapter (env_data) and the MLS provider adapter |
| 128 |
* (mls_env_data); missing config falls back to an empty stdClass. |
| 129 |
* |
| 130 |
* @param string $plugin_name Plugin slug passed to ThemeImport. |
| 131 |
* @param string $mls_enviroment MLS provider adapter base name (e.g. BridgeReso). |
| 132 |
* @param string $theme_enviroment Ignored on input; recomputed from the saved theme_id. |
| 133 |
* @since 1.0.0 |
| 134 |
*/ |
| 135 |
public function admin_setup( $plugin_name, $mls_enviroment, $theme_enviroment ) { |
| 136 |
|
| 137 |
// Load saved options and resolve the configured theme id (0 when unset). |
| 138 |
$options = get_option( $this->plugin_name . '_admin_options' ); |
| 139 |
$theme_id = 0; |
| 140 |
if ( isset( $options['mlsimport_theme_used'] ) ) { |
| 141 |
$theme_id = intval( $options['mlsimport_theme_used'] ); |
| 142 |
} |
| 143 |
$themes = $this->themes; |
| 144 |
|
| 145 |
// Map the theme id to its adapter name; blank when the id is unknown. |
| 146 |
$theme_enviroment = ''; |
| 147 |
if ( isset( $themes[ $theme_id ] ) ) { |
| 148 |
$theme_enviroment = $themes[ $theme_id ]; |
| 149 |
} |
| 150 |
|
| 151 |
// Always create the API client. |
| 152 |
$this->theme_importer = new ThemeImport( $plugin_name ); |
| 153 |
|
| 154 |
$options_api = get_option( $this->plugin_name . '_admin_options' ); |
| 155 |
|
| 156 |
// Instantiate the theme adapter (WpResidence -> ResidenceClass), else stub. |
| 157 |
if ( '' !== $theme_enviroment ) { |
| 158 |
$classname = str_replace('Wp','',$theme_enviroment ). 'Class'; |
| 159 |
$this->env_data = new $classname(); |
| 160 |
} else { |
| 161 |
$this->env_data = new stdClass(); |
| 162 |
} |
| 163 |
|
| 164 |
// Instantiate the MLS provider adapter (name + "Class"), else stub. |
| 165 |
if ( '' !== $mls_enviroment ) { |
| 166 |
$mls_classname = $mls_enviroment . 'Class'; |
| 167 |
|
| 168 |
$this->mls_env_data = new $mls_classname( $this->theme_importer ); |
| 169 |
} else { |
| 170 |
$this->mls_env_data = new stdClass(); |
| 171 |
} |
| 172 |
} |
| 173 |
|
| 174 |
/** |
| 175 |
* Register the stylesheets for the admin area. |
| 176 |
* |
| 177 |
* Enqueues the main admin CSS plus the onboarding and field-selector styles. |
| 178 |
* |
| 179 |
* @since 1.0.0 |
| 180 |
*/ |
| 181 |
public function enqueue_styles() { |
| 182 |
// Main admin stylesheet. |
| 183 |
wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/mlsimport-admin.css', array(), MLSIMPORT_VERSION, 'all' ); |
| 184 |
// Onboarding wizard styles. |
| 185 |
wp_enqueue_style( 'mlsimport-onboarding', plugin_dir_url( __FILE__ ) . 'css/mlsimport-onboarding.css', array(), MLSIMPORT_VERSION, 'all' ); |
| 186 |
// Drag-and-drop field selector styles. |
| 187 |
wp_enqueue_style( 'mlsimport-field-selector', plugin_dir_url( __FILE__ ) . 'css/mlsimport-field-selector.css', array(), MLSIMPORT_VERSION, 'all' ); |
| 188 |
} |
| 189 |
|
| 190 |
|
| 191 |
|
| 192 |
|
| 193 |
/** |
| 194 |
* Register the JavaScript for the admin area. |
| 195 |
* |
| 196 |
* Enqueues the core admin script, the field-selector + progressive-save |
| 197 |
* scripts, and conditionally (by page/hook) injects inline bootstraps for |
| 198 |
* metadata fetch and MLS autocomplete, plus the searchable-select and |
| 199 |
* deactivation-survey scripts on their respective screens. |
| 200 |
* |
| 201 |
* @param string $hook_suffix Current admin page hook suffix. |
| 202 |
* @since 1.0.0 |
| 203 |
*/ |
| 204 |
public function enqueue_scripts($hook_suffix) { |
| 205 |
// jQuery UI autocomplete backs the MLS-name search box. |
| 206 |
wp_enqueue_script( 'jquery-ui-autocomplete' ); |
| 207 |
// Pull the cached MLS list (used later for the autocomplete bootstrap). |
| 208 |
$mls_import_list = mlsimport_saas_request_list(); |
| 209 |
// Core admin script + AJAX endpoint. |
| 210 |
wp_enqueue_script( 'mlsimport-admin', plugin_dir_url( __FILE__ ) . 'js/mlsimport-admin.js', array( 'jquery' ), $this->version, true ); |
| 211 |
wp_localize_script( |
| 212 |
'mlsimport-admin', |
| 213 |
'mlsimport_vars', |
| 214 |
array( |
| 215 |
'ajax_url' => admin_url( 'admin-ajax.php' ) |
| 216 |
) |
| 217 |
); |
| 218 |
|
| 219 |
// Field-selector UI depends on jQuery UI sortable + tooltip. |
| 220 |
wp_enqueue_script( 'mlsimport-field-selector', plugin_dir_url( ( __FILE__ ) ) . 'js/mlsimport-field-selector.js', array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-tooltip' ), '1.0.0', true ); |
| 221 |
|
| 222 |
// Pass AJAX parameters to script |
| 223 |
wp_localize_script( 'mlsimport-field-selector', 'mlsimport_params', array( |
| 224 |
'ajax_url' => admin_url( 'admin-ajax.php' ), |
| 225 |
'nonce' => wp_create_nonce( 'mlsimport_field_selector_nonce' ) |
| 226 |
)); |
| 227 |
|
| 228 |
|
| 229 |
// Progressive save batches field-selector changes; depends on it. |
| 230 |
wp_enqueue_script( 'mlsimport-progressive-save', plugin_dir_url( ( __FILE__ ) ) . 'js/progressive-save.js', array('mlsimport-field-selector' ), '1.0.0', true ); |
| 231 |
|
| 232 |
|
| 233 |
|
| 234 |
// On the settings page Field Options tab: if metadata was never fetched, |
| 235 |
// auto-trigger the metadata pull on DOM ready. |
| 236 |
if ('toplevel_page_mlsimport_plugin_options' === $hook_suffix && |
| 237 |
isset($_GET['page']) && $_GET['page'] === 'mlsimport_plugin_options' && |
| 238 |
isset($_GET['tab']) && $_GET['tab'] === 'field_options') { |
| 239 |
$mlsimport_mls_metadata_populated = get_option( 'mlsimport_mls_metadata_populated', '' ); |
| 240 |
if ( 'yes' !== $mlsimport_mls_metadata_populated ) { |
| 241 |
$inline_script = 'jQuery(document).ready(function($){ mlsimport_saas_get_metadata(); });'; |
| 242 |
wp_add_inline_script('mlsimport-admin', $inline_script); |
| 243 |
} |
| 244 |
} |
| 245 |
|
| 246 |
// Same auto-metadata bootstrap, but for the onboarding wizard screen. |
| 247 |
if ( |
| 248 |
'admin_page_mlsimport-onboarding' === $hook_suffix && |
| 249 |
isset($_GET['page']) && $_GET['page'] === 'mlsimport-onboarding' |
| 250 |
) { |
| 251 |
$mlsimport_mls_metadata_populated = get_option('mlsimport_mls_metadata_populated', ''); |
| 252 |
if ('yes' !== $mlsimport_mls_metadata_populated) { |
| 253 |
$inline_script = 'jQuery(document).ready(function($){ mlsimport_saas_get_metadata(); });'; |
| 254 |
wp_add_inline_script('mlsimport-admin', $inline_script); |
| 255 |
} |
| 256 |
} |
| 257 |
|
| 258 |
|
| 259 |
|
| 260 |
|
| 261 |
// On the settings Display Options tab (or the page with no tab), seed the |
| 262 |
// MLS-name autocomplete with the fetched list when it is not an array. |
| 263 |
if ('toplevel_page_mlsimport_plugin_options' === $hook_suffix && |
| 264 |
( isset($_GET['page']) && $_GET['page'] === 'mlsimport_plugin_options' && isset($_GET['tab']) && $_GET['tab'] === 'display_options') || |
| 265 |
(isset($_GET['page']) && $_GET['page'] === 'mlsimport_plugin_options' && !isset($_GET['tab']) ) ) { |
| 266 |
|
| 267 |
// Re-fetch the MLS list and, when it is a raw string payload, |
| 268 |
// hand it to the JS autocomplete initializer. |
| 269 |
$mls_import_list = mlsimport_saas_request_list(); |
| 270 |
if(!is_array($mls_import_list)){ |
| 271 |
$inline_script = 'jQuery(document).ready(function($){ var autofill='.wp_kses_post($mls_import_list).';mlsimport_autocomplte_mls_selection(autofill); });'; |
| 272 |
wp_add_inline_script('mlsimport-admin', $inline_script); |
| 273 |
} |
| 274 |
} |
| 275 |
|
| 276 |
// Searchable City/County multi-select — only on the Import Task edit screen. |
| 277 |
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; |
| 278 |
$post_type = $screen ? (string) $screen->post_type : ''; |
| 279 |
if ( $this->mlsimport_is_import_task_edit_screen( (string) $hook_suffix, $post_type ) ) { |
| 280 |
wp_enqueue_script( 'mlsimport-searchable-select', plugin_dir_url( __FILE__ ) . 'js/mlsimport-searchable-select.js', array(), MLSIMPORT_VERSION, true ); |
| 281 |
} |
| 282 |
|
| 283 |
// Deactivation exit survey — only needed on the Plugins screen. |
| 284 |
if ( 'plugins.php' === $hook_suffix ) { |
| 285 |
// Enqueue the survey modal script and hand it the nonce, options and i18n. |
| 286 |
wp_enqueue_script( 'mlsimport-deactivation-survey', plugin_dir_url( __FILE__ ) . 'js/mlsimport-deactivation-survey.js', array( 'jquery' ), MLSIMPORT_VERSION, true ); |
| 287 |
wp_localize_script( 'mlsimport-deactivation-survey', 'mlsimport_deact_survey', array( |
| 288 |
'ajax_url' => admin_url( 'admin-ajax.php' ), |
| 289 |
'nonce' => wp_create_nonce( 'mlsimport_exit_survey' ), |
| 290 |
'plugin_basename' => plugin_basename( MLSIMPORT_PLUGIN_PATH . 'mlsimport.php' ), |
| 291 |
'options' => $this->get_exit_survey_options(), |
| 292 |
'i18n' => array( |
| 293 |
'title' => esc_html__( 'Before you go — quick question', 'mlsimport' ), |
| 294 |
'intro' => esc_html__( 'Why are you deactivating MLS Import? Your answer helps us improve.', 'mlsimport' ), |
| 295 |
'other_placeholder' => esc_html__( 'Tell us more (optional)', 'mlsimport' ), |
| 296 |
'submit' => esc_html__( 'Submit & Deactivate', 'mlsimport' ), |
| 297 |
'skip' => esc_html__( 'Skip & Deactivate', 'mlsimport' ), |
| 298 |
), |
| 299 |
) ); |
| 300 |
} |
| 301 |
|
| 302 |
} |
| 303 |
|
| 304 |
|
| 305 |
|
| 306 |
|
| 307 |
|
| 308 |
/** |
| 309 |
* Register the administration menu for this plugin into the WordPress Dashboard menu. |
| 310 |
* |
| 311 |
* Adds the top-level "MLS Import Settings" page and the "Import History" |
| 312 |
* submenu; in standalone mode it also adds the separate "Design Settings" |
| 313 |
* React page and enqueues its bundle only on that hook. |
| 314 |
* |
| 315 |
* @since 1.0.0 |
| 316 |
*/ |
| 317 |
public function add_plugin_admin_menu() { |
| 318 |
// Top-level settings menu (capability: administrator). |
| 319 |
add_menu_page( |
| 320 |
esc_html__( 'MLS Import Settings', 'mlsimport'), |
| 321 |
esc_html__( 'MLS Import Settings', 'mlsimport' ), |
| 322 |
'administrator', |
| 323 |
'mlsimport_plugin_options', |
| 324 |
array( $this, 'display_plugin_setup_page' ), |
| 325 |
MLSIMPORT_PLUGIN_URL . '/img/mlsimport_menu.png', |
| 326 |
// Fractional slot right after Import Tasks (21). Fractions are only |
| 327 |
// honoured by add_menu_page, so the two settings pages take 21.1/21.2 |
| 328 |
// and leave integer slots 22/23 for the Properties/Agents CPTs — keeping |
| 329 |
// all MLSImport menus grouped above core Comments (25). |
| 330 |
21.1 |
| 331 |
); |
| 332 |
|
| 333 |
// Import History submenu under the settings menu. |
| 334 |
add_submenu_page( |
| 335 |
'mlsimport_plugin_options', |
| 336 |
esc_html__( 'Import History', 'mlsimport' ), |
| 337 |
esc_html__( 'Import History', 'mlsimport' ), |
| 338 |
'administrator', |
| 339 |
'mlsimport_history', |
| 340 |
array( $this, 'display_history_page' ) |
| 341 |
); |
| 342 |
|
| 343 |
// Standalone (theme_id 990) front-end design. Its own top-level menu, |
| 344 |
// deliberately separate from MLS import settings because it controls |
| 345 |
// the public-facing visuals. React app; see admin/settings-app/. |
| 346 |
if ( function_exists( 'mlsimport_is_standalone_mode' ) && mlsimport_is_standalone_mode() ) { |
| 347 |
// Separate top-level menu for the standalone front-end design app. |
| 348 |
$standalone_hook = add_menu_page( |
| 349 |
esc_html__( 'MLS Import Design Settings', 'mlsimport' ), |
| 350 |
esc_html__( 'MLS Import Design Settings', 'mlsimport' ), |
| 351 |
'manage_options', |
| 352 |
'mlsimport_standalone_settings', |
| 353 |
array( $this, 'display_standalone_settings_page' ), |
| 354 |
MLSIMPORT_PLUGIN_URL . '/img/mlsimport_menu.png', |
| 355 |
21.2 |
| 356 |
); |
| 357 |
|
| 358 |
// Load the React bundle only when this exact page hook is rendering. |
| 359 |
add_action( |
| 360 |
'admin_enqueue_scripts', |
| 361 |
function ( $current_hook ) use ( $standalone_hook ) { |
| 362 |
if ( $current_hook === $standalone_hook ) { |
| 363 |
$this->enqueue_standalone_settings_app(); |
| 364 |
} |
| 365 |
} |
| 366 |
); |
| 367 |
} |
| 368 |
} |
| 369 |
|
| 370 |
/** |
| 371 |
* Render the Standalone Design page — just the React mount point. All |
| 372 |
* fields, save and validation live in the app (admin/settings-app/) and the |
| 373 |
* settings REST endpoint. |
| 374 |
* |
| 375 |
* @return void |
| 376 |
*/ |
| 377 |
public function display_standalone_settings_page() { |
| 378 |
echo '<div class="wrap">'; |
| 379 |
echo '<h1>' . esc_html__( 'MLS Import Design Settings', 'mlsimport' ) . '</h1>'; |
| 380 |
echo '<div id="mlsimport-standalone-app"></div>'; |
| 381 |
echo '</div>'; |
| 382 |
} |
| 383 |
|
| 384 |
/** |
| 385 |
* Enqueue the compiled Standalone Design React bundle and its WP component |
| 386 |
* styles. Dependencies + cache-busting version come from the build's |
| 387 |
* generated index.asset.php. |
| 388 |
* |
| 389 |
* @return void |
| 390 |
*/ |
| 391 |
private function enqueue_standalone_settings_app() { |
| 392 |
// The build emits index.asset.php with dependencies + a content hash; |
| 393 |
// bail quietly if the app was never built. |
| 394 |
$asset_file = MLSIMPORT_PLUGIN_PATH . 'admin/settings-app/build/index.asset.php'; |
| 395 |
if ( ! file_exists( $asset_file ) ) { |
| 396 |
return; |
| 397 |
} |
| 398 |
$asset = require $asset_file; |
| 399 |
|
| 400 |
// The MLS-logo control opens the native WordPress media modal (wp.media). |
| 401 |
wp_enqueue_media(); |
| 402 |
|
| 403 |
// wp-color-picker (Iris) powers the native colour control in the React app; |
| 404 |
// it pulls in jQuery + iris, so the window.jQuery global is available. |
| 405 |
wp_enqueue_script( |
| 406 |
'mlsimport-standalone-settings', |
| 407 |
MLSIMPORT_PLUGIN_URL . 'admin/settings-app/build/index.js', |
| 408 |
array_merge( $asset['dependencies'], array( 'wp-color-picker' ) ), |
| 409 |
$asset['version'], |
| 410 |
true |
| 411 |
); |
| 412 |
// Enable JS translation loading for the app's strings. |
| 413 |
wp_set_script_translations( 'mlsimport-standalone-settings', 'mlsimport' ); |
| 414 |
|
| 415 |
// The field tree the React app renders from — tabs/sub-tabs/fields generated |
| 416 |
// from the ONE registry (mlsimport_standalone_settings_app_config). The app |
| 417 |
// reads window.mlsimportFields instead of a hard-coded list, so a field added |
| 418 |
// to the registry appears here (and, via the schema, in the Customizer) with |
| 419 |
// no JS change. |
| 420 |
if ( function_exists( 'mlsimport_standalone_settings_app_config' ) ) { |
| 421 |
wp_add_inline_script( |
| 422 |
'mlsimport-standalone-settings', |
| 423 |
'window.mlsimportFields = ' . wp_json_encode( mlsimport_standalone_settings_app_config() ) . ';', |
| 424 |
'before' |
| 425 |
); |
| 426 |
} |
| 427 |
|
| 428 |
// Feed the "Arrange Sections" control its catalog (slug + label) from the |
| 429 |
// property section registry, so the list matches what the front end renders. |
| 430 |
if ( function_exists( 'mlsimport_standalone_section_catalog' ) ) { |
| 431 |
$catalog = array(); |
| 432 |
foreach ( mlsimport_standalone_section_catalog() as $slug => $label ) { |
| 433 |
$catalog[] = array( 'slug' => $slug, 'label' => $label ); |
| 434 |
} |
| 435 |
wp_add_inline_script( |
| 436 |
'mlsimport-standalone-settings', |
| 437 |
'window.mlsimportSections = ' . wp_json_encode( $catalog ) . ';', |
| 438 |
'before' |
| 439 |
); |
| 440 |
} |
| 441 |
|
| 442 |
// The Overview "Arrange Fields" control reads the Overview tile catalog — the |
| 443 |
// stat tiles the Overview section can draw (Updated, MLS #, Bedrooms, …). |
| 444 |
if ( function_exists( 'mlsimport_standalone_overview_fields_catalog' ) ) { |
| 445 |
$overview_fields = array(); |
| 446 |
foreach ( mlsimport_standalone_overview_fields_catalog() as $slug => $label ) { |
| 447 |
$overview_fields[] = array( 'slug' => $slug, 'label' => $label ); |
| 448 |
} |
| 449 |
wp_add_inline_script( |
| 450 |
'mlsimport-standalone-settings', |
| 451 |
'window.mlsimportOverviewFields = ' . wp_json_encode( $overview_fields ) . ';', |
| 452 |
'before' |
| 453 |
); |
| 454 |
} |
| 455 |
|
| 456 |
// The agent "Arrange Sections" control reads its own catalog (the agent page's |
| 457 |
// reorderable content-column sections), kept separate from the property catalog. |
| 458 |
if ( function_exists( 'mlsimport_standalone_agent_section_catalog' ) ) { |
| 459 |
$agent_catalog = array(); |
| 460 |
foreach ( mlsimport_standalone_agent_section_catalog() as $slug => $label ) { |
| 461 |
$agent_catalog[] = array( 'slug' => $slug, 'label' => $label ); |
| 462 |
} |
| 463 |
wp_add_inline_script( |
| 464 |
'mlsimport-standalone-settings', |
| 465 |
'window.mlsimportAgentSections = ' . wp_json_encode( $agent_catalog ) . ';', |
| 466 |
'before' |
| 467 |
); |
| 468 |
} |
| 469 |
|
| 470 |
// The archive "Taxonomy filters" control reads its own catalog (the search |
| 471 |
// form's toggleable filter fields), so the on/off toggle list matches what |
| 472 |
// the taxonomy/CPT archive search bar can render. |
| 473 |
if ( function_exists( 'mlsimport_standalone_archive_filters_catalog' ) ) { |
| 474 |
$archive_filters = array(); |
| 475 |
foreach ( mlsimport_standalone_archive_filters_catalog() as $slug => $label ) { |
| 476 |
$archive_filters[] = array( 'slug' => $slug, 'label' => $label ); |
| 477 |
} |
| 478 |
wp_add_inline_script( |
| 479 |
'mlsimport-standalone-settings', |
| 480 |
'window.mlsimportArchiveFilters = ' . wp_json_encode( $archive_filters ) . ';', |
| 481 |
'before' |
| 482 |
); |
| 483 |
} |
| 484 |
|
| 485 |
// The saved MLS logo's preview URL, so the media control can show the |
| 486 |
// current image before the user opens the picker. |
| 487 |
if ( function_exists( 'mlsimport_standalone_mls_logo_url' ) ) { |
| 488 |
wp_add_inline_script( |
| 489 |
'mlsimport-standalone-settings', |
| 490 |
'window.mlsimportLogoUrl = ' . wp_json_encode( mlsimport_standalone_mls_logo_url() ) . ';', |
| 491 |
'before' |
| 492 |
); |
| 493 |
} |
| 494 |
|
| 495 |
// Component + color-picker styles the React controls rely on, then the |
| 496 |
// app's own stylesheet. Cache-bust by file mtime so edits to the CSS are |
| 497 |
// picked up immediately — the plugin version (MLSIMPORT_VERSION) doesn't |
| 498 |
// change between design tweaks, so keying the ?ver on it left browsers |
| 499 |
// serving a stale cached copy under the same URL. |
| 500 |
$standalone_css_path = MLSIMPORT_PLUGIN_PATH . 'admin/css/mlsimport-standalone-settings.css'; |
| 501 |
$standalone_css_ver = file_exists( $standalone_css_path ) |
| 502 |
? (string) filemtime( $standalone_css_path ) |
| 503 |
: ( defined( 'MLSIMPORT_VERSION' ) ? MLSIMPORT_VERSION : false ); |
| 504 |
wp_enqueue_style( 'wp-components' ); |
| 505 |
wp_enqueue_style( 'wp-color-picker' ); |
| 506 |
wp_enqueue_style( |
| 507 |
'mlsimport-standalone-settings', |
| 508 |
MLSIMPORT_PLUGIN_URL . 'admin/css/mlsimport-standalone-settings.css', |
| 509 |
array( 'wp-components' ), |
| 510 |
$standalone_css_ver |
| 511 |
); |
| 512 |
} |
| 513 |
|
| 514 |
/** |
| 515 |
* Renders the Import History admin page. |
| 516 |
* |
| 517 |
* @return void |
| 518 |
*/ |
| 519 |
public function display_history_page() { |
| 520 |
// Delegates the whole page to the history partial template. |
| 521 |
include_once plugin_dir_path( __FILE__ ) . 'partials/mlsimport-history.php'; |
| 522 |
} |
| 523 |
|
| 524 |
|
| 525 |
|
| 526 |
|
| 527 |
|
| 528 |
|
| 529 |
|
| 530 |
|
| 531 |
/** |
| 532 |
* Add a "Settings" action link to this plugin's row on the Plugins page. |
| 533 |
* |
| 534 |
* @param array $links Existing plugin action links. |
| 535 |
* @return array Links with the Settings link prepended. |
| 536 |
* @since 1.0.0 |
| 537 |
*/ |
| 538 |
public function add_action_links( $links ) { |
| 539 |
// Build the Settings link and place it before the default action links. |
| 540 |
$settings_link = array( |
| 541 |
'<a href="' . admin_url( 'admin.php?page=mlsimport_plugin_options' ) . '">' . esc_html__( 'Settings', 'mlsimport') . '</a>', |
| 542 |
); |
| 543 |
return array_merge( $settings_link, $links ); |
| 544 |
} |
| 545 |
|
| 546 |
|
| 547 |
|
| 548 |
|
| 549 |
|
| 550 |
|
| 551 |
|
| 552 |
|
| 553 |
/** |
| 554 |
* Render the main settings page for this plugin. |
| 555 |
* |
| 556 |
* Loads the admin-display partial (whose filename is prefixed with the slug). |
| 557 |
* |
| 558 |
* @since 1.0.0 |
| 559 |
*/ |
| 560 |
public function display_plugin_setup_page() { |
| 561 |
// Delegates the whole page to the slug-prefixed admin-display partial. |
| 562 |
include_once 'partials/' . $this->plugin_name . '-admin-display.php'; |
| 563 |
} |
| 564 |
|
| 565 |
|
| 566 |
|
| 567 |
|
| 568 |
|
| 569 |
|
| 570 |
/** |
| 571 |
* Sanitize/whitelist the main plugin options on save (register_setting callback). |
| 572 |
* |
| 573 |
* Copies only the known keys from $input (esc_attr'd), then invalidates the |
| 574 |
* connection-test / metadata flags and cached tokens/schema so the next page |
| 575 |
* load re-tests the connection with the new credentials. |
| 576 |
* |
| 577 |
* @param array $input Raw submitted options. |
| 578 |
* @return array Whitelisted, escaped options. |
| 579 |
* @since 1.0.0 |
| 580 |
*/ |
| 581 |
public function validate_admin_options( $input ) { |
| 582 |
|
| 583 |
// Whitelist of accepted option keys (value = label/help metadata, unused |
| 584 |
// beyond documentation here); anything not listed is dropped on save. |
| 585 |
$valid = array(); |
| 586 |
$settings_list = array( |
| 587 |
'auth_username' => array( |
| 588 |
'name' => esc_html__( 'Api auth_username ', 'mlsimport' ), |
| 589 |
'details' => 'to be added', |
| 590 |
), |
| 591 |
'auth_password' => array( |
| 592 |
'name' => esc_html__( 'Api auth_password', 'mlsimport' ), |
| 593 |
'details' => 'to be added', |
| 594 |
), |
| 595 |
'client_id' => array( |
| 596 |
'name' => esc_html__( 'Api client_id', 'mlsimport' ), |
| 597 |
'details' => 'to be added', |
| 598 |
), |
| 599 |
'client_secret' => array( |
| 600 |
'name' => esc_html__( 'client_secret', 'mlsimport' ), |
| 601 |
'details' => 'to be added', |
| 602 |
), |
| 603 |
'redirect_uri' => array( |
| 604 |
'name' => esc_html__( 'redirect_uri', 'mlsimport' ), |
| 605 |
'details' => 'to be added', |
| 606 |
), |
| 607 |
'title_format' => array( |
| 608 |
'name' => esc_html__( 'title_format', 'mlsimport' ), |
| 609 |
'details' => 'to be added', |
| 610 |
), |
| 611 |
'force_rand' => array( |
| 612 |
'name' => esc_html__( 'title_format', 'mlsimport' ), |
| 613 |
'details' => 'to be added', |
| 614 |
), |
| 615 |
'mlsimport_username' => array( |
| 616 |
'name' => esc_html__( 'MLSImport.com Username (not your email)', 'mlsimport' ), |
| 617 |
'details' => 'to be added', |
| 618 |
), |
| 619 |
'mlsimport_password' => array( |
| 620 |
'name' => esc_html__( 'MLSImport.com Password', 'mlsimport' ), |
| 621 |
'details' => 'to be added', |
| 622 |
), |
| 623 |
'mlsimport_mls_name' => array( |
| 624 |
'name' => esc_html__( 'MLSImport Name', 'mlsimport' ), |
| 625 |
'details' => 'to be added', |
| 626 |
), |
| 627 |
'mlsimport_mls_token' => array( |
| 628 |
'name' => esc_html__( 'MLSImport Token', 'mlsimport' ), |
| 629 |
'details' => 'to be added', |
| 630 |
), |
| 631 |
|
| 632 |
'mlsimport_tresle_client_id' => array( |
| 633 |
'name' => esc_html__( 'MLSImport Tresle Client id', 'mlsimport' ), |
| 634 |
'details' => 'to be added', |
| 635 |
), |
| 636 |
|
| 637 |
'mlsimport_tresle_client_secret' => array( |
| 638 |
'name' => esc_html__( 'MLSImport Client Secret', 'mlsimport' ), |
| 639 |
'details' => 'to be added', |
| 640 |
), |
| 641 |
|
| 642 |
'mlsimport_connectmls_username' => array( |
| 643 |
'name' => esc_html__( 'MLSImport ConnectMLS Username', 'mlsimport' ), |
| 644 |
'details' => 'to be added', |
| 645 |
), |
| 646 |
|
| 647 |
'mlsimport_connectmls_password' => array( |
| 648 |
'name' => esc_html__( 'MLSImport ConnectMLS Password', 'mlsimport' ), |
| 649 |
'details' => 'to be added', |
| 650 |
), |
| 651 |
|
| 652 |
'mlsimport_rapattoni_client_id' => array( |
| 653 |
'name' => esc_html__( 'MLSImport Rapattoni Client id','mlsimport'), |
| 654 |
'details' => 'to be added', |
| 655 |
), |
| 656 |
|
| 657 |
'mlsimport_rapattoni_client_secret' => array( |
| 658 |
'name' => esc_html__( 'MLSImport Rapattoni Secret', 'mlsimport' ), |
| 659 |
'details' => 'to be added', |
| 660 |
), |
| 661 |
|
| 662 |
'mlsimport_rapattoni_username' => array( |
| 663 |
'name' => esc_html__( 'MLSImport Rapattoni Username', 'mlsimport' ), |
| 664 |
'details' => 'to be added', |
| 665 |
), |
| 666 |
|
| 667 |
'mlsimport_rapattoni_password' => array( |
| 668 |
'name' => esc_html__( 'MLSImport Rapattoni Password', 'mlsimport' ), |
| 669 |
'details' => 'to be added', |
| 670 |
), |
| 671 |
|
| 672 |
'mlsimport_paragon_client_id' => array( |
| 673 |
'name' => esc_html__( 'MLSImport Paragon Client id','mlsimport' ), |
| 674 |
'details' => 'to be added', |
| 675 |
), |
| 676 |
|
| 677 |
'mlsimport_paragon_client_secret' => array( |
| 678 |
'name' => esc_html__( 'MLSImport Paragon Secret', 'mlsimport' ), |
| 679 |
'details' => 'to be added', |
| 680 |
), |
| 681 |
'mlsimport_realtorca_client_id' => array( |
| 682 |
'name' => esc_html__( 'MLSImport Realtor.ca Client id','mlsimport' ), |
| 683 |
'details' => 'to be added', |
| 684 |
), |
| 685 |
|
| 686 |
'mlsimport_realtorca_client_secret' => array( |
| 687 |
'name' => esc_html__( 'MLSImport Realtor.ca Secret', 'mlsimport' ), |
| 688 |
'details' => 'to be added', |
| 689 |
), |
| 690 |
'mlsimport_brightmls_client_id' => array( |
| 691 |
'name' => esc_html__( 'MLSImport BrightMLS Client id', 'mlsimport' ), |
| 692 |
'details' => 'to be added', |
| 693 |
), |
| 694 |
|
| 695 |
'mlsimport_brightmls_client_secret' => array( |
| 696 |
'name' => esc_html__( 'MLSImport BrightMLS Secret', 'mlsimport' ), |
| 697 |
'details' => 'to be added', |
| 698 |
), |
| 699 |
|
| 700 |
'mlsimport_theme_used' => array( |
| 701 |
'name' => esc_html__( 'Your Wordpress Theme', 'mlsimport' ), |
| 702 |
'details' => 'to be added', |
| 703 |
), |
| 704 |
'mlsimport_mls_name_front' => array( |
| 705 |
'name' => '', |
| 706 |
'details' => 'to be added', |
| 707 |
), |
| 708 |
'mlsimport-disable-logs' => array( |
| 709 |
'name' => '', |
| 710 |
'details' => 'to be added', |
| 711 |
), |
| 712 |
); |
| 713 |
|
| 714 |
// Copy each whitelisted key, escaping the value; missing/empty => ''. |
| 715 |
foreach ( $settings_list as $key => $setting ) { |
| 716 |
$valid[ $key ] = ( isset( $input[ $key ] ) && ! empty( $input[ $key ] ) ) ? esc_attr( $input[ $key ] ) : ''; |
| 717 |
} |
| 718 |
|
| 719 |
// Credentials may have changed: force a fresh connection test + metadata pull. |
| 720 |
delete_option( 'mlsimport_connection_test' ); |
| 721 |
delete_option( 'mlsimport_mls_metadata_populated' ); |
| 722 |
|
| 723 |
// Reset cached encoding and drop cached token/schema transients. |
| 724 |
update_option( 'mlsimport_encoding_array', '' ); |
| 725 |
delete_transient( 'mlsimport_token_request' ); |
| 726 |
delete_transient( 'mlsimport_schema' ); |
| 727 |
delete_transient( 'mlsimport_plugin_data_schema' ); |
| 728 |
|
| 729 |
delete_transient( 'mlsimport_saas_token' ); |
| 730 |
return $valid; |
| 731 |
} |
| 732 |
|
| 733 |
|
| 734 |
|
| 735 |
|
| 736 |
|
| 737 |
/** |
| 738 |
* Sanitize the selected-fields option on save (register_setting callback). |
| 739 |
* |
| 740 |
* Iterates the MLS metadata field list and copies, for each known field, the |
| 741 |
* display/admin flags, label, post-meta and taxonomy mappings and drag order. |
| 742 |
* On a first save it seeds sensible defaults via mlsimport_seed_field_defaults. |
| 743 |
* |
| 744 |
* @param array $input Raw submitted field-select data. |
| 745 |
* @return array Whitelisted field configuration. |
| 746 |
* @since 1.0.0 |
| 747 |
*/ |
| 748 |
public function validate_admin_fields_select( $input ) { |
| 749 |
$valid = array(); |
| 750 |
|
| 751 |
// The authoritative field list comes from the fetched MLS metadata. |
| 752 |
$mlsimport_mls_metadata_mls_data = get_option( 'mlsimport_mls_metadata_mls_data', '' ); |
| 753 |
$metadata_api_call = json_decode( $mlsimport_mls_metadata_mls_data, true ); |
| 754 |
|
| 755 |
// Only accept keys that exist in the MLS metadata. |
| 756 |
foreach ( $metadata_api_call as $key => $value ) { |
| 757 |
// Front-end display flag for this field. |
| 758 |
if ( isset( $input['mls-fields'][ $key ] ) ) { |
| 759 |
$valid['mls-fields'][ $key ] = esc_attr( $input['mls-fields'][ $key ] ); |
| 760 |
} |
| 761 |
|
| 762 |
// Admin-display flag + its associated label/mapping/order settings. |
| 763 |
if ( isset( $input['mls-fields-admin'][ $key ] ) ) { |
| 764 |
$valid['mls-fields-admin'][ $key ] = esc_attr( $input['mls-fields-admin'][ $key ] ); |
| 765 |
$valid['mls-fields-label'][ $key ] = esc_attr( $input['mls-fields-label'][ $key ] ); |
| 766 |
$valid['mls-fields-map-postmeta'][ $key ] = esc_attr( $input['mls-fields-map-postmeta'][ $key ] ); |
| 767 |
$valid['mls-fields-map-taxonomy'][ $key ] = esc_attr( $input['mls-fields-map-taxonomy'][ $key ] ); |
| 768 |
$valid['field_order'][ $key ] = esc_attr( $input['field_order'][ $key ] ); |
| 769 |
} |
| 770 |
} |
| 771 |
//$valid['mls-fields-admin']['force_rand'] = esc_attr( $input['mls-fields-admin']['force_rand'] ); |
| 772 |
|
| 773 |
// First save has nothing configured yet: seed the drag order and hide the |
| 774 |
// plumbing fields (keys, timestamps, coordinates) from visitors, so the |
| 775 |
// property page reads sensibly out of the box. Never overwrites a site the |
| 776 |
// user has already arranged. |
| 777 |
if ( function_exists( 'mlsimport_seed_field_defaults' ) ) { |
| 778 |
$valid = mlsimport_seed_field_defaults( $valid ); |
| 779 |
} |
| 780 |
|
| 781 |
return $valid; |
| 782 |
} |
| 783 |
|
| 784 |
/** |
| 785 |
* Validate the MLS-sync option group on save (register_setting callback). |
| 786 |
* |
| 787 |
* Copies a fixed whitelist of sync/import parameter keys straight through. |
| 788 |
* |
| 789 |
* @param array $input Raw submitted sync settings. |
| 790 |
* @return array Whitelisted sync settings. |
| 791 |
* @since 1.0.0 |
| 792 |
*/ |
| 793 |
public function validate_admin_mls_sync( $input ) { |
| 794 |
$valid = array(); |
| 795 |
|
| 796 |
// Fixed whitelist of sync parameters (price, title, agent/user, the enum |
| 797 |
// filters and their "select-all" _check flags). |
| 798 |
$field_import = array( 'force_rand', 'min_price', 'max_price', 'title_format', 'property_agent', 'property_user', 'City', 'City_check', 'CountyOrParish', 'CountyOrParish_check', 'MlsStatus', 'MlsStatus_check', 'PropertySubType', 'PropertySubType_check', 'PropertyType', 'PropertyType_check', |
| 799 |
'StandardStatus_delete', 'StandardStatus_delete_check', 'InternetEntireListingDisplayYN', 'InternetAddressDisplayYN' ); |
| 800 |
// Pass each whitelisted key through unchanged. |
| 801 |
foreach ( $field_import as $key ) { |
| 802 |
$valid[ $key ] = $input[ $key ]; |
| 803 |
} |
| 804 |
|
| 805 |
return $valid; |
| 806 |
} |
| 807 |
|
| 808 |
|
| 809 |
/** |
| 810 |
* Validate the administrative options group on save (register_setting callback). |
| 811 |
* |
| 812 |
* Only carries the raw "import" payload through (a JSON blob of exported settings). |
| 813 |
* |
| 814 |
* @param array $input Raw submitted administrative options. |
| 815 |
* @return array Whitelisted administrative options. |
| 816 |
* @since 1.0.0 |
| 817 |
*/ |
| 818 |
public function validate_administrative_options( $input ) { |
| 819 |
|
| 820 |
$valid = array(); |
| 821 |
|
| 822 |
// Pass the single 'import' payload through. |
| 823 |
$field_import = array( 'import' ); |
| 824 |
foreach ( $field_import as $key ) { |
| 825 |
$valid[ $key ] = $input[ $key ]; |
| 826 |
} |
| 827 |
|
| 828 |
return $valid; |
| 829 |
} |
| 830 |
|
| 831 |
/** |
| 832 |
* Validate the import-options group on save (register_setting callback). |
| 833 |
* |
| 834 |
* Casts import_number to int, and when an 'import' JSON payload is present it |
| 835 |
* restores the field-select / mls-sync / import-options / transients options |
| 836 |
* from it (used by the settings import/export feature). |
| 837 |
* |
| 838 |
* @param array $input Raw submitted import options. |
| 839 |
* @return array Whitelisted import options. |
| 840 |
* @since 1.0.0 |
| 841 |
*/ |
| 842 |
public function validate_admin_import_options( $input ) { |
| 843 |
$valid = array(); |
| 844 |
|
| 845 |
// import_number is numeric-only. |
| 846 |
$field_import = array( 'import_number' ); |
| 847 |
foreach ( $field_import as $key ) { |
| 848 |
$valid[ $key ] = intval( $input[ $key ] ); |
| 849 |
} |
| 850 |
|
| 851 |
// When an exported-settings JSON blob is supplied, decode it and restore |
| 852 |
// the four related option groups from it. |
| 853 |
if ( isset( $input['import'] ) && '' !== $input['import'] ) { |
| 854 |
$decode = json_decode( $input['import'] ); |
| 855 |
update_option( 'mlsimport_admin_fields_select', $decode['mlsimport_admin_fields_select'] ); |
| 856 |
update_option( 'mlsimport_admin_mls_sync', $decode['mlsimport_admin_mls_sync'] ); |
| 857 |
update_option( 'mlsimport_admin_import_options', $decode['mlsimport_admin_import_options'] ); |
| 858 |
update_option( 'mlsimport_admin_use_transients', $decode['mlsimport_admin_use_transients'] ); |
| 859 |
} |
| 860 |
|
| 861 |
return $valid; |
| 862 |
} |
| 863 |
|
| 864 |
|
| 865 |
|
| 866 |
|
| 867 |
|
| 868 |
|
| 869 |
/** |
| 870 |
* Register all plugin option groups with the Settings API and bind each to |
| 871 |
* its validation callback. Hooked on admin_init. |
| 872 |
*/ |
| 873 |
public function options_update() { |
| 874 |
// One register_setting per option group -> validate_* sanitizer above. |
| 875 |
register_setting( $this->plugin_name . '_admin_options', $this->plugin_name . '_admin_options', array( $this, 'validate_admin_options' ) ); |
| 876 |
register_setting( $this->plugin_name . '_admin_fields_select', $this->plugin_name . '_admin_fields_select', array( $this, 'validate_admin_fields_select' ) ); |
| 877 |
register_setting( $this->plugin_name . '_admin_mls_sync', $this->plugin_name . '_admin_mls_sync', array( $this, 'validate_admin_mls_sync' ) ); |
| 878 |
register_setting( $this->plugin_name . '_admin_import_options', $this->plugin_name . '_admin_import_options', array( $this, 'validate_admin_import_options' ) ); |
| 879 |
register_setting( $this->plugin_name . '_administrative_options', $this->plugin_name . '_administrative_options', array( $this, 'validate_administrative_options' ) ); |
| 880 |
// The standalone option is registered in class-mlsimport-standalone-settings.php |
| 881 |
// (on init, with show_in_rest) so the dedicated React design page can read/write it. |
| 882 |
} |
| 883 |
|
| 884 |
/** |
| 885 |
* Update-option hook for the administrative options group. |
| 886 |
* |
| 887 |
* When the administrative options carry an 'import' JSON payload, decode it |
| 888 |
* and restore the field-select / mls-sync / import-options option groups. |
| 889 |
*/ |
| 890 |
public function update_option_mlsimport_administrative_options() { |
| 891 |
// Read the saved administrative options and, if present, restore the |
| 892 |
// three related option groups from the embedded JSON payload. |
| 893 |
$import = get_option( 'mlsimport_administrative_options' ); |
| 894 |
if ( '' !== $import ) { |
| 895 |
$decode = json_decode( $import['import'], true ); |
| 896 |
update_option( 'mlsimport_admin_fields_select', $decode['mlsimport_admin_fields_select'] ); |
| 897 |
update_option( 'mlsimport_admin_mls_sync', $decode['mlsimport_admin_mls_sync'] ); |
| 898 |
update_option( 'mlsimport_admin_import_options', $decode['mlsimport_admin_import_options'] ); |
| 899 |
} |
| 900 |
} |
| 901 |
|
| 902 |
/** |
| 903 |
* Update-option hook for the field-select group: ask the active theme |
| 904 |
* adapter to (re)register its custom fields/taxonomies for the mapped fields. |
| 905 |
*/ |
| 906 |
public function update_option_mlsimport_admin_fields_select() { |
| 907 |
|
| 908 |
// Delegate to the theme adapter to sync its custom fields. |
| 909 |
$this->env_data->enviroment_custom_fields( $this->plugin_name ); |
| 910 |
} |
| 911 |
|
| 912 |
|
| 913 |
/** |
| 914 |
* Register the "Hidden Fields" metabox on the theme's property post type. |
| 915 |
* |
| 916 |
* Only added when the theme adapter exposes get_property_post_type(). |
| 917 |
*/ |
| 918 |
public function mlsimport_meta_options() { |
| 919 |
// Add the metabox to whatever post type the active theme uses for listings. |
| 920 |
if ( method_exists( $this->env_data, 'get_property_post_type' ) ) { |
| 921 |
add_meta_box( 'mlsimport_hidden_fields', esc_html__( 'Mls Import Hidden Fields', 'mlsimport' ), array( $this, 'mlsimport_hidden_fields' ), $this->env_data->get_property_post_type(), 'normal', 'low' ); |
| 922 |
} |
| 923 |
} |
| 924 |
|
| 925 |
/** |
| 926 |
* Render the "Hidden Fields" metabox for a single property post. |
| 927 |
* |
| 928 |
* Shows the ListingKey, the source Import Task (inserted/updated), any |
| 929 |
* protected statuses, every admin-flagged imported field value, and the |
| 930 |
* property change history. |
| 931 |
*/ |
| 932 |
public function mlsimport_hidden_fields() { |
| 933 |
global $post; |
| 934 |
|
| 935 |
// Field-select config drives which imported meta values to display. |
| 936 |
$options = get_option( $this->plugin_name . '_admin_fields_select' ); |
| 937 |
|
| 938 |
// Which Import Task created / last updated this property, and its RESO key. |
| 939 |
$MLSimport_item_inserted = get_post_meta( $post->ID, 'MLSimport_item_inserted', true ); |
| 940 |
$MLSimport_item_updated = get_post_meta( $post->ID, 'MLSimport_item_updated', true ); |
| 941 |
$listing_key = get_post_meta( $post->ID, 'ListingKey', true ); |
| 942 |
|
| 943 |
// Get the import task ID to retrieve protected statuses |
| 944 |
// (prefer the inserting task, fall back to the updating task). |
| 945 |
$import_task_id = !empty( $MLSimport_item_inserted ) ? $MLSimport_item_inserted : ( !empty( $MLSimport_item_updated ) ? $MLSimport_item_updated : null ); |
| 946 |
$mlsImportItemStatusProtect = $import_task_id ? get_post_meta( $import_task_id, 'mlsimport_item_standardstatusprotect', true ) : null; |
| 947 |
|
| 948 |
// Check if the ListingKey exists |
| 949 |
if ( !empty( $listing_key ) ) { |
| 950 |
echo 'ListingKey: ' . esc_html( $listing_key ) . '<br>'; |
| 951 |
} |
| 952 |
|
| 953 |
// Check if MLSimport_item_inserted exists |
| 954 |
if ( !empty( $MLSimport_item_inserted ) ) { |
| 955 |
echo 'Added via MLS item id: ' . esc_html( $MLSimport_item_inserted ) . ' - ' . esc_html( get_the_title( $MLSimport_item_inserted ) ) . '<br>'; |
| 956 |
} |
| 957 |
|
| 958 |
// Check if MLSimport_item_updated exists |
| 959 |
if ( !empty( $MLSimport_item_updated ) ) { |
| 960 |
echo 'Updated via MLS item id: ' . esc_html( $MLSimport_item_updated ) . ' - ' . esc_html( get_the_title( $MLSimport_item_updated ) ) . '<br>'; |
| 961 |
} |
| 962 |
|
| 963 |
// Show any protected statuses (array or scalar) configured on the task. |
| 964 |
if(!empty($mlsImportItemStatusProtect)) { |
| 965 |
if(is_array($mlsImportItemStatusProtect)) { |
| 966 |
echo 'Protected statuses: ' . esc_html( implode(', ', $mlsImportItemStatusProtect) ) . '<br>'; |
| 967 |
} else { |
| 968 |
echo 'Protected statuses: ' . esc_html($mlsImportItemStatusProtect) . '<br>'; |
| 969 |
} |
| 970 |
} |
| 971 |
|
| 972 |
// Print each admin-flagged field: label + stored meta value. |
| 973 |
foreach ( ( is_array( $options ) && ! empty( $options['mls-fields-admin'] ) ? $options['mls-fields-admin'] : array() ) as $key => $value ) { |
| 974 |
// Only fields explicitly marked for admin display (flag === 1). |
| 975 |
if ( 1 === intval($options['mls-fields-admin'][ $key ] ) ) { |
| 976 |
// Prefer a custom label if one was set for this field. |
| 977 |
$display_label = $key; |
| 978 |
if ( isset( $options['mls-fields-label'][ $key ] ) && '' !== $options['mls-fields-label'][ $key ] ) { |
| 979 |
$display_label = $options['mls-fields-label'][ $key ]; |
| 980 |
} |
| 981 |
|
| 982 |
// Resolve the stored value. Standalone (990) stores every imported |
| 983 |
// field as mlsimport_<Field> (with an _x_ fallback); the theme modes |
| 984 |
// store them lowercase (except ListingKey). Reading the wrong casing |
| 985 |
// is why hidden fields (e.g. ParcelNumber) showed here without a value. |
| 986 |
if ( function_exists( 'mlsimport_is_standalone_mode' ) && mlsimport_is_standalone_mode() && function_exists( 'mlsimport_property_field_value' ) ) { |
| 987 |
$field_value = mlsimport_property_field_value( (int) $post->ID, (string) $key ); |
| 988 |
} else { |
| 989 |
$meta_key = ( 'ListingKey' !== $key ) ? strtolower( $key ) : $key; |
| 990 |
$field_value = (string) get_post_meta( $post->ID, $meta_key, true ); |
| 991 |
} |
| 992 |
?> |
| 993 |
|
| 994 |
<strong><?php echo esc_html($display_label);?>:</strong> |
| 995 |
<?php echo esc_html( $field_value ); ?> </br> |
| 996 |
<?php |
| 997 |
} |
| 998 |
} |
| 999 |
?> |
| 1000 |
|
| 1001 |
<h2 style="font-weight:bold;padding-left:0px;">Mls Import History</h2> |
| 1002 |
<?php |
| 1003 |
// Property change history (only populated when history logging is enabled). |
| 1004 |
$meta = get_post_meta( $post->ID, 'mlsimport_property_history', true ); |
| 1005 |
if ( '' === trim( $meta ) ) { ?> |
| 1006 |
<strong>Property history is blank - you can enable it in Settings/ Tools page </strong> |
| 1007 |
<?php |
| 1008 |
} else { |
| 1009 |
print wp_kses_post($meta); |
| 1010 |
} |
| 1011 |
} |
| 1012 |
|
| 1013 |
|
| 1014 |
|
| 1015 |
|
| 1016 |
/** |
| 1017 |
* AJAX (Tools page): clear all MLSImport caches/transients and the |
| 1018 |
* metadata-populated flag, forcing the next request to re-fetch everything. |
| 1019 |
*/ |
| 1020 |
function mlsimport_delete_cache() { |
| 1021 |
|
| 1022 |
// CSRF: Tools-page nonce. |
| 1023 |
check_ajax_referer( 'mlsimport_tool_actions', 'security' ); |
| 1024 |
|
| 1025 |
// Drop every cached token/metadata/schema transient. |
| 1026 |
delete_transient( 'mlsimport_token_request' ); |
| 1027 |
delete_transient( 'mlsimport_metadata_api_call_data_service_property' ); |
| 1028 |
delete_transient( 'mls_import_meta_enums' ); |
| 1029 |
delete_transient( 'mls_import_meta' ); |
| 1030 |
delete_transient( 'mlsimport_plugin_data_schema' ); |
| 1031 |
delete_transient( 'mlsimport_ready_to_go_mlsimport_data' ); |
| 1032 |
delete_transient( 'mlsimport_saas_token' ); |
| 1033 |
|
| 1034 |
// Force a fresh metadata pull next load. |
| 1035 |
delete_option( 'mlsimport_mls_metadata_populated' ); |
| 1036 |
|
| 1037 |
die( 'deleted' ); |
| 1038 |
} |
| 1039 |
|
| 1040 |
/** |
| 1041 |
* AJAX (Tools page): reset the field-mapping configuration so the field |
| 1042 |
* selector starts fresh (also clears the metadata-populated flag). |
| 1043 |
*/ |
| 1044 |
function mlsimport_clear_fields_data() { |
| 1045 |
|
| 1046 |
// CSRF: Tools-page nonce. |
| 1047 |
check_ajax_referer( 'mlsimport_tool_actions', 'security' ); |
| 1048 |
|
| 1049 |
// Wipe the metadata flag and the saved field-select configuration. |
| 1050 |
delete_option( 'mlsimport_mls_metadata_populated' ); |
| 1051 |
delete_option( 'mlsimport_admin_fields_select' ); |
| 1052 |
|
| 1053 |
die( 'deleted' ); |
| 1054 |
} |
| 1055 |
|
| 1056 |
/** |
| 1057 |
* AJAX (Tools page): return the terms of a taxonomy for the "delete |
| 1058 |
* properties by term" picker. Admin-only; validates the taxonomy exists. |
| 1059 |
* |
| 1060 |
* @return void Emits a JSON success payload of {slug,name,count} rows. |
| 1061 |
*/ |
| 1062 |
function mlsimport_get_taxonomy_terms() { |
| 1063 |
// CSRF + capability. |
| 1064 |
check_ajax_referer( 'mlsimport_tool_actions', 'security' ); |
| 1065 |
if ( ! current_user_can( 'administrator' ) ) { |
| 1066 |
wp_send_json_error( 'Unauthorized' ); |
| 1067 |
} |
| 1068 |
|
| 1069 |
// Reject unknown taxonomies. |
| 1070 |
$taxonomy = sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ); |
| 1071 |
if ( ! taxonomy_exists( $taxonomy ) ) { |
| 1072 |
wp_send_json_error( 'Invalid taxonomy' ); |
| 1073 |
} |
| 1074 |
|
| 1075 |
// Fetch all terms (including empties) and flatten to slug/name/count. |
| 1076 |
$terms = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'orderby' => 'name' ) ); |
| 1077 |
$result = array(); |
| 1078 |
if ( ! is_wp_error( $terms ) ) { |
| 1079 |
foreach ( $terms as $term ) { |
| 1080 |
$result[] = array( |
| 1081 |
'slug' => $term->slug, |
| 1082 |
'name' => $term->name, |
| 1083 |
'count' => $term->count, |
| 1084 |
); |
| 1085 |
} |
| 1086 |
} |
| 1087 |
wp_send_json_success( $result ); |
| 1088 |
} |
| 1089 |
|
| 1090 |
/** |
| 1091 |
* AJAX (Tools page): delete imported properties matching selected taxonomy |
| 1092 |
* terms, in batches of 20. Admin-only. Reports progress so the client can |
| 1093 |
* loop until done; refreshes term counts once the last batch completes. |
| 1094 |
* |
| 1095 |
* @return void Emits a JSON success payload {deleted,remaining,total,done}. |
| 1096 |
*/ |
| 1097 |
function mlsimport_delete_properties() { |
| 1098 |
global $mlsimport; |
| 1099 |
|
| 1100 |
// CSRF + capability. |
| 1101 |
check_ajax_referer( 'mlsimport_tool_actions', 'security' ); |
| 1102 |
|
| 1103 |
if ( ! current_user_can( 'administrator' ) ) { |
| 1104 |
wp_send_json_error( 'Unauthorized' ); |
| 1105 |
} |
| 1106 |
|
| 1107 |
// Selected taxonomy and its chosen term slugs. |
| 1108 |
$taxonomy = sanitize_text_field( wp_unslash( $_POST['mlsimport_delete_category'] ) ); |
| 1109 |
$terms = array(); |
| 1110 |
|
| 1111 |
// Collect and sanitize the selected term slugs. |
| 1112 |
if ( isset( $_POST['mlsimport_delete_category_term'] ) && is_array( $_POST['mlsimport_delete_category_term'] ) ) { |
| 1113 |
foreach ( $_POST['mlsimport_delete_category_term'] as $term ) { |
| 1114 |
$terms[] = sanitize_text_field( wp_unslash( $term ) ); |
| 1115 |
} |
| 1116 |
} |
| 1117 |
|
| 1118 |
// Require a taxonomy. |
| 1119 |
if ( '' === $taxonomy ) { |
| 1120 |
wp_send_json_error( esc_html__( 'Please select a taxonomy', 'mlsimport' ) ); |
| 1121 |
} |
| 1122 |
|
| 1123 |
// Require at least one term. |
| 1124 |
if ( empty( $terms ) ) { |
| 1125 |
wp_send_json_error( esc_html__( 'Please select at least one term', 'mlsimport' ) ); |
| 1126 |
} |
| 1127 |
|
| 1128 |
// Query one page of property IDs matching the term selection. |
| 1129 |
$post_type = $mlsimport->admin->env_data->get_property_post_type(); |
| 1130 |
|
| 1131 |
$args = array( |
| 1132 |
'post_type' => $post_type, |
| 1133 |
'post_status' => 'any', |
| 1134 |
'posts_per_page' => 20, |
| 1135 |
'tax_query' => array( |
| 1136 |
array( |
| 1137 |
'taxonomy' => $taxonomy, |
| 1138 |
'field' => 'slug', |
| 1139 |
'terms' => $terms, |
| 1140 |
), |
| 1141 |
), |
| 1142 |
'fields' => 'ids', |
| 1143 |
); |
| 1144 |
|
| 1145 |
$prop_selection = new WP_Query( $args ); |
| 1146 |
$deleted = 0; |
| 1147 |
|
| 1148 |
// Delete each property in this batch via the theme importer's SQL delete. |
| 1149 |
foreach ( $prop_selection->posts as $delete_id ) { |
| 1150 |
$mlsimport->admin->theme_importer->mlsimportSaasDeletePropertyViaMysql( $delete_id, ' delete from tools ' ); |
| 1151 |
++$deleted; |
| 1152 |
} |
| 1153 |
|
| 1154 |
// Compute how many still match after this batch; done when none remain. |
| 1155 |
$remaining = $prop_selection->found_posts - $deleted; |
| 1156 |
$done = ( $remaining <= 0 ); |
| 1157 |
|
| 1158 |
// Update term counts only when all deletions are complete |
| 1159 |
if ( $done ) { |
| 1160 |
// Recount every taxonomy on the property post type in one pass. |
| 1161 |
$all_taxonomies = get_object_taxonomies( $post_type ); |
| 1162 |
foreach ( $all_taxonomies as $tax_name ) { |
| 1163 |
$all_terms = get_terms( array( 'taxonomy' => $tax_name, 'hide_empty' => false, 'fields' => 'ids' ) ); |
| 1164 |
if ( ! is_wp_error( $all_terms ) && ! empty( $all_terms ) ) { |
| 1165 |
wp_update_term_count_now( $all_terms, $tax_name ); |
| 1166 |
} |
| 1167 |
} |
| 1168 |
} |
| 1169 |
|
| 1170 |
// Report progress back to the client loop. |
| 1171 |
wp_send_json_success( array( |
| 1172 |
'deleted' => $deleted, |
| 1173 |
'remaining' => max( 0, $remaining ), |
| 1174 |
'total' => $prop_selection->found_posts, |
| 1175 |
'done' => $done, |
| 1176 |
) ); |
| 1177 |
} |
| 1178 |
|
| 1179 |
|
| 1180 |
|
| 1181 |
|
| 1182 |
|
| 1183 |
|
| 1184 |
|
| 1185 |
|
| 1186 |
/** |
| 1187 |
* Convert a PHP shorthand byte value (e.g. "256M", "1G", "-1") to bytes. |
| 1188 |
* |
| 1189 |
* @param string|int $value Raw ini/constant value. |
| 1190 |
* @return int Bytes, or -1 for an unlimited (-1) setting. |
| 1191 |
*/ |
| 1192 |
private function mlsimport_parse_bytes( $value ) { |
| 1193 |
$value = trim( (string) $value ); |
| 1194 |
if ( '' === $value ) { |
| 1195 |
return 0; |
| 1196 |
} |
| 1197 |
if ( '-1' === $value ) { |
| 1198 |
return -1; // Unlimited. |
| 1199 |
} |
| 1200 |
$unit = strtolower( substr( $value, -1 ) ); |
| 1201 |
$number = (int) $value; |
| 1202 |
switch ( $unit ) { |
| 1203 |
case 'g': |
| 1204 |
$number *= 1024 * 1024 * 1024; |
| 1205 |
break; |
| 1206 |
case 'm': |
| 1207 |
$number *= 1024 * 1024; |
| 1208 |
break; |
| 1209 |
case 'k': |
| 1210 |
$number *= 1024; |
| 1211 |
break; |
| 1212 |
} |
| 1213 |
return $number; |
| 1214 |
} |
| 1215 |
|
| 1216 |
/** |
| 1217 |
* Print admin warnings when the PHP/WordPress environment is too constrained |
| 1218 |
* for large imports (effective memory below 256MB, or a positive |
| 1219 |
* max_execution_time below 600s). Suppressed during AJAX and on the |
| 1220 |
* onboarding screen. |
| 1221 |
* |
| 1222 |
* Memory is judged from the effective runtime limit: the larger of |
| 1223 |
* WP_MEMORY_LIMIT (wp-config) and the actual PHP ini memory_limit |
| 1224 |
* (which may be raised at the server/php.ini level), and -1 counts as |
| 1225 |
* unlimited. This avoids a false warning when memory is fine but only set |
| 1226 |
* outside wp-config.php. |
| 1227 |
*/ |
| 1228 |
public function mlsimport_saas_setting_up() { |
| 1229 |
// Do not output warnings during AJAX requests |
| 1230 |
if ( ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) || |
| 1231 |
( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { |
| 1232 |
return; |
| 1233 |
} |
| 1234 |
|
| 1235 |
// Skip all warnings on the onboarding wizard. |
| 1236 |
$is_onboarding = isset( $_GET['page'] ) && 'mlsimport-onboarding' === $_GET['page']; |
| 1237 |
if ( $is_onboarding ) { |
| 1238 |
return; |
| 1239 |
} |
| 1240 |
|
| 1241 |
// Effective memory limit: the larger of wp-config's WP_MEMORY_LIMIT and |
| 1242 |
// the actual PHP runtime limit; either being -1 means unlimited. |
| 1243 |
$min_bytes = 256 * 1024 * 1024; |
| 1244 |
$wp_bytes = $this->mlsimport_parse_bytes( WP_MEMORY_LIMIT ); |
| 1245 |
$php_bytes = $this->mlsimport_parse_bytes( ini_get( 'memory_limit' ) ); |
| 1246 |
$memory_ok = ( -1 === $wp_bytes ) || ( -1 === $php_bytes ) |
| 1247 |
|| ( $wp_bytes >= $min_bytes ) || ( $php_bytes >= $min_bytes ); |
| 1248 |
|
| 1249 |
// Memory-limit warning. |
| 1250 |
if ( ! $memory_ok ) { ?> |
| 1251 |
<div class="mlsimport_warning long_warning"> |
| 1252 |
<?php |
| 1253 |
printf( |
| 1254 |
/* translators: 1: current WordPress memory limit, 2: URL to the WordPress documentation on increasing memory. */ |
| 1255 |
wp_kses( |
| 1256 |
__( '<strong>WordPress Memory Limit</strong> is set to <strong>%1$s</strong>. Allocated Memory should be at least <strong>256MB</strong>. Please refer to: <a href="%2$s" target="_blank">Increasing memory allocated to PHP</a>', 'mlsimport' ), |
| 1257 |
array( |
| 1258 |
'strong' => array(), |
| 1259 |
'a' => array( |
| 1260 |
'href' => array(), |
| 1261 |
'target' => array(), |
| 1262 |
), |
| 1263 |
) |
| 1264 |
), |
| 1265 |
esc_html( WP_MEMORY_LIMIT ), |
| 1266 |
'https://wordpress.org/support/article/editing-wp-config-php/#increasing-memory-allocated-to-php' |
| 1267 |
); |
| 1268 |
?> |
| 1269 |
</div> |
| 1270 |
<?php |
| 1271 |
} |
| 1272 |
|
| 1273 |
// Execution-time warning: 0 or -1 means unlimited (fine); only a |
| 1274 |
// positive value below 600s is flagged. |
| 1275 |
$max_time = (int) ini_get( 'max_execution_time' ); |
| 1276 |
if ( $max_time > 0 && $max_time < 600 ) { |
| 1277 |
?> |
| 1278 |
<div class="mlsimport_warning long_warning"> |
| 1279 |
<?php |
| 1280 |
printf( |
| 1281 |
/* translators: %s: current max_execution_time value. */ |
| 1282 |
wp_kses( |
| 1283 |
__( 'Your <strong>max_execution_time</strong> setting in php is set to <strong>%s</strong>. Importing hundreds of listings requires extra time. Please set max_execution_time to <strong>0 (unlimited)</strong>. If that is not possible, set it to a minimum of <strong>600 (10 minutes)</strong>.', 'mlsimport' ), |
| 1284 |
array( 'strong' => array() ) |
| 1285 |
), |
| 1286 |
esc_html( $max_time ) |
| 1287 |
); |
| 1288 |
?> |
| 1289 |
</div> |
| 1290 |
|
| 1291 |
<?php |
| 1292 |
} |
| 1293 |
} |
| 1294 |
|
| 1295 |
/** |
| 1296 |
* Test the configured MLS credentials against the SaaS API. |
| 1297 |
* |
| 1298 |
* Gathers every provider's stored credentials, short-circuits (returns |
| 1299 |
* early) when the mandatory credentials for the selected provider are |
| 1300 |
* blank, PATCHes them to the 'clients' endpoint, and stores/clears the |
| 1301 |
* 'mlsimport_connection_test' flag based on whether the API reports the |
| 1302 |
* connection tested successfully. |
| 1303 |
* |
| 1304 |
* @since 4.0.1 |
| 1305 |
* @return array|void The API response, or void on an early return. |
| 1306 |
*/ |
| 1307 |
public function mlsimport_saas_check_mls_connection() { |
| 1308 |
|
| 1309 |
// Load saved options; $values will accumulate the credentials to send. |
| 1310 |
$values = array(); |
| 1311 |
$options = get_option( $this->plugin_name . '_admin_options' ); |
| 1312 |
|
| 1313 |
// --- Selected MLS id + generic token --- |
| 1314 |
$mls_id = ''; |
| 1315 |
if ( isset( $options['mlsimport_mls_name'] ) ) { |
| 1316 |
$mls_id = sanitize_text_field( trim( $options['mlsimport_mls_name'] ) ); |
| 1317 |
} |
| 1318 |
|
| 1319 |
$mls_token = ''; |
| 1320 |
if ( isset( $options['mlsimport_mls_name'] ) ) { |
| 1321 |
$mls_token = sanitize_text_field( trim( $options['mlsimport_mls_token'] ) ); |
| 1322 |
} |
| 1323 |
|
| 1324 |
// Numeric MLS id drives the provider-family branching below. |
| 1325 |
$mls_id_int = intval( $mls_id ); |
| 1326 |
|
| 1327 |
// --- Trestle credentials --- |
| 1328 |
$mlsimport_tresle_client_id = ''; |
| 1329 |
if ( isset( $options['mlsimport_tresle_client_id'] ) ) { |
| 1330 |
$mlsimport_tresle_client_id = sanitize_text_field( trim( $options['mlsimport_tresle_client_id'] ) ); |
| 1331 |
} |
| 1332 |
|
| 1333 |
$mlsimport_tresle_client_secret = ''; |
| 1334 |
if ( isset( $options['mlsimport_tresle_client_secret'] ) ) { |
| 1335 |
$mlsimport_tresle_client_secret = sanitize_text_field( trim( $options['mlsimport_tresle_client_secret'] ) ); |
| 1336 |
} |
| 1337 |
|
| 1338 |
// --- ConnectMLS credentials --- |
| 1339 |
$mlsimport_connectmls_username = ''; |
| 1340 |
if ( isset( $options['mlsimport_connectmls_username'] ) ) { |
| 1341 |
$mlsimport_connectmls_username = sanitize_text_field( trim( $options['mlsimport_connectmls_username'] ) ); |
| 1342 |
} |
| 1343 |
|
| 1344 |
$mlsimport_connectmls_password = ''; |
| 1345 |
if ( isset( $options['mlsimport_connectmls_password'] ) ) { |
| 1346 |
$mlsimport_connectmls_password = sanitize_text_field( trim( $options['mlsimport_connectmls_password'] ) ); |
| 1347 |
} |
| 1348 |
|
| 1349 |
// rapattoni data |
| 1350 |
$mlsimport_rapattoni_client_id = ''; |
| 1351 |
if ( isset( $options['mlsimport_rapattoni_client_id'] ) ) { |
| 1352 |
$mlsimport_rapattoni_client_id = sanitize_text_field( trim( $options['mlsimport_rapattoni_client_id'] ) ); |
| 1353 |
} |
| 1354 |
$mlsimport_rapattoni_client_secret = ''; |
| 1355 |
if ( isset( $options['mlsimport_rapattoni_client_secret'] ) ) { |
| 1356 |
$mlsimport_rapattoni_client_secret = sanitize_text_field( trim( $options['mlsimport_rapattoni_client_secret'] ) ); |
| 1357 |
} |
| 1358 |
|
| 1359 |
$mlsimport_rapattoni_username = ''; |
| 1360 |
if ( isset( $options['mlsimport_rapattoni_username'] ) ) { |
| 1361 |
$mlsimport_rapattoni_username = sanitize_text_field( trim( $options['mlsimport_rapattoni_username'] ) ); |
| 1362 |
} |
| 1363 |
|
| 1364 |
$mlsimport_rapattoni_password = ''; |
| 1365 |
if ( isset( $options['mlsimport_rapattoni_password'] ) ) { |
| 1366 |
$mlsimport_rapattoni_password = sanitize_text_field( trim( $options['mlsimport_rapattoni_password'] ) ); |
| 1367 |
} |
| 1368 |
|
| 1369 |
// paragon data |
| 1370 |
$mlsimport_paragon_client_id = ''; |
| 1371 |
if ( isset( $options['mlsimport_paragon_client_id'] ) ) { |
| 1372 |
$mlsimport_paragon_client_id = sanitize_text_field( trim( $options['mlsimport_paragon_client_id'] ) ); |
| 1373 |
} |
| 1374 |
$mlsimport_paragon_client_secret = ''; |
| 1375 |
if ( isset( $options['mlsimport_paragon_client_secret'] ) ) { |
| 1376 |
$mlsimport_paragon_client_secret = sanitize_text_field( trim( $options['mlsimport_paragon_client_secret'] ) ); |
| 1377 |
} |
| 1378 |
|
| 1379 |
// realtor.ca data |
| 1380 |
$mlsimport_realtorca_client_id = ''; |
| 1381 |
if ( isset( $options['mlsimport_realtorca_client_id'] ) ) { |
| 1382 |
$mlsimport_realtorca_client_id = sanitize_text_field( trim( $options['mlsimport_realtorca_client_id'] ) ); |
| 1383 |
} |
| 1384 |
$mlsimport_realtorca_client_secret = ''; |
| 1385 |
if ( isset( $options['mlsimport_realtorca_client_secret'] ) ) { |
| 1386 |
$mlsimport_realtorca_client_secret = sanitize_text_field( trim( $options['mlsimport_realtorca_client_secret'] ) ); |
| 1387 |
} |
| 1388 |
|
| 1389 |
// brightmls data |
| 1390 |
$mlsimport_brightmls_client_id = ''; |
| 1391 |
if ( isset( $options['mlsimport_brightmls_client_id'] ) ) { |
| 1392 |
$mlsimport_brightmls_client_id = sanitize_text_field( trim( $options['mlsimport_brightmls_client_id'] ) ); |
| 1393 |
} |
| 1394 |
$mlsimport_brightmls_client_secret = ''; |
| 1395 |
if ( isset( $options['mlsimport_brightmls_client_secret'] ) ) { |
| 1396 |
$mlsimport_brightmls_client_secret = sanitize_text_field( trim( $options['mlsimport_brightmls_client_secret'] ) ); |
| 1397 |
} |
| 1398 |
|
| 1399 |
|
| 1400 |
|
| 1401 |
|
| 1402 |
|
| 1403 |
|
| 1404 |
|
| 1405 |
// When the generic token is blank, the selected provider's own |
| 1406 |
// credentials are mandatory: bail (no test) if any are missing. |
| 1407 |
// Each branch maps an mls_id range to one provider family. |
| 1408 |
if ( trim( $mls_token ) === '' ) { |
| 1409 |
if ( $this->mlsimport_is_brightmls_provider( $mls_id_int ) ) { // BrightMLS |
| 1410 |
if ( trim( $mlsimport_brightmls_client_id ) === '' || trim( $mlsimport_brightmls_client_secret ) === '' ) { |
| 1411 |
return; |
| 1412 |
} |
| 1413 |
} elseif ( $mls_id_int > 900 && $mls_id_int < 3000 ) { // Trestle |
| 1414 |
if ( trim( $mlsimport_tresle_client_id ) === '' || trim( $mlsimport_tresle_client_secret ) === '' ) { |
| 1415 |
return; |
| 1416 |
} |
| 1417 |
} elseif ( $this->mlsimport_is_connectmls_provider( $mls_id_int ) ) { // ConnectMLS |
| 1418 |
if ( |
| 1419 |
trim( $mlsimport_connectmls_username ) === '' || |
| 1420 |
trim( $mlsimport_connectmls_password ) === '' |
| 1421 |
) { |
| 1422 |
return; |
| 1423 |
} |
| 1424 |
} elseif ( $mls_id_int >= 5000 && $mls_id_int < 6000 ) { // Rapattoni |
| 1425 |
if ( |
| 1426 |
trim( $mlsimport_rapattoni_client_id ) === '' || |
| 1427 |
trim( $mlsimport_rapattoni_client_secret ) === '' || |
| 1428 |
trim( $mlsimport_rapattoni_username ) === '' || |
| 1429 |
trim( $mlsimport_rapattoni_password ) === '' |
| 1430 |
) { |
| 1431 |
return; |
| 1432 |
} |
| 1433 |
} elseif ( $mls_id_int >= 6000 && $mls_id_int < 7000 ) { // Paragon |
| 1434 |
if ( |
| 1435 |
trim( $mlsimport_paragon_client_id ) === '' || |
| 1436 |
trim( $mlsimport_paragon_client_secret ) === '' |
| 1437 |
) { |
| 1438 |
return; |
| 1439 |
} |
| 1440 |
} elseif ( $mls_id_int >= 7000 && $mls_id_int < 8000 ) { // Realtor.ca |
| 1441 |
if ( |
| 1442 |
trim( $mlsimport_realtorca_client_id ) === '' || |
| 1443 |
trim( $mlsimport_realtorca_client_secret ) === '' |
| 1444 |
) { |
| 1445 |
return; |
| 1446 |
} |
| 1447 |
} elseif ( mlsimport_is_proptx_provider( $mls_id_int ) ) { // PropTx / AMPRE - static bearer token is the only credential |
| 1448 |
return; |
| 1449 |
} |
| 1450 |
} |
| 1451 |
|
| 1452 |
// Assemble the full credential payload for every provider. |
| 1453 |
$values['mls_token'] = $mls_token; |
| 1454 |
$values['mls_id'] = $mls_id; |
| 1455 |
$values['mlsimport_tresle_client_id'] = $mlsimport_tresle_client_id; |
| 1456 |
$values['mlsimport_tresle_client_secret'] = $mlsimport_tresle_client_secret; |
| 1457 |
$values['mlsimport_connectmls_username'] = $mlsimport_connectmls_username; |
| 1458 |
$values['mlsimport_connectmls_password'] = $mlsimport_connectmls_password; |
| 1459 |
|
| 1460 |
$values['mlsimport_rapattoni_client_id'] = $mlsimport_rapattoni_client_id; |
| 1461 |
$values['mlsimport_rapattoni_client_secret'] = $mlsimport_rapattoni_client_secret; |
| 1462 |
$values['mlsimport_rapattoni_username'] = $mlsimport_rapattoni_username; |
| 1463 |
$values['mlsimport_rapattoni_password'] = $mlsimport_rapattoni_password; |
| 1464 |
|
| 1465 |
$values['mlsimport_paragon_client_id'] = $mlsimport_paragon_client_id; |
| 1466 |
$values['mlsimport_paragon_client_secret'] = $mlsimport_paragon_client_secret; |
| 1467 |
|
| 1468 |
|
| 1469 |
$values['mlsimport_realtorca_client_id'] = $mlsimport_realtorca_client_id; |
| 1470 |
$values['mlsimport_realtorca_client_secret'] = $mlsimport_realtorca_client_secret; |
| 1471 |
|
| 1472 |
$values['mlsimport_brightmls_client_id'] = $mlsimport_brightmls_client_id; |
| 1473 |
$values['mlsimport_brightmls_client_secret'] = $mlsimport_brightmls_client_secret; |
| 1474 |
|
| 1475 |
|
| 1476 |
|
| 1477 |
|
| 1478 |
|
| 1479 |
|
| 1480 |
|
| 1481 |
// PATCH the credentials to the SaaS 'clients' endpoint, which validates |
| 1482 |
// them against the live MLS and reports back whether it "tested". |
| 1483 |
$answer = $this->theme_importer->globalApiRequestSaas( 'clients', $values, 'PATCH' ); |
| 1484 |
|
| 1485 |
|
| 1486 |
|
| 1487 |
|
| 1488 |
// Persist the connection-test flag only on a confirmed successful test; |
| 1489 |
// any other outcome clears it (and the metadata flag) so the UI re-tests. |
| 1490 |
if ( isset( $answer['success'] ) && true === $answer['success'] ) { |
| 1491 |
if ( isset( $answer['tested'] ) && true === $answer['tested'] ) { |
| 1492 |
update_option( 'mlsimport_connection_test', 'yes' ); |
| 1493 |
mlsimport_telemetry_set_once( 'mls_connected_at', time() ); |
| 1494 |
} else { |
| 1495 |
delete_option( 'mlsimport_connection_test' ); |
| 1496 |
delete_option( 'mlsimport_mls_metadata_populated' ); |
| 1497 |
} |
| 1498 |
} else { |
| 1499 |
delete_option( 'mlsimport_connection_test' ); |
| 1500 |
delete_option( 'mlsimport_mls_metadata_populated' ); |
| 1501 |
} |
| 1502 |
|
| 1503 |
return $answer; |
| 1504 |
} |
| 1505 |
|
| 1506 |
/** |
| 1507 |
* Whether an MLS id belongs to the ConnectMLS family (8000-8999, but not |
| 1508 |
* 8001 which is BrightMLS). |
| 1509 |
* |
| 1510 |
* @param int $mls_id_int Numeric MLS id. |
| 1511 |
* @return bool |
| 1512 |
*/ |
| 1513 |
private function mlsimport_is_connectmls_provider( $mls_id_int ) { |
| 1514 |
return $mls_id_int >= 8000 && $mls_id_int < 9000 && 8001 !== (int) $mls_id_int; |
| 1515 |
} |
| 1516 |
|
| 1517 |
/** |
| 1518 |
* Whether an MLS id is BrightMLS (the single reserved id 8001). |
| 1519 |
* |
| 1520 |
* @param int $mls_id_int Numeric MLS id. |
| 1521 |
* @return bool |
| 1522 |
*/ |
| 1523 |
private function mlsimport_is_brightmls_provider( int $mls_id_int ): bool { |
| 1524 |
return 8001 === $mls_id_int; |
| 1525 |
} |
| 1526 |
|
| 1527 |
/** |
| 1528 |
* AJAX handler for the plugin-deactivation exit survey. |
| 1529 |
* |
| 1530 |
* Thin wrapper: it verifies the nonce and capability, sanitizes input, |
| 1531 |
* delegates the real work to mlsimport_exit_survey_record(), and POSTs |
| 1532 |
* the result to the SaaS API. The POST is fire-and-forget — a failed or |
| 1533 |
* not-yet-deployed endpoint must never stop the admin from deactivating. |
| 1534 |
*/ |
| 1535 |
public function mlsimport_exit_survey_submit() { |
| 1536 |
check_ajax_referer( 'mlsimport_exit_survey', 'security' ); |
| 1537 |
if ( ! current_user_can( 'administrator' ) ) { |
| 1538 |
wp_send_json_error( 'Unauthorized' ); |
| 1539 |
} |
| 1540 |
|
| 1541 |
$input = array( |
| 1542 |
'reason' => sanitize_text_field( wp_unslash( $_POST['reason'] ?? '' ) ), |
| 1543 |
'details' => sanitize_textarea_field( wp_unslash( $_POST['details'] ?? '' ) ), |
| 1544 |
); |
| 1545 |
|
| 1546 |
// Only record a recognized reason; an unknown value is dropped |
| 1547 |
// silently rather than blocking the user or storing junk. |
| 1548 |
if ( $this->mlsimport_exit_survey_is_valid_reason( $input['reason'] ) ) { |
| 1549 |
$payload = $this->mlsimport_exit_survey_record( $input ); |
| 1550 |
try { |
| 1551 |
ThemeImport::globalApiRequestSaas( 'user-activity', $payload, 'POST' ); |
| 1552 |
} catch ( \Throwable $e ) { |
| 1553 |
// Swallow: deactivation proceeds regardless of transport failure. |
| 1554 |
} |
| 1555 |
} |
| 1556 |
|
| 1557 |
wp_send_json_success(); |
| 1558 |
} |
| 1559 |
|
| 1560 |
/** |
| 1561 |
* Whether a submitted exit-survey reason is one of the known options. |
| 1562 |
* |
| 1563 |
* Pure predicate — no WordPress functions, no translation. |
| 1564 |
*/ |
| 1565 |
private function mlsimport_exit_survey_is_valid_reason( string $reason ): bool { |
| 1566 |
return in_array( $reason, $this->mlsimport_exit_survey_reasons(), true ); |
| 1567 |
} |
| 1568 |
|
| 1569 |
/** |
| 1570 |
* Whether the current admin request is the Import Task (mlsimport_item) |
| 1571 |
* post edit screen. Used to scope the Select2 asset enqueue so the |
| 1572 |
* searchable-select library does not load across all of wp-admin. |
| 1573 |
* |
| 1574 |
* @param string $hook_suffix Current admin page hook suffix. |
| 1575 |
* @param string $post_type Post type of the screen being rendered. |
| 1576 |
*/ |
| 1577 |
private function mlsimport_is_import_task_edit_screen( string $hook_suffix, string $post_type ): bool { |
| 1578 |
return 'mlsimport_item' === $post_type |
| 1579 |
&& in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true ); |
| 1580 |
} |
| 1581 |
|
| 1582 |
/** |
| 1583 |
* Extra CSS class for an Import Task select field. City and County lists |
| 1584 |
* can hold 300+ entries, so they are upgraded to a searchable multi-select |
| 1585 |
* (Select2) via this marker class; every other field keeps the plain select. |
| 1586 |
* |
| 1587 |
* @param string $field_key Field key from the $field_import definition. |
| 1588 |
* @return string Leading-space class string, or '' when not searchable. |
| 1589 |
*/ |
| 1590 |
private function mlsimport_searchable_select_class( string $field_key ): string { |
| 1591 |
return in_array( $field_key, array( 'City', 'CountyOrParish' ), true ) |
| 1592 |
? ' mlsimport-searchable-select' |
| 1593 |
: ''; |
| 1594 |
} |
| 1595 |
|
| 1596 |
/** |
| 1597 |
* Exit-survey testable core: resolve identity and count, build payload. |
| 1598 |
* |
| 1599 |
* Calls no dying functions — the AJAX wrapper handles nonce/capability |
| 1600 |
* and wp_send_json_*. Always returns the payload to transmit. |
| 1601 |
* |
| 1602 |
* @param array $input Sanitized survey input (reason, details). |
| 1603 |
*/ |
| 1604 |
private function mlsimport_exit_survey_record( array $input ): array { |
| 1605 |
$opts = get_option( 'mlsimport_admin_options', array() ); |
| 1606 |
if ( empty( $opts['mlsimport_install_uuid'] ) ) { |
| 1607 |
$opts['mlsimport_install_uuid'] = wp_generate_uuid4(); |
| 1608 |
update_option( 'mlsimport_admin_options', $opts ); |
| 1609 |
} |
| 1610 |
|
| 1611 |
$count = (int) get_option( 'mlsimport_deactivation_count', 0 ) + 1; |
| 1612 |
update_option( 'mlsimport_deactivation_count', $count ); |
| 1613 |
|
| 1614 |
$reason = (string) ( $input['reason'] ?? '' ); |
| 1615 |
$options = $this->get_exit_survey_options(); |
| 1616 |
|
| 1617 |
return array( |
| 1618 |
'event_type' => 'exit_survey', |
| 1619 |
'reason' => $reason, |
| 1620 |
'reason_label' => $options[ $reason ] ?? '', |
| 1621 |
'details' => (string) ( $input['details'] ?? '' ), |
| 1622 |
'account' => (string) ( $opts['mlsimport_username'] ?? '' ), |
| 1623 |
'install_uuid' => $opts['mlsimport_install_uuid'], |
| 1624 |
'deactivation_count' => $count, |
| 1625 |
'environment' => wp_get_environment_type(), |
| 1626 |
'site_url' => home_url(), |
| 1627 |
'admin_email' => (string) get_option( 'admin_email' ), |
| 1628 |
'timestamp' => time(), |
| 1629 |
); |
| 1630 |
} |
| 1631 |
|
| 1632 |
/** |
| 1633 |
* The known exit-survey reason keys — the single source of truth. |
| 1634 |
* |
| 1635 |
* Pure: keys only, no labels, no translation. The label map |
| 1636 |
* (get_exit_survey_options) builds on top of this for the modal. |
| 1637 |
* |
| 1638 |
* @return string[] |
| 1639 |
*/ |
| 1640 |
private function mlsimport_exit_survey_reasons(): array { |
| 1641 |
return array( |
| 1642 |
'built_website', |
| 1643 |
'no_leads', |
| 1644 |
'technical_issues', |
| 1645 |
'too_expensive', |
| 1646 |
'switched_tool', |
| 1647 |
'other', |
| 1648 |
); |
| 1649 |
} |
| 1650 |
|
| 1651 |
/** |
| 1652 |
* Exit-survey reason key => display label, for the modal and payload. |
| 1653 |
* |
| 1654 |
* Uses translation, so it is not exercised by the pure unit suite — |
| 1655 |
* is_valid_reason() relies on mlsimport_exit_survey_reasons() instead. |
| 1656 |
* |
| 1657 |
* @return array<string,string> |
| 1658 |
*/ |
| 1659 |
private function get_exit_survey_options(): array { |
| 1660 |
return array( |
| 1661 |
'built_website' => esc_html__( "Built the website, don't need ongoing sync", 'mlsimport' ), |
| 1662 |
'no_leads' => esc_html__( 'Not getting leads from my site', 'mlsimport' ), |
| 1663 |
'technical_issues' => esc_html__( "Technical issues I couldn't fix", 'mlsimport' ), |
| 1664 |
'too_expensive' => esc_html__( 'Too expensive', 'mlsimport' ), |
| 1665 |
'switched_tool' => esc_html__( 'Switched to another tool', 'mlsimport' ), |
| 1666 |
'other' => esc_html__( 'Other', 'mlsimport' ), |
| 1667 |
); |
| 1668 |
} |
| 1669 |
|
| 1670 |
|
| 1671 |
|
| 1672 |
|
| 1673 |
|
| 1674 |
|
| 1675 |
/** |
| 1676 |
* Return a valid SaaS API bearer token, using the cached transient when |
| 1677 |
* present, otherwise requesting a fresh one and caching it for ~58 minutes. |
| 1678 |
* |
| 1679 |
* @since 4.0.1 |
| 1680 |
* @return string|array The token string, or the raw answer/'' on failure. |
| 1681 |
*/ |
| 1682 |
public function mlsimport_saas_get_mls_api_token_from_transient() { |
| 1683 |
|
| 1684 |
// Prefer the cached token. |
| 1685 |
$token = get_transient( 'mlsimport_saas_token' ); |
| 1686 |
|
| 1687 |
// Cache miss/empty: request a new token and cache it on success. |
| 1688 |
if ( false === $token || '' === $token ) { |
| 1689 |
$token_json_answer = $this->mlsimport_saas_get_mls_api_token(); |
| 1690 |
|
| 1691 |
if ( isset( $token_json_answer['success'] ) && true === $token_json_answer['success'] ) { |
| 1692 |
$token = $token_json_answer['token']; |
| 1693 |
|
| 1694 |
// 3500s < the token's 1h life, leaving headroom before expiry. |
| 1695 |
set_transient( 'mlsimport_saas_token', $token, 3500 ); |
| 1696 |
} |
| 1697 |
} |
| 1698 |
|
| 1699 |
return $token; |
| 1700 |
} |
| 1701 |
|
| 1702 |
|
| 1703 |
/** |
| 1704 |
* Request a fresh SaaS API token using the stored account username/password. |
| 1705 |
* |
| 1706 |
* If the selected MLS changed since the last run, all cached token/metadata |
| 1707 |
* transients and the field-select option are purged first so nothing leaks |
| 1708 |
* across providers. Returns '' when credentials are missing. |
| 1709 |
* |
| 1710 |
* @since 4.0.1 |
| 1711 |
* @return array|string The 'token' API response, or '' when unconfigured. |
| 1712 |
*/ |
| 1713 |
protected function mlsimport_saas_get_mls_api_token() { |
| 1714 |
$values = array(); |
| 1715 |
$options = get_option( $this->plugin_name . '_admin_options' ); |
| 1716 |
|
| 1717 |
// Check if the MLS provider has changed since the last run |
| 1718 |
$prev_mls = get_option( 'mlsimport_prev_mls_name', '' ); |
| 1719 |
|
| 1720 |
|
| 1721 |
$username = ''; |
| 1722 |
if ( isset( $options['mlsimport_username'] ) ) { |
| 1723 |
$username = sanitize_text_field( trim( $options['mlsimport_username'] ) ); |
| 1724 |
} |
| 1725 |
|
| 1726 |
$password = ''; |
| 1727 |
if ( isset( $options['mlsimport_password'] ) ) { |
| 1728 |
$password = sanitize_text_field( trim( $options['mlsimport_password'] ) ); |
| 1729 |
} |
| 1730 |
$mls_name = ''; |
| 1731 |
if ( isset( $options['mlsimport_mls_name'] ) ) { |
| 1732 |
$mls_name = sanitize_text_field( trim( $options['mlsimport_mls_name'] ) ); |
| 1733 |
} |
| 1734 |
|
| 1735 |
$mls_token = ''; |
| 1736 |
if ( isset( $options['mlsimport_mls_token'] ) ) { |
| 1737 |
$mls_token = sanitize_text_field( trim( $options['mlsimport_mls_token'] ) ); |
| 1738 |
} |
| 1739 |
|
| 1740 |
// Provider switch detected: purge all cross-provider cached state. |
| 1741 |
if ( $prev_mls !== '' && $prev_mls !== $mls_name ) { |
| 1742 |
delete_transient( 'mlsimport_token_request' ); |
| 1743 |
delete_transient( 'mlsimport_metadata_api_call_data_service_property' ); |
| 1744 |
delete_transient( 'mls_import_meta_enums' ); |
| 1745 |
delete_transient( 'mls_import_meta' ); |
| 1746 |
delete_transient( 'mlsimport_plugin_data_schema' ); |
| 1747 |
delete_transient( 'mlsimport_ready_to_go_mlsimport_data' ); |
| 1748 |
delete_transient( 'mlsimport_saas_token' ); |
| 1749 |
|
| 1750 |
delete_option( 'mlsimport_mls_metadata_populated' ); |
| 1751 |
|
| 1752 |
delete_option( 'mlsimport_admin_fields_select' ); |
| 1753 |
} |
| 1754 |
|
| 1755 |
// Remember the current MLS so the next call can detect a switch. |
| 1756 |
update_option( 'mlsimport_prev_mls_name', $mls_name ); |
| 1757 |
|
| 1758 |
|
| 1759 |
|
| 1760 |
// Credentials to exchange for a token. |
| 1761 |
$values['username'] = $username; |
| 1762 |
$values['password'] = $password; |
| 1763 |
|
| 1764 |
// No account credentials -> nothing to request. |
| 1765 |
if ( '' === $username || '' === $password ) { |
| 1766 |
return ''; |
| 1767 |
} |
| 1768 |
|
| 1769 |
// POST to the SaaS 'token' endpoint and return its response. |
| 1770 |
$theme_Start = new ThemeImport(); |
| 1771 |
$answer = $theme_Start::globalApiRequestSaas( 'token', $values, 'POST' ); |
| 1772 |
|
| 1773 |
|
| 1774 |
|
| 1775 |
return $answer; |
| 1776 |
} |
| 1777 |
|
| 1778 |
|
| 1779 |
|
| 1780 |
|
| 1781 |
|
| 1782 |
|
| 1783 |
/** |
| 1784 |
* Register the "Set Import data" metabox on the mlsimport_item post type. |
| 1785 |
* |
| 1786 |
* @since 3.0.1 |
| 1787 |
*/ |
| 1788 |
public function mlsimport_item_product_metaboxes() { |
| 1789 |
// The metabox renders the import-parameter form for an Import Task. |
| 1790 |
add_meta_box( 'mlsimport_item_metaboxes-sectionid', __( 'Set Import data', 'mlsimport' ), array( $this, 'mlsimport_saas_display_meta_options' ), 'mlsimport_item', 'normal', 'default' ); |
| 1791 |
} |
| 1792 |
|
| 1793 |
|
| 1794 |
|
| 1795 |
/** |
| 1796 |
* Save the Import Task metabox fields to post meta (save_post callback). |
| 1797 |
* |
| 1798 |
* Only acts on mlsimport_item posts. Sanitizes and stores each posted |
| 1799 |
* whitelisted key; separately, any "blank_keys" absent from the POST (e.g. |
| 1800 |
* unchecked multi-selects) are explicitly reset to '' so cleared selections |
| 1801 |
* actually clear. |
| 1802 |
* |
| 1803 |
* @param int $post_id Post being saved. |
| 1804 |
* @param WP_Post $post Post object. |
| 1805 |
* @since 3.0.1 |
| 1806 |
*/ |
| 1807 |
public function mlsimport_item_product_save_metaboxes( $post_id, $post ) { |
| 1808 |
|
| 1809 |
// Guard against non-post contexts. |
| 1810 |
if ( ! is_object( $post ) || ! isset( $post->post_type ) ) { |
| 1811 |
return; |
| 1812 |
} |
| 1813 |
|
| 1814 |
// Only handle Import Task posts. |
| 1815 |
if ( 'mlsimport_item' !== $post->post_type ) { |
| 1816 |
return; |
| 1817 |
} |
| 1818 |
|
| 1819 |
// Never persist metabox fields from autosaves or revision saves. |
| 1820 |
if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) ) { |
| 1821 |
return; |
| 1822 |
} |
| 1823 |
|
| 1824 |
// The nonce rendered by mlsimport_saas_display_meta_options(). |
| 1825 |
if ( ! isset( $_POST['estate_agent_noncename'] ) |
| 1826 |
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['estate_agent_noncename'] ) ), plugin_basename( __FILE__ ) ) ) { |
| 1827 |
return; |
| 1828 |
} |
| 1829 |
|
| 1830 |
// Import Tasks are admin-only: require edit rights on this task. |
| 1831 |
if ( ! current_user_can( 'edit_post', $post_id ) ) { |
| 1832 |
return; |
| 1833 |
} |
| 1834 |
|
| 1835 |
// Every import-parameter meta key this metabox may write. |
| 1836 |
$allowed_keys = array( |
| 1837 |
'mlsimport_item_how_many', |
| 1838 |
'mlsimport_item_title_format', |
| 1839 |
'mlsimport_item_agent', |
| 1840 |
'mlsimport_item_use_mls_agent', |
| 1841 |
'mlsimport_item_property_status', |
| 1842 |
'mlsimport_item_property_user', |
| 1843 |
'mlsimport_item_min_price', |
| 1844 |
'mlsimport_item_max_price', |
| 1845 |
'mlsimport_item_city_check', |
| 1846 |
'mlsimport_item_city', |
| 1847 |
'mlsimport_item_city[]', |
| 1848 |
'mlsimport_item_countyorparish_check', |
| 1849 |
'mlsimport_item_countyorparish', |
| 1850 |
'mlsimport_item_mlsstatus_check', |
| 1851 |
'mlsimport_item_mlsstatus', |
| 1852 |
'mlsimport_item_propertysubtype_check', |
| 1853 |
'mlsimport_item_propertysubtype', |
| 1854 |
'mlsimport_item_propertytype_check', |
| 1855 |
'mlsimport_item_propertytype', |
| 1856 |
'mlsimport_item_standardstatus_check', |
| 1857 |
'mlsimport_item_standardstatus', |
| 1858 |
'mlsimport_item_standardstatusprotect_check', |
| 1859 |
'mlsimport_item_standardstatusprotect', |
| 1860 |
|
| 1861 |
'mlsimport_item_internetentirelistingdisplayyn', |
| 1862 |
'mlsimport_item_internetaddressdisplayyn', |
| 1863 |
'mlsimport_item_stat_cron', |
| 1864 |
'mlsimport_item_listagentkey', |
| 1865 |
'mlsimport_item_listagentmlsid', |
| 1866 |
'mlsimport_item_buyeragentmlsid', |
| 1867 |
'mlsimport_item_listofficekey', |
| 1868 |
'mlsimport_item_postalcode', |
| 1869 |
'mlsimport_item_listofficemlsid', |
| 1870 |
'mlsimport_item_listingid', |
| 1871 |
'mlsimport_item_extracity', |
| 1872 |
'mlsimport_item_extracounty', |
| 1873 |
'mlsimport_item_exclude_listofficemlsid', |
| 1874 |
'mlsimport_item_exclude_listofficekey', |
| 1875 |
'mlsimport_item_exclude_listagentmlsid', |
| 1876 |
'mlsimport_item_exclude_listagentkey', |
| 1877 |
'mlsimport_item_customparameters', |
| 1878 |
'mlsimport_item_mlsareamajor', |
| 1879 |
'mlsimport_item_subdivisionname', |
| 1880 |
); |
| 1881 |
|
| 1882 |
|
| 1883 |
|
| 1884 |
|
| 1885 |
// Store each posted key (recursively sanitized; key sanitized too). |
| 1886 |
foreach ( $allowed_keys as $key => $key_value ) { |
| 1887 |
if( isset($_POST[$key_value]) ){ |
| 1888 |
$postmeta = mlsimport_sanitize_multi_dimensional_array ( $_POST[$key_value] ) ; |
| 1889 |
update_post_meta( $post_id, sanitize_key( $key_value ), $postmeta ); |
| 1890 |
} |
| 1891 |
|
| 1892 |
} |
| 1893 |
|
| 1894 |
// Keys that must be reset to '' when omitted from the POST (cleared). |
| 1895 |
$blank_keys = array( |
| 1896 |
'mlsimport_item_use_mls_agent', |
| 1897 |
'mlsimport_item_standardstatus', |
| 1898 |
'mlsimport_item_standardstatusprotect', |
| 1899 |
'mlsimport_item_city', |
| 1900 |
'mlsimport_item_countyorparish', |
| 1901 |
'mlsimport_item_propertysubtype', |
| 1902 |
'mlsimport_item_propertytype', |
| 1903 |
'mlsimport_item_standardstatus', |
| 1904 |
'mlsimport_item_listingid', |
| 1905 |
'mlsimport_item_customparameters', |
| 1906 |
'mlsimport_item_mlsareamajor', |
| 1907 |
'mlsimport_item_subdivisionname', |
| 1908 |
|
| 1909 |
); |
| 1910 |
|
| 1911 |
// Reset any whitelisted-blank key that was not submitted this save. |
| 1912 |
foreach ( $blank_keys as $key ) { |
| 1913 |
if ( ! isset( $_POST[ $key ] ) ) { |
| 1914 |
update_post_meta( $post_id, $key, '' ); |
| 1915 |
} |
| 1916 |
} |
| 1917 |
|
| 1918 |
|
| 1919 |
} |
| 1920 |
|
| 1921 |
|
| 1922 |
/** |
| 1923 |
* Render the Import Task metabox content. |
| 1924 |
* |
| 1925 |
* Ensures a live SaaS token + MLS connection, prints a warning and stops if |
| 1926 |
* either is missing, otherwise runs a listing count request and hands off to |
| 1927 |
* generateMetaOptionsHtml() to build the parameter form. |
| 1928 |
* |
| 1929 |
* @param WP_Post $post The post object. |
| 1930 |
*/ |
| 1931 |
public function mlsimport_saas_display_meta_options($post) { |
| 1932 |
// Nonce for the metabox save. |
| 1933 |
wp_nonce_field(plugin_basename(__FILE__), 'estate_agent_noncename'); |
| 1934 |
global $mlsimport; |
| 1935 |
|
| 1936 |
// Ensure a token, read the cached connection flag, print env warnings. |
| 1937 |
$token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient(); |
| 1938 |
$is_mls_connected = get_option('mlsimport_connection_test', ''); |
| 1939 |
$mlsimport->admin->mlsimport_saas_setting_up(); |
| 1940 |
|
| 1941 |
// If not marked connected, run the connection test once and re-read the flag. |
| 1942 |
if ('yes' !== $is_mls_connected) { |
| 1943 |
$mlsimport->admin->mlsimport_saas_check_mls_connection(); |
| 1944 |
$is_mls_connected = get_option('mlsimport_connection_test', ''); |
| 1945 |
} |
| 1946 |
|
| 1947 |
// No token -> account not authenticated; stop with a notice. |
| 1948 |
if (trim($token) === '') { |
| 1949 |
echo '<div class="mlsimport_warning">' . esc_html__('You are not connected to MlsImport - Please check your Username and Password.', 'mlsimport') . '</div>'; |
| 1950 |
return; |
| 1951 |
} |
| 1952 |
|
| 1953 |
// Token OK but MLS connection failed -> stop with a notice. |
| 1954 |
if ('yes' !== $is_mls_connected) { |
| 1955 |
echo '<div class="mlsimport_warning">' . esc_html__('The connection to your MLS was NOT succesful. Please check the authentication token is correct and check your MLS Data Access Application is approved.', 'mlsimport') . '</div>'; |
| 1956 |
return; |
| 1957 |
} |
| 1958 |
|
| 1959 |
// Load current task settings for the form. |
| 1960 |
$postId = $post->ID; |
| 1961 |
$mlsimportItemHowMany = esc_html(get_post_meta($postId, 'mlsimport_item_how_many', true)); |
| 1962 |
$mlsimportItemStatCron = esc_html(get_post_meta($postId, 'mlsimport_item_stat_cron', true)); |
| 1963 |
$lastDate = get_post_meta($postId, 'mlsimport_last_date', true); |
| 1964 |
$status = get_option('mlsimport_force_stop_' . $postId); |
| 1965 |
$fieldImport = $this->mlsimport_saas_return_mls_fields(); |
| 1966 |
$options = get_option('mlsimport_admin_options'); |
| 1967 |
$mlsimportMlsId = isset($options['mlsimport_mls_name']) && $options['mlsimport_mls_name'] !== '' |
| 1968 |
|
| 1969 |
? intval($options['mlsimport_mls_name']) |
| 1970 |
: 0; |
| 1971 |
|
| 1972 |
// Ask the MLS how many listings currently match this task. |
| 1973 |
$mlsRequest = $this->mlsimport_make_listing_requests($postId); |
| 1974 |
// print_r($mlsRequest); |
| 1975 |
|
| 1976 |
// Surface any API error message inline. |
| 1977 |
$hasError = isset($mlsRequest['success']) && !$mlsRequest['success']; |
| 1978 |
if ($hasError) { |
| 1979 |
echo '<div class="mlsimport_warning">' . esc_html($mlsRequest['message']) . '</div>'; |
| 1980 |
} |
| 1981 |
|
| 1982 |
// 'none' means no results key -> likely an expired token; re-test. |
| 1983 |
$foundItems = isset($mlsRequest['results']) ? intval($mlsRequest['results']) : 'none'; |
| 1984 |
if ($foundItems === 'none') { |
| 1985 |
$mlsimport->admin->mlsimport_saas_check_mls_connection(); |
| 1986 |
esc_html_e('Your Token was expired. Please refresh the page to renew it wait while we renew it.', 'mlsimport'); |
| 1987 |
} |
| 1988 |
|
| 1989 |
// Build and print the parameter form. |
| 1990 |
echo $this->generateMetaOptionsHtml($postId, $foundItems, $lastDate, $mlsimportItemHowMany, $mlsimportItemStatCron, $mlsimportMlsId, $fieldImport, $hasError); |
| 1991 |
} |
| 1992 |
|
| 1993 |
|
| 1994 |
|
| 1995 |
|
| 1996 |
/** |
| 1997 |
* Generate Meta Options HTML |
| 1998 |
* |
| 1999 |
* @param int $postId The post ID. |
| 2000 |
* @param int $foundItems The number of found items. |
| 2001 |
* @param string $lastDate The last date checked. |
| 2002 |
* @param string $mlsimportItemHowMany How many items to import. |
| 2003 |
* @param string $mlsimportItemStatCron The status of the cron job. |
| 2004 |
* @param int $mlsimportMlsId The MLS import ID. |
| 2005 |
* @param array $fieldImport The fields to import. |
| 2006 |
* @return string The generated HTML. |
| 2007 |
*/ |
| 2008 |
private function generateMetaOptionsHtml($postId, $foundItems, $lastDate, $mlsimportItemHowMany, $mlsimportItemStatCron, $mlsimportMlsId, $fieldImport, $hasError = false) { |
| 2009 |
|
| 2010 |
|
| 2011 |
// Buffer all HTML and return it as a string. |
| 2012 |
ob_start(); |
| 2013 |
|
| 2014 |
// Decode the saved MLS enums so City/County/PropertyType options can |
| 2015 |
// carry their human-readable labels alongside the raw values. |
| 2016 |
$metadata_api_call_city = array(); |
| 2017 |
$metadata_api_call_county = array(); |
| 2018 |
$metadata_api_call_property_type = array(); |
| 2019 |
$mlsimport_mls_metadata_mls_enums = get_option('mlsimport_mls_metadata_mls_enums', ''); |
| 2020 |
if ('' !== $mlsimport_mls_metadata_mls_enums) { |
| 2021 |
$metadata_api_call_full = json_decode($mlsimport_mls_metadata_mls_enums, true); |
| 2022 |
if (isset($metadata_api_call_full['global_array']['PropertyEnums'])) { |
| 2023 |
$property_enums = $metadata_api_call_full['global_array']['PropertyEnums']; |
| 2024 |
if (isset($property_enums['City']) && is_array($property_enums['City'])) { |
| 2025 |
$metadata_api_call_city = $property_enums['City']; |
| 2026 |
} |
| 2027 |
|
| 2028 |
if (isset($property_enums['CountyOrParish']) && is_array($property_enums['CountyOrParish'])) { |
| 2029 |
$metadata_api_call_county = $property_enums['CountyOrParish']; |
| 2030 |
} |
| 2031 |
|
| 2032 |
if (isset($property_enums['PropertyType']) && is_array($property_enums['PropertyType'])) { |
| 2033 |
$metadata_api_call_property_type = $property_enums['PropertyType']; |
| 2034 |
} |
| 2035 |
} |
| 2036 |
} |
| 2037 |
|
| 2038 |
?> |
| 2039 |
<div class="mlsimport_item_search_url" style="display:none;"><?php echo esc_html__('Last date/time we check :', 'mlsimport') . ' ' . esc_html($lastDate); ?></div> |
| 2040 |
<ul> |
| 2041 |
<li>1. Set the import parameters.</li> |
| 2042 |
<li>2. Hit Publish or Update, otherwise import will not work correctly.</li> |
| 2043 |
<li>3. Click the Start Import button. Most MLS limit the import number to 1000. If you need to import more create additional import items.</li> |
| 2044 |
<li>4. Press the Update button after you make any change in the import settings.</li> |
| 2045 |
</ul> |
| 2046 |
|
| 2047 |
<?php if (is_numeric($foundItems) && $foundItems >= 500): ?> |
| 2048 |
<div class="mlsimport_notification"> |
| 2049 |
<?php esc_html_e('You found a large number of listings. While MlsImport import can handle such a large number, you need to make sure that your server can do this operation. This import will take some time. Make sure your server has the capacity, there are no time limits for a long-running process and consider splitting the import between multiple MLS Import Tasks.', 'mlsimport'); ?> |
| 2050 |
</div> |
| 2051 |
<?php endif; ?> |
| 2052 |
|
| 2053 |
<div class="mlsimport_import_no"> |
| 2054 |
<?php esc_html_e('We found', 'mlsimport'); ?> |
| 2055 |
<strong><?php echo esc_html($foundItems); ?></strong> listings. If you decide to import all of them make sure your server database can handle the load. Please do a database backup before initial import. |
| 2056 |
</div> |
| 2057 |
|
| 2058 |
<fieldset class="mlsimport-fieldset"> |
| 2059 |
<label class="mlsimport-label" for="mlsimport_item_how_many"> |
| 2060 |
<?php esc_html_e('How Many to import. Use 0 if you want to import all listings found.', 'mlsimport'); ?> |
| 2061 |
</label> |
| 2062 |
<input type="text" id="mlsimport_item_how_many" name="mlsimport_item_how_many" |
| 2063 |
class="mlsimport-input mlsimport-2025-input " value="<?php echo esc_attr($mlsimportItemHowMany); ?>"/> |
| 2064 |
</fieldset> |
| 2065 |
|
| 2066 |
<fieldset class="mlsimport-fieldset mlsimport_auto_switch"> |
| 2067 |
<?php esc_html_e('Enable Auto Update every hour?', 'mlsimport'); ?> |
| 2068 |
<label class="mlsimport_switch"> |
| 2069 |
<input type="hidden" value="0" name="mlsimport_item_stat_cron"> |
| 2070 |
<input type="checkbox" class="mlsimport-import-checkbox" value="1" name="mlsimport_item_stat_cron"<?php if (intval($mlsimportItemStatCron) !== 0) echo esc_html(' checked'); ?>> |
| 2071 |
<span class="slider round"></span> |
| 2072 |
</label> |
| 2073 |
</fieldset> |
| 2074 |
|
| 2075 |
<?php if ($mlsimportItemStatCron !== '' && !$hasError): ?> |
| 2076 |
<div id="mlsimport_item_status">Ready to import!</div> |
| 2077 |
<div id="mlsimport_item_progress" class="mlsimport-progress-bar"> |
| 2078 |
<div class="mlsimport-progress-bar-inner" style="width:0%;"></div> |
| 2079 |
</div> |
| 2080 |
<input class="button mlsimport_button save_data " type="button" id="mlsimport-start_item" |
| 2081 |
data-post-number="<?php echo intval($foundItems); ?>" |
| 2082 |
data-post_id="<?php echo intval($postId); ?>" value="Start Import"> |
| 2083 |
<input class="button mlsimport_button error_action" type="button" id="mlsimport_stop_item" |
| 2084 |
data-post-number="<?php echo intval($foundItems); ?>" |
| 2085 |
data-post_id="<?php echo intval($postId); ?>" value="Stop Import"> |
| 2086 |
<?php endif; ?> |
| 2087 |
|
| 2088 |
<input type="hidden" id="mlsimport_item_actions" value="<?php echo esc_attr(wp_create_nonce("mlsimport_item_actions")); ?>"/> |
| 2089 |
<div class="mlsimport_param_wrapper"><h2><?php esc_html_e('Import Parameters', 'mlsimport'); ?></h2> |
| 2090 |
|
| 2091 |
<?php |
| 2092 |
$mlsimportItemTitleFormat = esc_html(get_post_meta($postId, 'mlsimport_item_title_format', true)); |
| 2093 |
?> |
| 2094 |
|
| 2095 |
<fieldset class="mlsimport-fieldset"> |
| 2096 |
<label class="mlsimport-label" for="mlsimport_item_title_format"> |
| 2097 |
<?php esc_html_e('Title Format', 'mlsimport'); ?> |
| 2098 |
</label> |
| 2099 |
|
| 2100 |
<p class="mlsimport-exp"><?php esc_html_e('You can use {Address}, {City}, {CountyOrParish}, {StateOrProvince}, {PostalCode}, {PropertyType}, {Bedrooms}, {Bathrooms}, {ListingKey}, {ListingId},{StreetNumberNumeric} or {StreetName}', 'mlsimport'); ?></p> |
| 2101 |
<input type="text" id="mlsimport_item_title_format" name="mlsimport_item_title_format" |
| 2102 |
class="mlsimport-input mlsimport-2025-input" |
| 2103 |
value="<?php echo '' !== $mlsimportItemTitleFormat ? trim(esc_html($mlsimportItemTitleFormat)) : esc_html('{Address},{City},{CountyOrParish},{PropertyType}'); ?>"/> |
| 2104 |
</fieldset> |
| 2105 |
|
| 2106 |
<?php |
| 2107 |
$mlsimportItemAgent = esc_html(get_post_meta($postId, 'mlsimport_item_agent', true)); |
| 2108 |
?> |
| 2109 |
|
| 2110 |
<fieldset class="mlsimport-fieldset"> |
| 2111 |
<label class="mlsimport-label" for="mlsimport_item_agent"> |
| 2112 |
<?php esc_html_e('Select Agent', 'mlsimport'); ?> |
| 2113 |
</label> |
| 2114 |
<select class="mlsimport-select mlsimport-2025-select" name="mlsimport_item_agent" id="mlsimport_item_agent"> |
| 2115 |
<?php |
| 2116 |
$permitedTags = mlsimport_allowed_html_tags_content(); |
| 2117 |
$selectAgent =$this->theme_importer->mlsimportSaasThemeImportSelectAgent($mlsimportItemAgent); |
| 2118 |
print wp_kses($selectAgent, $permitedTags); |
| 2119 |
?> |
| 2120 |
</select> |
| 2121 |
</fieldset> |
| 2122 |
|
| 2123 |
<?php if ( mlsimport_is_standalone_mode() ) : |
| 2124 |
$mlsimportItemUseMlsAgent = get_post_meta($postId, 'mlsimport_item_use_mls_agent', true); |
| 2125 |
?> |
| 2126 |
<fieldset class="mlsimport-fieldset"> |
| 2127 |
<label class="mlsimport-label" for="mlsimport_item_use_mls_agent"> |
| 2128 |
<?php esc_html_e('Which agent shows on these properties', 'mlsimport'); ?> |
| 2129 |
</label> |
| 2130 |
<p class="mlsimport-exp"><?php esc_html_e('Off: every property from this task shows the agent you picked above. On: each property shows its own listing agent instead — the name, phone, email and office that came with that listing in the MLS feed, and the agent picked above is ignored. No agent profiles are created either way.', 'mlsimport'); ?></p> |
| 2131 |
<label class="mlsimport-switch"> |
| 2132 |
<input type="checkbox" id="mlsimport_item_use_mls_agent" name="mlsimport_item_use_mls_agent" value="1" <?php checked('1', (string) $mlsimportItemUseMlsAgent); ?> /> |
| 2133 |
<?php esc_html_e('Show each property\'s own listing agent from the MLS feed', 'mlsimport'); ?> |
| 2134 |
</label> |
| 2135 |
</fieldset> |
| 2136 |
<?php endif; ?> |
| 2137 |
|
| 2138 |
<?php |
| 2139 |
$mlsimportItemPropertyStatus = esc_html(get_post_meta($postId, 'mlsimport_item_property_status', true)); |
| 2140 |
if ('' === $mlsimportItemPropertyStatus) { |
| 2141 |
$mlsimportItemPropertyStatus = 'publish'; |
| 2142 |
} |
| 2143 |
$statusArray = array('publish', 'draft'); |
| 2144 |
?> |
| 2145 |
<fieldset class="mlsimport-fieldset"> |
| 2146 |
<label class="mlsimport-label" for="mlsimport_item_property_status"> |
| 2147 |
<?php esc_html_e('Select Property Status on import', 'mlsimport'); ?> |
| 2148 |
</label> |
| 2149 |
<select class="mlsimport-select mlsimport-2025-select" name="mlsimport_item_property_status" id="mlsimport_item_property_status"> |
| 2150 |
<?php foreach ($statusArray as $value): ?> |
| 2151 |
<option value="<?php echo esc_attr($value); ?>" <?php if ($value === $mlsimportItemPropertyStatus) echo esc_html('selected'); ?>> |
| 2152 |
<?php echo esc_html($value); ?> |
| 2153 |
</option> |
| 2154 |
<?php endforeach; ?> |
| 2155 |
</select> |
| 2156 |
</fieldset> |
| 2157 |
|
| 2158 |
<?php |
| 2159 |
$mlsimportItemPropertyUser = esc_html(get_post_meta($postId, 'mlsimport_item_property_user', true)); |
| 2160 |
?> |
| 2161 |
<fieldset class="mlsimport-fieldset"> |
| 2162 |
<label class="mlsimport-label" for="mlsimport_item_property_user"> |
| 2163 |
<?php esc_html_e('User', 'mlsimport'); ?> |
| 2164 |
</label> |
| 2165 |
<select class="mlsimport-select mlsimport-2025-select" id="mlsimport_item_property_user" name="mlsimport_item_property_user"> |
| 2166 |
<?php |
| 2167 |
$selectUser = $this->theme_importer->mlsimportSaasThemeImportSelectUser($mlsimportItemPropertyUser); |
| 2168 |
print wp_kses($selectUser, $permitedTags); |
| 2169 |
?> |
| 2170 |
</select> |
| 2171 |
</fieldset> |
| 2172 |
|
| 2173 |
<?php |
| 2174 |
$mlsimportItemMinPrice = floatval(get_post_meta($postId, 'mlsimport_item_min_price', true)); |
| 2175 |
$mlsimportItemMaxPrice = floatval(get_post_meta($postId, 'mlsimport_item_max_price', true)); |
| 2176 |
if (0 === intval($mlsimportItemMaxPrice)) { |
| 2177 |
$mlsimportItemMaxPrice = 10000000; |
| 2178 |
} |
| 2179 |
?> |
| 2180 |
<fieldset class="mlsimport-fieldset"> |
| 2181 |
<label class="mlsimport-label"> |
| 2182 |
<?php esc_html_e('Price Between', 'mlsimport'); ?> |
| 2183 |
</label> |
| 2184 |
<input type="text" class="mlsimport-select mlsimport-input mlsimport-2025-input " id="mlsimport_item_min_price" name="mlsimport_item_min_price" value="<?php echo esc_attr($mlsimportItemMinPrice); ?>"> and |
| 2185 |
<input type="text" class="mlsimport-select mlsimport-input mlsimport-2025-input " id="mlsimport_item_max_price" name="mlsimport_item_max_price" value="<?php echo esc_attr($mlsimportItemMaxPrice); ?>"> |
| 2186 |
</fieldset> |
| 2187 |
|
| 2188 |
<?php |
| 2189 |
// Provider-specific tweaks to the field list before rendering. |
| 2190 |
$options = get_option($this->plugin_name . '_admin_options'); |
| 2191 |
|
| 2192 |
$mlsId = ''; |
| 2193 |
if (isset($options['mlsimport_mls_name'])) { |
| 2194 |
$mlsId = sanitize_text_field(trim($options['mlsimport_mls_name'])); |
| 2195 |
} |
| 2196 |
|
| 2197 |
// Rapattoni (5000+): PropertyType becomes single-select. |
| 2198 |
if ($mlsId > 5000) { |
| 2199 |
$fieldImport['PropertyType']['multiple'] = 'no'; |
| 2200 |
} |
| 2201 |
|
| 2202 |
|
| 2203 |
if ($mlsId >= 7000) { |
| 2204 |
// there is no such thing for realtor.ca |
| 2205 |
unset($fieldImport['PropertyType']); |
| 2206 |
} |
| 2207 |
|
| 2208 |
|
| 2209 |
|
| 2210 |
|
| 2211 |
// Render one fieldset per import parameter. |
| 2212 |
foreach ($fieldImport as $key => $field): |
| 2213 |
// Skip fields flagged hidden. |
| 2214 |
if (!empty($field['hidden'])) { |
| 2215 |
continue; |
| 2216 |
} |
| 2217 |
// Derive the meta key + its companion "_check" (select-all) key. |
| 2218 |
$nameCheck = strtolower('mlsimport_item_' . $key . '_check'); |
| 2219 |
$name = strtolower('mlsimport_item_' . $key); |
| 2220 |
|
| 2221 |
// Current saved value + select-all flag for this field. |
| 2222 |
$value = get_post_meta($postId, $name, true); |
| 2223 |
$valueCheck = get_post_meta($postId, $nameCheck, true); |
| 2224 |
// extraCity/extraCounty render as a toggle button, not a plain label. |
| 2225 |
$extraClass = ''; |
| 2226 |
if ('extraCity' === $key || 'extraCounty' === $key) { |
| 2227 |
$extraClass = ' mlsimport_hidden_field_button button mlsimport_button'; |
| 2228 |
} |
| 2229 |
?> |
| 2230 |
<fieldset class="mlsimport-fieldset"> |
| 2231 |
<label class="mlsimport-label <?php echo esc_attr($extraClass); ?>" for="<?php echo esc_attr($name); ?>"> |
| 2232 |
<?php echo esc_html($field['label']); ?> |
| 2233 |
</label> |
| 2234 |
<?php if ('extraCity' === $key || 'extraCounty' === $key): ?> |
| 2235 |
<div class="mlsimport-input-wrapper" style="display:none"> |
| 2236 |
<?php endif; ?> |
| 2237 |
<p class="mlsimport-exp"><?php echo wp_kses_post($this->mlsimport_notes_for_mls($mlsimportMlsId, $name, $field['description'])); ?> |
| 2238 |
<?php |
| 2239 |
// Whether the "select all" checkbox is currently on. |
| 2240 |
$isCheckboxAdmin = 0; |
| 2241 |
if (1 === intval($valueCheck)) { |
| 2242 |
$isCheckboxAdmin = 1; |
| 2243 |
} |
| 2244 |
|
| 2245 |
// Fields that must NOT offer a "select all" checkbox. |
| 2246 |
$selectAllNone = [ |
| 2247 |
'InternetAddressDisplayYN', |
| 2248 |
'InternetEntireListingDisplayYN', |
| 2249 |
'PostalCode', |
| 2250 |
'ListAgentKey', |
| 2251 |
'ListAgentMlsId', |
| 2252 |
'BuyerAgentMlsId', |
| 2253 |
'ListOfficeKey', |
| 2254 |
'ListOfficeMlsId', |
| 2255 |
'StandardStatus', |
| 2256 |
'ListingId', |
| 2257 |
'extraCity', |
| 2258 |
'extraCounty', |
| 2259 |
'Exclude_ListOfficeKey', |
| 2260 |
'Exclude_ListOfficeMlsId', |
| 2261 |
'Exclude_ListAgentKey', |
| 2262 |
'Exclude_ListAgentMlsId', |
| 2263 |
'CustomParameters', |
| 2264 |
'MLSAreaMajor', |
| 2265 |
'SubdivisionName', |
| 2266 |
]; |
| 2267 |
|
| 2268 |
if ($mlsId > 5000) { |
| 2269 |
$selectAllNone[] = 'PropertyType'; |
| 2270 |
} |
| 2271 |
|
| 2272 |
if (!in_array($key, $selectAllNone)): ?> |
| 2273 |
<?php |
| 2274 |
esc_html_e('- Or Select All ', 'mlsimport'); |
| 2275 |
|
| 2276 |
?> |
| 2277 |
<input type="hidden" name="<?php echo esc_attr($nameCheck); ?>" value="0"/> |
| 2278 |
<input type="checkbox" class="mlsimport-import-checkbox" name="<?php echo esc_attr($nameCheck); ?>" value="1" <?php print esc_attr(checked($isCheckboxAdmin, 1, 0)); ?>/> |
| 2279 |
<?php endif; ?> |
| 2280 |
</p> |
| 2281 |
|
| 2282 |
<?php |
| 2283 |
$permittedStatus = ['active', 'active under contract', 'coming soon', 'activeundercontract', 'comingsoon', 'pending']; |
| 2284 |
|
| 2285 |
if ($field['type'] === 'select'): ?> |
| 2286 |
<?php |
| 2287 |
// Multi-select fields need the multiple attr + [] name. |
| 2288 |
$multiple = ''; |
| 2289 |
if ('yes' === $field['multiple']) { |
| 2290 |
$multiple = 'multiple'; |
| 2291 |
$name .= '[]'; |
| 2292 |
} |
| 2293 |
|
| 2294 |
// Default StandardStatus to Active when nothing saved. |
| 2295 |
if ('StandardStatus' === $key && '' === $value) { |
| 2296 |
$value = ['Active']; |
| 2297 |
} |
| 2298 |
|
| 2299 |
|
| 2300 |
|
| 2301 |
// City/County lists can hold 300+ entries — render a filter |
| 2302 |
// input above the full native multi-select listbox. |
| 2303 |
$searchableClass = $this->mlsimport_searchable_select_class($key); |
| 2304 |
$isSearchable = '' !== $searchableClass; |
| 2305 |
$searchPlaceholder = $isSearchable |
| 2306 |
? esc_html__('Type to search…', 'mlsimport') |
| 2307 |
: ''; |
| 2308 |
|
| 2309 |
// Additional conditions can be placed here. |
| 2310 |
?> |
| 2311 |
<?php if ($isSearchable): ?> |
| 2312 |
<div class="mlsimport-selected-chips" aria-live="polite"></div> |
| 2313 |
<input type="text" class="mlsimport-select-search" placeholder="<?php echo esc_attr($searchPlaceholder); ?>" aria-label="<?php echo esc_attr($searchPlaceholder); ?>" autocomplete="off"> |
| 2314 |
<?php endif; ?> |
| 2315 |
<select class="mlsimport-select mlsimport-2025-select<?php echo esc_attr($searchableClass); ?>" id="<?php echo esc_attr($name); ?>" name="<?php echo esc_attr($name); ?>"<?php echo $isSearchable ? ' size="12"' : ''; ?> <?php echo esc_attr($multiple); ?>> |
| 2316 |
<?php foreach ($field['values'] as $selectKey): ?> |
| 2317 |
|
| 2318 |
<?php if ('' !== $selectKey): ?> |
| 2319 |
<?php |
| 2320 |
// Match saved value against the raw key AND its |
| 2321 |
// enum-mapped label, so either form stays selected. |
| 2322 |
$option_value = $selectKey; |
| 2323 |
$option_label = $selectKey; |
| 2324 |
$comparison_values = array($option_value); |
| 2325 |
|
| 2326 |
if ('City' === $key && isset($metadata_api_call_city[$selectKey])) { |
| 2327 |
$option_label = $selectKey; |
| 2328 |
$comparison_values[] = $metadata_api_call_city[$selectKey]; |
| 2329 |
} elseif ('CountyOrParish' === $key && isset($metadata_api_call_county[$selectKey])) { |
| 2330 |
$option_label = $selectKey; |
| 2331 |
$comparison_values[] = $metadata_api_call_county[$selectKey]; |
| 2332 |
} elseif ('PropertyType' === $key && isset($metadata_api_call_property_type[$selectKey])) { |
| 2333 |
$option_label = $selectKey; |
| 2334 |
$comparison_values[] = $metadata_api_call_property_type[$selectKey]; |
| 2335 |
} |
| 2336 |
|
| 2337 |
$comparison_values = array_values(array_unique(array_filter($comparison_values, static function ($compare_value) { |
| 2338 |
return '' !== $compare_value && null !== $compare_value; |
| 2339 |
}))); |
| 2340 |
|
| 2341 |
// Selected if any comparison value matches the saved |
| 2342 |
// value (array for multi-selects, scalar otherwise). |
| 2343 |
$is_selected = false; |
| 2344 |
if (is_array($value)) { |
| 2345 |
$is_selected = count(array_intersect($comparison_values, $value)) > 0; |
| 2346 |
} else { |
| 2347 |
$is_selected = in_array($value, $comparison_values, true); |
| 2348 |
} |
| 2349 |
?> |
| 2350 |
<option value="<?php echo esc_attr($option_value); ?>" <?php echo $is_selected ? 'selected' : ''; ?>> |
| 2351 |
<?php echo esc_html($option_label); ?> |
| 2352 |
</option> |
| 2353 |
<?php endif; ?> |
| 2354 |
|
| 2355 |
<?php endforeach; ?> |
| 2356 |
</select> |
| 2357 |
|
| 2358 |
<?php elseif ($field['type'] === 'input'): ?> |
| 2359 |
<input type="text" class="mlsimport-select mlsimport-input mlsimport-2025-input" id="<?php echo esc_attr($name); ?>" name="<?php echo esc_attr($name); ?>" value="<?php echo esc_attr($value); ?>"> |
| 2360 |
<?php endif; ?> |
| 2361 |
<?php if ('extraCity' === $key || 'extraCounty' === $key): ?> |
| 2362 |
</div> |
| 2363 |
<?php endif; ?> |
| 2364 |
</fieldset> |
| 2365 |
<?php endforeach; ?> |
| 2366 |
|
| 2367 |
</div> |
| 2368 |
<?php |
| 2369 |
// Return the buffered form markup. |
| 2370 |
return ob_get_clean(); |
| 2371 |
} |
| 2372 |
|
| 2373 |
|
| 2374 |
|
| 2375 |
|
| 2376 |
|
| 2377 |
|
| 2378 |
// Placeholder hook target for injecting additional Import Task fields (no-op). |
| 2379 |
public function mlsimport_add_extra_fields() { |
| 2380 |
} |
| 2381 |
|
| 2382 |
/** |
| 2383 |
* Per-field help text override, keyed by MLS + meta field. |
| 2384 |
* |
| 2385 |
* Currently only special-cases MLS 111 (Rae Edmonton), which has no status |
| 2386 |
* field; every other case returns the field's default description unchanged. |
| 2387 |
* |
| 2388 |
* @param int $mlsimport_mls_id Numeric MLS id. |
| 2389 |
* @param string $name Meta field name (e.g. mlsimport_item_standardstatus). |
| 2390 |
* @param string $description Default description to fall back to. |
| 2391 |
* @return string |
| 2392 |
*/ |
| 2393 |
function mlsimport_notes_for_mls( $mlsimport_mls_id, $name, $description ) { |
| 2394 |
// 111 - Rae Edmonton |
| 2395 |
|
| 2396 |
if ( 111 === intval($mlsimport_mls_id) && 'mlsimport_item_standardstatus' === $name ) { |
| 2397 |
return esc_html__( 'Your MLS does not use this field - all listings are considered Active.', 'mlsimport' ); |
| 2398 |
} else { |
| 2399 |
return $description; |
| 2400 |
} |
| 2401 |
} |
| 2402 |
|
| 2403 |
|
| 2404 |
/** |
| 2405 |
* Return the "last checked" timestamp for an Import Task, seeding it if unset. |
| 2406 |
* |
| 2407 |
* @param int $item_id Import Task post id. |
| 2408 |
* @return string A 'Y-m-d\TH:i' timestamp. |
| 2409 |
*/ |
| 2410 |
public function mlsimport_saas_get_last_date( $item_id ) { |
| 2411 |
// Stored watermark used as the modification-time filter for syncs. |
| 2412 |
$last_date = get_post_meta( $item_id, 'mlsimport_last_date', true ); |
| 2413 |
|
| 2414 |
// First run: initialize it. |
| 2415 |
if ( '' === $last_date ) { |
| 2416 |
$last_date = $this->mlsimport_saas_update_last_date( $item_id ); |
| 2417 |
} |
| 2418 |
return $last_date; |
| 2419 |
} |
| 2420 |
|
| 2421 |
|
| 2422 |
/** |
| 2423 |
* Set the Import Task's "last checked" watermark to 2 hours ago and store it. |
| 2424 |
* |
| 2425 |
* The 2-hour backdate provides overlap so listings modified right around the |
| 2426 |
* run boundary are not missed. Note: also echoes the value as a side effect. |
| 2427 |
* |
| 2428 |
* @param int $item_id Import Task post id. |
| 2429 |
* @return string The stored 'Y-m-d\TH:i' timestamp. |
| 2430 |
*/ |
| 2431 |
public function mlsimport_saas_update_last_date( $item_id ) { |
| 2432 |
|
| 2433 |
// Current site time minus 2 hours, formatted as an ISO-ish local stamp. |
| 2434 |
$unix_time = current_time( 'timestamp', 0 ) - ( 2 * 60 * 60 ); |
| 2435 |
print $last_date_to_save = date( 'Y-m-d\TH:i', $unix_time ); |
| 2436 |
update_post_meta( $item_id, 'mlsimport_last_date', $last_date_to_save ); |
| 2437 |
|
| 2438 |
return $last_date_to_save; |
| 2439 |
} |
| 2440 |
|
| 2441 |
|
| 2442 |
|
| 2443 |
|
| 2444 |
|
| 2445 |
/** |
| 2446 |
* Check and process MLSimport item for modified listings in the last 2 hours. |
| 2447 |
* Optimized for memory: logs memory, unsets large arrays, and triggers garbage collection. |
| 2448 |
* |
| 2449 |
* @param int $item_id |
| 2450 |
* @return int Number of listings found in the MLS feed, or 0 on failure. |
| 2451 |
*/ |
| 2452 |
public function mlsimport_saas_start_cron_links_per_item( int $item_id ): int { |
| 2453 |
// Log memory before start |
| 2454 |
|
| 2455 |
$found_items = 0; |
| 2456 |
|
| 2457 |
// The cron only runs on a task that has fully completed a previous |
| 2458 |
// import. That excludes a task never imported by hand (no |
| 2459 |
// 'mlsimport_spawn_status' meta) AND a task with a manual import in |
| 2460 |
// flight ('started'): starting a second loop there would make both |
| 2461 |
// runs write the same task/progress meta and corrupt each other. |
| 2462 |
if ( ! mlsimport_cron_should_process_task( get_post_meta( $item_id, 'mlsimport_spawn_status', true ) ) ) { |
| 2463 |
return 0; |
| 2464 |
} |
| 2465 |
|
| 2466 |
// Only pull listings modified since the task's watermark. |
| 2467 |
$last_date = $this->mlsimport_saas_get_last_date( $item_id ); |
| 2468 |
print 'MLSitem id: ' . $item_id . ' - '; |
| 2469 |
esc_html_e('date to consider: ','mlsimport'); |
| 2470 |
print esc_html($last_date) . '. '; |
| 2471 |
|
| 2472 |
// Make request to MLS API |
| 2473 |
$mlsrequest = $this->mlsimport_make_listing_requests( $item_id, $last_date, '', '', true ); |
| 2474 |
|
| 2475 |
// Success: record the count. Failure: drop the token (force refresh |
| 2476 |
// next run) and record the failure telemetry. |
| 2477 |
if ( isset( $mlsrequest['results'] ) ) { |
| 2478 |
$found_items = intval( $mlsrequest['results'] ); |
| 2479 |
} else { |
| 2480 |
delete_transient( 'mlsimport_saas_token' ); |
| 2481 |
mlsimport_telemetry_set( 'last_sync_failed', time() ); |
| 2482 |
mlsimport_telemetry_set( 'last_sync_failed_code', (string) ( $mlsrequest['error_code'] ?? 'unknown' ) ); |
| 2483 |
} |
| 2484 |
print esc_html__('We found ','mlsimport') . esc_html( $found_items ) . ' listings.</br>' . PHP_EOL; |
| 2485 |
|
| 2486 |
// Only process if items found |
| 2487 |
if ( $found_items > 0 ) { |
| 2488 |
|
| 2489 |
$item_id_array = array( |
| 2490 |
'item_id' => $item_id, |
| 2491 |
'how_many' => 0, |
| 2492 |
'max_number' => $found_items, |
| 2493 |
'batch_counter' => 1, |
| 2494 |
); |
| 2495 |
|
| 2496 |
// Build the paginated batch of request-argument sets to run. |
| 2497 |
// Potentially large array, log memory before/after |
| 2498 |
$attachments_to_move = (array) $this->mlsimport_saas_generate_import_requests_per_item( $item_id_array, $last_date, true ); |
| 2499 |
|
| 2500 |
// Persist the batch + mark the cron job started. |
| 2501 |
// Store in post meta (beware if array is huge) |
| 2502 |
update_post_meta( $item_id, 'mlsimport_spawn_status_cron_job', 'started' ); |
| 2503 |
update_post_meta( $item_id, 'mlsimport_cron_attach_to_move_' . $item_id, $attachments_to_move ); |
| 2504 |
|
| 2505 |
// Save last date for next run |
| 2506 |
$this->mlsimport_saas_update_last_date( $item_id ); |
| 2507 |
|
| 2508 |
// Prepare and pass only necessary arguments to background process |
| 2509 |
$attachments_to_send = array( |
| 2510 |
'args' => array( |
| 2511 |
'attachments_to_move' => $item_id, |
| 2512 |
'item_id_array' => $item_id_array, |
| 2513 |
), |
| 2514 |
); |
| 2515 |
|
| 2516 |
// Run the batch synchronously (this is already inside cron). |
| 2517 |
$this->mlsimport_background_process_per_item_cron_function( $attachments_to_send['args'] ); |
| 2518 |
|
| 2519 |
// Unset large arrays/objects after use |
| 2520 |
unset($attachments_to_move, $attachments_to_send, $mlsrequest, $item_id_array); |
| 2521 |
gc_collect_cycles(); |
| 2522 |
} |
| 2523 |
|
| 2524 |
return $found_items; |
| 2525 |
} |
| 2526 |
|
| 2527 |
|
| 2528 |
|
| 2529 |
|
| 2530 |
|
| 2531 |
|
| 2532 |
/** |
| 2533 |
* Daily reconciliation: delete local listings that should no longer exist. |
| 2534 |
* |
| 2535 |
* Fetches the full set of ListingKeys currently in the MLS feed, guards against |
| 2536 |
* a truncated feed (which would trigger mass deletion), then walks every local |
| 2537 |
* listing in batches of 1000 and, per the status rules, deletes those that are |
| 2538 |
* gone from the feed (or present but in a delete-worthy status). Batched with |
| 2539 |
* explicit GC to keep memory bounded. Optimized, batched, memory logged. |
| 2540 |
* |
| 2541 |
* @return void |
| 2542 |
*/ |
| 2543 |
public function mlsimport_saas_start_doing_reconciliation() { |
| 2544 |
global $mlsimport, $wpdb; |
| 2545 |
|
| 2546 |
|
| 2547 |
// Pull every ListingKey the MLS currently reports; free the wrapper array. |
| 2548 |
// Get all MLS keys in memory (we assume this is necessary for lookup) |
| 2549 |
$mls_data = $this->mlsimport_saas_get_mls_reconciliation_data(); |
| 2550 |
$listingKey_in_MLS = $mls_data['all_data'] ?? []; |
| 2551 |
|
| 2552 |
unset($mls_data); |
| 2553 |
gc_collect_cycles(); |
| 2554 |
|
| 2555 |
// Empty feed -> nothing to reconcile; never delete on an empty response. |
| 2556 |
if (empty($listingKey_in_MLS)) { |
| 2557 |
return; |
| 2558 |
} |
| 2559 |
|
| 2560 |
// Sanity guard against a well-formed but truncated feed. Reconciliation |
| 2561 |
// deletes every local listing missing from this list, so a short feed |
| 2562 |
// would become a mass deletion. Compare the feed size against the local |
| 2563 |
// ListingKey count (same status filter as the delete loop below) and bail |
| 2564 |
// if the feed is implausibly small. Log both numbers either way so a |
| 2565 |
// future incident is diagnosable. |
| 2566 |
$feed_count = count($listingKey_in_MLS); |
| 2567 |
$local_count = (int) $wpdb->get_var( |
| 2568 |
$wpdb->prepare( |
| 2569 |
"SELECT COUNT(*) |
| 2570 |
FROM {$wpdb->posts} p |
| 2571 |
INNER JOIN {$wpdb->postmeta} pm |
| 2572 |
ON p.ID = pm.post_id AND pm.meta_key = %s |
| 2573 |
WHERE p.post_status NOT IN ('draft', 'trash')", |
| 2574 |
'ListingKey' |
| 2575 |
) |
| 2576 |
); |
| 2577 |
|
| 2578 |
error_log(sprintf('MLSimport reconciliation: feed=%d local=%d', $feed_count, $local_count)); |
| 2579 |
|
| 2580 |
if (!mlsimport_reconciliation_feed_is_plausible($feed_count, $local_count)) { |
| 2581 |
error_log(sprintf( |
| 2582 |
'MLSimport reconciliation ABORTED: feed of %d is below %d%% of %d local listings; no deletions performed.', |
| 2583 |
$feed_count, |
| 2584 |
(int) round(MLSIMPORT_RECONCILIATION_MIN_FEED_FRACTION * 100), |
| 2585 |
$local_count |
| 2586 |
)); |
| 2587 |
return; |
| 2588 |
} |
| 2589 |
|
| 2590 |
// Flip so isset($listingKey_in_MLS[$key]) is an O(1) membership test. |
| 2591 |
// Flip for fast lookup |
| 2592 |
$listingKey_in_MLS = array_flip($listingKey_in_MLS); |
| 2593 |
|
| 2594 |
// Batch fetch local listings |
| 2595 |
$batch = 1000; |
| 2596 |
$offset = 0; |
| 2597 |
$to_delete = 0; |
| 2598 |
$counter = 0; |
| 2599 |
|
| 2600 |
// One-query preload of status/protect meta for every Import Task, keyed by id. |
| 2601 |
$mlsimport_preload_all_mls_item_status_meta = $this->mlsimport_preload_all_mls_item_status_meta(); |
| 2602 |
//print_r($mlsimport_preload_all_mls_item_status_meta); |
| 2603 |
|
| 2604 |
// Page through all local listings 1000 at a time. |
| 2605 |
do { |
| 2606 |
$local = $wpdb->get_results( |
| 2607 |
$wpdb->prepare( |
| 2608 |
"SELECT |
| 2609 |
p.ID, |
| 2610 |
listingkey_meta.meta_value AS listingkey, |
| 2611 |
inserted_meta.meta_value AS mlsimport_item_inserted |
| 2612 |
FROM {$wpdb->posts} p |
| 2613 |
INNER JOIN {$wpdb->postmeta} listingkey_meta |
| 2614 |
ON p.ID = listingkey_meta.post_id |
| 2615 |
AND listingkey_meta.meta_key = %s |
| 2616 |
LEFT JOIN {$wpdb->postmeta} inserted_meta |
| 2617 |
ON p.ID = inserted_meta.post_id |
| 2618 |
AND inserted_meta.meta_key = %s |
| 2619 |
WHERE p.post_status NOT IN ('draft', 'trash') |
| 2620 |
LIMIT %d OFFSET %d", |
| 2621 |
'ListingKey', |
| 2622 |
'MLSimport_item_inserted', |
| 2623 |
$batch, |
| 2624 |
$offset |
| 2625 |
), |
| 2626 |
ARRAY_A |
| 2627 |
); |
| 2628 |
|
| 2629 |
$count = count($local); |
| 2630 |
|
| 2631 |
|
| 2632 |
// Decide keep-vs-delete for each local listing in this page. |
| 2633 |
foreach ($local as $item) { |
| 2634 |
$listingkey = $item['listingkey']; // not 'meta_value' anymore |
| 2635 |
$property_id = $item['ID']; |
| 2636 |
$mlsimportItemId = $item['mlsimport_item_inserted']; |
| 2637 |
++$counter; |
| 2638 |
// IN MLS |
| 2639 |
if (isset($listingKey_in_MLS[$listingkey])) { |
| 2640 |
|
| 2641 |
if (!empty($mlsimportItemId) && isset($mlsimport_preload_all_mls_item_status_meta[$mlsimportItemId])) { |
| 2642 |
$mlsimport_item_standardstatus = $mlsimport_preload_all_mls_item_status_meta[$mlsimportItemId]['mlsimport_item_standardstatus'] ?? null; |
| 2643 |
$mlsimport_item_standardstatusprotect = $mlsimport_preload_all_mls_item_status_meta[$mlsimportItemId]['mlsimport_item_standardstatusprotect'] ?? null; |
| 2644 |
} else { |
| 2645 |
$mlsimport_item_standardstatus = null; |
| 2646 |
$mlsimport_item_standardstatusprotect = null; |
| 2647 |
} |
| 2648 |
|
| 2649 |
// Still in the feed: delete only if its status rules say so. |
| 2650 |
$keep_when_in_mls = $mlsimport->admin->theme_importer->check_if_delete_when_status_when_in_mls($property_id, $mlsimport_item_standardstatus, $mlsimport_item_standardstatusprotect); |
| 2651 |
if (!$keep_when_in_mls) { |
| 2652 |
++$to_delete; |
| 2653 |
$mlsimport->admin->theme_importer->mlsimportSaasDeletePropertyViaMysql($property_id, $listingkey); |
| 2654 |
} |
| 2655 |
} else { |
| 2656 |
// NOT IN MLS |
| 2657 |
|
| 2658 |
if (!empty($mlsimportItemId) && isset($mlsimport_preload_all_mls_item_status_meta[$mlsimportItemId])) { |
| 2659 |
$mlsimport_item_standardstatus = $mlsimport_preload_all_mls_item_status_meta[$mlsimportItemId]['mlsimport_item_standardstatus'] ?? null; |
| 2660 |
$mlsimport_item_standardstatusprotect = $mlsimport_preload_all_mls_item_status_meta[$mlsimportItemId]['mlsimport_item_standardstatusprotect'] ?? null; |
| 2661 |
} else { |
| 2662 |
$mlsimport_item_standardstatus = null; |
| 2663 |
$mlsimport_item_standardstatusprotect = null; |
| 2664 |
} |
| 2665 |
// Gone from the feed: delete unless a protected status keeps it. |
| 2666 |
$keep = $mlsimport->admin->theme_importer->check_if_delete_when_status($property_id, $mlsimport_item_standardstatus, null, $mlsimport_item_standardstatusprotect); |
| 2667 |
if (!$keep) { |
| 2668 |
++$to_delete; |
| 2669 |
$mlsimport->admin->theme_importer->mlsimportSaasDeletePropertyViaMysql($property_id, $listingkey); |
| 2670 |
} |
| 2671 |
} |
| 2672 |
|
| 2673 |
// Memory housekeeping |
| 2674 |
unset($listingkey, $property_id, $mlsimportItemId, $mlsImportItemStatus, $mlsimport_item_standardstatusprotect, $keep_when_in_mls, $keep); |
| 2675 |
if ($counter % 250 == 0) { |
| 2676 |
gc_collect_cycles(); |
| 2677 |
} |
| 2678 |
} |
| 2679 |
|
| 2680 |
unset($local); |
| 2681 |
gc_collect_cycles(); |
| 2682 |
|
| 2683 |
// Advance; loop until a short page signals the last batch. |
| 2684 |
$offset += $batch; |
| 2685 |
} while ($count === $batch); |
| 2686 |
|
| 2687 |
|
| 2688 |
print esc_html(' to delete:' . $to_delete); |
| 2689 |
|
| 2690 |
// Final cleanup |
| 2691 |
unset($listingKey_in_MLS); |
| 2692 |
gc_collect_cycles(); |
| 2693 |
|
| 2694 |
|
| 2695 |
return; |
| 2696 |
} |
| 2697 |
|
| 2698 |
|
| 2699 |
|
| 2700 |
/** |
| 2701 |
* Preload all status meta for ALL mlsimport_item posts in ONE QUERY. |
| 2702 |
* Returns: [mlsimport_item_id => ['mlsimport_item_standardstatus' => ..., 'mlsimport_item_standardstatusprotect' => ...], ...] |
| 2703 |
* |
| 2704 |
* Avoids per-listing get_post_meta() calls during reconciliation. |
| 2705 |
* |
| 2706 |
* @return array<int,array<string,mixed>> |
| 2707 |
*/ |
| 2708 |
function mlsimport_preload_all_mls_item_status_meta() { |
| 2709 |
global $wpdb; |
| 2710 |
|
| 2711 |
// Fetch the status + protect meta rows for every Import Task in one pass. |
| 2712 |
$sql = " |
| 2713 |
SELECT p.ID as post_id, pm.meta_key, pm.meta_value |
| 2714 |
FROM {$wpdb->posts} p |
| 2715 |
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id |
| 2716 |
WHERE p.post_type = 'mlsimport_item' |
| 2717 |
AND pm.meta_key IN ('mlsimport_item_standardstatus', 'mlsimport_item_standardstatusprotect') |
| 2718 |
"; |
| 2719 |
|
| 2720 |
$rows = $wpdb->get_results($sql); |
| 2721 |
|
| 2722 |
// Index by post id; unserialize each stored value. |
| 2723 |
$meta = []; |
| 2724 |
foreach ($rows as $row) { |
| 2725 |
if (!isset($meta[$row->post_id])) { |
| 2726 |
$meta[$row->post_id] = [ |
| 2727 |
'mlsimport_item_standardstatus' => null, |
| 2728 |
'mlsimport_item_standardstatusprotect' => null |
| 2729 |
]; |
| 2730 |
} |
| 2731 |
$meta[$row->post_id][$row->meta_key] = maybe_unserialize($row->meta_value); |
| 2732 |
} |
| 2733 |
return $meta; |
| 2734 |
} |
| 2735 |
|
| 2736 |
|
| 2737 |
|
| 2738 |
/** |
| 2739 |
* Fetch the reconciliation feed (all current ListingKeys) from the SaaS API. |
| 2740 |
* |
| 2741 |
* @return array The API response, expected to carry an 'all_data' key. |
| 2742 |
*/ |
| 2743 |
public function mlsimport_saas_get_mls_reconciliation_data() { |
| 2744 |
|
| 2745 |
// GET /reconciliation with no arguments. |
| 2746 |
$arguments = array(); |
| 2747 |
$answer = $this->theme_importer->globalApiRequestCurlSaas( 'reconciliation', $arguments, 'GET' ); |
| 2748 |
return $answer; |
| 2749 |
} |
| 2750 |
|
| 2751 |
/** |
| 2752 |
* Return all published posts' values for a given meta key, with their post ids. |
| 2753 |
* |
| 2754 |
* @param string $key Meta key to fetch. |
| 2755 |
* @return array Rows of {meta_value, ID}. |
| 2756 |
*/ |
| 2757 |
public function mlsimport_saas_get_all_meta_values($key) { |
| 2758 |
global $wpdb; |
| 2759 |
$result = $wpdb->get_results( |
| 2760 |
$wpdb->prepare( |
| 2761 |
" |
| 2762 |
SELECT pm.meta_value, p.ID |
| 2763 |
FROM {$wpdb->postmeta} pm |
| 2764 |
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id |
| 2765 |
WHERE pm.meta_key = %s |
| 2766 |
AND p.post_status = 'publish' |
| 2767 |
", |
| 2768 |
$key |
| 2769 |
), |
| 2770 |
ARRAY_A // Lighter than OBJECT, unless you need objects |
| 2771 |
); |
| 2772 |
return $result; |
| 2773 |
} |
| 2774 |
|
| 2775 |
|
| 2776 |
|
| 2777 |
/** |
| 2778 |
* Run a single listings request for an Import Task and return the API result. |
| 2779 |
* |
| 2780 |
* Builds the RESO query arguments, rejects invalid combinations (Rapattoni |
| 2781 |
* requiring a property type; over-long argument strings), POSTs to the SaaS |
| 2782 |
* 'listings' endpoint, normalizes a non-array failure into a success=false |
| 2783 |
* array, records feed-count telemetry, and returns the response array. |
| 2784 |
* |
| 2785 |
* @param int $item_id Import Task post id. |
| 2786 |
* @param string $last_date Modification-time watermark (optional). |
| 2787 |
* @param string $skip Pagination offset (optional). |
| 2788 |
* @param string $top Page size (optional). |
| 2789 |
* @param bool $is_hourly_sync Whether this call is from the hourly cron. |
| 2790 |
* @return array The (normalized) API response. |
| 2791 |
*/ |
| 2792 |
public function mlsimport_make_listing_requests( $item_id, $last_date = '', $skip = '', $top = '', $is_hourly_sync = false ) { |
| 2793 |
// Resolve the configured MLS id (drives provider-specific validation). |
| 2794 |
$options = get_option( $this->plugin_name . '_admin_options' ); |
| 2795 |
$mls_id = ''; |
| 2796 |
if ( isset( $options['mlsimport_mls_name'] ) ) { |
| 2797 |
$mls_id = sanitize_text_field( trim( $options['mlsimport_mls_name'] ) ); |
| 2798 |
} |
| 2799 |
|
| 2800 |
// Build the full RESO query argument set from the task's meta. |
| 2801 |
$arguments = $this->mlsimport_saas_make_listing_requests_arguments( $item_id, $last_date, $skip, $top, $is_hourly_sync ); |
| 2802 |
|
| 2803 |
|
| 2804 |
// Rapattoni (5000-5999) requires a property_type; bail with a clear |
| 2805 |
// message when it is unset/empty (checks the scalar and first element). |
| 2806 |
if ( |
| 2807 |
$mls_id > 5000 && $mls_id < 6000 && |
| 2808 |
( ! isset( $arguments['property_type'] ) or |
| 2809 |
( isset( $arguments['property_type'] ) && '' === $arguments['property_type'] ) or |
| 2810 |
( isset( $arguments['property_type'][0] ) && '' === $arguments['property_type'][0] ) |
| 2811 |
) |
| 2812 |
) { |
| 2813 |
return array( |
| 2814 |
'success' => false, |
| 2815 |
'type' => 'rapattoni', |
| 2816 |
'message' => esc_html__( 'This MLS requires to have one item selected from "Property Action Category" dropdown', 'mlsimport' ), |
| 2817 |
); |
| 2818 |
} |
| 2819 |
|
| 2820 |
// Guard against an over-long query string (too many parameters selected). |
| 2821 |
$potential_leght = strlen( wp_json_encode( $arguments ) ); |
| 2822 |
if ( $potential_leght > 1750 ) { |
| 2823 |
return array( |
| 2824 |
'success' => false, |
| 2825 |
'potential_leght' => $potential_leght, |
| 2826 |
'message' => esc_html__( 'You have too many parameters selected. Split the import beween multiple MLS Import Tasks: For ex : Import per County instead of selecting 10 cities or import listing between certain price range.', 'mlsimport' ), |
| 2827 |
); |
| 2828 |
} |
| 2829 |
|
| 2830 |
//print_r($arguments); |
| 2831 |
//print '----------------------------'.PHP_EOL; |
| 2832 |
// POST the query to the SaaS 'listings' endpoint. |
| 2833 |
$answer = $this->theme_importer->globalApiRequestCurlSaas( 'listings', $arguments, 'POST' ); |
| 2834 |
|
| 2835 |
// globalApiRequestCurlSaas() returns a plain string on failure (token |
| 2836 |
// validation, network/WP error, JSON decode). Callers expect an array, |
| 2837 |
// so normalize the failure into the success=>false shape they handle. |
| 2838 |
if ( ! is_array( $answer ) ) { |
| 2839 |
$answer = array( |
| 2840 |
'success' => false, |
| 2841 |
'message' => is_string( $answer ) ? $answer : esc_html__( 'The request to the MLS could not be completed.', 'mlsimport' ), |
| 2842 |
); |
| 2843 |
} |
| 2844 |
|
| 2845 |
// Echo the computed argument length back on the response for diagnostics. |
| 2846 |
$answer['potential_leght'] = $potential_leght; |
| 2847 |
|
| 2848 |
// Record the pre-filter MLS feed count for telemetry. Every import path |
| 2849 |
// — manual, hourly cron, and onboarding — routes through this method, so |
| 2850 |
// recording here is the single rule that keeps the metric complete. |
| 2851 |
if ( isset( $answer['results'] ) ) { |
| 2852 |
mlsimport_telemetry_set( 'last_feed_found', (int) $answer['results'] ); |
| 2853 |
} |
| 2854 |
|
| 2855 |
return ( $answer ); |
| 2856 |
} |
| 2857 |
|
| 2858 |
|
| 2859 |
|
| 2860 |
|
| 2861 |
|
| 2862 |
|
| 2863 |
/** |
| 2864 |
* Assemble the RESO listings query arguments from an Import Task's meta. |
| 2865 |
* |
| 2866 |
* Reads the task's saved filters (price, city/county, area, subdivision, |
| 2867 |
* postal code, status, property (sub)type, internet-display flags, agent / |
| 2868 |
* office keys and their exclusions, custom parameters) and maps them to the |
| 2869 |
* SaaS API parameter names, applying provider-specific quirks (Edmonton has |
| 2870 |
* no status; Rapattoni collapses property_type; Realtor.ca / PropTx need a |
| 2871 |
* specific modification-time format). |
| 2872 |
* |
| 2873 |
* @param int $item_id Import Task post id. |
| 2874 |
* @param string $last_date Modification-time watermark (optional). |
| 2875 |
* @param string $skip Pagination offset (optional). |
| 2876 |
* @param string $top Page size (optional). |
| 2877 |
* @param bool $is_hourly_sync Whether this call is from the hourly cron. |
| 2878 |
* @return array|string The argument array, or '' when core options are missing. |
| 2879 |
*/ |
| 2880 |
public function mlsimport_saas_make_listing_requests_arguments( $item_id, $last_date = '', $skip = '', $top = '', $is_hourly_sync = false ) { |
| 2881 |
|
| 2882 |
// MLS id is mandatory. |
| 2883 |
$options = get_option( $this->plugin_name . '_admin_options' ); |
| 2884 |
if ( isset( $options['mlsimport_mls_name'] ) ) { |
| 2885 |
$mls_id = intval( $options['mlsimport_mls_name'] ); |
| 2886 |
} else { |
| 2887 |
return ''; |
| 2888 |
} |
| 2889 |
|
| 2890 |
// Theme id is mandatory (selects the server-side field schema). |
| 2891 |
if ( isset( $options['mlsimport_theme_used'] ) ) { |
| 2892 |
$theme_id = intval( $options['mlsimport_theme_used'] ); |
| 2893 |
} else { |
| 2894 |
return ''; |
| 2895 |
} |
| 2896 |
|
| 2897 |
// Base parameters every request carries. |
| 2898 |
$values = array(); |
| 2899 |
$values['mls_id'] = $mls_id; |
| 2900 |
$values['theme_id'] = $theme_id; |
| 2901 |
// Flag hourly-sync calls so the backend can treat them differently. |
| 2902 |
if ( $is_hourly_sync ) { |
| 2903 |
$values['hourly_sync'] = 1; |
| 2904 |
} |
| 2905 |
|
| 2906 |
// Pagination (only when a page size was supplied). |
| 2907 |
if ( '' !== $top ) { |
| 2908 |
$values['top'] = $top; |
| 2909 |
$values['skip'] = intval( $skip ); |
| 2910 |
} |
| 2911 |
|
| 2912 |
// // add price |
| 2913 |
// Price range (only when both bounds are set). |
| 2914 |
$mlsimport_item_min_price = get_post_meta( $item_id, 'mlsimport_item_min_price', true ); |
| 2915 |
$mlsimport_item_max_price = get_post_meta( $item_id, 'mlsimport_item_max_price', true ); |
| 2916 |
if ( '' !== $mlsimport_item_min_price && '' !== $mlsimport_item_max_price ) { |
| 2917 |
$values['list_price_min'] = floatval( $mlsimport_item_min_price ); |
| 2918 |
$values['list_price_max'] = floatval( $mlsimport_item_max_price ); |
| 2919 |
} |
| 2920 |
|
| 2921 |
// add city |
| 2922 |
$values = $this->mls_import_return_multiple_param_value( 'city', $item_id, 'city', $values ); |
| 2923 |
|
| 2924 |
// add county |
| 2925 |
$values = $this->mls_import_return_multiple_param_value( 'countyorparish', $item_id, 'county_or_parish', $values ); |
| 2926 |
|
| 2927 |
// add MLSAreaMajor |
| 2928 |
$values = $this->mls_import_saas_add_to_parms_input( 'MLSAreaMajor', $item_id, 'mls_area_major', $values ); |
| 2929 |
|
| 2930 |
// add SubdivisionName |
| 2931 |
$values = $this->mls_import_saas_add_to_parms_input( 'SubdivisionName', $item_id, 'subdivision_name', $values ); |
| 2932 |
|
| 2933 |
// add postal code |
| 2934 |
$values = $this->mls_import_saas_add_to_parms_input( 'PostalCode', $item_id, 'postal_code', $values ); |
| 2935 |
|
| 2936 |
// add status |
| 2937 |
|
| 2938 |
// Edmonton (111) has no StandardStatus field, so skip it there. |
| 2939 |
if ( 111 !== $mls_id ) { // edmonton check |
| 2940 |
$values = $this->mls_import_return_multiple_param_value( 'StandardStatus', $item_id, 'status', $values ); |
| 2941 |
} |
| 2942 |
|
| 2943 |
// add property_subtype |
| 2944 |
$values = $this->mls_import_return_multiple_param_value( 'PropertySubType', $item_id, 'property_subtype', $values ); |
| 2945 |
|
| 2946 |
// add property_type |
| 2947 |
$values = $this->mls_import_return_multiple_param_value( 'PropertyType', $item_id, 'property_type', $values ); |
| 2948 |
|
| 2949 |
// Rapattoni exception: property_type must be a single, space-stripped, |
| 2950 |
// single-element array rather than the multi-select list. |
| 2951 |
// rapattoni exception |
| 2952 |
if ( $mls_id > 5000 && |
| 2953 |
( isset($values['property_type']) && $values['property_type'] !='' ) ) { |
| 2954 |
|
| 2955 |
$values = $this->mls_import_saas_add_to_parms_input( 'PropertyType', $item_id, 'property_type', $values ); |
| 2956 |
$temp = $values['property_type']; |
| 2957 |
$temp = str_replace( ' ', '', $temp ); |
| 2958 |
$values['property_type'] = array(); |
| 2959 |
$values['property_type'][] = $temp; |
| 2960 |
} |
| 2961 |
|
| 2962 |
// add internet_entirelisting_displayyn |
| 2963 |
$values = $this->mls_import_saas_add_to_parms_input( 'InternetEntireListingDisplayYN', $item_id, 'internet_entirelisting_displayyn', $values ); |
| 2964 |
|
| 2965 |
// add internet_address_displayyn |
| 2966 |
$values = $this->mls_import_saas_add_to_parms_input( 'InternetAddressDisplayYN', $item_id, 'internet_address_displayyn', $values ); |
| 2967 |
|
| 2968 |
// add ListAgentKey |
| 2969 |
$values = $this->mls_import_saas_add_to_parms_input( 'ListAgentKey', $item_id, 'list_agentkey', $values ); |
| 2970 |
// add ListAgentKey |
| 2971 |
$values = $this->mls_import_saas_add_to_parms_input( 'ListAgentMlsId', $item_id, 'list_agentmlsid', $values ); |
| 2972 |
// add BuyerAgentMlsId |
| 2973 |
$values = $this->mls_import_saas_add_to_parms_input( 'BuyerAgentMlsId', $item_id, 'buyer_agentmlsid', $values ); |
| 2974 |
// add ListOfficeKey |
| 2975 |
$values = $this->mls_import_saas_add_to_parms_input( 'ListOfficeKey', $item_id, 'list_officekey', $values ); |
| 2976 |
// add ListOfficeMlsId |
| 2977 |
$values = $this->mls_import_saas_add_to_parms_input( 'ListOfficeMlsId', $item_id, 'list_officemlsid', $values ); |
| 2978 |
|
| 2979 |
// add ListingId |
| 2980 |
$values = $this->mls_import_saas_add_to_parms_input( 'ListingId', $item_id, 'listingid', $values ); |
| 2981 |
|
| 2982 |
//add Exclude_ListOfficeKey |
| 2983 |
$values = $this->mls_import_saas_add_to_parms_input( 'Exclude_ListOfficeKey', $item_id, 'exclude_list_officekey', $values ); |
| 2984 |
// add Exclude_ListOfficeMlsId |
| 2985 |
$values = $this->mls_import_saas_add_to_parms_input( 'Exclude_ListOfficeMlsId', $item_id, 'exclude_list_officemlsid', $values ); |
| 2986 |
|
| 2987 |
|
| 2988 |
|
| 2989 |
//add Exclude_ListAgentKey |
| 2990 |
$values = $this->mls_import_saas_add_to_parms_input( 'Exclude_ListAgentKey', $item_id, 'exclude_list_agentkey', $values ); |
| 2991 |
// add Exclude_ListAgentMlsId |
| 2992 |
$values = $this->mls_import_saas_add_to_parms_input( 'Exclude_ListAgentMlsId', $item_id, 'exclude_list_agentmlsid', $values ); |
| 2993 |
// add CustomParameters |
| 2994 |
$values = $this->mls_import_saas_add_to_parms_input( 'CustomParameters', $item_id, 'custom_parameters', $values ); |
| 2995 |
|
| 2996 |
|
| 2997 |
// Realtor.ca (7000-7999) expects an ISO UTC timestamp with seconds/Z. |
| 2998 |
// if we have realtorca |
| 2999 |
if ($mls_id >= 7000 && $mls_id < 8000 && $last_date!=='') { |
| 3000 |
$dateTime_realtorca = new DateTime($last_date, new DateTimeZone('UTC')); |
| 3001 |
// Format with seconds and UTC timezone marker |
| 3002 |
$last_date = $dateTime_realtorca->format('Y-m-d\TH:i:s.000\Z'); |
| 3003 |
} |
| 3004 |
|
| 3005 |
// PropTx / AMPRE requires a full OData DateTimeOffset literal for the ModificationTimestamp filter. |
| 3006 |
if ( mlsimport_is_proptx_provider( $mls_id ) && $last_date !== '' ) { |
| 3007 |
$last_date = mlsimport_format_odata_modification_time( $last_date ); |
| 3008 |
} |
| 3009 |
|
| 3010 |
// Attach the (possibly reformatted) modification-time watermark. |
| 3011 |
if ( '' !== $last_date ) { |
| 3012 |
$values['modification_time'] = $last_date; |
| 3013 |
} |
| 3014 |
|
| 3015 |
return( $values ); |
| 3016 |
} |
| 3017 |
|
| 3018 |
|
| 3019 |
|
| 3020 |
/** |
| 3021 |
* Copy a single scalar Import Task meta value into the arguments array. |
| 3022 |
* |
| 3023 |
* Reads mlsimport_item_<key> and, when non-empty, stores it under $new_name. |
| 3024 |
* |
| 3025 |
* @param string $key Field key (used to build the meta key). |
| 3026 |
* @param int $post_id Import Task post id. |
| 3027 |
* @param string $new_name API parameter name to store under. |
| 3028 |
* @param array $all_values Accumulating arguments array. |
| 3029 |
* @return array The updated arguments array. |
| 3030 |
*/ |
| 3031 |
public function mls_import_saas_add_to_parms_input( $key, $post_id, $new_name, $all_values ) { |
| 3032 |
// Read the scalar meta value and add it only when set. |
| 3033 |
$name = strtolower( 'mlsimport_item_' . $key ); |
| 3034 |
$value = get_post_meta( $post_id, $name, true ); |
| 3035 |
if ( '' !== $value ) { |
| 3036 |
$all_values[ $new_name ] = $value; |
| 3037 |
} |
| 3038 |
|
| 3039 |
return $all_values; |
| 3040 |
} |
| 3041 |
|
| 3042 |
|
| 3043 |
/** |
| 3044 |
* Copy a multi-value (list) Import Task meta value into the arguments array. |
| 3045 |
* |
| 3046 |
* Reads the list value plus its "_check" (select-all) flag; for city/county |
| 3047 |
* it also merges any comma-separated "extra" free-text values. The value is |
| 3048 |
* added only when select-all is off and it is non-empty — except 'status', |
| 3049 |
* which is always written. |
| 3050 |
* |
| 3051 |
* @param string $key Field key (used to build the meta keys). |
| 3052 |
* @param int $post_id Import Task post id. |
| 3053 |
* @param string $new_name API parameter name to store under. |
| 3054 |
* @param array $all_values Accumulating arguments array. |
| 3055 |
* @return array The updated arguments array. |
| 3056 |
*/ |
| 3057 |
public function mls_import_return_multiple_param_value( $key, $post_id, $new_name, $all_values ) { |
| 3058 |
// The selected list value and its companion select-all flag. |
| 3059 |
$name_check = strtolower( 'mlsimport_item_' . $key . '_check' ); |
| 3060 |
$name = strtolower( 'mlsimport_item_' . $key ); |
| 3061 |
|
| 3062 |
$value = get_post_meta( $post_id, $name, true ); |
| 3063 |
|
| 3064 |
// add extra county - should be moved into function if pass tests |
| 3065 |
if ( 'countyorparish' === $key ) { |
| 3066 |
$extracounty_values = get_post_meta( $post_id, 'mlsimport_item_extracounty', true ); |
| 3067 |
|
| 3068 |
if ( '' !== $extracounty_values ) { |
| 3069 |
$extracounty_array = explode( ',', $extracounty_values ); |
| 3070 |
|
| 3071 |
if ( ! is_array( $value ) ) { |
| 3072 |
if ( '' === $value ) { |
| 3073 |
$value = array(); |
| 3074 |
} else { |
| 3075 |
$value = array( $value ); |
| 3076 |
} |
| 3077 |
} |
| 3078 |
|
| 3079 |
foreach ( $extracounty_array as $extra ) { |
| 3080 |
$value[] = $extra; |
| 3081 |
} |
| 3082 |
} |
| 3083 |
} |
| 3084 |
|
| 3085 |
// add extra city - should be moved into function if pass tests |
| 3086 |
if ( 'city' === $key ) { |
| 3087 |
$extracity_values = get_post_meta( $post_id, 'mlsimport_item_extracity', true ); |
| 3088 |
if ( '' !== $extracity_values ) { |
| 3089 |
$extracity_array = explode( ',', $extracity_values ); |
| 3090 |
|
| 3091 |
if ( ! is_array( $value ) ) { |
| 3092 |
if ('' === $value ) { |
| 3093 |
$value = array(); |
| 3094 |
} else { |
| 3095 |
$value = array( $value ); |
| 3096 |
} |
| 3097 |
} |
| 3098 |
|
| 3099 |
foreach ( $extracity_array as $extra ) { |
| 3100 |
$value[] = $extra; |
| 3101 |
} |
| 3102 |
} |
| 3103 |
} |
| 3104 |
|
| 3105 |
// Only include the list when "select all" is off and there is a value. |
| 3106 |
$value_check = get_post_meta( $post_id, $name_check, true ); |
| 3107 |
|
| 3108 |
if ( 0 === intval($value_check) && '' !== $value ) { |
| 3109 |
$all_values[ $new_name ] = $value; |
| 3110 |
} |
| 3111 |
|
| 3112 |
// status exception: always send status, regardless of the check flag. |
| 3113 |
if ( 'status' === $new_name ) { |
| 3114 |
$all_values[ $new_name ] = $value; |
| 3115 |
} |
| 3116 |
|
| 3117 |
return $all_values; |
| 3118 |
} |
| 3119 |
|
| 3120 |
|
| 3121 |
|
| 3122 |
/** |
| 3123 |
* Build the Import Task field definition list (labels, types, enum values). |
| 3124 |
* |
| 3125 |
* Reads the saved MLS enums option, extracts the available City / County / |
| 3126 |
* status / property (sub)type value lists, and returns the ordered field |
| 3127 |
* definition array the metabox renders from. Falls back StandardStatus to |
| 3128 |
* MlsStatus when the MLS has no StandardStatus enum. Emits a warning when no |
| 3129 |
* metadata has been fetched yet. |
| 3130 |
* |
| 3131 |
* @return array Field key => definition (label, description, type, multiple, values). |
| 3132 |
*/ |
| 3133 |
public function mlsimport_saas_return_mls_fields() { |
| 3134 |
|
| 3135 |
// Saved MLS enum metadata (JSON); empty until fields have been fetched. |
| 3136 |
$mlsimport_mls_metadata_mls_enums = get_option( 'mlsimport_mls_metadata_mls_enums', '' ); |
| 3137 |
|
| 3138 |
// Warn the user when no metadata is available yet. |
| 3139 |
if ( '' === $mlsimport_mls_metadata_mls_enums ) { |
| 3140 |
?> |
| 3141 |
<div class="mlsimport_warning long_warning">Please select the import fields(from MLS Import Settings) before starting a MLS import process.</div> |
| 3142 |
<?php |
| 3143 |
} |
| 3144 |
|
| 3145 |
// Decode and reach into the enum container. |
| 3146 |
$metadata_api_call_full = json_decode( $mlsimport_mls_metadata_mls_enums, true ); |
| 3147 |
|
| 3148 |
if ( isset( $metadata_api_call_full['global_array'] ) ) { |
| 3149 |
$metadata_api_call = $metadata_api_call_full['global_array']; |
| 3150 |
} |
| 3151 |
|
| 3152 |
// Extract each enum list as a flat array of option keys (empty if absent). |
| 3153 |
$city_array = array(); |
| 3154 |
if ( isset( $metadata_api_call['PropertyEnums']['City'] ) && is_array( $metadata_api_call['PropertyEnums']['City'] ) ) { |
| 3155 |
$city_array = array_keys( $metadata_api_call['PropertyEnums']['City'] ); |
| 3156 |
} |
| 3157 |
|
| 3158 |
$county_array = array(); |
| 3159 |
if ( isset( $metadata_api_call['PropertyEnums']['CountyOrParish'] ) && is_array( $metadata_api_call['PropertyEnums']['CountyOrParish'] ) ) { |
| 3160 |
$county_array = array_keys( $metadata_api_call['PropertyEnums']['CountyOrParish'] ); |
| 3161 |
} |
| 3162 |
|
| 3163 |
$mlsstatus_array = array(); |
| 3164 |
if ( isset( $metadata_api_call['PropertyEnums']['MlsStatus'] ) && is_array( $metadata_api_call['PropertyEnums']['MlsStatus'] ) ) { |
| 3165 |
$mlsstatus_array = array_keys( $metadata_api_call['PropertyEnums']['MlsStatus'] ); |
| 3166 |
} |
| 3167 |
|
| 3168 |
$propertysubtype_array = array(); |
| 3169 |
if ( isset( $metadata_api_call['PropertyEnums']['PropertySubType'] ) && is_array( $metadata_api_call['PropertyEnums']['PropertySubType'] ) ) { |
| 3170 |
$propertysubtype_array = array_keys( $metadata_api_call['PropertyEnums']['PropertySubType'] ); |
| 3171 |
} |
| 3172 |
|
| 3173 |
$propertytype_array = array(); |
| 3174 |
if ( isset( $metadata_api_call['PropertyEnums']['PropertyType'] ) && is_array( $metadata_api_call['PropertyEnums']['PropertyType'] ) ) { |
| 3175 |
$propertytype_array = array_keys( $metadata_api_call['PropertyEnums']['PropertyType'] ); |
| 3176 |
} |
| 3177 |
|
| 3178 |
|
| 3179 |
$standardstatus_array = array(); |
| 3180 |
if ( isset( $metadata_api_call['PropertyEnums']['StandardStatus'] ) && is_array( $metadata_api_call['PropertyEnums']['StandardStatus'] ) ) { |
| 3181 |
$standardstatus_array = array_keys( $metadata_api_call['PropertyEnums']['StandardStatus'] ); |
| 3182 |
} |
| 3183 |
|
| 3184 |
// if we do not have standart status |
| 3185 |
// Fall back to MlsStatus values when the MLS exposes no StandardStatus. |
| 3186 |
if ( empty( $standardstatus_array ) ) { |
| 3187 |
$standardstatus_array = $mlsstatus_array; |
| 3188 |
} |
| 3189 |
|
| 3190 |
|
| 3191 |
|
| 3192 |
|
| 3193 |
// Free-text "extra" inputs render empty; they hold comma-separated values. |
| 3194 |
$extracounty_values = ''; |
| 3195 |
$extracity_values = ''; |
| 3196 |
|
| 3197 |
// Ordered field definitions consumed by the Import Task metabox renderer. |
| 3198 |
$field_import = array( |
| 3199 |
'City' => array( |
| 3200 |
'label' => esc_html__( 'Select cities', 'mlsimport' ), |
| 3201 |
'description' => esc_html__( 'Select the cities from where we will import data.', 'mlsimport' ), |
| 3202 |
'type' => 'select', |
| 3203 |
'multiple' => 'yes', |
| 3204 |
'values' => $city_array, |
| 3205 |
), |
| 3206 |
|
| 3207 |
'extraCity' => array( |
| 3208 |
'label' => esc_html__( 'Add extra Cities', 'mlsimport' ), |
| 3209 |
'description' => esc_html__( 'Add extra cities, separated by comma. They need to be written exactly like they are stored in MLS (for example all caps)', 'mlsimport' ), |
| 3210 |
'type' => 'input', |
| 3211 |
'multiple' => 'no', |
| 3212 |
'values' => $extracity_values, |
| 3213 |
), |
| 3214 |
|
| 3215 |
'CountyOrParish' => array( |
| 3216 |
'label' => esc_html__( 'Select Counties', 'mlsimport' ), |
| 3217 |
'description' => esc_html__( 'Select the counties from where we will import data.', 'mlsimport' ), |
| 3218 |
'type' => 'select', |
| 3219 |
'multiple' => 'yes', |
| 3220 |
'values' => $county_array, |
| 3221 |
'show_extra_field' => true, |
| 3222 |
), |
| 3223 |
|
| 3224 |
'extraCounty' => array( |
| 3225 |
'label' => esc_html__( 'Add extra Counties', 'mlsimport' ), |
| 3226 |
'description' => esc_html__( 'Add extra counties, separated by comma. They need to be written exactly like they are stored in MLS (for example all caps)', 'mlsimport' ), |
| 3227 |
'type' => 'input', |
| 3228 |
'multiple' => 'no', |
| 3229 |
'values' => $extracounty_values, |
| 3230 |
), |
| 3231 |
|
| 3232 |
'MLSAreaMajor' => array( |
| 3233 |
'label' => esc_html__( 'MLS Area Major', 'mlsimport' ), |
| 3234 |
'description' => esc_html__( 'Filter listings by MLSAreaMajor.', 'mlsimport' ), |
| 3235 |
'type' => 'input', |
| 3236 |
'multiple' => 'no', |
| 3237 |
), |
| 3238 |
|
| 3239 |
'SubdivisionName' => array( |
| 3240 |
'label' => esc_html__( 'Subdivision Name', 'mlsimport' ), |
| 3241 |
'description' => esc_html__( 'Filter listings by SubDivisionName.', 'mlsimport' ), |
| 3242 |
'type' => 'input', |
| 3243 |
'multiple' => 'no', |
| 3244 |
), |
| 3245 |
|
| 3246 |
'PostalCode' => array( |
| 3247 |
'label' => esc_html__( 'Select Postal Code', 'mlsimport' ), |
| 3248 |
'description' => esc_html__( 'Enter one or more postal codes to import listings from, separated by commas (e.g. 12345, 23456).', 'mlsimport' ), |
| 3249 |
'type' => 'input', |
| 3250 |
'multiple' => 'no', |
| 3251 |
), |
| 3252 |
|
| 3253 |
'PropertySubType' => array( |
| 3254 |
'label' => esc_html__( 'Select Property Category', 'mlsimport' ), |
| 3255 |
'description' => esc_html__( 'Property Category', 'mlsimport' ), |
| 3256 |
'type' => 'select', |
| 3257 |
'multiple' => 'yes', |
| 3258 |
'values' => $propertysubtype_array, |
| 3259 |
), |
| 3260 |
'PropertyType' => array( |
| 3261 |
'label' => esc_html__( 'Select Property Action Category', 'mlsimport' ), |
| 3262 |
'description' => esc_html__( 'Property Action Category', 'mlsimport' ), |
| 3263 |
'type' => 'select', |
| 3264 |
'multiple' => 'yes', |
| 3265 |
'values' => $propertytype_array, |
| 3266 |
), |
| 3267 |
'StandardStatus' => array( |
| 3268 |
'label' => esc_html__( 'Select Status', 'mlsimport' ), |
| 3269 |
'description' => __( 'The list is auto-populated with MLS available statuses. To select multiple statuses, use Ctrl (Windows) or Command (Mac).', 'mlsimport' ), |
| 3270 |
'type' => 'select', |
| 3271 |
'multiple' => 'yes', |
| 3272 |
'values' => $standardstatus_array, |
| 3273 |
), |
| 3274 |
'StandardStatusProtect' => array( |
| 3275 |
'label' => esc_html__( 'Protected Statuses', 'mlsimport' ), |
| 3276 |
'description' => __( 'Properties with these statuses will NEVER be deleted from your website during reconciliation, even if they are no longer found in the MLS. Use this to protect Closed, Expired, or other non-active listings from automatic deletion.', 'mlsimport' ), |
| 3277 |
'type' => 'select', |
| 3278 |
'multiple' => 'yes', |
| 3279 |
'values' => $standardstatus_array, |
| 3280 |
), |
| 3281 |
|
| 3282 |
'InternetEntireListingDisplayYN' => array( |
| 3283 |
'label' => esc_html__( 'Internet Entire Listing Display ', 'mlsimport'), |
| 3284 |
'description' => esc_html__( 'A yes/no field that states the seller has allowed the listing to be displayed on Internet sites.', 'mlsimport' ), |
| 3285 |
'type' => 'select', |
| 3286 |
'multiple' => 'no', |
| 3287 |
'values' => array( |
| 3288 |
'yes', |
| 3289 |
'no', |
| 3290 |
), |
| 3291 |
), |
| 3292 |
'InternetAddressDisplayYN' => array( |
| 3293 |
'label' => esc_html__( 'Internet Address display', 'mlsimport' ), |
| 3294 |
'description' => esc_html__( 'A yes/no field that states the seller has allowed the listing address to be displayed on Internet sites.', 'mlsimport' ), |
| 3295 |
'type' => 'select', |
| 3296 |
'multiple' => 'no', |
| 3297 |
'values' => array( |
| 3298 |
'yes', |
| 3299 |
'no', |
| 3300 |
), |
| 3301 |
), |
| 3302 |
'ListAgentKey' => array( |
| 3303 |
'label' => esc_html__( 'ListAgentKey', 'mlsimport' ), |
| 3304 |
'description' => esc_html__( 'Import listings from a specific Agent (contact your MLS for this information)', 'mlsimport' ), |
| 3305 |
'type' => 'input', |
| 3306 |
'multiple' => 'no', |
| 3307 |
), |
| 3308 |
'ListAgentMlsId' => array( |
| 3309 |
'label' => esc_html__( 'ListAgentMlsId', 'mlsimport' ), |
| 3310 |
'description' => esc_html__( 'Import listings from a specific Agent (contact your MLS for this information)', 'mlsimport' ), |
| 3311 |
'type' => 'input', |
| 3312 |
'multiple' => 'no', |
| 3313 |
), |
| 3314 |
'BuyerAgentMlsId' => array( |
| 3315 |
'label' => esc_html__( 'BuyerAgentMlsId', 'mlsimport' ), |
| 3316 |
'description' => esc_html__( 'Import listings from a specific Buyer Agent (contact your MLS for this information)', 'mlsimport' ), |
| 3317 |
'type' => 'input', |
| 3318 |
'multiple' => 'no', |
| 3319 |
), |
| 3320 |
'ListOfficeKey' => array( |
| 3321 |
'label' => esc_html__( 'ListOfficeKey', 'mlsimport' ), |
| 3322 |
'description' => esc_html__( 'Import listings from a specific Office (contact your MLS for this information)', 'mlsimport'), |
| 3323 |
'type' => 'input', |
| 3324 |
'multiple' => 'no', |
| 3325 |
), |
| 3326 |
'ListOfficeMlsId' => array( |
| 3327 |
'label' => esc_html__( 'ListOfficeMlsId', 'mlsimport' ), |
| 3328 |
'description' => esc_html__( 'Import listings from a specific Office (contact your MLS for this information)', 'mlsimport' ), |
| 3329 |
'type' => 'input', |
| 3330 |
'multiple' => 'no', |
| 3331 |
), |
| 3332 |
'ListingId' => array( |
| 3333 |
'label' => esc_html__( 'ListingId', 'mlsimport' ), |
| 3334 |
'description' => esc_html__( 'Import One Property Only via parameter ListingID. If this does not work for you, please contact us to check if the field exists in your MLS.', 'mlsimport'), |
| 3335 |
'type' => 'input', |
| 3336 |
'multiple' => 'no', |
| 3337 |
), |
| 3338 |
'Exclude_ListOfficeMlsId' => array( |
| 3339 |
'label' => esc_html__( 'Exclude listings with ListOfficeMlsId', 'mlsimport' ), |
| 3340 |
'description' => esc_html__( 'Exclude listings that belong to one or more ListOfficeMlsId.', 'mlsimport' ), |
| 3341 |
'type' => 'input', |
| 3342 |
'multiple' => 'no', |
| 3343 |
), |
| 3344 |
'Exclude_ListOfficeKey' => array( |
| 3345 |
'label' => esc_html__( 'Exclude listings with ListOfficeKey', 'mlsimport' ), |
| 3346 |
'description' => esc_html__( 'Exclude listings that belong to one or more ListOfficeKey', 'mlsimport'), |
| 3347 |
'type' => 'input', |
| 3348 |
'multiple' => 'no', |
| 3349 |
), |
| 3350 |
|
| 3351 |
|
| 3352 |
'Exclude_ListAgentMlsId' => array( |
| 3353 |
'label' => esc_html__( 'Exclude listings with ListAgentMlsId', 'mlsimport' ), |
| 3354 |
'description' => esc_html__( 'Exclude listings that belong to one or more ListAgentMlsId.', 'mlsimport' ), |
| 3355 |
'type' => 'input', |
| 3356 |
'multiple' => 'no', |
| 3357 |
), |
| 3358 |
'Exclude_ListAgentKey' => array( |
| 3359 |
'label' => esc_html__( 'Exclude listings with ListAgentKey ', 'mlsimport' ), |
| 3360 |
'description' => esc_html__( 'Exclude listings that belong to one or more ListAgentKey ', 'mlsimport'), |
| 3361 |
'type' => 'input', |
| 3362 |
'multiple' => 'no', |
| 3363 |
), |
| 3364 |
'CustomParameters' => array( |
| 3365 |
'label' => esc_html__( 'Custom parameters', 'mlsimport' ), |
| 3366 |
'description' => esc_html__( 'Add raw query fragment parameters (for example: $filter=WaterfrontYN eq true). They will be forwarded to the RESO API request.', 'mlsimport' ), |
| 3367 |
'type' => 'input', |
| 3368 |
'multiple' => 'no', |
| 3369 |
), |
| 3370 |
|
| 3371 |
|
| 3372 |
); |
| 3373 |
return $field_import; |
| 3374 |
} |
| 3375 |
|
| 3376 |
|
| 3377 |
|
| 3378 |
|
| 3379 |
|
| 3380 |
|
| 3381 |
|
| 3382 |
/** |
| 3383 |
* AJAX: kick off a manual import for one Import Task. |
| 3384 |
* |
| 3385 |
* Resets the force-stop flag, builds the paginated batch of request-argument |
| 3386 |
* sets, stores them (and zeroed progress meta), marks the task 'started', and |
| 3387 |
* enqueues the Action Scheduler background job that does the actual import. |
| 3388 |
* Returns any build error immediately, otherwise {success:true}. |
| 3389 |
* |
| 3390 |
* @return void Emits JSON. |
| 3391 |
*/ |
| 3392 |
public function mlsimport_move_files_per_item() { |
| 3393 |
// CSRF. |
| 3394 |
check_ajax_referer( 'mlsimport_item_actions', 'security' ); |
| 3395 |
// Read + default the task id and count inputs. |
| 3396 |
$post_id = 0; |
| 3397 |
$how_many = 0; |
| 3398 |
$max_number = 0; |
| 3399 |
if(isset( $_POST['post_id'] )){ |
| 3400 |
$post_id = intval( $_POST['post_id'] ); |
| 3401 |
} |
| 3402 |
if(isset( $_POST['how_many'] )){ |
| 3403 |
$how_many = intval( $_POST['how_many'] ); |
| 3404 |
} |
| 3405 |
if(isset( $_POST['post_number'] )){ |
| 3406 |
$max_number = intval( $_POST['post_number'] ); |
| 3407 |
} |
| 3408 |
|
| 3409 |
// Admin boundary: the target must be an Import Task the user can edit. |
| 3410 |
if ( 'mlsimport_item' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { |
| 3411 |
wp_send_json_error( array( 'message' => esc_html__( 'You are not allowed to manage this import task.', 'mlsimport' ) ), 403 ); |
| 3412 |
} |
| 3413 |
|
| 3414 |
// Whether the request comes from the onboarding wizard. |
| 3415 |
$is_onboard=intval($_POST['is_onboard']); |
| 3416 |
|
| 3417 |
// Clear any prior stop flag so this run proceeds (autoload off). |
| 3418 |
update_option( 'mlsimport_force_stop_' . $post_id, 'no', false ); |
| 3419 |
|
| 3420 |
$item_id_array = array( |
| 3421 |
'item_id' => $post_id, |
| 3422 |
'how_many' => $how_many, |
| 3423 |
'max_number' => $max_number, |
| 3424 |
'batch_counter' => 1, |
| 3425 |
); |
| 3426 |
|
| 3427 |
|
| 3428 |
// Clear any stale batch, then build the fresh batch of requests. |
| 3429 |
update_post_meta( $post_id, 'mlsimport_attach_to_move_' . $post_id, '' ); |
| 3430 |
|
| 3431 |
$attachments_to_move = (array) $this->mlsimport_saas_generate_import_requests_per_item( $item_id_array ); |
| 3432 |
|
| 3433 |
// If an error was returned from the API, sanitize and send it back to the client and stop further processing. |
| 3434 |
if ( isset( $attachments_to_move['success'] ) && false === $attachments_to_move['success'] ) { |
| 3435 |
if ( isset( $attachments_to_move['message'] ) ) { |
| 3436 |
$attachments_to_move['message'] = wp_strip_all_tags( $attachments_to_move['message'] ); |
| 3437 |
} |
| 3438 |
wp_send_json( $attachments_to_move ); |
| 3439 |
wp_die(); |
| 3440 |
} |
| 3441 |
|
| 3442 |
// Persist the batch for the background worker to consume. |
| 3443 |
update_post_meta( $post_id, 'mlsimport_attach_to_move_' . $post_id, $attachments_to_move ); |
| 3444 |
|
| 3445 |
// net stat data |
| 3446 |
// Reset progress counters shown in the UI. |
| 3447 |
update_post_meta( $post_id, 'mlsimport_progress_properties', 0 ); |
| 3448 |
update_post_meta( $post_id, 'mlsimport_progress_batches', 0 ); |
| 3449 |
update_post_meta( $post_id, 'mlsimport_progress_memory', 0 ); |
| 3450 |
|
| 3451 |
|
| 3452 |
|
| 3453 |
$attachments_to_send = array( |
| 3454 |
'args' => array( |
| 3455 |
'attachments_to_move' => $post_id, |
| 3456 |
'item_id_array' => $item_id_array, |
| 3457 |
'is_onboard' =>$is_onboard, |
| 3458 |
), |
| 3459 |
); |
| 3460 |
|
| 3461 |
mlsimport_saas_single_write_import_custom_logs( 'Preparing the import. Please hold on.' . PHP_EOL ); |
| 3462 |
mlsimport_debuglogs_per_plugin( 'Preparing the import. Please hold on.' . PHP_EOL ); |
| 3463 |
|
| 3464 |
// Mark the task started, then enqueue the async import job. |
| 3465 |
update_post_meta( $post_id, 'mlsimport_spawn_status', 'started' ); |
| 3466 |
|
| 3467 |
// old |
| 3468 |
as_enqueue_async_action( 'mlsimport_background_process_per_item', $attachments_to_send ); |
| 3469 |
|
| 3470 |
// Remove any pending async jobs for this item and enqueue a unique one |
| 3471 |
//bad ideea |
| 3472 |
// as_unschedule_all_actions( 'mlsimport_background_process_per_item', $attachments_to_send ); |
| 3473 |
//as_enqueue_async_action( 'mlsimport_background_process_per_item', $attachments_to_send, '', true ); |
| 3474 |
|
| 3475 |
|
| 3476 |
// Nudge WP-Cron so the queued action runs promptly. |
| 3477 |
spawn_cron(); |
| 3478 |
|
| 3479 |
unset( $attachments_to_send ); |
| 3480 |
|
| 3481 |
// Return success response to the AJAX caller. |
| 3482 |
wp_send_json( array( 'success' => true ) ); |
| 3483 |
} |
| 3484 |
|
| 3485 |
/** |
| 3486 |
* Process MLS Import attachments via background cron. |
| 3487 |
* Memory-optimized with detailed memory usage logging. |
| 3488 |
* |
| 3489 |
* @param array $input_arg |
| 3490 |
* @return void |
| 3491 |
*/ |
| 3492 |
public function mlsimport_background_process_per_item_cron_function( $input_arg ) { |
| 3493 |
global $mlsimport; |
| 3494 |
|
| 3495 |
$log = 'In cron processing function ->' . wp_json_encode( $input_arg['item_id_array'] ) . PHP_EOL; |
| 3496 |
mlsimport_saas_single_write_import_custom_logs( $log, 'cron' ); |
| 3497 |
mlsimport_saas_single_write_import_custom_logs( '[Memory] Start: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB', 'cron' ); |
| 3498 |
|
| 3499 |
// Load attachments to move from post meta |
| 3500 |
$attachments_to_move = get_post_meta( |
| 3501 |
$input_arg['item_id_array']['item_id'], |
| 3502 |
'mlsimport_cron_attach_to_move_' . $input_arg['item_id_array']['item_id'], |
| 3503 |
true |
| 3504 |
); |
| 3505 |
$log = '[Memory] After loading attachments: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB' . PHP_EOL; |
| 3506 |
mlsimport_saas_single_write_import_custom_logs( $log, 'cron' ); |
| 3507 |
|
| 3508 |
// Run each request batch: call the API and parse the results, freeing |
| 3509 |
// memory between iterations. |
| 3510 |
if (!empty($attachments_to_move) && is_array($attachments_to_move)) { |
| 3511 |
foreach ($attachments_to_move as $key => $import_arguments) { |
| 3512 |
// Optionally clear any cache for this batch |
| 3513 |
if ( isset($GLOBALS['wp_object_cache']) ) { |
| 3514 |
$GLOBALS['wp_object_cache']->delete('mlsimport_force_stop_' . $input_arg['item_id_array']['item_id'], 'options'); |
| 3515 |
} |
| 3516 |
|
| 3517 |
$log = '[Memory] Before API batch ' . $key . ': ' . (memory_get_usage(true) / 1024 / 1024) . ' MB' . PHP_EOL; |
| 3518 |
mlsimport_saas_single_write_import_custom_logs( $log, 'cron' ); |
| 3519 |
|
| 3520 |
// API call |
| 3521 |
$api_call_array = $this->theme_importer->globalApiRequestCurlSaas('listings', $import_arguments, 'POST'); |
| 3522 |
|
| 3523 |
$log = '[Memory] After API batch ' . $key . ': ' . (memory_get_usage(true) / 1024 / 1024) . ' MB' . PHP_EOL; |
| 3524 |
mlsimport_saas_single_write_import_custom_logs( $log, 'cron' ); |
| 3525 |
|
| 3526 |
// Parse/process response |
| 3527 |
$mlsimport->admin->theme_importer->mlsimportSaasCronParseSearchArrayPerItem( |
| 3528 |
$api_call_array, $input_arg['item_id_array'], $key |
| 3529 |
); |
| 3530 |
|
| 3531 |
// Free per-iteration memory |
| 3532 |
unset($api_call_array, $import_arguments); |
| 3533 |
gc_collect_cycles(); |
| 3534 |
|
| 3535 |
$log = '[Memory] After cleanup batch ' . $key . ': ' . (memory_get_usage(true) / 1024 / 1024) . ' MB' . PHP_EOL; |
| 3536 |
mlsimport_saas_single_write_import_custom_logs( $log, 'cron' ); |
| 3537 |
} |
| 3538 |
} |
| 3539 |
|
| 3540 |
mlsimport_saas_single_write_import_custom_logs('[Memory] End: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB', 'cron' ); |
| 3541 |
mlsimport_saas_single_write_import_custom_logs('CRON JOB Import Completed ' . PHP_EOL, 'cron'); |
| 3542 |
mlsimport_debuglogs_per_plugin('CRON JOB Import Completed ' . PHP_EOL); |
| 3543 |
update_post_meta($input_arg['item_id_array']['item_id'], 'mlsimport_spawn_status', 'completed'); |
| 3544 |
|
| 3545 |
unset($attachments_to_move, $input_arg, $log); |
| 3546 |
gc_collect_cycles(); |
| 3547 |
} |
| 3548 |
|
| 3549 |
|
| 3550 |
|
| 3551 |
|
| 3552 |
/** |
| 3553 |
* Build the paginated list of request-argument sets for an import run. |
| 3554 |
* |
| 3555 |
* Resolves how many listings to fetch (0 => all found, capped at max_found |
| 3556 |
* and a hard 10000 ceiling), then produces one argument set per page of 25. |
| 3557 |
* Propagates an API error set immediately instead of a batch list. |
| 3558 |
* |
| 3559 |
* @param array $item_id_array {item_id, how_many, max_number, batch_counter}. |
| 3560 |
* @param string $last_date Modification-time watermark (optional). |
| 3561 |
* @param bool $is_hourly_sync Whether this is an hourly-cron run. |
| 3562 |
* @return array Array of argument sets, or an API error array. |
| 3563 |
*/ |
| 3564 |
public function mlsimport_saas_generate_import_requests_per_item( $item_id_array, $last_date = '', $is_hourly_sync = false ) { |
| 3565 |
// Page size for each batched listings request. |
| 3566 |
$import_step = 25; |
| 3567 |
|
| 3568 |
// Resolve the effective count: 0 means "all found". |
| 3569 |
$prop_id = $item_id_array['item_id']; |
| 3570 |
$max_found = $item_id_array['max_number']; |
| 3571 |
$how_many = $item_id_array['how_many']; |
| 3572 |
if ( 0=== intval($how_many) ) { |
| 3573 |
$how_many = $max_found; |
| 3574 |
} |
| 3575 |
// Never request more than actually exist. |
| 3576 |
if ( $how_many > $max_found ) { |
| 3577 |
$how_many = $max_found; |
| 3578 |
} |
| 3579 |
|
| 3580 |
$search_url_step = ''; |
| 3581 |
$urls_array = array(); |
| 3582 |
|
| 3583 |
// Hard ceiling of 10000 per task; record the planned total for progress. |
| 3584 |
$skip = 0; |
| 3585 |
if ( $how_many > 10000 ) { |
| 3586 |
$how_many = 10000; |
| 3587 |
} |
| 3588 |
update_post_meta($prop_id,'mlsimport_task_to_import', intval($how_many) ); |
| 3589 |
|
| 3590 |
// Shrink the page size if fewer than one page remain. |
| 3591 |
if ( $how_many < $import_step ) { |
| 3592 |
$import_step = $how_many; |
| 3593 |
} |
| 3594 |
|
| 3595 |
// Emit one argument set per page until the target count is reached. |
| 3596 |
while ( $skip < $how_many ) { |
| 3597 |
|
| 3598 |
// Determine how many items to request for this batch. |
| 3599 |
$batch_step = min( $import_step, $how_many - $skip ); |
| 3600 |
|
| 3601 |
// Build the request arguments using the remaining count. |
| 3602 |
$search_url_step = $this->mlsimport_saas_make_listing_requests_arguments( $prop_id, $last_date, $skip, $batch_step, $is_hourly_sync ); |
| 3603 |
|
| 3604 |
// If the API returned an error, propagate it immediately. |
| 3605 |
if ( isset( $search_url_step['success'] ) && false === $search_url_step['success'] ) { |
| 3606 |
return $search_url_step; |
| 3607 |
} |
| 3608 |
|
| 3609 |
$skip += $batch_step; |
| 3610 |
$urls_array[] = $search_url_step; |
| 3611 |
|
| 3612 |
|
| 3613 |
} |
| 3614 |
return $urls_array; |
| 3615 |
} |
| 3616 |
|
| 3617 |
|
| 3618 |
|
| 3619 |
|
| 3620 |
|
| 3621 |
|
| 3622 |
|
| 3623 |
/** |
| 3624 |
* Action Scheduler worker: run a manual import's batches for one task. |
| 3625 |
* |
| 3626 |
* Loads the pre-built batch and the task's option meta once, then iterates |
| 3627 |
* each batch — calling the listings API and parsing results into posts — |
| 3628 |
* while honoring the force-stop flag and flushing memory between batches. |
| 3629 |
* Marks the task 'completed' and clears the batch meta when finished. |
| 3630 |
* |
| 3631 |
* @param array $input_arg {item_id_array:{item_id,...}, ...}. |
| 3632 |
* @return void |
| 3633 |
*/ |
| 3634 |
public function mlsimport_background_process_per_item_function( $input_arg ) { |
| 3635 |
|
| 3636 |
// Task id + a log prefix identifying it. |
| 3637 |
$mlsimportItemId = $input_arg['item_id_array']['item_id']; |
| 3638 |
$log_prefix = 'In processing function - Item ID: ' . $mlsimportItemId . ' -> '; |
| 3639 |
mlsimport_saas_single_write_import_custom_logs( $log_prefix . wp_json_encode( $input_arg['item_id_array'] ) . PHP_EOL ); |
| 3640 |
|
| 3641 |
|
| 3642 |
// Get from MLS Import the big argument array only once |
| 3643 |
$attachments_to_move = get_post_meta( $mlsimportItemId, 'mlsimport_attach_to_move_' . $mlsimportItemId, true ); |
| 3644 |
|
| 3645 |
// No batch payload: the meta is '' (stale clear, failed start, or already |
| 3646 |
// deleted by a completed run). count()/foreach on a string throws TypeError |
| 3647 |
// on PHP 8, so bail out and release the task instead. |
| 3648 |
if ( ! is_array( $attachments_to_move ) || empty( $attachments_to_move ) ) { |
| 3649 |
mlsimport_saas_single_write_import_custom_logs( $log_prefix . 'No batch payload found - nothing to import.' . PHP_EOL ); |
| 3650 |
update_post_meta( $mlsimportItemId, 'mlsimport_spawn_status', 'completed' ); |
| 3651 |
return; |
| 3652 |
} |
| 3653 |
|
| 3654 |
// Retrieve all meta data in one go to reduce database queries |
| 3655 |
$mlsimport_item_option_data = array( |
| 3656 |
'mlsimport_item_standardstatus' => get_post_meta( $mlsimportItemId, 'mlsimport_item_standardstatus', true ), |
| 3657 |
'mlsimport_item_standardstatusprotect' => get_post_meta( $mlsimportItemId, 'mlsimport_item_standardstatusprotect', true ), |
| 3658 |
'mlsimport_item_property_user' => get_post_meta( $mlsimportItemId, 'mlsimport_item_property_user', true ), |
| 3659 |
'mlsimport_item_agent' => get_post_meta( $mlsimportItemId, 'mlsimport_item_agent', true ), |
| 3660 |
'mlsimport_item_use_mls_agent' => get_post_meta( $mlsimportItemId, 'mlsimport_item_use_mls_agent', true ), |
| 3661 |
'mlsimport_item_property_status' => get_post_meta( $mlsimportItemId, 'mlsimport_item_property_status', true ), |
| 3662 |
); |
| 3663 |
|
| 3664 |
$total_batches = count( $attachments_to_move ); |
| 3665 |
|
| 3666 |
// removed because $this |
| 3667 |
global $mlsimport; |
| 3668 |
|
| 3669 |
$log = 'In processing function $attachments_to_move ->' . wp_json_encode( $attachments_to_move ) . PHP_EOL; |
| 3670 |
mlsimport_saas_single_write_import_custom_logs( $log ); |
| 3671 |
|
| 3672 |
|
| 3673 |
// Process each batch unless the user has requested a stop. |
| 3674 |
foreach ( $attachments_to_move as $key => $import_arguments ) { |
| 3675 |
// reconsider use |
| 3676 |
// $GLOBALS['wp_object_cache']->delete('mlsimport_force_stop_' . $input_arg['item_id_array']['item_id'], 'options'); |
| 3677 |
// Re-read the stop flag each batch so a stop takes effect mid-run. |
| 3678 |
$status = get_option( 'mlsimport_force_stop_' . $mlsimportItemId ); |
| 3679 |
if ( 'no' === $status ) { |
| 3680 |
// Clear memory before processing each batch |
| 3681 |
wp_cache_flush(); |
| 3682 |
gc_collect_cycles(); |
| 3683 |
|
| 3684 |
// wp_cache_flush(); |
| 3685 |
$mem_usage = memory_get_usage( true ); |
| 3686 |
$mem_usage_show = round( $mem_usage / 1048576, 2 ); |
| 3687 |
|
| 3688 |
update_post_meta( $mlsimportItemId, 'mlsimport_progress_batches', $key + 1 ); |
| 3689 |
update_post_meta( $mlsimportItemId, 'mlsimport_progress_memory', $mem_usage_show ); |
| 3690 |
|
| 3691 |
|
| 3692 |
|
| 3693 |
mlsimport_saas_single_write_import_custom_logs( $log ); |
| 3694 |
$log = 'Parsing import batch: ' . ( $key + 1 ) . ' of ' . $total_batches . '. Memory used: ' . $mem_usage_show . ' MB.' . PHP_EOL; |
| 3695 |
|
| 3696 |
|
| 3697 |
// Combine logs and reduce function calls |
| 3698 |
mlsimport_saas_single_write_import_custom_logs( $log ); |
| 3699 |
mlsimport_debuglogs_per_plugin( $log ); |
| 3700 |
print esc_html($log); |
| 3701 |
|
| 3702 |
// Fetch this batch of listings and turn them into posts. |
| 3703 |
$api_call_array = $this->theme_importer->globalApiRequestCurlSaas( 'listings', $import_arguments, 'POST' ); |
| 3704 |
|
| 3705 |
$mlsimport->admin->theme_importer->mlsimportSaasParseSearchArrayPerItem( $api_call_array, $input_arg['item_id_array'], $key, $mlsimport_item_option_data ); |
| 3706 |
|
| 3707 |
|
| 3708 |
|
| 3709 |
// Explicitly unset large variables after each batch |
| 3710 |
unset($api_call_array); |
| 3711 |
|
| 3712 |
// Force garbage collection again after processing |
| 3713 |
wp_cache_flush(); |
| 3714 |
gc_collect_cycles(); |
| 3715 |
|
| 3716 |
|
| 3717 |
// Add a small delay to allow memory to be freed |
| 3718 |
if (($key + 1) < $total_batches) { |
| 3719 |
usleep(100000); // 100ms pause between batches |
| 3720 |
} |
| 3721 |
|
| 3722 |
} else { |
| 3723 |
|
| 3724 |
// Force-stop requested: finalize as completed and break out. |
| 3725 |
$final_mem_usage = memory_get_usage( true ); |
| 3726 |
$final_mem_usage_show = round( $final_mem_usage / 1048576, 2 ); |
| 3727 |
|
| 3728 |
|
| 3729 |
update_post_meta( $mlsimportItemId, 'mlsimport_spawn_status', 'completed' ); |
| 3730 |
delete_post_meta( $mlsimportItemId, 'mlsimport_attach_to_move_' . $mlsimportItemId ); |
| 3731 |
|
| 3732 |
//new stats |
| 3733 |
update_post_meta( $mlsimportItemId, 'mlsimport_progress_batches', $total_batches ); |
| 3734 |
update_post_meta( $mlsimportItemId, 'mlsimport_progress_memory', $final_mem_usage_show ); |
| 3735 |
|
| 3736 |
|
| 3737 |
mlsimport_saas_single_write_import_custom_logs( PHP_EOL . 'Parsing importing link FORCE STOP : ' ); |
| 3738 |
mlsimport_debuglogs_per_plugin( 'Parsing importing link FORCE STOP : ' ); |
| 3739 |
break; // Exit the loop if forced to stop |
| 3740 |
} |
| 3741 |
} |
| 3742 |
|
| 3743 |
// All batches done: mark completed and drop the stored batch payload. |
| 3744 |
mlsimport_saas_single_write_import_custom_logs( 'Import Completed ' . PHP_EOL ); |
| 3745 |
mlsimport_debuglogs_per_plugin( 'Import Completed ' . PHP_EOL ); |
| 3746 |
|
| 3747 |
update_post_meta( $mlsimportItemId, 'mlsimport_spawn_status', 'completed' ); |
| 3748 |
delete_post_meta( $mlsimportItemId, 'mlsimport_attach_to_move_' . $mlsimportItemId ); |
| 3749 |
|
| 3750 |
// Final cleanup |
| 3751 |
unset($attachments_to_move); |
| 3752 |
unset($mlsimport_item_option_data); |
| 3753 |
unset($input_arg); |
| 3754 |
unset($log); |
| 3755 |
|
| 3756 |
// One final garbage collection |
| 3757 |
wp_cache_flush(); |
| 3758 |
gc_collect_cycles(); |
| 3759 |
} |
| 3760 |
|
| 3761 |
|
| 3762 |
|
| 3763 |
|
| 3764 |
|
| 3765 |
|
| 3766 |
|
| 3767 |
|
| 3768 |
|
| 3769 |
/** |
| 3770 |
* AJAX: poll import status/logs for a task (drives the progress UI). |
| 3771 |
* |
| 3772 |
* Admin-only; accepts either the import-task or onboarding nonce. Reads the |
| 3773 |
* status log file plus progress meta and returns a JSON payload flagged |
| 3774 |
* 'done' (stopped/completed) or 'wip' (in progress). |
| 3775 |
* |
| 3776 |
* @return void Emits JSON then dies. |
| 3777 |
*/ |
| 3778 |
public function mlsimport_logger_per_item() { |
| 3779 |
// Authorization: only administrators may read import logs/status |
| 3780 |
// (consistent with mlsimport_get_taxonomy_terms()). |
| 3781 |
if ( ! current_user_can( 'administrator' ) ) { |
| 3782 |
wp_send_json_error( 'Unauthorized' ); |
| 3783 |
} |
| 3784 |
// CSRF: accept the nonce from either legitimate caller — the import-task |
| 3785 |
// screen (mlsimport_item_actions) or the onboarding wizard (mlsimport_onboarding_nonce). |
| 3786 |
if ( ! check_ajax_referer( 'mlsimport_item_actions', 'security', false ) |
| 3787 |
&& ! check_ajax_referer( 'mlsimport_onboarding_nonce', 'security', false ) ) { |
| 3788 |
wp_send_json_error( array( 'message' => 'invalid nonce' ), 403 ); |
| 3789 |
} |
| 3790 |
$post_id=0; |
| 3791 |
if(isset($_POST['post_id'] )){ |
| 3792 |
$post_id = intval( $_POST['post_id'] ); |
| 3793 |
} |
| 3794 |
|
| 3795 |
// Current spawn status + the shared status log file contents. |
| 3796 |
$status = get_post_meta( $post_id, 'mlsimport_spawn_status', true ); |
| 3797 |
$path = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log'; |
| 3798 |
$logs = file_get_contents( $path ); |
| 3799 |
|
| 3800 |
//get neww status data |
| 3801 |
// Progress counters written by the background worker. |
| 3802 |
$current = intval( get_post_meta( $post_id, 'mlsimport_progress_properties', true ) ); |
| 3803 |
$total = intval( get_post_meta( $post_id, 'mlsimport_progress_batches', true ) ); |
| 3804 |
$memory = get_post_meta( $post_id, 'mlsimport_progress_memory', true ); |
| 3805 |
$mlsimport_task_to_import = intval( get_post_meta($post_id, 'mlsimport_task_to_import',true)); |
| 3806 |
|
| 3807 |
|
| 3808 |
|
| 3809 |
// Stop flag: the option is authoritative (second assignment wins). |
| 3810 |
$force_status = intval( get_post_meta( $post_id, 'mlsimport_force_stop', true ) ); |
| 3811 |
$force_status = get_option( 'mlsimport_force_stop_' . $post_id ); |
| 3812 |
|
| 3813 |
// Stop requested -> report done. |
| 3814 |
if ( 'no' !== $force_status ) { |
| 3815 |
echo wp_json_encode( |
| 3816 |
array( |
| 3817 |
'is_done' => 'done', |
| 3818 |
'status' => $status, |
| 3819 |
'logs' => $logs, |
| 3820 |
) |
| 3821 |
); |
| 3822 |
die(); |
| 3823 |
} |
| 3824 |
|
| 3825 |
// Empty/completed status -> report done; otherwise report wip. |
| 3826 |
if ( '' === $status || 'completed' === $status ) { |
| 3827 |
echo wp_json_encode( |
| 3828 |
array( |
| 3829 |
'is_done' => 'done', |
| 3830 |
'status' => $status, |
| 3831 |
'logs' => $logs, |
| 3832 |
'mlsimport_progress_properties' => $current, |
| 3833 |
'mlsimport_task_to_import' => $total, |
| 3834 |
) |
| 3835 |
); |
| 3836 |
} else { |
| 3837 |
// return from log |
| 3838 |
echo wp_json_encode( |
| 3839 |
array( |
| 3840 |
'is_done' => 'wip', |
| 3841 |
'status' => $status, |
| 3842 |
'logs' => $logs, |
| 3843 |
'mlsimport_progress_properties' => $current, |
| 3844 |
'mlsimport_progress_batches' => $total, |
| 3845 |
'memory' => $memory, |
| 3846 |
'mlsimport_task_to_import'=>$mlsimport_task_to_import, |
| 3847 |
'post_id'=>$post_id |
| 3848 |
|
| 3849 |
) |
| 3850 |
); |
| 3851 |
} |
| 3852 |
die(); |
| 3853 |
} |
| 3854 |
|
| 3855 |
|
| 3856 |
|
| 3857 |
|
| 3858 |
|
| 3859 |
|
| 3860 |
/** |
| 3861 |
* AJAX: request a force-stop of a running import for one task. |
| 3862 |
* |
| 3863 |
* Sets the per-task force-stop option to 'yes' and clears its object-cache |
| 3864 |
* entry so the in-flight background loop notices on its next iteration. |
| 3865 |
* |
| 3866 |
* @return void Emits JSON success. |
| 3867 |
*/ |
| 3868 |
public function mlsimport_stop_import_per_item() { |
| 3869 |
|
| 3870 |
|
| 3871 |
// CSRF + read the task id. |
| 3872 |
check_ajax_referer( 'mlsimport_item_actions', 'security' ); |
| 3873 |
$post_id=0; |
| 3874 |
if(isset($_POST['post_id'] )){ |
| 3875 |
$post_id = intval( $_POST['post_id'] ); |
| 3876 |
} |
| 3877 |
// Admin boundary: the target must be an Import Task the user can edit. |
| 3878 |
if ( 'mlsimport_item' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { |
| 3879 |
wp_send_json_error( array( 'message' => esc_html__( 'You are not allowed to manage this import task.', 'mlsimport' ) ), 403 ); |
| 3880 |
} |
| 3881 |
// Flip the stop flag (autoload off). |
| 3882 |
update_option( 'mlsimport_force_stop_' . $post_id, 'yes', false ); |
| 3883 |
// ensure caches are cleared so running processes see the update immediately |
| 3884 |
if ( function_exists( 'wp_cache_delete' ) ) { |
| 3885 |
wp_cache_delete( 'mlsimport_force_stop_' . $post_id, 'options' ); |
| 3886 |
} |
| 3887 |
mlsimport_saas_single_write_import_custom_logs( 'Stopped for ' . $post_id . PHP_EOL ); |
| 3888 |
mlsimport_debuglogs_per_plugin( 'Stopped for ' . $post_id . PHP_EOL ); |
| 3889 |
wp_send_json_success(); |
| 3890 |
} |
| 3891 |
|
| 3892 |
|
| 3893 |
|
| 3894 |
/** |
| 3895 |
* AJAX: fetch the MLS metadata (theme schema + field data + enums) for the |
| 3896 |
* configured theme and cache it in options, marking metadata as populated. |
| 3897 |
* |
| 3898 |
* @return void |
| 3899 |
*/ |
| 3900 |
public function mlsimport_saas_get_metadata_function() { |
| 3901 |
// CSRF. |
| 3902 |
check_ajax_referer( 'mlsimport_saas_get_metadata', 'security' ); |
| 3903 |
$theme_Start = new ThemeImport(); |
| 3904 |
|
| 3905 |
// GET /clients?theme_id=<id> to retrieve the schema + MLS metadata. |
| 3906 |
$values = array(); |
| 3907 |
$options = get_option( $this->plugin_name . '_admin_options' ); |
| 3908 |
$url = 'clients?theme_id=' . intval( $options['mlsimport_theme_used'] ); |
| 3909 |
|
| 3910 |
$answer = $theme_Start::globalApiRequestSaas( $url, $values, 'GET' ); |
| 3911 |
|
| 3912 |
// Mark populated and cache the three metadata blobs in options. |
| 3913 |
update_option( 'mlsimport_mls_metadata_populated', 'yes' ); |
| 3914 |
|
| 3915 |
update_option( 'mlsimport_mls_metadata_theme_schema', $answer['theme_schema'] ); |
| 3916 |
update_option( 'mlsimport_mls_metadata_mls_data', $answer['mls_data']['mls_meta_data'] ); |
| 3917 |
update_option( 'mlsimport_mls_metadata_mls_enums', $answer['mls_data']['mls_meta_enums'] ); |
| 3918 |
} |
| 3919 |
|
| 3920 |
|
| 3921 |
|
| 3922 |
|
| 3923 |
|
| 3924 |
|
| 3925 |
|
| 3926 |
|
| 3927 |
|
| 3928 |
|
| 3929 |
|
| 3930 |
|
| 3931 |
/** |
| 3932 |
* Append a timestamped message to the cron log file. |
| 3933 |
* |
| 3934 |
* Arrays are JSON-encoded; ensures the WP filesystem is initialized before |
| 3935 |
* writing (append + exclusive lock). |
| 3936 |
* |
| 3937 |
* @param string|array $message Message to log. |
| 3938 |
* @return void |
| 3939 |
*/ |
| 3940 |
public function mlsimport_debuglog_cron( $message ) { |
| 3941 |
// Encode arrays for readability. |
| 3942 |
if ( is_array( $message ) ) { |
| 3943 |
$message = wp_json_encode( $message ); |
| 3944 |
} |
| 3945 |
// Prefix with a human-readable timestamp. |
| 3946 |
$message = date( 'F j, Y, g:i a' ) . ' -> ' . $message; |
| 3947 |
// Ensure WP_Filesystem is available (harmless if already set up). |
| 3948 |
global $wp_filesystem; |
| 3949 |
if ( empty( $wp_filesystem ) ) { |
| 3950 |
require_once ABSPATH . '/wp-admin/includes/file.php'; |
| 3951 |
WP_Filesystem(); |
| 3952 |
} |
| 3953 |
|
| 3954 |
// Append to the cron log with an exclusive lock. |
| 3955 |
$path = WP_PLUGIN_DIR . '/mlsimport/logs/cron_logs.log'; |
| 3956 |
|
| 3957 |
file_put_contents( $path, $message, FILE_APPEND | LOCK_EX ); |
| 3958 |
} |
| 3959 |
|
| 3960 |
} |
| 3961 |
|