PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.2.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.2.1
7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 6.0.5 All 36 releases
mlsimport / includes / mlsimport-onboarding.php

mlsimport-onboarding.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.2.1, at includes/mlsimport-onboarding.php

1,048 lines 37.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MLSImport Onboarding Wizard
4 *
5 * This file contains all the functionality for the onboarding wizard
6 * that guides users through the initial setup of the MLSImport plugin.
7 * Manual account checks discard the prior login verdict before testing the
8 * submitted credentials, so a failed retry cannot reuse a subscription notice.
9 * The account identifier is a username or email; both use the established
10 * mlsimport_username option and token API parameter for compatibility.
11 *includes\mlsimport-onboarding.php
12 * @link https://mlsimport.com/
13 * @since 6.1.0
14 *
15 * @package Mlsimport
16 * @subpackage Mlsimport/includes
17 */
18
19 // If this file is called directly, abort.
20 if (!defined('WPINC')) {
21 die;
22 }
23
24 /**
25 * Initialize the onboarding functionality
26 *
27 * @since 6.1.0
28 */
29 function mlsimport_init_onboarding() {
30 // Only initialize for admin pages
31 if (!is_admin()) {
32 return;
33 }
34
35 // Check if we need to start or continue onboarding
36 mlsimport_check_onboarding_status();
37
38
39 // Register the onboarding page
40 add_action('admin_menu', 'mlsimport_register_onboarding_page');
41
42 // Add menu item to start/resume onboarding
43 add_action('admin_menu', 'mlsimport_add_onboarding_menu_item');
44
45 // Add assets for onboarding
46 add_action('admin_enqueue_scripts', 'mlsimport_enqueue_onboarding_assets');
47
48 // Register the AJAX handlers used by the onboarding wizard.
49 add_action('wp_ajax_mlsimport_run_test_import', 'mlsimport_ajax_run_test_import');
50 add_action('wp_ajax_mlsimport_save_step_data', 'mlsimport_ajax_save_step_data');
51
52 // Intercept form submissions
53 add_action('admin_init', 'mlsimport_handle_step_submission');
54 }
55
56 /**
57 * Check if onboarding is complete or in progress
58 *
59 * @since 6.1.0
60 */
61 function mlsimport_check_onboarding_status() {
62 // Check if onboarding is complete
63 $onboarding_completed = get_option('mlsimport_onboarding_completed', false);
64
65 // If onboarding is complete, we don't need to do anything
66 if ($onboarding_completed) {
67 return;
68 }
69
70 // Redirect to onboarding if activation flag is set
71 if (get_option('mlsimport_do_onboarding_redirect', false)) {
72 delete_option('mlsimport_do_onboarding_redirect');
73 wp_safe_redirect(admin_url('admin.php?page=mlsimport-onboarding'));
74 exit;
75 }
76
77 // Check if we're on the plugin activation page
78 // Fires when WP's plugins screen just activated (activate=true) a plugin
79 // (plugin=...) whose path contains 'mlsimport'.
80 if (isset($_GET['activate']) && $_GET['activate'] == 'true' && isset($_GET['plugin']) && strpos($_GET['plugin'], 'mlsimport') !== false) {
81 // Redirect to onboarding welcome page
82 wp_redirect(admin_url('admin.php?page=mlsimport-onboarding'));
83 exit;
84 }
85 }
86
87 /**
88 * Register the onboarding admin page
89 *
90 * @since 6.1.0
91 */
92 function mlsimport_register_onboarding_page() {
93 add_submenu_page(
94 '', // No parent - won't appear in menu
95 __('MLS Import Setup Wizard', 'mlsimport'),
96 __('Setup Wizard', 'mlsimport'),
97 'manage_options',
98 'mlsimport-onboarding',
99 'mlsimport_render_onboarding_wizard'
100 );
101 }
102
103 /**
104 * Add onboarding menu item to the MLS Import menu
105 *
106 * @since 6.1.0
107 */
108 function mlsimport_add_onboarding_menu_item() {
109 // Only show if onboarding hasn't been completed
110
111 add_submenu_page(
112 'mlsimport_plugin_options',
113 __('Setup Wizard', 'mlsimport'),
114 __('Setup Wizard', 'mlsimport'),
115 'manage_options',
116 'mlsimport-onboarding',
117 'mlsimport_render_onboarding_wizard'
118 );
119
120 }
121
122 /**
123 * Enqueue scripts and styles for the onboarding wizard
124 *
125 * @since 6.1.0
126 * @param string $hook The current admin page
127 */
128 function mlsimport_enqueue_onboarding_assets($hook) {
129 /*
130 * Bail unless we are on the wizard page.
131 *
132 * This deliberately tests the page slug rather than $hook. WordPress does not
133 * build a submenu's hook from its parent SLUG — it uses the sanitized parent
134 * MENU TITLE. The wizard's parent is registered with the title "MLS Import
135 * Settings", so the real hook is 'mls-import-settings_page_mlsimport-onboarding',
136 * which matched neither of the two hook strings previously hardcoded here.
137 * The result was that this function returned immediately on the wizard page:
138 * mlsimport-onboarding.js and the MLS autocomplete data were never enqueued,
139 * so the "search your MLS" field had no autocomplete at all.
140 *
141 * The page slug is what the wizard itself is registered and routed by, and it
142 * does not change when a menu title is edited or a parent is moved.
143 */
144 if ( ! isset( $_GET['page'] ) || 'mlsimport-onboarding' !== $_GET['page'] ) {
145 return;
146 }
147 // Fetch the list of MLS providers (used to feed the account-step autocomplete).
148 $mls_import_list = mlsimport_saas_request_list();
149 // Enqueue styles
150 wp_enqueue_style(
151 'mlsimport-onboarding-style',
152 MLSIMPORT_PLUGIN_URL . 'admin/css/mlsimport-onboarding.css',
153 array(),
154 MLSIMPORT_VERSION
155 );
156
157 // Enqueue script
158 wp_enqueue_script(
159 'mlsimport-onboarding-script',
160 MLSIMPORT_PLUGIN_URL . 'admin/js/mlsimport-onboarding.js',
161 array('jquery','mlsimport-admin','jquery-ui-autocomplete'),
162 MLSIMPORT_VERSION,
163 true
164 );
165
166 // Localize script with data
167 wp_localize_script(
168 'mlsimport-onboarding-script',
169 'mlsimportOnboarding',
170 array(
171 'ajaxurl' => admin_url('admin-ajax.php'),
172 'nonce' => wp_create_nonce('mlsimport_onboarding_nonce'),
173 'current_step' => mlsimport_get_current_step(),
174 'steps' => mlsimport_get_steps(),
175 'strings' => array(
176 'saving' => __('Saving...', 'mlsimport'),
177 'next' => __('Next', 'mlsimport'),
178 'back' => __('Back', 'mlsimport'),
179 'connecting' => __('Connecting...', 'mlsimport'),
180 'testing' => __('Testing...', 'mlsimport'),
181 'importing' => __('Importing...', 'mlsimport'),
182 'success' => __('Success!', 'mlsimport'),
183 'error' => __('Error', 'mlsimport'),
184 )
185 )
186 );
187
188 /*
189 * Only the account step needs the MLS-provider autocomplete data.
190 *
191 * The step is resolved with mlsimport_get_current_step() — the SAME call the
192 * wizard itself uses to decide which step to render — rather than by reading
193 * $_GET['step'] directly. Those are not equivalent: the step falls back to the
194 * saved 'mlsimport_onboarding_current_step' option when the URL has no step
195 * parameter, which is what happens on the two entry points that matter most —
196 * the post-activation redirect and the "Setup Wizard" submenu link, both of
197 * which point at plain admin.php?page=mlsimport-onboarding. On those the
198 * account step renders while $_GET['step'] is unset, so a $_GET-based test
199 * skips the script and the MLS field silently has no autocomplete.
200 *
201 * The hook slug is likewise not re-tested here: this function already returned
202 * early above unless $hook is one of the wizard's two slugs. Re-testing only
203 * 'admin_page_mlsimport-onboarding' dropped the script whenever WordPress
204 * resolved the page under the submenu hook instead.
205 */
206 if ( mlsimport_get_current_step() === 'account' && ! empty( $mls_import_list ) ) {
207
208 // Build a ready-handler that primes the MLS-selection autocomplete widget.
209 $inline_script = 'jQuery(document).ready(function($){ var autofill=' . wp_kses_post($mls_import_list) . '; mlsimport_autocomplte_mls_selection(autofill); });';
210 wp_add_inline_script('mlsimport-onboarding-script', $inline_script);
211 }
212
213
214
215 }
216
217 /**
218 * Display the onboarding wizard
219 *
220 * @since 6.1.0
221 */
222 function mlsimport_render_onboarding_wizard() {
223 // Check current step
224 $current_step = mlsimport_get_current_step();
225
226 // Get all steps
227 $steps = mlsimport_get_steps();
228
229 // Load the wizard template
230 include MLSIMPORT_PLUGIN_PATH . 'admin/partials/mlsimport-onboarding-wizard.php';
231 }
232
233 /**
234 * Get the current onboarding step
235 *
236 * @since 6.1.0
237 * @return string The current step ID
238 */
239 function mlsimport_get_current_step() {
240 // Check if step is set in URL
241 if (isset($_GET['step']) && !empty($_GET['step'])) {
242 $step = sanitize_text_field($_GET['step']);
243
244 // Validate step
245 $steps = mlsimport_get_steps();
246 if (array_key_exists($step, $steps)) {
247 // Save current step
248 update_option('mlsimport_onboarding_current_step', $step);
249 return $step;
250 }
251 }
252
253 // Check if step is saved in options
254 $saved_step = get_option('mlsimport_onboarding_current_step', '');
255 if (!empty($saved_step)) {
256 return $saved_step;
257 }
258
259 // Default to first step
260 $steps = mlsimport_get_steps();
261 $first_step = array_key_first($steps);
262 update_option('mlsimport_onboarding_current_step', $first_step);
263
264 return $first_step;
265 }
266
267 /**
268 * Get all onboarding steps
269 *
270 * @since 6.1.0
271 * @return array The onboarding steps
272 */
273 function mlsimport_get_steps() {
274 return array(
275 'welcome' => array(
276 'title' => __('Welcome', 'mlsimport'),
277 'description' =>'',
278 'template' => 'step-welcome.php',
279 ),
280 'account' => array(
281 'title' => __('Account & MLS Connection', 'mlsimport'),
282 'description' => __('Connect to your MLS Import account and MLS provider', 'mlsimport'),
283 'template' => 'step-account.php',
284 ),
285 'field-mapping' => array(
286 'title' => __('Field Mapping', 'mlsimport'),
287 'description' => __('Configure how MLS fields map to your website', 'mlsimport'),
288 'template' => 'step-field-mapping.php',
289 ),
290 'import-config' => array(
291 'title' => __('Import Configuration', 'mlsimport'),
292 'description' => __('Set up your first import configuration', 'mlsimport'),
293 'template' => 'step-import-config.php',
294 ),
295 'test-import' => array(
296 'title' => __('Test Import', 'mlsimport'),
297 'description' => __('Run a test import to verify your setup', 'mlsimport'),
298 'template' => 'step-test-import.php',
299 ),
300 'success' => array(
301 'title' => __('Success', 'mlsimport'),
302 'description' => __('Your MLS Import is now configured', 'mlsimport'),
303 'template' => 'step-success.php',
304 ),
305 );
306 }
307
308 /**
309 * Save data for the current step
310 *
311 * @since 6.1.0
312 * @param string $step The step ID
313 * @param array $data The step data to save
314 * @return bool Success or failure
315 */
316 function mlsimport_save_step_data($step, $data) {
317 $user_data = get_option('mlsimport_onboarding_user_data', array());
318
319 // Sanitize data
320 // Walk each posted field; array values are sanitized element-by-element.
321 // Credential keys (password/secret/token) are kept verbatim — the
322 // sanitizer strips %[hex][hex] sequences and would corrupt them (#204).
323 $sanitized_data = array();
324 foreach ($data as $key => $value) {
325 if (mlsimport_is_credential_key($key)) {
326 $sanitized_data[$key] = trim((string) $value);
327 } elseif (is_array($value)) {
328 $sanitized_data[$key] = array_map('sanitize_text_field', $value);
329 } else {
330 $sanitized_data[$key] = sanitize_text_field($value);
331 }
332 }
333
334 // Update user data
335 // Store this step's sanitized data under its step key in the aggregate option.
336 $user_data[$step] = $sanitized_data;
337
338 // Record onboarding-step completion (lifecycle telemetry).
339 mlsimport_telemetry_mark_onboarding_step( $step );
340
341 // Save user data
342 return update_option('mlsimport_onboarding_user_data', $user_data);
343 }
344
345 /**
346 * Get saved data for a specific step
347 *
348 * @since 6.1.0
349 * @param string $step The step ID
350 * @return array The step data
351 */
352 function mlsimport_get_onboarding_step_data($step) {
353 $user_data = get_option('mlsimport_onboarding_user_data', array());
354
355 if (isset($user_data[$step])) {
356 return $user_data[$step];
357 }
358
359 return array();
360 }
361
362 /**
363 * Redirect to the next step
364 *
365 * @since 6.1.0
366 * @param string $current_step The current step ID
367 */
368 function mlsimport_redirect_to_next_step($current_step) {
369 $next_step = mlsimport_get_next_step($current_step);
370
371 if ($next_step) {
372 wp_redirect(admin_url('admin.php?page=mlsimport-onboarding&step=' . $next_step));
373 exit;
374 }
375 }
376
377 /**
378 * Get the next step ID
379 *
380 * @since 6.1.0
381 * @param string $current_step The current step ID
382 * @return string|null The next step ID or null if there is no next step
383 */
384 function mlsimport_get_next_step($current_step) {
385 $steps = mlsimport_get_steps();
386 $step_keys = array_keys($steps);
387
388 $current_index = array_search($current_step, $step_keys);
389
390 if ($current_index !== false && isset($step_keys[$current_index + 1])) {
391 return $step_keys[$current_index + 1];
392 }
393
394 return null;
395 }
396
397 /**
398 * Get the previous step ID
399 *
400 * @since 6.1.0
401 * @param string $current_step The current step ID
402 * @return string|null The previous step ID or null if there is no previous step
403 */
404 function mlsimport_get_previous_step($current_step) {
405 $steps = mlsimport_get_steps();
406 $step_keys = array_keys($steps);
407
408 $current_index = array_search($current_step, $step_keys);
409
410 if ($current_index !== false && $current_index > 0) {
411 return $step_keys[$current_index - 1];
412 }
413
414 return null;
415 }
416
417 /**
418 * Handle step form submission
419 *
420 * @since 6.1.0
421 */
422 function mlsimport_handle_step_submission() {
423 // Only process on onboarding page
424 if (!isset($_GET['page']) || $_GET['page'] !== 'mlsimport-onboarding') {
425 return;
426 }
427
428 // Check if form was submitted
429 if (!isset($_POST['mlsimport_onboarding_submit'])) {
430 return;
431 }
432
433 // Verify nonce
434 if (!mlsimport_verify_onboarding_nonce()) {
435 wp_die(__('Security check failed. Please try again.', 'mlsimport'));
436 }
437
438 // Get current step
439 $current_step = mlsimport_get_current_step();
440
441 // Process based on step
442 // Each wizard step persists its own fields, then redirects to the next step.
443 switch ($current_step) {
444 case 'welcome':
445 // Nothing to save, just redirect to next step
446 mlsimport_redirect_to_next_step($current_step);
447 break;
448
449 case 'account':
450 /*
451 * The account partial uses Settings API names such as
452 * mlsimport_admin_options[mlsimport_password]. Read that real form
453 * contract as one array; the former flat-key reads could never see
454 * a submitted value and made every native form POST a silent no-op.
455 */
456 $submitted_options = isset($_POST['mlsimport_admin_options']) && is_array($_POST['mlsimport_admin_options'])
457 ? wp_unslash($_POST['mlsimport_admin_options'])
458 : array();
459
460 // Usernames and ids are plain text. Credential values are trimmed
461 // only because text sanitization corrupts valid %xx sequences (#204).
462 $username = isset($submitted_options['mlsimport_username'])
463 ? sanitize_text_field($submitted_options['mlsimport_username'])
464 : '';
465 $password = isset($submitted_options['mlsimport_password'])
466 ? trim((string) $submitted_options['mlsimport_password'])
467 : '';
468 $mls_id = isset($submitted_options['mlsimport_mls_name'])
469 ? sanitize_text_field($submitted_options['mlsimport_mls_name'])
470 : '';
471 $token = isset($submitted_options['mlsimport_mls_token'])
472 ? trim((string) $submitted_options['mlsimport_mls_token'])
473 : '';
474
475 // Build one message from administrator-facing labels so every
476 // missing value is actionable on the same re-rendered form.
477 $required_fields = array(
478 __('MLSImport.com Username or email', 'mlsimport') => $username,
479 __('MLSImport.com Password', 'mlsimport') => $password,
480 __('Your MLS', 'mlsimport') => $mls_id,
481 __('Your API Server token', 'mlsimport') => $token,
482 );
483 $missing_fields = array();
484 foreach ($required_fields as $label => $value) {
485 if ('' === $value) {
486 $missing_fields[] = $label;
487 }
488 }
489
490 if (!empty($missing_fields)) {
491 add_settings_error(
492 'mlsimport_onboarding',
493 'mlsimport_onboarding_required_fields',
494 sprintf(
495 /* translators: %s: comma-separated required onboarding field labels. */
496 __('Please complete the following required field(s): %s.', 'mlsimport'),
497 implode(', ', $missing_fields)
498 ),
499 'error'
500 );
501 break;
502 }
503
504 $account_data = array(
505 'username' => $username,
506 'password' => $password,
507 'mls_id' => $mls_id,
508 'mls_token' => $token,
509 );
510
511 mlsimport_save_step_data($current_step, $account_data);
512
513 // Mirror accepted values into the live settings used by the
514 // account and MLS connection clients.
515 $options = get_option('mlsimport_admin_options', array());
516 $options['mlsimport_username'] = $username;
517 $options['mlsimport_password'] = $password;
518 $options['mlsimport_mls_name'] = $mls_id;
519 $options['mlsimport_mls_token'] = $token;
520 update_option('mlsimport_admin_options', $options);
521
522 mlsimport_redirect_to_next_step($current_step);
523 break;
524
525
526 case 'field-mapping':
527 // Save field mapping template selection
528 $field_data = array(
529 'template' => isset($_POST['mlsimport_field_template']) ? $_POST['mlsimport_field_template'] : 'standard',
530 'custom_fields' => isset($_POST['mlsimport_custom_fields']) ? $_POST['mlsimport_custom_fields'] : array(),
531 );
532
533 mlsimport_save_step_data($current_step, $field_data);
534
535 // Redirect to next step
536 mlsimport_redirect_to_next_step($current_step);
537 break;
538
539 case 'import-config':
540 // Save import configuration
541 $import_data = array(
542 'import_title' => isset($_POST['mlsimport_import_title']) ? $_POST['mlsimport_import_title'] : '',
543 'property_status' => isset($_POST['mlsimport_property_status']) ? $_POST['mlsimport_property_status'] : 'publish',
544 'agent_id' => isset($_POST['mlsimport_agent_id']) ? $_POST['mlsimport_agent_id'] : '',
545 'property_user' => isset($_POST['mlsimport_property_user']) ? $_POST['mlsimport_property_user'] : '',
546 'min_price' => isset($_POST['mlsimport_min_price']) && $_POST['mlsimport_min_price'] !== ''
547 ? $_POST['mlsimport_min_price']
548 : '0',
549 'max_price' => isset($_POST['mlsimport_max_price']) && $_POST['mlsimport_max_price'] !== ''
550 ? $_POST['mlsimport_max_price']
551 : '10000000',
552 'property_cities' => isset($_POST['mlsimport_property_cities']) ? $_POST['mlsimport_property_cities'] : array(),
553 'property_types' => isset($_POST['mlsimport_property_types']) ? $_POST['mlsimport_property_types'] : array(),
554 'auto_update' => isset($_POST['mlsimport_auto_update']) ? 1 : 0,
555 );
556
557 mlsimport_save_step_data($current_step, $import_data);
558
559 // Create import item
560 $import_id = mlsimport_create_initial_import_item($import_data);
561
562 // Save the import ID
563 $user_data = get_option('mlsimport_onboarding_user_data', array());
564 $user_data['import_id'] = $import_id;
565 update_option('mlsimport_onboarding_user_data', $user_data);
566
567 // Redirect to next step
568 mlsimport_redirect_to_next_step($current_step);
569 break;
570
571 case 'test-import':
572 // Nothing to save here, just redirect to next step
573 mlsimport_redirect_to_next_step($current_step);
574 break;
575
576 case 'success':
577 // Mark onboarding as complete
578 mlsimport_mark_onboarding_complete();
579
580 // Redirect to main plugin page
581 wp_redirect(admin_url('admin.php?page=mlsimport_plugin_options'));
582 exit;
583 break;
584 }
585 }
586
587 /**
588 * Verify onboarding nonce
589 *
590 * @since 6.1.0
591 * @return bool True if nonce is valid, false otherwise
592 */
593 function mlsimport_verify_onboarding_nonce() {
594 return isset($_POST['mlsimport_onboarding_nonce']) &&
595 wp_verify_nonce($_POST['mlsimport_onboarding_nonce'], 'mlsimport_onboarding');
596 }
597
598 /**
599 * Mark onboarding as complete
600 *
601 * @since 6.1.0
602 */
603 function mlsimport_mark_onboarding_complete() {
604 update_option('mlsimport_onboarding_completed', true);
605
606 // Log completion event
607 mlsimport_log_onboarding_event('Onboarding completed successfully', 'info');
608 }
609
610 /**
611 * Render a specific onboarding step
612 *
613 * @since 6.1.0
614 * @param string $step The step ID to render
615 */
616 function mlsimport_render_onboarding_step($step) {
617 $steps = mlsimport_get_steps();
618
619 if (!isset($steps[$step])) {
620 return;
621 }
622
623 $template = $steps[$step]['template'];
624 $path = MLSIMPORT_PLUGIN_PATH . 'admin/partials/mlsimport-onboarding-steps/' . $template;
625
626 if (file_exists($path)) {
627 // Get step data
628 $step_data = mlsimport_get_onboarding_step_data($step);
629
630 // Include template
631 include $path;
632 }
633 }
634
635 /**
636 * Create the initial import item
637 *
638 * @since 6.1.0
639 * @param array $import_data The import configuration data
640 * @return int The post ID of the created import item
641 */
642 function mlsimport_create_initial_import_item($import_data) {
643 // Create post
644 $post_data = array(
645 'post_title' => !empty($import_data['import_title']) ? $import_data['import_title'] : __('Initial Import', 'mlsimport'),
646 'post_status' => 'publish',
647 'post_type' => 'mlsimport_item',
648 );
649
650 $post_id = wp_insert_post($post_data);
651
652 if (!is_wp_error($post_id)) {
653 // Connection binding (#277): stamp the new task's MLS at creation —
654 // onboarding always runs against the connection being set up, which
655 // the helper resolves (single registered connection or the current
656 // selection).
657 mlsimport_bind_task_connection((int) $post_id, 0);
658 // Set up import item defaults
659 mlsimport_setup_import_item_defaults($post_id, $import_data);
660 }
661
662 return $post_id;
663 }
664
665 /**
666 * Set up default meta values for an import item
667 *
668 * @since 6.1.0
669 * @param int $post_id The post ID of the import item
670 * @param array $import_data The import configuration data
671 * @return bool Success or failure
672 */
673 function mlsimport_setup_import_item_defaults($post_id, $import_data) {
674 // Set basic meta
675 update_post_meta($post_id, 'mlsimport_item_property_status', $import_data['property_status']);
676 update_post_meta($post_id, 'mlsimport_item_agent', $import_data['agent_id']);
677 update_post_meta($post_id, 'mlsimport_item_property_user', $import_data['property_user']);
678 update_post_meta($post_id, 'mlsimport_item_min_price', $import_data['min_price']);
679 update_post_meta($post_id, 'mlsimport_item_max_price', $import_data['max_price']);
680 update_post_meta($post_id, 'mlsimport_item_stat_cron', $import_data['auto_update']);
681
682 // Default statuses and visibility options
683 update_post_meta($post_id, 'mlsimport_item_standardstatus', array('Active'));
684 update_post_meta($post_id, 'mlsimport_item_standardstatusprotect', array('Active', 'ActiveUnderContract', 'ComingSoon', 'Pending'));
685 update_post_meta($post_id, 'mlsimport_item_internetentirelistingdisplayyn', 'yes');
686 update_post_meta($post_id, 'mlsimport_item_internetaddressdisplayyn', 'yes');
687
688 // Set title format
689 update_post_meta($post_id, 'mlsimport_item_title_format', '{Address}, {City}, {CountyOrParish}, {PropertyType}');
690
691 // Set locations
692 if (!empty($import_data['property_cities'])) {
693 update_post_meta($post_id, 'mlsimport_item_city', $import_data['property_cities']);
694 }
695
696 // Set property types
697 if (!empty($import_data['property_types'])) {
698 update_post_meta($post_id, 'mlsimport_item_propertytype', $import_data['property_types']);
699 }
700
701 // Set creation date for reference
702 update_post_meta($post_id, 'mlsimport_item_created_date', current_time('mysql'));
703
704 // Log action
705 mlsimport_log_onboarding_event(
706 sprintf('Created initial import item (ID: %d)', $post_id),
707 'info'
708 );
709
710 return true;
711 }
712
713 /**
714 * Log onboarding event
715 *
716 * @since 6.1.0
717 * @param string $message The log message
718 * @param string $type The log type (info, warning, error)
719 */
720 function mlsimport_log_onboarding_event($message, $type = 'info') {
721 // Format log message
722 $formatted_message = '[' . current_time('mysql') . '] [ONBOARDING] [' . strtoupper($type) . '] ' . $message;
723
724 // Write to plugin logs
725 mlsimport_saas_single_write_import_custom_logs($formatted_message, 'onboarding');
726 }
727
728 add_action('wp_ajax_mlsimport_save_account', 'mlsimport_save_account_callback');
729 /**
730 * AJAX handler: save the MLSImport username or email/password and test login.
731 *
732 * Verifies the onboarding nonce, stores credentials in mlsimport_admin_options,
733 * fetches a fresh API token, and returns connected/not-connected HTML + flag.
734 * Clears the prior account verdict before that request: HTTP 400 or a transport
735 * failure must not inherit a no-subscription message from an earlier login.
736 * A successful login also re-reads the account's entitlements (connection
737 * cap) from the SaaS — see mlsimport_refresh_entitlements().
738 *
739 * Defined in the onboarding module so the callback and its credential-handling
740 * dependencies are loadable by the pure-PHP credentials regression harness.
741 *
742 * @return void
743 */
744 function mlsimport_save_account_callback() {
745 // Verify the shared onboarding AJAX nonce.
746 check_ajax_referer('mlsimport_onboarding_nonce', 'security');
747 if ( ! current_user_can( 'manage_options' ) ) {
748 wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
749 }
750
751 /*
752 * Validate the live Save Account payload before loading prior account state.
753 * Falling through on blanks let a warm token from the saved account answer
754 * "connected" for an empty form (#306).
755 */
756 $username = isset($_POST['mlsimport_username'])
757 ? sanitize_text_field(wp_unslash($_POST['mlsimport_username']))
758 : '';
759 $password = isset($_POST['mlsimport_password'])
760 ? trim(wp_unslash($_POST['mlsimport_password']))
761 : '';
762 $missing_fields = array();
763 if ('' === $username) {
764 $missing_fields[] = __('MLSImport.com Username or email', 'mlsimport');
765 }
766 if ('' === $password) {
767 $missing_fields[] = __('MLSImport.com Password', 'mlsimport');
768 }
769
770 if (!empty($missing_fields)) {
771 $message = 1 === count($missing_fields)
772 ? sprintf(
773 /* translators: %s: one missing MLSImport account field label. */
774 __('%s is required.', 'mlsimport'),
775 $missing_fields[0]
776 )
777 : sprintf(
778 /* translators: 1: username label, 2: password label. */
779 __('%1$s and %2$s are required.', 'mlsimport'),
780 $missing_fields[0],
781 $missing_fields[1]
782 );
783
784 wp_send_json_error(array('message' => $message));
785 }
786
787 // Load current plugin options.
788 $options = get_option('mlsimport_admin_options', []);
789 // Both values are present. Preserve the password verbatim after unslashing
790 // and trim because text sanitization strips valid %xx sequences (#204).
791 $options['mlsimport_username'] = $username;
792 $options['mlsimport_password'] = $password;
793 update_option('mlsimport_admin_options', $options);
794
795 // Force the check below to exercise the credentials just saved rather than
796 // a token minted from the previous password (#205).
797 delete_transient('mlsimport_saas_token');
798 delete_option('mlsimport_token_expiry');
799 // This is a new login attempt. Only its response may confirm no subscription;
800 // short passwords (HTTP 400) and timeouts do not overwrite an old verdict.
801 delete_option( MLSIMPORT_ACCOUNT_STATUS_OPTION );
802
803 global $mlsimport;
804
805 // Refresh token
806 $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
807
808 // Empty token means the login failed. The box names the reason the
809 // server gave (no subscription vs wrong password) — see
810 // includes/mlsimport-account-status.php.
811 if (trim($token) === '') {
812 $html = mlsimport_account_not_connected_html();
813 $account_status = mlsimport_account_status();
814 $is_unsubscribed = 'no_subscription' === $account_status;
815
816 // Return one public account-state contract for both consumers. The
817 // onboarding and Connections screens both render the shared escaped
818 // notice HTML. Plain message and link fields remain available to callers.
819 // Link data is present only for a confirmed no-subscription verdict, so
820 // invalid credentials never receive a misleading purchase action.
821 wp_send_json_success([
822 'message' => mlsimport_account_not_connected_message(),
823 'html' => $html,
824 'connected' => false,
825 'account_status' => $account_status,
826 'subscribe_url' => $is_unsubscribed ? MLSIMPORT_ACCOUNT_SUBSCRIBE_URL : '',
827 'subscribe_label' => $is_unsubscribed ? esc_html__( 'View plans', 'mlsimport' ) : '',
828 ]);
829 } else {
830 // Signed in: re-read the account's entitlements (connection cap +
831 // registered MLS blocks) so a plan change shows up on reconnect.
832 mlsimport_refresh_entitlements();
833
834 ob_start();
835 ?>
836 <div class="mlsimport_warning mlsimport_validated">
837 <?php esc_html_e('You are connected to your MlsImport account!', 'mlsimport'); ?>
838 </div>
839 <?php
840 $html = ob_get_clean();
841
842 wp_send_json_success([
843 'message' => __('Connected successfully!', 'mlsimport'),
844 'html' => $html,
845 'connected' => true
846 ]);
847 }
848 }
849
850 /**
851 * Handle AJAX run test import
852 *
853 * Verifies the 'mlsimport_onboarding_nonce' nonce; performs no capability
854 * check. Caps the configured import item at 5 listings, builds the import
855 * request set, and enqueues the background async action that performs the
856 * actual import.
857 *
858 * @since 6.1.0
859 */
860 function mlsimport_ajax_run_test_import() {
861 // Prove request intent before resolving and authorizing the saved Import Task.
862 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mlsimport_onboarding_nonce')) {
863 wp_send_json_error(array('message' => __('Security check failed', 'mlsimport')));
864 }
865
866 // Get import ID
867 // The import item id was stashed during the import-config step.
868 $user_data = get_option('mlsimport_onboarding_user_data', array());
869 $import_id = isset($user_data['import_id']) ? $user_data['import_id'] : 0;
870
871 // Without an import item there is nothing to run.
872 if (empty($import_id)) {
873 wp_send_json_error(array('message' => __('No import configuration found', 'mlsimport')));
874 }
875
876 // The saved onboarding id must still be a task this user may manage.
877 if ( 'mlsimport_item' !== get_post_type( $import_id ) || ! current_user_can( 'edit_post', $import_id ) ) {
878 wp_send_json_error( array( 'message' => __( 'You are not allowed to manage this import task.', 'mlsimport' ) ), 403 );
879 }
880
881 update_post_meta( $import_id, 'mlsimport_item_how_many', 5 );
882 global $mlsimport;
883
884 try {
885 // Setup has no count displayed by the page, so this small scheduling
886 // adapter performs one count and passes it into the shared manual run.
887 $mlsrequest = $mlsimport->admin->mlsimport_make_listing_requests( $import_id );
888 if ( ! isset( $mlsrequest['results'] ) || 0 === intval( $mlsrequest['results'] ) ) {
889 wp_send_json_error( array( 'message' => __( 'No listings found with current configuration', 'mlsimport' ) ) );
890 }
891 $found_items = min( 5, max( 0, intval( $mlsrequest['results'] ) ) );
892 $start = $mlsimport->admin->mlsimport_import_task_execution()->start(
893 array(
894 'task_id' => (int) $import_id,
895 'source' => 'manual',
896 'found' => $found_items,
897 'limit' => 5,
898 'is_onboard' => 1,
899 )
900 );
901 if ( true !== ( $start['accepted'] ?? false ) ) {
902 wp_send_json_error(
903 array( 'message' => __( 'Another import is already running. Please wait for it to finish.', 'mlsimport' ) )
904 );
905 }
906
907 mlsimport_log_onboarding_event( 'Starting test import of up to 5 properties', 'info' );
908 // Shared worker scheduling: clears dead/superseded queue entries first
909 // so a previously crashed worker can never block this start.
910 $mlsimport->admin->mlsimport_enqueue_import_worker( (string) $start['run_id'] );
911
912 wp_send_json_success(
913 array(
914 'message' => __( 'Import process started', 'mlsimport' ),
915 'import_id' => (int) $import_id,
916 'run_id' => (string) $start['run_id'],
917 )
918 );
919 } catch (Exception $e) {
920 mlsimport_log_onboarding_event('Test import failed: ' . $e->getMessage(), 'error');
921 wp_send_json_error(array('message' => __('Import failed: ', 'mlsimport') . $e->getMessage()));
922 }
923 }
924
925 /**
926 * Handle AJAX save step data
927 *
928 * Verifies the 'mlsimport_onboarding_nonce' nonce, then requires the same
929 * manage_options capability as the onboarding settings screen before reading
930 * or persisting any submitted values. Delegates accepted requests to
931 * mlsimport_save_step_data(), which sanitizes and persists the posted
932 * per-step form data.
933 *
934 * @since 6.1.0
935 */
936 function mlsimport_ajax_save_step_data() {
937 // First prove that the request originated from the onboarding screen.
938 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mlsimport_onboarding_nonce')) {
939 wp_send_json_error(array('message' => __('Security check failed', 'mlsimport')));
940 }
941
942 /*
943 * A nonce prevents cross-site request forgery but does not grant permission.
944 * Stop non-administrators before the submitted step or data is inspected and
945 * before the persistence helper reaches the shared option write boundary.
946 */
947 if ( ! current_user_can( 'manage_options' ) ) {
948 wp_send_json_error(
949 array( 'message' => __( 'You are not allowed to change onboarding settings.', 'mlsimport' ) ),
950 403
951 );
952 }
953
954 // Get step and data
955 // Step id is sanitized; the raw data array is sanitized inside save_step_data().
956 $step = isset($_POST['step']) ? sanitize_text_field($_POST['step']) : '';
957 $data = isset($_POST['data']) ? $_POST['data'] : array();
958
959 // A step id is required to know where to store the data.
960 if (empty($step)) {
961 wp_send_json_error(array('message' => __('No step specified', 'mlsimport')));
962 }
963
964 // Save step data
965 $result = mlsimport_save_step_data($step, $data);
966
967 // update_option returns false when the write fails (or value is unchanged).
968 if (!$result) {
969 wp_send_json_error(array('message' => __('Failed to save data', 'mlsimport')));
970 }
971
972 wp_send_json_success(array('message' => __('Data saved successfully', 'mlsimport')));
973 }
974
975 /**
976 * Save current onboarding state
977 *
978 * @since 6.1.0
979 * @param string $step_id The current step ID
980 * @param array $form_data The form data
981 * @return bool Success or failure
982 */
983 function mlsimport_save_onboarding_state($step_id, $form_data) {
984 $state = array(
985 'current_step' => $step_id,
986 'form_data' => $form_data,
987 'timestamp' => current_time('timestamp'),
988 );
989
990 return update_option('mlsimport_onboarding_state', $state);
991 }
992
993 /**
994 * Restore onboarding state
995 *
996 * @since 6.1.0
997 * @return array The saved state data
998 */
999 function mlsimport_restore_onboarding_state() {
1000 return get_option('mlsimport_onboarding_state', array());
1001 }
1002
1003 /**
1004 * Clear onboarding state
1005 *
1006 * @since 6.1.0
1007 * @return bool Success or failure
1008 */
1009 function mlsimport_clear_onboarding_state() {
1010 return delete_option('mlsimport_onboarding_state');
1011 }
1012
1013 /**
1014 * Maybe restart wizard
1015 *
1016 * @since 6.1.0
1017 */
1018 function mlsimport_maybe_restart_wizard() {
1019 if (isset($_GET['restart_wizard']) && $_GET['restart_wizard'] == 1) {
1020 // Clear onboarding state
1021 mlsimport_clear_onboarding_state();
1022
1023 // Reset current step
1024 update_option('mlsimport_onboarding_current_step', '');
1025
1026 // Clear user data
1027 delete_option('mlsimport_onboarding_user_data');
1028
1029 // Mark onboarding as not completed
1030 update_option('mlsimport_onboarding_completed', false);
1031
1032 // Redirect to first step
1033 wp_redirect(admin_url('admin.php?page=mlsimport-onboarding'));
1034 exit;
1035 }
1036 }
1037 add_action('admin_init', 'mlsimport_maybe_restart_wizard');
1038
1039 // Initialize onboarding
1040 add_action('init', 'mlsimport_init_onboarding');
1041
1042 // Register activation hook to redirect to onboarding
1043 function mlsimport_activation_redirect() {
1044 // Set a flag so the next admin request redirects to the onboarding wizard
1045 update_option('mlsimport_do_onboarding_redirect', true);
1046 }
1047 register_activation_hook(MLSIMPORT_PLUGIN_PATH . 'mlsimport.php', 'mlsimport_activation_redirect');
1048