PluginProbe ʕ •ᴥ•ʔ
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress / 5.2.9
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress v5.2.9
5.6.8 5.6.7 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 1 year ago agent_helper.php 1 year ago auth_helper.php 9 months ago blocks_helper.php 1 year ago booking_helper.php 1 year ago bricks_helper.php 1 year ago bundles_helper.php 1 year ago calendar_helper.php 9 months ago carts_helper.php 1 year ago connector_helper.php 9 months ago csv_helper.php 9 months ago customer_helper.php 9 months ago customer_import_helper.php 4 months ago database_helper.php 9 months ago debug_helper.php 1 year ago defaults_helper.php 1 year ago elementor_helper.php 1 year ago email_helper.php 9 months ago encrypt_helper.php 1 year ago events_helper.php 9 months ago form_helper.php 9 months ago icalendar_helper.php 1 year ago image_helper.php 1 year ago invoices_helper.php 9 months ago license_helper.php 1 year ago location_helper.php 1 year ago marketing_systems_helper.php 1 year ago meeting_systems_helper.php 1 year ago menu_helper.php 9 months ago meta_helper.php 1 year ago migrations_helper.php 1 year ago money_helper.php 1 year ago notifications_helper.php 9 months ago order_intent_helper.php 9 months ago orders_helper.php 1 year ago otp_helper.php 9 months ago pages_helper.php 1 year ago params_helper.php 1 year ago payments_helper.php 1 year ago price_breakdown_helper.php 1 year ago process_jobs_helper.php 1 year ago processes_helper.php 1 year ago replacer_helper.php 1 year ago resource_helper.php 1 year ago roles_helper.php 7 months ago router_helper.php 1 year ago service_helper.php 1 year ago sessions_helper.php 1 year ago settings_helper.php 4 months ago short_links_systems_helper.php 9 months ago shortcodes_helper.php 9 months ago sms_helper.php 1 year ago steps_helper.php 5 months ago stripe_connect_helper.php 9 months ago styles_helper.php 9 months ago support_topics_helper.php 1 year ago time_helper.php 9 months ago timeline_helper.php 9 months ago transaction_helper.php 1 year ago transaction_intent_helper.php 1 year ago util_helper.php 7 months ago version_specific_updates_helper.php 9 months ago whatsapp_helper.php 1 year ago work_periods_helper.php 5 months ago wp_datetime.php 1 year ago wp_user_helper.php 1 year ago
otp_helper.php
361 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 ['valid' => false, 'error' => 'Token required'];
31 }
32
33 $parts = explode('.', $token);
34 if (count($parts) !== 2) {
35 return ['valid' => false, 'error' => 'Malformed token'];
36 }
37
38 [$payload, $signature] = $parts;
39
40 // Verify signature
41 $expected_signature = hash_hmac('sha256', $payload, self::get_secret());
42 if (!hash_equals($expected_signature, $signature)) {
43 return ['valid' => false, 'error' => 'Invalid signature'];
44 }
45
46 // Decode payload
47 $data = json_decode(base64_decode($payload), true);
48 if (!$data) {
49 return ['valid' => false, 'error' => 'Invalid payload'];
50 }
51
52 // Check expiration
53 if (time() > $data['exp']) {
54 return ['valid' => false, 'error' => 'Token expired'];
55 }
56
57 return ['valid' => true, 'data' => $data];
58 }
59
60
61 public static function generateAndSendOTP($contact_value, $contact_type, $delivery_method) {
62 if (!self::isValidCombination($contact_type, $delivery_method)) {
63 return new WP_Error('otp_generation_error', 'Invalid delivery method for contact type');
64 }
65
66 if (!self::checkRateLimit($contact_value)) {
67 return new WP_Error('otp_generation_error', 'Too many attempts. Please try again later.');
68 }
69
70 if($contact_type == 'email'){
71 if(!OsUtilHelper::is_valid_email($contact_value)){
72 return new WP_Error('otp_generation_error', 'Invalid email address');
73 }
74 }
75
76 // Cancel old active OTPs for this contact
77 self::cancelOldOTPs($contact_value);
78
79 // Generate new OTP
80 $otp_code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
81 $otp_hash = wp_hash_password($otp_code);
82 $expires_at = OsTimeHelper::custom_datetime_utc_in_db_format(sprintf('+%d minutes', self::$otp_expires_in_minutes));
83
84 $otp = new OsOTPModel();
85 $otp->contact_value = $contact_value;
86 $otp->contact_type = $contact_type;
87 $otp->delivery_method = $delivery_method;
88 $otp->otp_hash = $otp_hash;
89 $otp->expires_at = $expires_at;
90 $otp->status = LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE;
91 $otp->attempts = 0;
92 if(!$otp->save()){
93 return new WP_Error('otp_generation_error', $otp->get_error_messages());
94 }
95
96 // Send OTP
97 return self::sendOTP($otp_code, $otp);
98 }
99
100
101 public static function otp_input_box_html( string $contact_type, string $contact_value, string $delivery_method ) : string {
102 $message = '';
103 $message.= '<div class="latepoint-customer-otp-input-wrapper os-customer-wrapped-box">';
104 $message.= '<div class="latepoint-customer-otp-close"><i class="latepoint-icon latepoint-icon-common-01"></i></div>';
105 $message.= '<div class="latepoint-customer-box-title">'.esc_html__('Verify your email', 'latepoint').'</div>';
106 $message.= '<div class="latepoint-customer-box-desc">'.sprintf(esc_html__('Enter the code we sent to %s', 'latepoint'), $contact_value).'</div>';
107 $message.= '<div class="latepoint-customer-otp-input-code-wrapper">';
108 $message.= OsFormHelper::otp_code_field('otp[otp_code]');
109 $message.= '</div>';
110 $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>';
111 $message.= '<div class="latepoint-customer-otp-sub-wrapper">';
112 $message.= '<div class="latepoint-customer-otp-sub">'.sprintf(esc_html__('The code will expire in %s minutes', 'latepoint'), OsOTPHelper::$otp_expires_in_minutes).'</div>';
113 $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>';
114 $message.= '</div>';
115 $message.= wp_nonce_field('otp_verify_otp_nonce', 'otp[verify_nonce]', true, false);
116 $message.= wp_nonce_field('otp_resend_otp_nonce', 'otp[resend_nonce]', true, false);
117 $message.= OsFormHelper::hidden_field('otp[contact_type]', $contact_type);
118 $message.= OsFormHelper::hidden_field('otp[contact_value]', $contact_value);
119 $message.= OsFormHelper::hidden_field('otp[delivery_method]', $delivery_method);
120 $message.= '</div>';
121 return $message;
122 }
123
124 public static function is_customer_contact_verified(OsCustomerModel $customer, string $contact_value, string $contact_type) : bool{
125 $verified_contact_values = json_decode($customer->get_meta_by_key('verified_contact_values', ''), true);
126 if($verified_contact_values){
127 return in_array($contact_value, $verified_contact_values[$contact_type]);
128 }else{
129 return false;
130 }
131 }
132
133
134 public static function add_verified_contact_for_customer_from_verification_token(OsCustomerModel $customer, string $verification_token) : void {
135 $verification_info = OsOTPHelper::validate_verification_token($verification_token);
136 if($verification_info['valid'] && !empty($verification_info['data']['contact_value'])){
137 self::add_verified_contact_for_customer($customer, $verification_info['data']['contact_value'], $verification_info['data']['contact_type']);
138 }
139 }
140
141 public static function is_token_matching_to_contact_value(string $verification_token, string $contact_value) : bool{
142 $verification_info = OsOTPHelper::validate_verification_token($verification_token);
143 if($verification_info['valid'] && !empty($verification_info['data']['contact_value']) && $verification_info['data']['contact_value'] == $contact_value){
144 return true;
145 }
146 return false;
147 }
148
149 public static function add_verified_contact_for_customer(OsCustomerModel $customer, string $contact_value, string $contact_type){
150 if(!$customer->is_new_record() && !empty($contact_value) && in_array($contact_type, self::valid_contact_types_for_customer())){
151 if(!self::is_customer_contact_verified($customer, $contact_value, $contact_type)){
152 $verified_contact_values = json_decode($customer->get_meta_by_key('verified_contact_values', ''), true);
153 $verified_contact_values[$contact_type][] = $contact_value;
154 $customer->save_meta_by_key('verified_contact_values', wp_json_encode($verified_contact_values));
155 }
156 }
157 }
158
159 public static function verifyOTP($otp_code, $contact_value, $contact_type = 'email', $delivery_method = 'email') {
160 // Expire old OTPs first
161 self::expireExpiredOTPs();
162
163 $otp = new OsOTPModel();
164 $active_otp = $otp->where([
165 'contact_value' => $contact_value,
166 'contact_type' => $contact_type,
167 'delivery_method' => $delivery_method,
168 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE,
169 'attempts <' => self::$max_verification_attempts
170 ])->set_limit(1)->get_results_as_models();
171
172 if (empty($active_otp)) {
173 return new WP_Error('otp_generation_error', 'Invalid Code');
174 }
175
176 if (wp_check_password($otp_code, $active_otp->otp_hash)) {
177 // Mark this OTP as used
178 $active_otp->update_attributes(
179 [
180 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_USED,
181 'used_at' => OsTimeHelper::now_datetime_in_format( LATEPOINT_DATETIME_DB_FORMAT )
182 ]
183 );
184
185 // Cancel other active OTPs for this contact
186 $other_otps = new OsOTPModel();
187 $other_otps = $other_otps->where(['contact_value' => $contact_value, 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE])->get_results_as_models();
188 if($other_otps){
189 foreach($other_otps as $otp){
190 $otp->update_attributes(['status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED]);
191 }
192 }
193
194 return [
195 'status' => LATEPOINT_STATUS_SUCCESS,
196 'contact_value' => $contact_value
197 ];
198 }
199
200 $active_otp->update_attributes(['attempts' => $active_otp->attempts + 1]);
201
202 return new WP_Error('otp_generation_error', 'Invalid Code');
203 }
204
205 private static function sendOTP(string $otp_code, OsOTPModel $otp) : array {
206
207 $result = [
208 'status' => LATEPOINT_STATUS_ERROR,
209 'message' => __('OTP was not sent.', 'latepoint'),
210 'to' => $otp->contact_value,
211 'delivery_method' => $otp->delivery_method,
212 'contact_type' => $otp->contact_type,
213 'processed_datetime' => '',
214 'extra_data' => [
215 'activity_data' => []
216 ],
217 'errors' => [],
218 ];
219 switch($otp->delivery_method) {
220 case 'email':
221 $subject = __('Your OTP Code', 'latepoint');
222 $content = sprintf(esc_html__('Your OTP code is: %s', 'latepoint'), $otp_code);
223 $send_result = OsNotificationsHelper::send($otp->delivery_method, ['to' => $otp->contact_value, 'subject' => $subject, 'content' => $content]);
224 if($send_result['status'] == LATEPOINT_STATUS_SUCCESS){
225 $result['processed_datetime'] = OsTimeHelper::now_datetime_in_db_format();
226 $result['status'] = LATEPOINT_STATUS_SUCCESS;
227 }else{
228 $result['message'] = __('Failed to send email', 'latepoint');
229 }
230 break;
231 case 'sms':
232 $subject = __('Your OTP Code', 'latepoint');
233 $content = sprintf(esc_html__('Your OTP code is: %s', 'latepoint'), $otp_code);
234 $send_result = OsNotificationsHelper::send($otp->delivery_method, ['to' => $otp->contact_value, 'subject' => $subject, 'content' => $content]);
235 if($send_result['status'] == LATEPOINT_STATUS_SUCCESS){
236 $result['processed_datetime'] = OsTimeHelper::now_datetime_in_db_format();
237 $result['status'] = LATEPOINT_STATUS_SUCCESS;
238 }else{
239 $result['message'] = __('Failed to send SMS', 'latepoint');
240 }
241 break;
242 }
243
244
245 /**
246 * Result of sending an OTP code
247 *
248 * @since 5.2.0
249 * @hook latepoint_notifications_send_otp_code
250 *
251 * @param {array} $result The array of data describing the result of operation
252 * @param {string} $otp_code
253 * @param {OsOTPModel} $otp
254 *
255 * @returns {array} The filtered array of data describing the result of operation
256 */
257 $result = apply_filters('latepoint_notifications_send_otp_code', $result, $otp_code, $otp);
258
259 return $result;
260 }
261
262 public static function valid_contact_types_for_customer() : array{
263 $contact_types = ['email', 'phone'];
264 /**
265 * List of valid contact types for customers
266 *
267 * @since 5.2.0
268 * @hook latepoint_valid_contact_types_for_customer
269 *
270 * @param {array} $contact_types The array of contact types
271 *
272 * @returns {array} The filtered array of contact types
273 */
274 $result = apply_filters('latepoint_valid_contact_types_for_customer', $contact_types);
275
276 return $result;
277 }
278
279 private static function cancelOldOTPs($contact_value) {
280 $old_otps = new OsOTPModel();
281 $old_otps = $old_otps->where(['contact_value' => $contact_value, 'status' => 'active'])->get_results_as_models();
282 if($old_otps){
283 foreach($old_otps as $otp){
284 $otp->update_attributes(['status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED]);
285 }
286 }
287 }
288
289 private static function expireExpiredOTPs() {
290 $otps = new OsOTPModel();
291 $expired_otps = $otps->where([
292 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE,
293 'expires_at <' => OsTimeHelper::now_datetime_utc_in_db_format()
294 ])->get_results_as_models();
295 if($expired_otps){
296 foreach($expired_otps as $otp){
297 $otp->update_attributes(['status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_EXPIRED]);
298 }
299 }
300 }
301
302 private static function isValidCombination($contact_type, $delivery_method) {
303 $valid_combinations = [
304 'email' => ['email'],
305 'phone' => ['sms', 'whatsapp']
306 ];
307 /**
308 * Delivery methods for contact types
309 *
310 * @since 5.2.0
311 * @hook latepoint_otp_delivery_methods_for_contact_types
312 *
313 * @param {array} $methods available delivery methods
314 * @returns {array} The filtered array of available delivery methods
315 */
316 $valid_combinations = apply_filters( 'latepoint_otp_delivery_methods_for_contact_types', $valid_combinations );
317
318 return in_array($delivery_method, $valid_combinations[$contact_type] ?? []);
319 }
320
321 private static function checkRateLimit($contact_value) : bool {
322
323 $otps = new OsOTPModel();
324 $recent_attempts = $otps->where([
325 'contact_value' => $contact_value,
326 'created_at >' => OsTimeHelper::custom_datetime_utc_in_db_format('-1 hour')])->count();
327
328
329 return $recent_attempts < self::$max_generation_attempts_per_hour;
330 }
331
332
333 // Cleanup old records
334 public static function scheduledCleanup() {
335 $otps = new OsOTPModel();
336 $otps->delete_where([
337 'created_at <' => OsTimeHelper::custom_datetime_utc_in_db_format('-30 days'),
338 'status' => [LATEPOINT_CUSTOMER_OTP_CODE_STATUS_USED, LATEPOINT_CUSTOMER_OTP_CODE_STATUS_EXPIRED, LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED]]);
339 }
340
341 public static function is_otp_enabled_for_contact_type( string $contact_type, string $delivery_method ) : bool {
342 $is_enabled = false;
343 if($contact_type == 'email' && $delivery_method == 'email'){
344 $is_enabled = true;
345 }
346
347 /**
348 * Determines if OTP is enabled for a selected contact type and delivery method
349 *
350 * @since 5.2.0
351 * @hook latepoint_is_otp_enabled_for_contact_type
352 *
353 * @param {bool} $is_enabled if otp delivery is enabled for a supplied contact and delivery method
354 * @param {string} $contact_type a contact type for OTP
355 * @param {string} $delivery_method a delivery method for OTP
356 *
357 * @returns {bool} Filtered value of whether OTP is enabled for this delivery method
358 */
359 return apply_filters('latepoint_is_otp_enabled_for_contact_type', $is_enabled, $contact_type, $delivery_method);
360 }
361 }