PluginProbe
DoLogin Security / trunk
DoLogin Security vtrunk
5.0.10 4.8.3 trunk 1.0 1.1 1.1.1 1.2 1.2.1 1.2.2 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 1.6 All 64 releases
dologin / src / sms.cls.php

sms.cls.php in DoLogin Security trunk, at src/sms.cls.php

301 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SMS class
4 *
5 * @since 1.3
6 * @package dologin
7 */
8
9 namespace dologin;
10
11 defined( 'WPINC' ) || exit;
12
13 class SMS extends Instance {
14
15 private $_dry_run = false;
16
17 /**
18 * GUI notice when no phone set.
19 */
20 public function gui_notice() {
21 $current_user_phone = $this->current_user_phone();
22 if ( ! $current_user_phone && Conf::val( 'sms' ) && Conf::val( 'sms_force' ) ) {
23 GUI::error( DOLOGIN_LOGO . __( 'You need to setup your Dologin Phone number before enabling this setting to avoid yourself being blocked from next time login.', 'dologin' ) . ' <a href="profile.php">' . __( 'Click here to set your Dologin Security phone number', 'dologin' ) . '</a>' );
24 }
25 }
26
27 /**
28 * Return current usre's phone number
29 *
30 * @since 1.3
31 */
32 public function current_user_phone() {
33 $uid = get_current_user_id();
34 $phone = get_user_meta( $uid, 'phone_number', true );
35 return $phone;
36 }
37
38 /**
39 * Check if is dry run (dry run = before sending sms) or not
40 *
41 * @since 1.6
42 */
43 public static function is_dry_run() {
44 return self::cls()->_dry_run;
45 }
46
47 /**
48 * Verify SMS after u+p authenticated
49 *
50 * @since 1.3
51 *
52 * @param mixed $user WP_User or WP_Error from earlier authenticate filters.
53 * @param string $username Submitted username.
54 * @param string $password Submitted password.
55 * @return mixed
56 */
57 public function authenticate( $user, $username, $password ) {
58 global $wpdb;
59
60 defined( 'debug' ) && debug( 'auth' );
61
62 if ( $this->_dry_run ) {
63 defined( 'debug' ) && debug( 'bypassed due to dryrun' );
64 return $user;
65 }
66
67 if ( empty( $username ) || empty( $password ) ) {
68 defined( 'debug' ) && debug( 'bypassed due to lack of u/p' );
69 return $user;
70 }
71
72 if ( is_wp_error( $user ) ) {
73 defined( 'debug' ) && debug( 'bypassed due to is_wp_error already' );
74 return $user;
75 }
76
77 // If sms is optional and the user doesn't have phone set, bypass.
78 $phone = get_user_meta( $user->ID, 'phone_number', true );
79 if ( ! $phone ) {
80 defined( 'debug' ) && debug( 'no phone number set' );
81 if ( ! Conf::val( 'sms_force' ) ) {
82 defined( 'debug' ) && debug( 'bypassed due to no force_sms check' );
83 return $user;
84 }
85 }
86
87 $error = new \WP_Error();
88
89 // Validate dynamic code.
90 // phpcs:ignore WordPress.Security.NonceVerification.Missing
91 if ( empty( $_POST['dologin-two_factor_code'] ) ) {
92 $error->add( 'dynamic_code_missing', Lang::msg( 'dynamic_code_missing' ) );
93 define( 'DOLOGIN_ERR', true );
94 defined( 'debug' ) && debug( '❌ sms missing' );
95 return $error;
96 }
97
98 $tb_sms = $this->cls( 'Data' )->tb( 'sms' );
99
100 $q = "SELECT id, code FROM $tb_sms WHERE user_id = %d AND used = 0";
101 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery --$tb_sms is a hardcoded internal table name; user_id is prepared.
102 $row = $wpdb->get_row( $wpdb->prepare( $q, array( $user->ID ) ) );
103
104 if ( $row && $row->id ) {
105 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery --$tb_sms is a hardcoded internal table name; id is prepared.
106 $wpdb->query( $wpdb->prepare( "UPDATE $tb_sms SET used = 1 WHERE id = %d", array( $row->id ) ) );
107 }
108
109 // phpcs:ignore WordPress.Security.NonceVerification.Missing
110 $submitted_code = sanitize_text_field( wp_unslash( $_POST['dologin-two_factor_code'] ) );
111 if ( ! $row || ! $row->code || ! hash_equals( (string) $row->code, (string) $submitted_code ) ) {
112 $error->add( 'dynamic_code_wrong', Lang::msg( 'dynamic_code_wrong' ) );
113 define( 'DOLOGIN_ERR', true );
114 defined( 'debug' ) && debug( '❌ sms wrong' );
115 return $error;
116 }
117
118 defined( 'debug' ) && debug( '�
119 auth successfully' );
120
121 return $user;
122 }
123
124 /**
125 * Send test SMS
126 *
127 * @since 1.3
128 */
129 public function test_send() {
130 // phpcs:ignore WordPress.Security.NonceVerification.Missing
131 if ( empty( $_POST['phone'] ) ) {
132 return REST::err( Lang::msg( 'not_phone_set_curr' ) );
133 }
134
135 // Check interval.
136 if ( time() - get_option( 'dologin_test' ) < 60 ) {
137 return REST::err( Lang::msg( 'try_after', 60 ) );
138 }
139
140 update_option( 'dologin_test', time() );
141
142 // phpcs:ignore WordPress.Security.NonceVerification.Missing
143 $phone = sanitize_text_field( wp_unslash( $_POST['phone'] ) );
144
145 // Send.
146 try {
147 $res = $this->_api( $phone, array( 'type' => 'test' ) );
148 } catch ( \Exception $ex ) {
149 return REST::err( $ex->getMessage() );
150 }
151
152 return REST::ok( array( 'info' => 'Sent to ***' . substr( $phone, -4 ) . ' at ' . gmdate( 'm/d/Y H:i:s', time() + get_option( 'gmt_offset' ) * 60 * 60 ) ) );
153 }
154
155 /**
156 * Send SMS
157 *
158 * @since 1.3
159 */
160 public function send() {
161 global $wpdb;
162
163 if ( ! Conf::val( 'sms' ) ) {
164 return REST::ok( array( 'bypassed' => 1 ) );
165 }
166
167 $field_u = 'log';
168 $field_p = 'pwd';
169 // phpcs:ignore WordPress.Security.NonceVerification.Missing
170 if ( isset( $_POST['woocommerce-login-nonce'] ) ) {
171 $field_u = 'username';
172 $field_p = 'password';
173 }
174
175 // phpcs:ignore WordPress.Security.NonceVerification.Missing
176 if ( empty( $_POST[ $field_u ] ) || empty( $_POST[ $field_p ] ) ) {
177 return REST::err( Lang::msg( 'empty_u_p' ) );
178 }
179
180 // Verify u & p first.
181 $this->_dry_run = true;
182 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
183 $user = wp_authenticate( $_POST[ $field_u ], $_POST[ $field_p ] );
184 $this->_dry_run = false;
185 if ( is_wp_error( $user ) ) {
186 return REST::err( $user->get_error_message() );
187 }
188
189 // Search if the user has number set in phone.
190 $phone = get_user_meta( $user->ID, 'phone_number', true );
191
192 if ( ! $phone ) {
193 if ( ! Conf::val( 'sms_force' ) ) {
194 defined( 'debug' ) && debug( 'bypassed due to no phone set' );
195 return REST::ok( array( 'bypassed' => 1 ) );
196 }
197 return REST::err( Lang::msg( 'not_phone_set_user' ) );
198 }
199
200 // Throttle real SMS sending per user to prevent SMS flooding / toll fraud abuse.
201 $throttle_key = 'dologin_sms_' . $user->ID;
202 if ( get_transient( $throttle_key ) ) {
203 return REST::err( Lang::msg( 'try_after', 60 ) );
204 }
205 set_transient( $throttle_key, time(), 60 );
206
207 // Generate dynamic code.
208 $code = s::rrand( 4, 1 );
209 $rid = s::rrand( 2, 1 );
210 $ip_info = ip::geo();
211 $info = sprintf(
212 /* translators: 1: the dynamic login code, 2: a short tag id. */
213 __( 'Dynamic Code:%1$s.(Tag:%2$s) From: ', 'dologin' ),
214 $code,
215 $rid
216 ) . $ip_info['country'] . '-' . $ip_info['city'] . '.';
217 $data = array(
218 'type' => 'login',
219 'lang' => get_locale(),
220 'code' => $code,
221 'tag' => $rid,
222 );
223
224 $tb_sms = $this->cls( 'Data' )->tb( 'sms' );
225
226 // Expire old ones.
227 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery --$tb_sms is a hardcoded internal table name; user_id is prepared.
228 $wpdb->query( $wpdb->prepare( "UPDATE $tb_sms SET used = -1 WHERE user_id = %d AND used = 0", array( $user->ID ) ) );
229
230 // Save to db.
231 $s = array(
232 'user_id' => $user->ID,
233 'sms' => $info,
234 'code' => $code,
235 'used' => 0,
236 'dateline' => time(),
237 );
238 $q = 'INSERT INTO ' . $tb_sms . ' ( ' . implode( ',', array_keys( $s ) ) . ' ) VALUES ( ' . implode( ',', array_fill( 0, count( $s ), '%s' ) ) . ' )';
239 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery --$tb_sms is a hardcoded internal table name; values are prepared.
240 $wpdb->query( $wpdb->prepare( $q, $s ) );
241 $id = $wpdb->insert_id;
242
243 // Send.
244 try {
245 $res = $this->_api( $phone, $data );
246 } catch ( \Exception $ex ) {
247 return REST::err( $ex->getMessage() );
248 }
249
250 // Update log.
251 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery --$tb_sms is a hardcoded internal table name; id is prepared.
252 $wpdb->query( $wpdb->prepare( "UPDATE $tb_sms SET res = %s WHERE id = %d", array( $res, $id ) ) );
253
254 $res_json = json_decode( $res, true );
255
256 // Expected response.
257 if ( ! empty( $res_json['_res'] ) && 'ok' === $res_json['_res'] ) {
258 return REST::ok( array( 'info' => "Tag:$rid. Sent to ***" . substr( $phone, -4 ) . '.' ) );
259 }
260
261 if ( ! empty( $res_json['_msg'] ) ) {
262 return REST::err( $res_json['_msg'] );
263 }
264
265 return REST::err( 'Unknown error' );
266 }
267
268 /**
269 * Call API to send msg
270 *
271 * @since 1.5
272 */
273 private function _api( $phone, $data ) {
274 // Send.
275 $url = 'https://doapi.us/text?format=json';
276 $data = array(
277 'app' => 'dologin',
278 'domain' => home_url(),
279 'ip' => ip::me(),
280 'phone' => $phone,
281 'data' => wp_json_encode( $data ),
282 );
283
284 $res = wp_remote_post(
285 $url,
286 array(
287 'body' => $data,
288 'timeout' => 15,
289 'sslverify' => true,
290 )
291 );
292
293 if ( is_wp_error( $res ) ) {
294 $error_message = $res->get_error_message();
295 throw new \Exception( esc_html( $error_message ) );
296 }
297
298 return $res['body'];
299 }
300 }
301