1+ <?php
2+
3+ namespace PhpMonsters \Otp ;
4+
5+ use Exception ;
6+ use Illuminate \Support \Facades \Cache ;
7+ use Illuminate \Support \Facades \Facade ;
8+ use Illuminate \Support \Str ;
9+ use Illuminate \Foundation \Application ;
10+
11+ class Otp
12+ {
13+ private string $ format = 'numeric ' ;
14+ private string $ customize ;
15+ private int $ length = 6 ;
16+ private string $ separator = '- ' ;
17+ private bool $ sensitive = false ;
18+ private int $ expires = 15 ;
19+ private int $ attempts = 5 ;
20+ private bool $ repeated = true ;
21+ private bool $ disposable = true ;
22+ private string $ prefix = 'OTPPX_ ' ;
23+ private $ data ;
24+ private bool $ skip = false ;
25+ private bool $ demo = false ;
26+ private array $ demo_passwords = ['1234 ' , '123456 ' , '12345678 ' ];
27+
28+ public function __construct ()
29+ {
30+ foreach (['format ' , 'customize ' , 'length ' , 'separator ' , 'sensitive ' , 'expires ' , 'attempts ' , 'repeated ' , 'disposable ' , 'prefix ' , 'data ' , 'demo ' , 'demo_passwords ' ] as $ value ) {
31+ if (!empty (config ('otp. ' . $ value ))) $ this ->{$ value } = config ('otp. ' . $ value );
32+ }
33+ }
34+
35+ public function __call (string $ method , $ params )
36+ {
37+ if (!str_starts_with ($ method , 'set ' )) {
38+ return ;
39+ }
40+
41+ $ property = Str::camel (substr ($ method , 3 ));
42+ if (!property_exists ($ this , $ property )) {
43+ return ;
44+ }
45+
46+ $ this ->{$ property } = $ params [0 ] ?? null ;
47+ if ($ property == 'customize ' ) {
48+ $ this ->format = 'customize ' ;
49+ }
50+ return $ this ;
51+ }
52+
53+ /**
54+ * @throws Exception
55+ */
56+ public function generate (string $ identifier = null , array $ options = []): string
57+ {
58+ if (!empty ($ options )) foreach (['format ' , 'customize ' , 'length ' , 'separator ' , 'sensitive ' , 'expires ' , 'repeated ' , 'prefix ' , 'data ' ] as $ value ) {
59+ if (isset ($ options [$ value ])) $ this ->{$ value } = $ options [$ value ];
60+ }
61+
62+ if ($ identifier === null ) $ identifier = session ()->getId ();
63+ $ array = $ this ->repeated ? $ this ->readData ($ identifier , []) : [];
64+ $ password = $ this ->generateNewPassword ();
65+ if (!$ this ->sensitive ) $ password = strtoupper ($ password );
66+ $ array [md5 ($ password )] = [
67+ 'expires ' => time () + $ this ->expires * 60 ,
68+ 'data ' => $ this ->data
69+ ];
70+ $ this ->writeData ($ identifier , $ array );
71+ return $ password ;
72+ }
73+
74+ public function validate (string $ identifier = null , string $ password = null , array $ options = []): object
75+ {
76+ if (!empty ($ options )) foreach (['attempts ' , 'sensitive ' , 'disposable ' , 'skip ' ] as $ value ) {
77+ if (isset ($ options [$ value ])) $ this ->{$ value } = $ options [$ value ];
78+ }
79+
80+ if ($ password === null ) {
81+ if ($ identifier === null ) {
82+ throw new Exception ("Validate parameter can not be null " );
83+ }
84+ $ password = $ identifier ;
85+ $ identifier = null ;
86+ }
87+
88+ if ($ this ->demo && in_array ($ password , $ this ->demo_passwords )) {
89+ return (object )[
90+ 'status ' => true ,
91+ 'demo ' => true
92+ ];
93+ }
94+
95+ if ($ identifier === null ) $ identifier = session ()->getId ();
96+ $ attempt = $ this ->readData ('_attempt_ ' . $ identifier , 0 );
97+ if ($ attempt >= $ this ->attempts ) {
98+ return (object )[
99+ 'status ' => false ,
100+ 'error ' => 'max_attempt ' ,
101+ ];
102+ }
103+
104+ $ codes = $ this ->readData ($ identifier , []);
105+ if (!$ this ->sensitive ) $ password = strtoupper ($ password );
106+
107+ if (!isset ($ codes [md5 ($ password )])) {
108+ $ this ->writeData ('_attempt_ ' . $ identifier , $ attempt + 1 );
109+ return (object )[
110+ 'status ' => false ,
111+ 'error ' => 'invalid ' ,
112+ ];
113+ }
114+
115+ if (time () > $ codes [md5 ($ password )]['expires ' ]) {
116+ $ this ->writeData ('_attempt_ ' . $ identifier , $ attempt + 1 );
117+ return (object )[
118+ 'status ' => false ,
119+ 'error ' => 'expired ' ,
120+ ];
121+ }
122+
123+ $ response = [
124+ 'status ' => true ,
125+ ];
126+
127+ if (!empty ($ codes [md5 ($ password )]['data ' ])) {
128+ $ response ['data ' ] = $ codes [md5 ($ password )]['data ' ];
129+ }
130+
131+ if (!$ this ->skip ) $ this ->forget ($ identifier , !$ this ->disposable ? $ password : null );
132+ $ this ->resetAttempt ($ identifier );
133+
134+ return (object )$ response ;
135+ }
136+
137+ public function forget (string $ identifier = null , string $ password = null ): bool
138+ {
139+ if ($ identifier === null ) $ identifier = session ()->getId ();
140+ if ($ password !== null ) {
141+ $ codes = $ this ->readData ($ identifier , []);
142+ if (!isset ($ codes [md5 ($ password )])) {
143+ return false ;
144+ }
145+ unset($ codes [md5 ($ password )]);
146+ $ this ->writeData ($ identifier , $ codes );
147+ return true ;
148+ }
149+
150+ $ this ->deleteData ($ identifier );
151+ return true ;
152+ }
153+
154+ public function resetAttempt (string $ identifier = null ): bool
155+ {
156+ if ($ identifier === null ) $ identifier = session ()->getId ();
157+ $ this ->deleteData ('_attempt_ ' . $ identifier );
158+ return true ;
159+ }
160+
161+ /**
162+ * @throws Exception
163+ */
164+ private function generateNewPassword (): string
165+ {
166+ try {
167+ $ formats = [
168+ 'string ' => $ this ->sensitive ? '23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ ' : '23456789ABCDEFGHJKLMNPQRSTUVWXYZ ' ,
169+ 'numeric ' => '0123456789 ' ,
170+ 'numeric-no-zero ' => '123456789 ' ,
171+ 'customize ' => $ this ->customize
172+ ];
173+
174+ $ lengths = is_array ($ this ->length ) ? $ this ->length : [$ this ->length ];
175+
176+ $ password = [];
177+ foreach ($ lengths as $ length ) {
178+ $ password [] = substr (str_shuffle (str_repeat ($ x = $ formats [$ this ->format ], ceil ($ length / strlen ($ x )))), 1 , $ length );
179+ }
180+
181+ return implode ($ this ->separator , $ password );
182+ } catch (Exception ) {
183+ throw new Exception ("Fail to generate password, please check the format is correct. " );
184+ }
185+ }
186+
187+ private function writeData (string $ key , mixed $ value ): void
188+ {
189+ Cache::put ($ this ->prefix . $ key , $ value , $ this ->expires * 60 * 3 );
190+ }
191+
192+ private function readData (string $ key , mixed $ default = null ): mixed
193+ {
194+ return Cache::get ($ this ->prefix . $ key , $ default );
195+ }
196+
197+ private function deleteData (string $ key ): void
198+ {
199+ Cache::forget ($ this ->prefix . $ key );
200+ }
201+ }
0 commit comments