PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.2
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.2
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.1.2, at includes/mlsimport-onboarding.php

1,180 lines 41.3 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 *includes\mlsimport-onboarding.php
8 * @link https://mlsimport.com/
9 * @since 6.1.0
10 *
11 * @package Mlsimport
12 * @subpackage Mlsimport/includes
13 */
14
15 // If this file is called directly, abort.
16 if (!defined('WPINC')) {
17 die;
18 }
19
20 /**
21 * Initialize the onboarding functionality
22 *
23 * @since 6.1.0
24 */
25 function mlsimport_init_onboarding() {
26 // Only initialize for admin pages
27 if (!is_admin()) {
28 return;
29 }
30
31 // Check if we need to start or continue onboarding
32 mlsimport_check_onboarding_status();
33
34
35 // Register the onboarding page
36 add_action('admin_menu', 'mlsimport_register_onboarding_page');
37
38 // Add menu item to start/resume onboarding
39 add_action('admin_menu', 'mlsimport_add_onboarding_menu_item');
40
41 // Add assets for onboarding
42 add_action('admin_enqueue_scripts', 'mlsimport_enqueue_onboarding_assets');
43
44 // Register AJAX handlers
45 add_action('wp_ajax_mlsimport_test_account_connection', 'mlsimport_ajax_test_account_connection');
46 add_action('wp_ajax_mlsimport_test_mls_connection', 'mlsimport_ajax_test_mls_connection');
47 add_action('wp_ajax_mlsimport_run_test_import', 'mlsimport_ajax_run_test_import');
48 add_action('wp_ajax_mlsimport_save_step_data', 'mlsimport_ajax_save_step_data');
49
50 // Add admin notice for incomplete onboarding
51 add_action('admin_notices', 'mlsimport_onboarding_admin_notice');
52
53 // Intercept form submissions
54 add_action('admin_init', 'mlsimport_handle_step_submission');
55 }
56
57 /**
58 * Check if onboarding is complete or in progress
59 *
60 * @since 6.1.0
61 */
62 function mlsimport_check_onboarding_status() {
63 // Check if onboarding is complete
64 $onboarding_completed = get_option('mlsimport_onboarding_completed', false);
65
66 // If onboarding is complete, we don't need to do anything
67 if ($onboarding_completed) {
68 return;
69 }
70
71 // Redirect to onboarding if activation flag is set
72 if (get_option('mlsimport_do_onboarding_redirect', false)) {
73 delete_option('mlsimport_do_onboarding_redirect');
74 wp_safe_redirect(admin_url('admin.php?page=mlsimport-onboarding'));
75 exit;
76 }
77
78 // Check if we're on the plugin activation page
79 // Fires when WP's plugins screen just activated (activate=true) a plugin
80 // (plugin=...) whose path contains 'mlsimport'.
81 if (isset($_GET['activate']) && $_GET['activate'] == 'true' && isset($_GET['plugin']) && strpos($_GET['plugin'], 'mlsimport') !== false) {
82 // Redirect to onboarding welcome page
83 wp_redirect(admin_url('admin.php?page=mlsimport-onboarding'));
84 exit;
85 }
86 }
87
88 /**
89 * Register the onboarding admin page
90 *
91 * @since 6.1.0
92 */
93 function mlsimport_register_onboarding_page() {
94 add_submenu_page(
95 '', // No parent - won't appear in menu
96 __('MLS Import Setup Wizard', 'mlsimport'),
97 __('Setup Wizard', 'mlsimport'),
98 'manage_options',
99 'mlsimport-onboarding',
100 'mlsimport_render_onboarding_wizard'
101 );
102 }
103
104 /**
105 * Add onboarding menu item to the MLS Import menu
106 *
107 * @since 6.1.0
108 */
109 function mlsimport_add_onboarding_menu_item() {
110 // Only show if onboarding hasn't been completed
111
112 add_submenu_page(
113 'mlsimport_plugin_options',
114 __('Setup Wizard', 'mlsimport'),
115 __('Setup Wizard', 'mlsimport'),
116 'manage_options',
117 'mlsimport-onboarding',
118 'mlsimport_render_onboarding_wizard'
119 );
120
121 }
122
123 /**
124 * Enqueue scripts and styles for the onboarding wizard
125 *
126 * @since 6.1.0
127 * @param string $hook The current admin page
128 */
129 function mlsimport_enqueue_onboarding_assets($hook) {
130 /*
131 * Bail unless we are on the wizard page.
132 *
133 * This deliberately tests the page slug rather than $hook. WordPress does not
134 * build a submenu's hook from its parent SLUG — it uses the sanitized parent
135 * MENU TITLE. The wizard's parent is registered with the title "MLS Import
136 * Settings", so the real hook is 'mls-import-settings_page_mlsimport-onboarding',
137 * which matched neither of the two hook strings previously hardcoded here.
138 * The result was that this function returned immediately on the wizard page:
139 * mlsimport-onboarding.js and the MLS autocomplete data were never enqueued,
140 * so the "search your MLS" field had no autocomplete at all.
141 *
142 * The page slug is what the wizard itself is registered and routed by, and it
143 * does not change when a menu title is edited or a parent is moved.
144 */
145 if ( ! isset( $_GET['page'] ) || 'mlsimport-onboarding' !== $_GET['page'] ) {
146 return;
147 }
148 // Fetch the list of MLS providers (used to feed the account-step autocomplete).
149 $mls_import_list = mlsimport_saas_request_list();
150 // Enqueue styles
151 wp_enqueue_style(
152 'mlsimport-onboarding-style',
153 MLSIMPORT_PLUGIN_URL . 'admin/css/mlsimport-onboarding.css',
154 array(),
155 MLSIMPORT_VERSION
156 );
157
158 // Enqueue script
159 wp_enqueue_script(
160 'mlsimport-onboarding-script',
161 MLSIMPORT_PLUGIN_URL . 'admin/js/mlsimport-onboarding.js',
162 array('jquery','mlsimport-admin','jquery-ui-autocomplete'),
163 MLSIMPORT_VERSION,
164 true
165 );
166
167 // Localize script with data
168 wp_localize_script(
169 'mlsimport-onboarding-script',
170 'mlsimportOnboarding',
171 array(
172 'ajaxurl' => admin_url('admin-ajax.php'),
173 'nonce' => wp_create_nonce('mlsimport_onboarding_nonce'),
174 'current_step' => mlsimport_get_current_step(),
175 'steps' => mlsimport_get_steps(),
176 'strings' => array(
177 'saving' => __('Saving...', 'mlsimport'),
178 'next' => __('Next', 'mlsimport'),
179 'back' => __('Back', 'mlsimport'),
180 'skip' => __('Skip', 'mlsimport'),
181 'connecting' => __('Connecting...', 'mlsimport'),
182 'testing' => __('Testing...', 'mlsimport'),
183 'importing' => __('Importing...', 'mlsimport'),
184 'success' => __('Success!', 'mlsimport'),
185 'error' => __('Error', 'mlsimport'),
186 )
187 )
188 );
189
190 /*
191 * Only the account step needs the MLS-provider autocomplete data.
192 *
193 * The step is resolved with mlsimport_get_current_step() — the SAME call the
194 * wizard itself uses to decide which step to render — rather than by reading
195 * $_GET['step'] directly. Those are not equivalent: the step falls back to the
196 * saved 'mlsimport_onboarding_current_step' option when the URL has no step
197 * parameter, which is what happens on the two entry points that matter most —
198 * the post-activation redirect and the "Setup Wizard" submenu link, both of
199 * which point at plain admin.php?page=mlsimport-onboarding. On those the
200 * account step renders while $_GET['step'] is unset, so a $_GET-based test
201 * skips the script and the MLS field silently has no autocomplete.
202 *
203 * The hook slug is likewise not re-tested here: this function already returned
204 * early above unless $hook is one of the wizard's two slugs. Re-testing only
205 * 'admin_page_mlsimport-onboarding' dropped the script whenever WordPress
206 * resolved the page under the submenu hook instead.
207 */
208 if ( mlsimport_get_current_step() === 'account' && ! empty( $mls_import_list ) ) {
209
210 // Build a ready-handler that primes the MLS-selection autocomplete widget.
211 $inline_script = 'jQuery(document).ready(function($){ var autofill=' . wp_kses_post($mls_import_list) . '; mlsimport_autocomplte_mls_selection(autofill); });';
212 wp_add_inline_script('mlsimport-onboarding-script', $inline_script);
213 }
214
215
216
217 }
218
219 /**
220 * Display the onboarding wizard
221 *
222 * @since 6.1.0
223 */
224 function mlsimport_render_onboarding_wizard() {
225 // Check current step
226 $current_step = mlsimport_get_current_step();
227
228 // Get all steps
229 $steps = mlsimport_get_steps();
230
231 // Load the wizard template
232 include MLSIMPORT_PLUGIN_PATH . 'admin/partials/mlsimport-onboarding-wizard.php';
233 }
234
235 /**
236 * Get the current onboarding step
237 *
238 * @since 6.1.0
239 * @return string The current step ID
240 */
241 function mlsimport_get_current_step() {
242 // Check if step is set in URL
243 if (isset($_GET['step']) && !empty($_GET['step'])) {
244 $step = sanitize_text_field($_GET['step']);
245
246 // Validate step
247 $steps = mlsimport_get_steps();
248 if (array_key_exists($step, $steps)) {
249 // Save current step
250 update_option('mlsimport_onboarding_current_step', $step);
251 return $step;
252 }
253 }
254
255 // Check if step is saved in options
256 $saved_step = get_option('mlsimport_onboarding_current_step', '');
257 if (!empty($saved_step)) {
258 return $saved_step;
259 }
260
261 // Default to first step
262 $steps = mlsimport_get_steps();
263 $first_step = array_key_first($steps);
264 update_option('mlsimport_onboarding_current_step', $first_step);
265
266 return $first_step;
267 }
268
269 /**
270 * Get all onboarding steps
271 *
272 * @since 6.1.0
273 * @return array The onboarding steps
274 */
275 function mlsimport_get_steps() {
276 return array(
277 'welcome' => array(
278 'title' => __('Welcome', 'mlsimport'),
279 'description' =>'',
280 'template' => 'step-welcome.php',
281 ),
282 'account' => array(
283 'title' => __('Account & MLS Connection', 'mlsimport'),
284 'description' => __('Connect to your MLS Import account and MLS provider', 'mlsimport'),
285 'template' => 'step-account.php',
286 ),
287 'field-mapping' => array(
288 'title' => __('Field Mapping', 'mlsimport'),
289 'description' => __('Configure how MLS fields map to your website', 'mlsimport'),
290 'template' => 'step-field-mapping.php',
291 ),
292 'import-config' => array(
293 'title' => __('Import Configuration', 'mlsimport'),
294 'description' => __('Set up your first import configuration', 'mlsimport'),
295 'template' => 'step-import-config.php',
296 ),
297 'test-import' => array(
298 'title' => __('Test Import', 'mlsimport'),
299 'description' => __('Run a test import to verify your setup', 'mlsimport'),
300 'template' => 'step-test-import.php',
301 ),
302 'success' => array(
303 'title' => __('Success', 'mlsimport'),
304 'description' => __('Your MLS Import is now configured', 'mlsimport'),
305 'template' => 'step-success.php',
306 ),
307 );
308 }
309
310 /**
311 * Save data for the current step
312 *
313 * @since 6.1.0
314 * @param string $step The step ID
315 * @param array $data The step data to save
316 * @return bool Success or failure
317 */
318 function mlsimport_save_step_data($step, $data) {
319 $user_data = get_option('mlsimport_onboarding_user_data', array());
320
321 // Sanitize data
322 // Walk each posted field; array values are sanitized element-by-element.
323 // Credential keys (password/secret/token) are kept verbatim — the
324 // sanitizer strips %[hex][hex] sequences and would corrupt them (#204).
325 $sanitized_data = array();
326 foreach ($data as $key => $value) {
327 if (mlsimport_is_credential_key($key)) {
328 $sanitized_data[$key] = trim((string) $value);
329 } elseif (is_array($value)) {
330 $sanitized_data[$key] = array_map('sanitize_text_field', $value);
331 } else {
332 $sanitized_data[$key] = sanitize_text_field($value);
333 }
334 }
335
336 // Update user data
337 // Store this step's sanitized data under its step key in the aggregate option.
338 $user_data[$step] = $sanitized_data;
339
340 // Record onboarding-step completion (lifecycle telemetry).
341 mlsimport_telemetry_mark_onboarding_step( $step );
342
343 // Save user data
344 return update_option('mlsimport_onboarding_user_data', $user_data);
345 }
346
347 /**
348 * Get saved data for a specific step
349 *
350 * @since 6.1.0
351 * @param string $step The step ID
352 * @return array The step data
353 */
354 function mlsimport_get_onboarding_step_data($step) {
355 $user_data = get_option('mlsimport_onboarding_user_data', array());
356
357 if (isset($user_data[$step])) {
358 return $user_data[$step];
359 }
360
361 return array();
362 }
363
364 /**
365 * Redirect to the next step
366 *
367 * @since 6.1.0
368 * @param string $current_step The current step ID
369 */
370 function mlsimport_redirect_to_next_step($current_step) {
371 $next_step = mlsimport_get_next_step($current_step);
372
373 if ($next_step) {
374 wp_redirect(admin_url('admin.php?page=mlsimport-onboarding&step=' . $next_step));
375 exit;
376 }
377 }
378
379 /**
380 * Get the next step ID
381 *
382 * @since 6.1.0
383 * @param string $current_step The current step ID
384 * @return string|null The next step ID or null if there is no next step
385 */
386 function mlsimport_get_next_step($current_step) {
387 $steps = mlsimport_get_steps();
388 $step_keys = array_keys($steps);
389
390 $current_index = array_search($current_step, $step_keys);
391
392 if ($current_index !== false && isset($step_keys[$current_index + 1])) {
393 return $step_keys[$current_index + 1];
394 }
395
396 return null;
397 }
398
399 /**
400 * Get the previous step ID
401 *
402 * @since 6.1.0
403 * @param string $current_step The current step ID
404 * @return string|null The previous step ID or null if there is no previous step
405 */
406 function mlsimport_get_previous_step($current_step) {
407 $steps = mlsimport_get_steps();
408 $step_keys = array_keys($steps);
409
410 $current_index = array_search($current_step, $step_keys);
411
412 if ($current_index !== false && $current_index > 0) {
413 return $step_keys[$current_index - 1];
414 }
415
416 return null;
417 }
418
419 /**
420 * Handle step form submission
421 *
422 * @since 6.1.0
423 */
424 function mlsimport_handle_step_submission() {
425 // Only process on onboarding page
426 if (!isset($_GET['page']) || $_GET['page'] !== 'mlsimport-onboarding') {
427 return;
428 }
429
430 // Check if form was submitted
431 if (!isset($_POST['mlsimport_onboarding_submit'])) {
432 return;
433 }
434
435 // Verify nonce
436 if (!mlsimport_verify_onboarding_nonce()) {
437 wp_die(__('Security check failed. Please try again.', 'mlsimport'));
438 }
439
440 // Get current step
441 $current_step = mlsimport_get_current_step();
442
443 // Process based on step
444 // Each wizard step persists its own fields, then redirects to the next step.
445 switch ($current_step) {
446 case 'welcome':
447 // Nothing to save, just redirect to next step
448 mlsimport_redirect_to_next_step($current_step);
449 break;
450
451 case 'account':
452 // Save account information only if all required fields are filled.
453 // Credentials (password/token) are unslashed + trimmed only, never
454 // sanitized — sanitizing strips %[hex][hex] sequences (#204).
455 $username = isset($_POST['mlsimport_username']) ? trim($_POST['mlsimport_username']) : '';
456 $password = isset($_POST['mlsimport_password']) ? trim(wp_unslash($_POST['mlsimport_password'])) : '';
457 $mls_id = isset($_POST['mlsimport_mls_name']) ? trim($_POST['mlsimport_mls_name']) : '';
458 $token = isset($_POST['mlsimport_mls_token']) ? trim(wp_unslash($_POST['mlsimport_mls_token'])) : '';
459
460 // Only save if all fields are non-empty
461 // Requires SaaS username AND password AND MLS id AND MLS token.
462 if ($username !== '' && $password !== '' && $mls_id !== '' && $token !== '') {
463 $account_data = array(
464 'username' => $username,
465 'password' => $password,
466 'mls_id' => $mls_id,
467 'mls_token' => $token,
468 );
469
470 mlsimport_save_step_data($current_step, $account_data);
471
472 // Save to plugin options
473 $options = get_option('mlsimport_admin_options', array());
474 $options['mlsimport_username'] = $username;
475 $options['mlsimport_password'] = $password;
476 $options['mlsimport_mls_name'] = $mls_id;
477 $options['mlsimport_mls_token'] = $token;
478 update_option('mlsimport_admin_options', $options);
479
480 // Redirect to next step
481 mlsimport_redirect_to_next_step($current_step);
482 }
483 break;
484
485
486 case 'field-mapping':
487 // Save field mapping template selection
488 $field_data = array(
489 'template' => isset($_POST['mlsimport_field_template']) ? $_POST['mlsimport_field_template'] : 'standard',
490 'custom_fields' => isset($_POST['mlsimport_custom_fields']) ? $_POST['mlsimport_custom_fields'] : array(),
491 );
492
493 mlsimport_save_step_data($current_step, $field_data);
494
495 // Redirect to next step
496 mlsimport_redirect_to_next_step($current_step);
497 break;
498
499 case 'import-config':
500 // Save import configuration
501 $import_data = array(
502 'import_title' => isset($_POST['mlsimport_import_title']) ? $_POST['mlsimport_import_title'] : '',
503 'property_status' => isset($_POST['mlsimport_property_status']) ? $_POST['mlsimport_property_status'] : 'publish',
504 'agent_id' => isset($_POST['mlsimport_agent_id']) ? $_POST['mlsimport_agent_id'] : '',
505 'property_user' => isset($_POST['mlsimport_property_user']) ? $_POST['mlsimport_property_user'] : '',
506 'min_price' => isset($_POST['mlsimport_min_price']) && $_POST['mlsimport_min_price'] !== ''
507 ? $_POST['mlsimport_min_price']
508 : '0',
509 'max_price' => isset($_POST['mlsimport_max_price']) && $_POST['mlsimport_max_price'] !== ''
510 ? $_POST['mlsimport_max_price']
511 : '10000000',
512 'property_cities' => isset($_POST['mlsimport_property_cities']) ? $_POST['mlsimport_property_cities'] : array(),
513 'property_types' => isset($_POST['mlsimport_property_types']) ? $_POST['mlsimport_property_types'] : array(),
514 'auto_update' => isset($_POST['mlsimport_auto_update']) ? 1 : 0,
515 );
516
517 mlsimport_save_step_data($current_step, $import_data);
518
519 // Create import item
520 $import_id = mlsimport_create_initial_import_item($import_data);
521
522 // Save the import ID
523 $user_data = get_option('mlsimport_onboarding_user_data', array());
524 $user_data['import_id'] = $import_id;
525 update_option('mlsimport_onboarding_user_data', $user_data);
526
527 // Redirect to next step
528 mlsimport_redirect_to_next_step($current_step);
529 break;
530
531 case 'test-import':
532 // Nothing to save here, just redirect to next step
533 mlsimport_redirect_to_next_step($current_step);
534 break;
535
536 case 'success':
537 // Mark onboarding as complete
538 mlsimport_mark_onboarding_complete();
539
540 // Redirect to main plugin page
541 wp_redirect(admin_url('admin.php?page=mlsimport_plugin_options'));
542 exit;
543 break;
544 }
545 }
546
547 /**
548 * Verify onboarding nonce
549 *
550 * @since 6.1.0
551 * @return bool True if nonce is valid, false otherwise
552 */
553 function mlsimport_verify_onboarding_nonce() {
554 return isset($_POST['mlsimport_onboarding_nonce']) &&
555 wp_verify_nonce($_POST['mlsimport_onboarding_nonce'], 'mlsimport_onboarding');
556 }
557
558 /**
559 * Mark onboarding as complete
560 *
561 * @since 6.1.0
562 */
563 function mlsimport_mark_onboarding_complete() {
564 update_option('mlsimport_onboarding_completed', true);
565
566 // Log completion event
567 mlsimport_log_onboarding_event('Onboarding completed successfully', 'info');
568 }
569
570 /**
571 * Render a specific onboarding step
572 *
573 * @since 6.1.0
574 * @param string $step The step ID to render
575 */
576 function mlsimport_render_onboarding_step($step) {
577 $steps = mlsimport_get_steps();
578
579 if (!isset($steps[$step])) {
580 return;
581 }
582
583 $template = $steps[$step]['template'];
584 $path = MLSIMPORT_PLUGIN_PATH . 'admin/partials/mlsimport-onboarding-steps/' . $template;
585
586 if (file_exists($path)) {
587 // Get step data
588 $step_data = mlsimport_get_onboarding_step_data($step);
589
590 // Include template
591 include $path;
592 }
593 }
594
595 /**
596 * Create the initial import item
597 *
598 * @since 6.1.0
599 * @param array $import_data The import configuration data
600 * @return int The post ID of the created import item
601 */
602 function mlsimport_create_initial_import_item($import_data) {
603 // Create post
604 $post_data = array(
605 'post_title' => !empty($import_data['import_title']) ? $import_data['import_title'] : __('Initial Import', 'mlsimport'),
606 'post_status' => 'publish',
607 'post_type' => 'mlsimport_item',
608 );
609
610 $post_id = wp_insert_post($post_data);
611
612 if (!is_wp_error($post_id)) {
613 // Set up import item defaults
614 mlsimport_setup_import_item_defaults($post_id, $import_data);
615 }
616
617 return $post_id;
618 }
619
620 /**
621 * Set up default meta values for an import item
622 *
623 * @since 6.1.0
624 * @param int $post_id The post ID of the import item
625 * @param array $import_data The import configuration data
626 * @return bool Success or failure
627 */
628 function mlsimport_setup_import_item_defaults($post_id, $import_data) {
629 // Set basic meta
630 update_post_meta($post_id, 'mlsimport_item_property_status', $import_data['property_status']);
631 update_post_meta($post_id, 'mlsimport_item_agent', $import_data['agent_id']);
632 update_post_meta($post_id, 'mlsimport_item_property_user', $import_data['property_user']);
633 update_post_meta($post_id, 'mlsimport_item_min_price', $import_data['min_price']);
634 update_post_meta($post_id, 'mlsimport_item_max_price', $import_data['max_price']);
635 update_post_meta($post_id, 'mlsimport_item_stat_cron', $import_data['auto_update']);
636
637 // Default statuses and visibility options
638 update_post_meta($post_id, 'mlsimport_item_standardstatus', array('Active'));
639 update_post_meta($post_id, 'mlsimport_item_standardstatusprotect', array('Active', 'ActiveUnderContract', 'ComingSoon', 'Pending'));
640 update_post_meta($post_id, 'mlsimport_item_internetentirelistingdisplayyn', 'yes');
641 update_post_meta($post_id, 'mlsimport_item_internetaddressdisplayyn', 'yes');
642
643 // Set title format
644 update_post_meta($post_id, 'mlsimport_item_title_format', '{Address}, {City}, {CountyOrParish}, {PropertyType}');
645
646 // Set locations
647 if (!empty($import_data['property_cities'])) {
648 update_post_meta($post_id, 'mlsimport_item_city', $import_data['property_cities']);
649 }
650
651 // Set property types
652 if (!empty($import_data['property_types'])) {
653 update_post_meta($post_id, 'mlsimport_item_propertytype', $import_data['property_types']);
654 }
655
656 // Set creation date for reference
657 update_post_meta($post_id, 'mlsimport_item_created_date', current_time('mysql'));
658
659 // Log action
660 mlsimport_log_onboarding_event(
661 sprintf('Created initial import item (ID: %d)', $post_id),
662 'info'
663 );
664
665 return true;
666 }
667
668 /**
669 * Log onboarding event
670 *
671 * @since 6.1.0
672 * @param string $message The log message
673 * @param string $type The log type (info, warning, error)
674 */
675 function mlsimport_log_onboarding_event($message, $type = 'info') {
676 // Format log message
677 $formatted_message = '[' . current_time('mysql') . '] [ONBOARDING] [' . strtoupper($type) . '] ' . $message;
678
679 // Write to plugin logs
680 mlsimport_saas_single_write_import_custom_logs($formatted_message, 'onboarding');
681 }
682
683 /**
684 * Display admin notice for incomplete onboarding
685 *
686 * @since 6.1.0
687 */
688 function mlsimport_onboarding_admin_notice() {
689 // Allow disabling the notice via constant
690 if (defined('MLSIMPORT_HIDE_SETUP_NOTICE') && MLSIMPORT_HIDE_SETUP_NOTICE) {
691 return;
692 }
693 // Only show on plugin pages
694 $screen = get_current_screen();
695 if (!$screen || strpos($screen->id, 'mlsimport') === false) {
696 return;
697 }
698
699 // Don't show on onboarding page
700 if (isset($_GET['page']) && $_GET['page'] === 'mlsimport-onboarding') {
701 return;
702 }
703
704 // Check if onboarding is complete
705 $onboarding_completed = get_option('mlsimport_onboarding_completed', false);
706 if ($onboarding_completed) {
707 return;
708 }
709
710 // Get current step
711 $current_step = get_option('mlsimport_onboarding_current_step', 'welcome');
712 $steps = mlsimport_get_steps();
713
714 // Display notice
715 ?>
716
717 <?php
718 }
719
720 /**
721 * Handle AJAX test account connection
722 *
723 * Verifies the 'mlsimport_onboarding_nonce' nonce; performs no capability
724 * check. Persists the posted SaaS username/password into mlsimport_admin_options,
725 * clears the cached token transient, then reports whether a fresh SaaS token can
726 * be obtained with those credentials.
727 *
728 * @since 6.1.0
729 */
730 function mlsimport_ajax_test_account_connection() {
731 // Check nonce (no current_user_can capability check is performed here).
732 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mlsimport_onboarding_nonce')) {
733 wp_send_json_error(array('message' => __('Security check failed', 'mlsimport')));
734 }
735
736 // Get credentials
737 // Read the posted SaaS account username/password. The password is only
738 // unslashed + trimmed: sanitize_text_field() strips %[hex][hex]
739 // sequences and would corrupt it (#204).
740 $username = isset($_POST['username']) ? sanitize_text_field($_POST['username']) : '';
741 $password = isset($_POST['password']) ? trim(wp_unslash($_POST['password'])) : '';
742
743 // Both credentials are required to attempt a connection.
744 if (empty($username) || empty($password)) {
745 wp_send_json_error(array('message' => __('Username and password are required', 'mlsimport')));
746 }
747
748 // Save to temporary storage for test
749 // Write the credentials into the plugin options so the token request uses them.
750 $options = get_option('mlsimport_admin_options', array());
751 $options['mlsimport_username'] = $username;
752 $options['mlsimport_password'] = $password;
753 update_option('mlsimport_admin_options', $options);
754
755 // Delete token to force fresh request
756 delete_transient('mlsimport_saas_token');
757
758 // Test connection using existing methods
759 // Ask the admin class for a token; a non-empty token means the login worked.
760 global $mlsimport;
761 $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
762
763 // No token returned -> credentials rejected or the SaaS was unreachable.
764 if (empty($token)) {
765 wp_send_json_error(array('message' => __('Unable to connect to MLS Import. Please check your credentials.', 'mlsimport')));
766 }
767
768 // Log success
769 mlsimport_log_onboarding_event('Successfully connected to MLS Import account', 'info');
770
771 wp_send_json_success(array('message' => __('Successfully connected to MLS Import', 'mlsimport')));
772 }
773
774 add_action('wp_ajax_mlsimport_save_account', 'mlsimport_save_account_callback');
775 /**
776 * AJAX handler: save the MLSImport account username/password and test the login.
777 *
778 * Verifies the onboarding nonce, stores credentials in mlsimport_admin_options,
779 * fetches a fresh API token, and returns connected/not-connected HTML + flag.
780 *
781 * Moved here from mlsimport.php so it sits next to its sibling handler
782 * mlsimport_ajax_test_account_connection() (same save-credentials-then-verify
783 * job) and is loadable by the pure-PHP unit harness in tests/unit/.
784 *
785 * @return void
786 */
787 function mlsimport_save_account_callback() {
788 // Verify the shared onboarding AJAX nonce.
789 check_ajax_referer('mlsimport_onboarding_nonce', 'security');
790
791 // Load current plugin options.
792 $options = get_option('mlsimport_admin_options', []);
793 // Persist the submitted credentials only when both are present. The
794 // password is only unslashed + trimmed: sanitize_text_field() strips
795 // %[hex][hex] sequences and would corrupt it (#204).
796 if ( ! empty($_POST['mlsimport_username']) && ! empty($_POST['mlsimport_password']) ) {
797 $options['mlsimport_username'] = sanitize_text_field($_POST['mlsimport_username']);
798 $options['mlsimport_password'] = trim(wp_unslash($_POST['mlsimport_password']));
799 update_option('mlsimport_admin_options', $options);
800
801 // Drop the cached token so the check below exercises the credentials
802 // that were JUST saved — answering from a token minted with the old
803 // password reported "connected" for a wrong new password (#205).
804 // The expiry timestamp goes too: ThemeImport::validateAndRefreshToken()
805 // trusts it and would keep treating the dead session as valid.
806 delete_transient('mlsimport_saas_token');
807 delete_option('mlsimport_token_expiry');
808 }
809
810 global $mlsimport;
811
812 // Refresh token
813 $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
814
815 // Empty token means the credentials did not authenticate.
816 if (trim($token) === '') {
817 // Buffer the "not connected" warning markup.
818 ob_start();
819
820 ?>
821 <div class="mlsimport_warning">
822 <?php esc_html_e('You are not connected to MlsImport - Please check your Username and Password.', 'mlsimport'); ?>
823 </div>
824 <?php
825 $html = ob_get_clean();
826
827 // Return failure HTML + connected=false.
828 wp_send_json_success([
829 'message' => __('You are not connected.', 'mlsimport'),
830 'html' => $html,
831 'connected' => false
832 ]);
833 } else {
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 test MLS connection
852 *
853 * Verifies the 'mlsimport_onboarding_nonce' nonce; performs no capability
854 * check. Persists the posted MLS id/token into mlsimport_admin_options, runs the
855 * admin class connection check, then reports the resulting
856 * mlsimport_connection_test option value.
857 *
858 * @since 6.1.0
859 */
860 function mlsimport_ajax_test_mls_connection() {
861 // Check nonce (no current_user_can capability check is performed here).
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 MLS info
867 // Read and sanitize the posted MLS id and (optional) provider token.
868 $mls_id = isset($_POST['mls_id']) ? sanitize_text_field($_POST['mls_id']) : '';
869 $mls_token = isset($_POST['mls_token']) ? sanitize_text_field($_POST['mls_token']) : '';
870
871 // An MLS selection is mandatory; the token may be blank for some providers.
872 if (empty($mls_id)) {
873 wp_send_json_error(array('message' => __('MLS selection is required', 'mlsimport')));
874 }
875
876 // Save to temporary storage for test
877 // Store the MLS id/token in the plugin options so the check uses them.
878 $options = get_option('mlsimport_admin_options', array());
879 $options['mlsimport_mls_name'] = $mls_id;
880 $options['mlsimport_mls_token'] = $mls_token;
881 update_option('mlsimport_admin_options', $options);
882
883 // Test connection using existing methods
884 // Run the connection check; it writes the 'yes'/'' flag we read back below.
885 global $mlsimport;
886 $connection_result = $mlsimport->admin->mlsimport_saas_check_mls_connection();
887 $is_connected = get_option('mlsimport_connection_test', '');
888
889 // Anything other than 'yes' is treated as a failed MLS connection.
890 if ($is_connected !== 'yes') {
891 wp_send_json_error(array('message' => __('Unable to connect to MLS. Please check your credentials.', 'mlsimport')));
892 }
893
894 // Log success
895 mlsimport_log_onboarding_event('Successfully connected to MLS provider', 'info');
896
897 wp_send_json_success(array('message' => __('Successfully connected to MLS', 'mlsimport')));
898 }
899
900 /**
901 * Handle AJAX run test import
902 *
903 * Verifies the 'mlsimport_onboarding_nonce' nonce; performs no capability
904 * check. Caps the configured import item at 5 listings, builds the import
905 * request set, and enqueues the background async action that performs the
906 * actual import.
907 *
908 * @since 6.1.0
909 */
910 function mlsimport_ajax_run_test_import() {
911 // Check nonce (no current_user_can capability check is performed here).
912 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mlsimport_onboarding_nonce')) {
913 wp_send_json_error(array('message' => __('Security check failed', 'mlsimport')));
914 }
915
916 // Get import ID
917 // The import item id was stashed during the import-config step.
918 $user_data = get_option('mlsimport_onboarding_user_data', array());
919 $import_id = isset($user_data['import_id']) ? $user_data['import_id'] : 0;
920
921 // Without an import item there is nothing to run.
922 if (empty($import_id)) {
923 wp_send_json_error(array('message' => __('No import configuration found', 'mlsimport')));
924 }
925
926 // The saved onboarding id must still be a task this user may manage.
927 if ( 'mlsimport_item' !== get_post_type( $import_id ) || ! current_user_can( 'edit_post', $import_id ) ) {
928 wp_send_json_error( array( 'message' => __( 'You are not allowed to manage this import task.', 'mlsimport' ) ), 403 );
929 }
930
931 update_post_meta( $import_id, 'mlsimport_item_how_many', 5 );
932 global $mlsimport;
933
934 try {
935 // Setup has no count displayed by the page, so this small scheduling
936 // adapter performs one count and passes it into the shared manual run.
937 $mlsrequest = $mlsimport->admin->mlsimport_make_listing_requests( $import_id );
938 if ( ! isset( $mlsrequest['results'] ) || 0 === intval( $mlsrequest['results'] ) ) {
939 wp_send_json_error( array( 'message' => __( 'No listings found with current configuration', 'mlsimport' ) ) );
940 }
941 $found_items = min( 5, max( 0, intval( $mlsrequest['results'] ) ) );
942 $start = $mlsimport->admin->mlsimport_import_task_execution()->start(
943 array(
944 'task_id' => (int) $import_id,
945 'source' => 'manual',
946 'found' => $found_items,
947 'limit' => 5,
948 'is_onboard' => 1,
949 )
950 );
951 if ( true !== ( $start['accepted'] ?? false ) ) {
952 wp_send_json_error(
953 array( 'message' => __( 'Another import is already running. Please wait for it to finish.', 'mlsimport' ) )
954 );
955 }
956
957 mlsimport_log_onboarding_event( 'Starting test import of up to 5 properties', 'info' );
958 // Shared worker scheduling: clears dead/superseded queue entries first
959 // so a previously crashed worker can never block this start.
960 $mlsimport->admin->mlsimport_enqueue_import_worker( (string) $start['run_id'] );
961
962 wp_send_json_success(
963 array(
964 'message' => __( 'Import process started', 'mlsimport' ),
965 'import_id' => (int) $import_id,
966 'run_id' => (string) $start['run_id'],
967 )
968 );
969 } catch (Exception $e) {
970 mlsimport_log_onboarding_event('Test import failed: ' . $e->getMessage(), 'error');
971 wp_send_json_error(array('message' => __('Import failed: ', 'mlsimport') . $e->getMessage()));
972 }
973 }
974
975 /**
976 * Handle AJAX save step data
977 *
978 * Verifies the 'mlsimport_onboarding_nonce' nonce; performs no capability
979 * check. Delegates to mlsimport_save_step_data(), which sanitizes and persists
980 * the posted per-step form data.
981 *
982 * @since 6.1.0
983 */
984 function mlsimport_ajax_save_step_data() {
985 // Check nonce (no current_user_can capability check is performed here).
986 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mlsimport_onboarding_nonce')) {
987 wp_send_json_error(array('message' => __('Security check failed', 'mlsimport')));
988 }
989
990 // Get step and data
991 // Step id is sanitized; the raw data array is sanitized inside save_step_data().
992 $step = isset($_POST['step']) ? sanitize_text_field($_POST['step']) : '';
993 $data = isset($_POST['data']) ? $_POST['data'] : array();
994
995 // A step id is required to know where to store the data.
996 if (empty($step)) {
997 wp_send_json_error(array('message' => __('No step specified', 'mlsimport')));
998 }
999
1000 // Save step data
1001 $result = mlsimport_save_step_data($step, $data);
1002
1003 // update_option returns false when the write fails (or value is unchanged).
1004 if (!$result) {
1005 wp_send_json_error(array('message' => __('Failed to save data', 'mlsimport')));
1006 }
1007
1008 wp_send_json_success(array('message' => __('Data saved successfully', 'mlsimport')));
1009 }
1010
1011 /**
1012 * Save current onboarding state
1013 *
1014 * @since 6.1.0
1015 * @param string $step_id The current step ID
1016 * @param array $form_data The form data
1017 * @return bool Success or failure
1018 */
1019 function mlsimport_save_onboarding_state($step_id, $form_data) {
1020 $state = array(
1021 'current_step' => $step_id,
1022 'form_data' => $form_data,
1023 'timestamp' => current_time('timestamp'),
1024 );
1025
1026 return update_option('mlsimport_onboarding_state', $state);
1027 }
1028
1029 /**
1030 * Restore onboarding state
1031 *
1032 * @since 6.1.0
1033 * @return array The saved state data
1034 */
1035 function mlsimport_restore_onboarding_state() {
1036 return get_option('mlsimport_onboarding_state', array());
1037 }
1038
1039 /**
1040 * Clear onboarding state
1041 *
1042 * @since 6.1.0
1043 * @return bool Success or failure
1044 */
1045 function mlsimport_clear_onboarding_state() {
1046 return delete_option('mlsimport_onboarding_state');
1047 }
1048
1049 /**
1050 * Maybe restart wizard
1051 *
1052 * @since 6.1.0
1053 */
1054 function mlsimport_maybe_restart_wizard() {
1055 if (isset($_GET['restart_wizard']) && $_GET['restart_wizard'] == 1) {
1056 // Clear onboarding state
1057 mlsimport_clear_onboarding_state();
1058
1059 // Reset current step
1060 update_option('mlsimport_onboarding_current_step', '');
1061
1062 // Clear user data
1063 delete_option('mlsimport_onboarding_user_data');
1064
1065 // Mark onboarding as not completed
1066 update_option('mlsimport_onboarding_completed', false);
1067
1068 // Redirect to first step
1069 wp_redirect(admin_url('admin.php?page=mlsimport-onboarding'));
1070 exit;
1071 }
1072 }
1073 add_action('admin_init', 'mlsimport_maybe_restart_wizard');
1074 /**
1075 * Get a template configuration based on MLS provider
1076 *
1077 * @since 6.1.0
1078 * @param int $mls_id The MLS provider ID
1079 * @return array Default settings for the specified MLS
1080 */
1081 function mlsimport_get_import_item_template($mls_id) {
1082 // Default template
1083 $template = array(
1084 'title_format' => '{Address}, {City}, {CountyOrParish}, {PropertyType}',
1085 'property_status' => 'publish',
1086 'auto_update' => 1,
1087 'standard_status' => array('Active', 'Coming Soon'),
1088 'property_types' => array('Residential', 'Condo/Townhome/Row Home/Co-Op'),
1089 );
1090
1091 // Customize based on MLS ID if needed
1092 switch ($mls_id) {
1093 // Add MLS-specific customizations here
1094 case '111': // Example - Rae Edmonton
1095 $template['standard_status'] = array('Active');
1096 break;
1097
1098 default:
1099 // Use defaults
1100 break;
1101 }
1102
1103 return $template;
1104 }
1105
1106 /**
1107 * Display a condensed log summary
1108 *
1109 * @since 6.1.0
1110 * @param int $num_entries Number of entries to show
1111 * @return string HTML output of log summary
1112 */
1113 function mlsimport_display_onboarding_log_summary($num_entries = 10) {
1114 $path = WP_PLUGIN_DIR . '/mlsimport/logs/onboarding_logs.log';
1115
1116 if (!file_exists($path)) {
1117 return '<div class="mlsimport-log-summary empty">' . __('No logs available', 'mlsimport') . '</div>';
1118 }
1119
1120 // Get the last N lines
1121 $lines = file($path);
1122 $lines = array_slice($lines, -$num_entries);
1123
1124 $output = '<div class="mlsimport-log-summary">';
1125 $output .= '<h4>' . __('Recent Activity', 'mlsimport') . '</h4>';
1126 $output .= '<ul class="mlsimport-logs">';
1127
1128 // Build one list item per log line, colour-coded by the severity tag it contains.
1129 foreach ($lines as $line) {
1130 // Extract log type for styling
1131 if (strpos($line, '[INFO]') !== false) {
1132 $class = 'info';
1133 } elseif (strpos($line, '[WARNING]') !== false) {
1134 $class = 'warning';
1135 } elseif (strpos($line, '[ERROR]') !== false) {
1136 $class = 'error';
1137 } else {
1138 // No recognised tag -> no severity class.
1139 $class = '';
1140 }
1141
1142 // esc_html() escapes the raw log line before embedding it in the markup.
1143 $output .= '<li class="log-item ' . $class . '">' . esc_html($line) . '</li>';
1144 }
1145
1146 $output .= '</ul>';
1147 $output .= '</div>';
1148
1149 return $output;
1150 }
1151
1152 /**
1153 * Register onboarding-specific log types with logging system
1154 *
1155 * @since 6.1.0
1156 */
1157 function mlsimport_register_onboarding_logs() {
1158 // Create logs directory if it doesn't exist
1159 $log_dir = WP_PLUGIN_DIR . '/mlsimport/logs';
1160 if (!file_exists($log_dir)) {
1161 mkdir($log_dir, 0755, true);
1162 }
1163
1164 // Create onboarding log file if it doesn't exist
1165 $log_file = $log_dir . '/onboarding_logs.log';
1166 if (!file_exists($log_file)) {
1167 touch($log_file);
1168 }
1169 }
1170
1171 // Initialize onboarding
1172 add_action('init', 'mlsimport_init_onboarding');
1173
1174 // Register activation hook to redirect to onboarding
1175 function mlsimport_activation_redirect() {
1176 // Set a flag so the next admin request redirects to the onboarding wizard
1177 update_option('mlsimport_do_onboarding_redirect', true);
1178 }
1179 register_activation_hook(MLSIMPORT_PLUGIN_PATH . 'mlsimport.php', 'mlsimport_activation_redirect');
1180