PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / trunk
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings vtrunk
7.2.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 All 37 releases
mlsimport / includes / standalone / class-mlsimport-property-lead.php

class-mlsimport-property-lead.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings trunk, at includes/standalone/class-mlsimport-property-lead.php

222 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Standalone (theme_id 990) single-property lead endpoint.
4 *
5 * One AJAX action (mlsimport_property_lead) backs all four lead forms (agent
6 * contact, agent form, sidebar form, schedule a tour). It validates a nonce +
7 * honeypot, then emails the listing's linked agent (falling back to the settings
8 * recipient, then the site admin). The work lives in process() so it is testable
9 * without the HTTP/nonce layer. See the build plan, decision 5.
10 *
11 * @package Mlsimport
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 require_once __DIR__ . '/property-sections.php';
19
20 /**
21 * Registers and handles the shared property-lead AJAX endpoint.
22 */
23 class Mlsimport_Property_Lead {
24
25 const ACTION = 'mlsimport_property_lead';
26 const NONCE = 'mlsimport_property_lead';
27
28 /**
29 * Hook the AJAX action for logged-in and anonymous visitors.
30 *
31 * @return void
32 */
33 public static function register(): void {
34 add_action( 'wp_ajax_' . self::ACTION, array( __CLASS__, 'handle' ) );
35 add_action( 'wp_ajax_nopriv_' . self::ACTION, array( __CLASS__, 'handle' ) );
36 }
37
38 /**
39 * AJAX entry: verify the nonce, run process(), return JSON.
40 *
41 * @return void
42 */
43 public static function handle(): void {
44 check_ajax_referer( self::NONCE, 'nonce' );
45
46 $result = self::process( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above.
47
48 if ( $result['ok'] ) {
49 wp_send_json_success( array( 'message' => $result['message'] ) );
50 }
51 wp_send_json_error( array( 'message' => $result['message'] ), 400 );
52 }
53
54 /**
55 * Validate and dispatch a lead. Pure of the nonce/HTTP layer.
56 *
57 * @param array $input Raw (unslashed) request fields.
58 * @return array{ok:bool,message:string,to:string}
59 */
60 public static function process( array $input ): array {
61 // Honeypot: a filled hidden field means a bot — drop silently as success.
62 if ( ! empty( $input['mlsimport_hp'] ) ) {
63 return array( 'ok' => true, 'message' => __( 'Thank you.', 'mlsimport' ), 'to' => '' );
64 }
65
66 $name = isset( $input['mlsimport_name'] ) ? sanitize_text_field( $input['mlsimport_name'] ) : '';
67 $email = isset( $input['mlsimport_email'] ) ? sanitize_email( $input['mlsimport_email'] ) : '';
68 $phone = isset( $input['mlsimport_phone'] ) ? sanitize_text_field( $input['mlsimport_phone'] ) : '';
69 $message = isset( $input['mlsimport_message'] ) ? sanitize_textarea_field( $input['mlsimport_message'] ) : '';
70 $tour = isset( $input['mlsimport_tour_date'] ) ? sanitize_text_field( $input['mlsimport_tour_date'] ) : '';
71 $pid = isset( $input['property_id'] ) ? absint( $input['property_id'] ) : 0;
72 $aid = isset( $input['agent_id'] ) ? absint( $input['agent_id'] ) : 0;
73
74 if ( '' === $name || ! is_email( $email ) ) {
75 return array(
76 'ok' => false,
77 'message' => __( 'Please provide your name and a valid email.', 'mlsimport' ),
78 'to' => '',
79 );
80 }
81
82 // Property lead forms (they alone carry the hidden property_id field)
83 // render a mandatory privacy-consent checkbox — enforce it here too, the
84 // browser `required` alone is not enough. Agent-rail and page-block
85 // contact submissions have no property_id and are unaffected.
86 if ( isset( $input['property_id'] ) && empty( $input['mlsimport_consent'] ) ) {
87 return array(
88 'ok' => false,
89 'message' => __( 'Please accept the privacy policy to send your message.', 'mlsimport' ),
90 'to' => '',
91 );
92 }
93
94 // Extension validation (reCAPTCHA, blocklists...): a non-empty errors list
95 // rejects the submission with the first message.
96 /** Filter lead validation errors. @since 6.3 */
97 $errors = (array) apply_filters( 'mlsimport_property_lead_validation', array(), $input, $pid );
98 if ( ! empty( $errors ) ) {
99 return array(
100 'ok' => false,
101 'message' => (string) reset( $errors ),
102 'to' => '',
103 );
104 }
105
106 $data = array(
107 'name' => $name,
108 'email' => $email,
109 'phone' => $phone,
110 'message' => $message,
111 'tour' => $tour,
112 'property_id' => $pid,
113 'agent_id' => $aid,
114 );
115
116 /** Fires when a valid lead is submitted (CRM capture point). @since 6.3 */
117 do_action( 'mlsimport_property_lead_submitted', $data, $pid );
118
119 $to = self::recipient( $pid, $aid );
120 $subject = sprintf(
121 /* translators: %s: listing title or agent name. */
122 __( 'New enquiry: %s', 'mlsimport' ),
123 $pid ? get_the_title( $pid ) : ( $aid ? get_the_title( $aid ) : __( 'Property', 'mlsimport' ) )
124 );
125 /** Filter the lead email subject. @since 6.3 */
126 $subject = (string) apply_filters( 'mlsimport_property_lead_subject', $subject, $pid );
127
128 $lines = array(
129 __( 'Name:', 'mlsimport' ) . ' ' . $name,
130 __( 'Email:', 'mlsimport' ) . ' ' . $email,
131 );
132 if ( '' !== $phone ) {
133 $lines[] = __( 'Phone:', 'mlsimport' ) . ' ' . $phone;
134 }
135 if ( '' !== $tour ) {
136 $lines[] = __( 'Requested tour date:', 'mlsimport' ) . ' ' . $tour;
137 }
138 // Custom contact-form fields (the page-block contact builder) — any extra
139 // mlsimport_* input beyond the standard set is appended by its label.
140 $standard = array( 'mlsimport_name', 'mlsimport_email', 'mlsimport_phone', 'mlsimport_message', 'mlsimport_tour_date', 'mlsimport_hp', 'mlsimport_context' );
141 foreach ( $input as $field_key => $field_value ) {
142 if ( 0 !== strpos( (string) $field_key, 'mlsimport_' ) || in_array( $field_key, $standard, true ) ) {
143 continue;
144 }
145 $value = sanitize_text_field( is_array( $field_value ) ? implode( ', ', $field_value ) : (string) $field_value );
146 if ( '' === $value ) {
147 continue;
148 }
149 $label = ucwords( str_replace( '_', ' ', substr( (string) $field_key, strlen( 'mlsimport_' ) ) ) );
150 $lines[] = $label . ': ' . $value;
151 }
152 if ( $pid ) {
153 $lines[] = __( 'Listing:', 'mlsimport' ) . ' ' . get_permalink( $pid );
154 }
155 if ( '' !== $message ) {
156 $lines[] = '';
157 $lines[] = $message;
158 }
159
160 $body = implode( "\n", $lines );
161 /** Filter the lead email body. @since 6.3 */
162 $body = (string) apply_filters( 'mlsimport_property_lead_email_body', $body, $data, $pid );
163
164 $headers = array( 'Reply-To: ' . $name . ' <' . $email . '>' );
165 $sent = wp_mail( $to, $subject, $body, $headers );
166
167 /** Fires after the lead mail attempt (CRM/observe point). @since 6.3 */
168 do_action( 'mlsimport_property_lead_mail_sent', $sent, $data, $pid );
169
170 return array(
171 'ok' => (bool) $sent,
172 'message' => $sent
173 ? __( 'Your message has been sent.', 'mlsimport' )
174 : __( 'Sorry, your message could not be sent. Please try again.', 'mlsimport' ),
175 'to' => $to,
176 );
177 }
178
179 /**
180 * Resolve the recipient: the listing's linked agent post (property lead) or
181 * the agent's own email (agent-page lead), else settings recipient, else the
182 * site admin. Feed-only agent data (no local agent post) never receives the
183 * lead. Filterable via mlsimport_property_lead_recipient.
184 *
185 * @param int $property_id Property post ID.
186 * @param int $agent_id Agent post ID (agent-page leads).
187 * @return string
188 */
189 public static function recipient( int $property_id, int $agent_id = 0 ): string {
190 $to = '';
191
192 if ( $property_id ) {
193 $vm = mlsimport_property_data( $property_id );
194 // Only an agent that exists on the site (a linked mlsimport_agent post,
195 // added or imported → agent.id > 0) receives the lead. Agent data shown
196 // straight from the property's MLS feed meta has no local post, so the
197 // lead routes to the settings recipient below.
198 if ( ! empty( $vm['agent']['id'] ) && ! empty( $vm['agent']['email'] ) && is_email( $vm['agent']['email'] ) ) {
199 $to = $vm['agent']['email'];
200 }
201 } elseif ( $agent_id ) {
202 $email = (string) get_post_meta( $agent_id, 'mlsimport_ListAgentEmail', true );
203 if ( is_email( $email ) ) {
204 $to = $email;
205 }
206 }
207 if ( '' === $to ) {
208 $setting = (string) mlsimport_standalone_option( 'lead_recipient', '' );
209 $to = is_email( $setting ) ? $setting : (string) get_option( 'admin_email' );
210 }
211
212 /**
213 * Filter the resolved lead recipient email.
214 *
215 * @param string $to Recipient email.
216 * @param int $property_id Property post ID.
217 * @param int $agent_id Agent post ID (agent-page leads).
218 */
219 return (string) apply_filters( 'mlsimport_property_lead_recipient', $to, $property_id, $agent_id );
220 }
221 }
222