PluginProbe ʕ •ᴥ•ʔ
LatePoint – Calendar Booking Plugin for Appointments and Events / 5.6.6
LatePoint – Calendar Booking Plugin for Appointments and Events v5.6.6
5.6.6 5.6.5 5.6.4 5.6.3 5.6.2 5.6.1 5.6.0 5.5.2 5.5.1 5.5.0 5.4.2 trunk 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.1.91 5.1.92 5.1.93 5.1.94 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1
latepoint / lib / helpers / otp_helper.php
latepoint / lib / helpers Last commit date
activities_helper.php 3 months ago agent_helper.php 3 months ago analytics_helper.php 1 week ago auth_helper.php 6 days ago blocks_helper.php 2 weeks ago booking_helper.php 4 days ago bricks_helper.php 3 months ago bundles_helper.php 4 days ago calendar_helper.php 2 days ago carts_helper.php 3 months ago connector_helper.php 3 months ago csv_helper.php 3 months ago customer_helper.php 1 month ago customer_import_helper.php 1 month ago database_helper.php 1 week ago debug_helper.php 3 months ago defaults_helper.php 3 months ago elementor_helper.php 3 months ago email_helper.php 3 months ago encrypt_helper.php 3 months ago events_helper.php 3 months ago form_helper.php 3 months ago icalendar_helper.php 3 months ago image_helper.php 3 months ago invoices_helper.php 2 weeks ago license_helper.php 3 months ago location_helper.php 3 months ago marketing_systems_helper.php 3 months ago meeting_systems_helper.php 3 months ago menu_helper.php 2 weeks ago meta_helper.php 3 months ago migrations_helper.php 3 months ago money_helper.php 3 months ago notifications_helper.php 3 months ago nps_survey_helper.php 3 months ago order_intent_helper.php 2 months ago orders_helper.php 3 months ago otp_helper.php 3 months ago pages_helper.php 3 months ago params_helper.php 3 months ago payments_helper.php 2 months ago plugin_version_update_helper.php 2 months ago price_breakdown_helper.php 3 months ago process_jobs_helper.php 3 months ago processes_helper.php 3 months ago razorpay_connect_helper.php 1 week ago replacer_helper.php 3 months ago resource_helper.php 4 days ago roles_helper.php 2 weeks ago router_helper.php 3 months ago service_helper.php 3 months ago sessions_helper.php 3 months ago settings_helper.php 1 week ago short_links_systems_helper.php 3 months ago shortcodes_helper.php 2 weeks ago sms_helper.php 3 months ago steps_helper.php 1 week ago stripe_connect_helper.php 1 week ago styles_helper.php 3 months ago support_topics_helper.php 3 months ago time_helper.php 2 months ago timeline_helper.php 2 months ago transaction_helper.php 1 month ago transaction_intent_helper.php 3 months ago util_helper.php 1 month ago version_specific_updates_helper.php 3 months ago whatsapp_helper.php 1 month ago work_periods_helper.php 3 months ago wp_datetime.php 3 months ago wp_user_helper.php 3 months ago
otp_helper.php
421 lines
1 <?php
2 class OsOTPHelper {
3
4 private static int $max_verification_attempts = 10; // Max attempts per OTP
5 private static int $max_generation_attempts_per_hour = 60; // Max OTP generations per hour
6 public static int $otp_expires_in_minutes = 10; // Max OTP generations per hour
7 public static int $verification_expires_in_minutes = 30;
8
9 public static function create_verification_token( $contact_value, $contact_type, $via = 'otp' ): string {
10 $payload_data = [
11 'contact_value' => $contact_value,
12 'contact_type' => $contact_type,
13 'verified_via' => $via,
14 'exp' => time() + ( self::$verification_expires_in_minutes * 60 ),
15 'iat' => time(),
16 ];
17
18 $payload = base64_encode( json_encode( $payload_data ) );
19 $signature = hash_hmac( 'sha256', $payload, self::get_secret() );
20
21 return $payload . '.' . $signature;
22 }
23
24 public static function get_secret() {
25 return wp_salt( 'secure_auth' );
26 }
27
28 public static function validate_verification_token( $token ): array {
29 if ( empty( $token ) ) {
30 return [
31 'valid' => false,
32 'error' => 'Token required',
33 ];
34 }
35
36 $parts = explode( '.', $token );
37 if ( count( $parts ) !== 2 ) {
38 return [
39 'valid' => false,
40 'error' => 'Malformed token',
41 ];
42 }
43
44 [$payload, $signature] = $parts;
45
46 // Verify signature
47 $expected_signature = hash_hmac( 'sha256', $payload, self::get_secret() );
48 if ( ! hash_equals( $expected_signature, $signature ) ) {
49 return [
50 'valid' => false,
51 'error' => 'Invalid signature',
52 ];
53 }
54
55 // Decode payload
56 $data = json_decode( base64_decode( $payload ), true );
57 if ( ! $data ) {
58 return [
59 'valid' => false,
60 'error' => 'Invalid payload',
61 ];
62 }
63
64 // Check expiration
65 if ( time() > $data['exp'] ) {
66 return [
67 'valid' => false,
68 'error' => 'Token expired',
69 ];
70 }
71
72 return [
73 'valid' => true,
74 'data' => $data,
75 ];
76 }
77
78
79 public static function generateAndSendOTP( $contact_value, $contact_type, $delivery_method ) {
80 if ( ! self::isValidCombination( $contact_type, $delivery_method ) ) {
81 return new WP_Error( 'otp_generation_error', 'Invalid delivery method for contact type' );
82 }
83
84 if ( ! self::checkRateLimit( $contact_value ) ) {
85 return new WP_Error( 'otp_generation_error', 'Too many attempts. Please try again later.' );
86 }
87
88 if ( $contact_type == 'email' ) {
89 if ( ! OsUtilHelper::is_valid_email( $contact_value ) ) {
90 return new WP_Error( 'otp_generation_error', 'Invalid email address' );
91 }
92 }
93
94 // Cancel old active OTPs for this contact
95 self::cancelOldOTPs( $contact_value );
96
97 // Generate new OTP
98 $otp_code = str_pad( random_int( 0, 999999 ), 6, '0', STR_PAD_LEFT );
99 $otp_hash = wp_hash_password( $otp_code );
100 $expires_at = OsTimeHelper::custom_datetime_utc_in_db_format( sprintf( '+%d minutes', self::$otp_expires_in_minutes ) );
101
102 $otp = new OsOTPModel();
103 $otp->contact_value = $contact_value;
104 $otp->contact_type = $contact_type;
105 $otp->delivery_method = $delivery_method;
106 $otp->otp_hash = $otp_hash;
107 $otp->expires_at = $expires_at;
108 $otp->status = LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE;
109 $otp->attempts = 0;
110 if ( ! $otp->save() ) {
111 return new WP_Error( 'otp_generation_error', $otp->get_error_messages() );
112 }
113
114 // Send OTP
115 return self::sendOTP( $otp_code, $otp );
116 }
117
118
119 public static function otp_input_box_html( string $contact_type, string $contact_value, string $delivery_method ): string {
120
121 $title = ( 'email' === $contact_type ) ? esc_html__( 'Verify your email', 'latepoint' ) : esc_html__( 'Verify your phone number', 'latepoint' );
122
123 $message = '';
124 $message .= '<div class="latepoint-customer-otp-input-wrapper os-customer-wrapped-box">';
125 $message .= '<div class="latepoint-customer-otp-close"><i class="latepoint-icon latepoint-icon-common-01"></i></div>';
126 $message .= '<div class="latepoint-customer-box-title">' . $title . '</div>';
127 $message .= '<div class="latepoint-customer-box-desc">' . sprintf( esc_html__( 'Enter the code we sent to %s', 'latepoint' ), $contact_value ) . '</div>';
128 $message .= '<div class="latepoint-customer-otp-input-code-wrapper">';
129 $message .= OsFormHelper::otp_code_field( 'otp[otp_code]' );
130 $message .= '</div>';
131 $message .= '<a tabindex="0" class="latepoint-btn latepoint-btn-block latepoint-btn-primary latepoint-verify-otp-button" data-route="' . OsRouterHelper::build_route_name( 'auth', 'verify_otp' ) . '"><span>' . __( 'Verify', 'latepoint' ) . '</span></a>';
132 $message .= '<div class="latepoint-customer-otp-sub-wrapper">';
133 $message .= '<div class="latepoint-customer-otp-sub">' . sprintf( esc_html__( 'The code will expire in %s minutes', 'latepoint' ), OsOTPHelper::$otp_expires_in_minutes ) . '</div>';
134 $message .= '<a tabindex="0" href="#" class="latepoint-customer-otp-resend" data-otp-resend-route="' . OsRouterHelper::build_route_name( 'auth', 'resend_otp' ) . '">' . esc_html__( 'Resend code', 'latepoint' ) . '</a>';
135 $message .= '</div>';
136 $message .= wp_nonce_field( 'otp_verify_otp_nonce', 'otp[verify_nonce]', true, false );
137 $message .= wp_nonce_field( 'otp_resend_otp_nonce', 'otp[resend_nonce]', true, false );
138 $message .= OsFormHelper::hidden_field( 'otp[contact_type]', $contact_type );
139 $message .= OsFormHelper::hidden_field( 'otp[contact_value]', $contact_value );
140 $message .= OsFormHelper::hidden_field( 'otp[delivery_method]', $delivery_method );
141 $message .= '</div>';
142 return $message;
143 }
144
145 public static function is_customer_contact_verified( OsCustomerModel $customer, string $contact_value, string $contact_type ): bool {
146 $verified_contact_values = json_decode( $customer->get_meta_by_key( 'verified_contact_values', '' ), true );
147 if ( $verified_contact_values ) {
148 return in_array( $contact_value, $verified_contact_values[ $contact_type ] );
149 } else {
150 return false;
151 }
152 }
153
154
155 public static function add_verified_contact_for_customer_from_verification_token( OsCustomerModel $customer, string $verification_token ): void {
156 $verification_info = OsOTPHelper::validate_verification_token( $verification_token );
157 if ( $verification_info['valid'] && ! empty( $verification_info['data']['contact_value'] ) ) {
158 self::add_verified_contact_for_customer( $customer, $verification_info['data']['contact_value'], $verification_info['data']['contact_type'] );
159 }
160 }
161
162 public static function is_token_matching_to_contact_value( string $verification_token, string $contact_value ): bool {
163 $verification_info = OsOTPHelper::validate_verification_token( $verification_token );
164 if ( $verification_info['valid'] && ! empty( $verification_info['data']['contact_value'] ) && $verification_info['data']['contact_value'] == $contact_value ) {
165 return true;
166 }
167 return false;
168 }
169
170 public static function add_verified_contact_for_customer( OsCustomerModel $customer, string $contact_value, string $contact_type ) {
171 if ( ! $customer->is_new_record() && ! empty( $contact_value ) && in_array( $contact_type, self::valid_contact_types_for_customer() ) ) {
172 if ( ! self::is_customer_contact_verified( $customer, $contact_value, $contact_type ) ) {
173 $verified_contact_values = json_decode( $customer->get_meta_by_key( 'verified_contact_values', '' ), true );
174 $verified_contact_values[ $contact_type ][] = $contact_value;
175 $customer->save_meta_by_key( 'verified_contact_values', wp_json_encode( $verified_contact_values ) );
176 }
177 }
178 }
179
180 public static function verifyOTP( $otp_code, $contact_value, $contact_type = 'email', $delivery_method = 'email' ) {
181 // Expire old OTPs first
182 self::expireExpiredOTPs();
183
184 $otp = new OsOTPModel();
185 $active_otp = $otp->where(
186 [
187 'contact_value' => $contact_value,
188 'contact_type' => $contact_type,
189 'delivery_method' => $delivery_method,
190 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE,
191 'attempts <' => self::$max_verification_attempts,
192 ]
193 )->set_limit( 1 )->get_results_as_models();
194
195 if ( empty( $active_otp ) ) {
196 return new WP_Error( 'otp_generation_error', 'Invalid Code' );
197 }
198
199 if ( wp_check_password( $otp_code, $active_otp->otp_hash ) ) {
200 // Mark this OTP as used
201 $active_otp->update_attributes(
202 [
203 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_USED,
204 'used_at' => OsTimeHelper::now_datetime_in_format( LATEPOINT_DATETIME_DB_FORMAT ),
205 ]
206 );
207
208 // Cancel other active OTPs for this contact
209 $other_otps = new OsOTPModel();
210 $other_otps = $other_otps->where(
211 [
212 'contact_value' => $contact_value,
213 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE,
214 ]
215 )->get_results_as_models();
216 if ( $other_otps ) {
217 foreach ( $other_otps as $otp ) {
218 $otp->update_attributes( [ 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED ] );
219 }
220 }
221
222 return [
223 'status' => LATEPOINT_STATUS_SUCCESS,
224 'contact_value' => $contact_value,
225 ];
226 }
227
228 $active_otp->update_attributes( [ 'attempts' => $active_otp->attempts + 1 ] );
229
230 return new WP_Error( 'otp_generation_error', 'Invalid Code' );
231 }
232
233 private static function sendOTP( string $otp_code, OsOTPModel $otp ): array {
234
235 $result = [
236 'status' => LATEPOINT_STATUS_ERROR,
237 'message' => __( 'OTP was not sent.', 'latepoint' ),
238 'to' => $otp->contact_value,
239 'delivery_method' => $otp->delivery_method,
240 'contact_type' => $otp->contact_type,
241 'processed_datetime' => '',
242 'extra_data' => [
243 'activity_data' => [],
244 ],
245 'errors' => [],
246 ];
247 switch ( $otp->delivery_method ) {
248 case 'email':
249 $subject = __( 'Your OTP Code', 'latepoint' );
250 $content = sprintf( esc_html__( 'Your OTP code is: %s', 'latepoint' ), $otp_code );
251 $send_result = OsNotificationsHelper::send(
252 $otp->delivery_method,
253 [
254 'to' => $otp->contact_value,
255 'subject' => $subject,
256 'content' => $content,
257 ]
258 );
259 if ( $send_result['status'] == LATEPOINT_STATUS_SUCCESS ) {
260 $result['processed_datetime'] = OsTimeHelper::now_datetime_in_db_format();
261 $result['status'] = LATEPOINT_STATUS_SUCCESS;
262 } else {
263 $result['message'] = __( 'Failed to send email', 'latepoint' );
264 }
265 break;
266 case 'sms':
267 $subject = __( 'Your OTP Code', 'latepoint' );
268 $content = sprintf( esc_html__( 'Your OTP code is: %s', 'latepoint' ), $otp_code );
269 $send_result = OsNotificationsHelper::send(
270 $otp->delivery_method,
271 [
272 'to' => $otp->contact_value,
273 'subject' => $subject,
274 'content' => $content,
275 ]
276 );
277 if ( $send_result['status'] == LATEPOINT_STATUS_SUCCESS ) {
278 $result['processed_datetime'] = OsTimeHelper::now_datetime_in_db_format();
279 $result['status'] = LATEPOINT_STATUS_SUCCESS;
280 } else {
281 $result['message'] = __( 'Failed to send SMS', 'latepoint' );
282 }
283 break;
284 }
285
286
287 /**
288 * Result of sending an OTP code
289 *
290 * @since 5.2.0
291 * @hook latepoint_notifications_send_otp_code
292 *
293 * @param {array} $result The array of data describing the result of operation
294 * @param {string} $otp_code
295 * @param {OsOTPModel} $otp
296 *
297 * @returns {array} The filtered array of data describing the result of operation
298 */
299 $result = apply_filters( 'latepoint_notifications_send_otp_code', $result, $otp_code, $otp );
300
301 return $result;
302 }
303
304 public static function valid_contact_types_for_customer(): array {
305 $contact_types = [ 'email', 'phone' ];
306 /**
307 * List of valid contact types for customers
308 *
309 * @since 5.2.0
310 * @hook latepoint_valid_contact_types_for_customer
311 *
312 * @param {array} $contact_types The array of contact types
313 *
314 * @returns {array} The filtered array of contact types
315 */
316 $result = apply_filters( 'latepoint_valid_contact_types_for_customer', $contact_types );
317
318 return $result;
319 }
320
321 private static function cancelOldOTPs( $contact_value ) {
322 $old_otps = new OsOTPModel();
323 $old_otps = $old_otps->where(
324 [
325 'contact_value' => $contact_value,
326 'status' => 'active',
327 ]
328 )->get_results_as_models();
329 if ( $old_otps ) {
330 foreach ( $old_otps as $otp ) {
331 $otp->update_attributes( [ 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED ] );
332 }
333 }
334 }
335
336 private static function expireExpiredOTPs() {
337 $otps = new OsOTPModel();
338 $expired_otps = $otps->where(
339 [
340 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE,
341 'expires_at <' => OsTimeHelper::now_datetime_utc_in_db_format(),
342 ]
343 )->get_results_as_models();
344 if ( $expired_otps ) {
345 foreach ( $expired_otps as $otp ) {
346 $otp->update_attributes( [ 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_EXPIRED ] );
347 }
348 }
349 }
350
351 private static function isValidCombination( $contact_type, $delivery_method ) {
352 $valid_combinations = [
353 'email' => [ 'email' ],
354 'phone' => [ 'sms', 'whatsapp' ],
355 ];
356 /**
357 * Delivery methods for contact types
358 *
359 * @since 5.2.0
360 * @hook latepoint_otp_delivery_methods_for_contact_types
361 *
362 * @param {array} $methods available delivery methods
363 * @returns {array} The filtered array of available delivery methods
364 */
365 $valid_combinations = apply_filters( 'latepoint_otp_delivery_methods_for_contact_types', $valid_combinations );
366
367 return in_array( $delivery_method, $valid_combinations[ $contact_type ] ?? [] );
368 }
369
370 private static function checkRateLimit( $contact_value ): bool {
371
372 $otps = new OsOTPModel();
373 $recent_attempts = $otps->where(
374 [
375 'contact_value' => $contact_value,
376 'created_at >' => OsTimeHelper::custom_datetime_utc_in_db_format( '-1 hour' ),
377 ]
378 )->count();
379
380
381 return $recent_attempts < self::$max_generation_attempts_per_hour;
382 }
383
384
385 // Cleanup old records
386 public static function scheduledCleanup() {
387 $otps = new OsOTPModel();
388 $otps->delete_where(
389 [
390 'created_at <' => OsTimeHelper::custom_datetime_utc_in_db_format( '-30 days' ),
391 'status' => [
392 LATEPOINT_CUSTOMER_OTP_CODE_STATUS_USED,
393 LATEPOINT_CUSTOMER_OTP_CODE_STATUS_EXPIRED,
394 LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED,
395 ],
396 ]
397 );
398 }
399
400 public static function is_otp_enabled_for_contact_type( string $contact_type, string $delivery_method ): bool {
401 $is_enabled = false;
402 if ( $contact_type == 'email' && $delivery_method == 'email' ) {
403 $is_enabled = true;
404 }
405
406 /**
407 * Determines if OTP is enabled for a selected contact type and delivery method
408 *
409 * @since 5.2.0
410 * @hook latepoint_is_otp_enabled_for_contact_type
411 *
412 * @param {bool} $is_enabled if otp delivery is enabled for a supplied contact and delivery method
413 * @param {string} $contact_type a contact type for OTP
414 * @param {string} $delivery_method a delivery method for OTP
415 *
416 * @returns {bool} Filtered value of whether OTP is enabled for this delivery method
417 */
418 return apply_filters( 'latepoint_is_otp_enabled_for_contact_type', $is_enabled, $contact_type, $delivery_method );
419 }
420 }
421