PluginProbe ʕ •ᴥ•ʔ
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress / 5.2.1
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress v5.2.1
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 1 year ago csv_helper.php 9 months ago customer_helper.php 9 months ago customer_import_helper.php 9 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 1 year 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 9 months ago router_helper.php 1 year ago service_helper.php 1 year ago sessions_helper.php 1 year ago settings_helper.php 9 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 9 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 9 months ago version_specific_updates_helper.php 9 months ago whatsapp_helper.php 1 year ago work_periods_helper.php 1 year ago wp_datetime.php 1 year ago wp_user_helper.php 1 year ago
otp_helper.php
348 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-request-route="'.OsRouterHelper::build_route_name('auth', 'request_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.= OsFormHelper::hidden_field('otp[contact_type]', $contact_type);
117 $message.= OsFormHelper::hidden_field('otp[contact_value]', $contact_value);
118 $message.= OsFormHelper::hidden_field('otp[delivery_method]', $delivery_method);
119 $message.= '</div>';
120 return $message;
121 }
122
123 public static function is_customer_contact_verified(OsCustomerModel $customer, string $contact_value, string $contact_type) : bool{
124 $verified_contact_values = json_decode($customer->get_meta_by_key('verified_contact_values', ''), true);
125 if($verified_contact_values){
126 return in_array($contact_value, $verified_contact_values[$contact_type]);
127 }else{
128 return false;
129 }
130 }
131
132
133 public static function add_verified_contact_for_customer_from_verification_token(OsCustomerModel $customer, string $verification_token) : void {
134 $verification_info = OsOTPHelper::validate_verification_token($verification_token);
135 if($verification_info['valid'] && !empty($verification_info['data']['contact_value'])){
136 self::add_verified_contact_for_customer($customer, $verification_info['data']['contact_value'], $verification_info['data']['contact_type']);
137 }
138 }
139
140 public static function add_verified_contact_for_customer(OsCustomerModel $customer, string $contact_value, string $contact_type){
141 if(!$customer->is_new_record() && !empty($contact_value) && in_array($contact_type, self::valid_contact_types_for_customer())){
142 if(!self::is_customer_contact_verified($customer, $contact_value, $contact_type)){
143 $verified_contact_values = json_decode($customer->get_meta_by_key('verified_contact_values', ''), true);
144 $verified_contact_values[$contact_type][] = $contact_value;
145 $customer->save_meta_by_key('verified_contact_values', wp_json_encode($verified_contact_values));
146 }
147 }
148 }
149
150 public static function verifyOTP($otp_code, $contact_value, $contact_type = 'email', $delivery_method = 'email') {
151 // Expire old OTPs first
152 self::expireExpiredOTPs();
153
154 $otp = new OsOTPModel();
155 $active_otp = $otp->where([
156 'contact_value' => $contact_value,
157 'contact_type' => $contact_type,
158 'delivery_method' => $delivery_method,
159 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE,
160 'attempts <' => self::$max_verification_attempts
161 ])->set_limit(1)->get_results_as_models();
162
163 if (empty($active_otp)) {
164 return new WP_Error('otp_generation_error', 'Invalid Code');
165 }
166
167 if (wp_check_password($otp_code, $active_otp->otp_hash)) {
168 // Mark this OTP as used
169 $active_otp->update_attributes(
170 [
171 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_USED,
172 'used_at' => OsTimeHelper::now_datetime_in_format( LATEPOINT_DATETIME_DB_FORMAT )
173 ]
174 );
175
176 // Cancel other active OTPs for this contact
177 $other_otps = new OsOTPModel();
178 $other_otps = $other_otps->where(['contact_value' => $contact_value, 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE])->get_results_as_models();
179 if($other_otps){
180 foreach($other_otps as $otp){
181 $otp->update_attributes(['status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED]);
182 }
183 }
184
185 return [
186 'status' => LATEPOINT_STATUS_SUCCESS,
187 'contact_value' => $contact_value
188 ];
189 }
190
191 $active_otp->update_attributes(['attempts' => $active_otp->attempts + 1]);
192
193 return new WP_Error('otp_generation_error', 'Invalid Code');
194 }
195
196 private static function sendOTP(string $otp_code, OsOTPModel $otp) : array {
197
198 $result = [
199 'status' => LATEPOINT_STATUS_ERROR,
200 'message' => __('OTP was not sent.', 'latepoint'),
201 'to' => $otp->contact_value,
202 'delivery_method' => $otp->delivery_method,
203 'contact_type' => $otp->contact_type,
204 'processed_datetime' => '',
205 'extra_data' => [
206 'activity_data' => []
207 ],
208 'errors' => [],
209 ];
210 switch($otp->delivery_method) {
211 case 'email':
212 $subject = __('Your OTP Code', 'latepoint');
213 $content = sprintf(esc_html__('Your OTP code is: %s', 'latepoint'), $otp_code);
214 $send_result = OsNotificationsHelper::send($otp->delivery_method, ['to' => $otp->contact_value, 'subject' => $subject, 'content' => $content]);
215 if($send_result['status'] == LATEPOINT_STATUS_SUCCESS){
216 $result['processed_datetime'] = OsTimeHelper::now_datetime_in_db_format();
217 $result['status'] = LATEPOINT_STATUS_SUCCESS;
218 }
219 break;
220 case 'sms':
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 }
228 break;
229 }
230
231
232 /**
233 * Result of sending an OTP code
234 *
235 * @since 5.2.0
236 * @hook latepoint_notifications_send_otp_code
237 *
238 * @param {array} $result The array of data describing the result of operation
239 * @param {string} $otp_code
240 * @param {OsOTPModel} $otp
241 *
242 * @returns {array} The filtered array of data describing the result of operation
243 */
244 $result = apply_filters('latepoint_notifications_send_otp_code', $result, $otp_code, $otp);
245
246 return $result;
247 }
248
249 public static function valid_contact_types_for_customer() : array{
250 $contact_types = ['email', 'phone'];
251 /**
252 * List of valid contact types for customers
253 *
254 * @since 5.2.0
255 * @hook latepoint_valid_contact_types_for_customer
256 *
257 * @param {array} $contact_types The array of contact types
258 *
259 * @returns {array} The filtered array of contact types
260 */
261 $result = apply_filters('latepoint_valid_contact_types_for_customer', $contact_types);
262
263 return $result;
264 }
265
266 private static function cancelOldOTPs($contact_value) {
267 $old_otps = new OsOTPModel();
268 $old_otps = $old_otps->where(['contact_value' => $contact_value, 'status' => 'active'])->get_results_as_models();
269 if($old_otps){
270 foreach($old_otps as $otp){
271 $otp->update_attributes(['status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED]);
272 }
273 }
274 }
275
276 private static function expireExpiredOTPs() {
277 $otps = new OsOTPModel();
278 $expired_otps = $otps->where([
279 'status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_ACTIVE,
280 'expires_at <' => OsTimeHelper::now_datetime_utc_in_db_format()
281 ])->get_results_as_models();
282 if($expired_otps){
283 foreach($expired_otps as $otp){
284 $otp->update_attributes(['status' => LATEPOINT_CUSTOMER_OTP_CODE_STATUS_EXPIRED]);
285 }
286 }
287 }
288
289 private static function isValidCombination($contact_type, $delivery_method) {
290 $valid_combinations = [
291 'email' => ['email'],
292 'phone' => ['sms', 'whatsapp']
293 ];
294 /**
295 * Delivery methods for contact types
296 *
297 * @since 5.2.0
298 * @hook latepoint_otp_delivery_methods_for_contact_types
299 *
300 * @param {array} $methods available delivery methods
301 * @returns {array} The filtered array of available delivery methods
302 */
303 $valid_combinations = apply_filters( 'latepoint_otp_delivery_methods_for_contact_types', $valid_combinations );
304
305 return in_array($delivery_method, $valid_combinations[$contact_type] ?? []);
306 }
307
308 private static function checkRateLimit($contact_value) : bool {
309
310 $otps = new OsOTPModel();
311 $recent_attempts = $otps->where([
312 'contact_value' => $contact_value,
313 'created_at >' => OsTimeHelper::custom_datetime_utc_in_db_format('-1 hour')])->count();
314
315
316 return $recent_attempts < self::$max_generation_attempts_per_hour;
317 }
318
319
320 // Cleanup old records
321 public static function scheduledCleanup() {
322 $otps = new OsOTPModel();
323 $otps->delete_where([
324 'created_at <' => OsTimeHelper::custom_datetime_utc_in_db_format('-30 days'),
325 'status' => [LATEPOINT_CUSTOMER_OTP_CODE_STATUS_USED, LATEPOINT_CUSTOMER_OTP_CODE_STATUS_EXPIRED, LATEPOINT_CUSTOMER_OTP_CODE_STATUS_CANCELLED]]);
326 }
327
328 public static function is_otp_enabled_for_contact_type( string $contact_type, string $delivery_method ) : bool {
329 $is_enabled = false;
330 if($contact_type == 'email' && $delivery_method == 'email'){
331 $is_enabled = true;
332 }
333
334 /**
335 * Determines if OTP is enabled for a selected contact type and delivery method
336 *
337 * @since 5.2.0
338 * @hook latepoint_is_otp_enabled_for_contact_type
339 *
340 * @param {bool} $is_enabled if otp delivery is enabled for a supplied contact and delivery method
341 * @param {string} $contact_type a contact type for OTP
342 * @param {string} $delivery_method a delivery method for OTP
343 *
344 * @returns {bool} Filtered value of whether OTP is enabled for this delivery method
345 */
346 return apply_filters('latepoint_is_otp_enabled_for_contact_type', $is_enabled, $contact_type, $delivery_method);
347 }
348 }