|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/** |
|
4
|
|
|
* _ __ __ _____ _____ ___ ____ _____ |
|
5
|
|
|
* | | / // // ___//_ _// || __||_ _| |
|
6
|
|
|
* | |/ // /(__ ) / / / /| || | | | |
|
7
|
|
|
* |___//_//____/ /_/ /_/ |_||_| |_| |
|
8
|
|
|
* @link https://vistart.me/ |
|
9
|
|
|
* @copyright Copyright (c) 2016 - 2017 vistart |
|
10
|
|
|
* @license https://vistart.me/license/ |
|
11
|
|
|
*/ |
|
12
|
|
|
|
|
13
|
|
|
namespace rhosocial\user\models\log; |
|
14
|
|
|
|
|
15
|
|
|
use rhosocial\base\models\models\BaseBlameableModel; |
|
16
|
|
|
use rhosocial\user\User; |
|
17
|
|
|
use Yii; |
|
18
|
|
|
use yii\base\ModelEvent; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* Login log. |
|
22
|
|
|
* |
|
23
|
|
|
* If you're using MySQL, we recommend that you create a data table using the following statement: |
|
24
|
|
|
```SQL |
|
25
|
|
|
CREATE TABLE `log_login` ( |
|
26
|
|
|
`guid` varbinary(16) NOT NULL, |
|
27
|
|
|
`id` varchar(4) COLLATE utf8_unicode_ci NOT NULL, |
|
28
|
|
|
`user_guid` varbinary(16) NOT NULL, |
|
29
|
|
|
`ip` varbinary(16) NOT NULL, |
|
30
|
|
|
`ip_type` smallint(6) NOT NULL DEFAULT '4', |
|
31
|
|
|
`created_at` datetime NOT NULL DEFAULT '1970-01-01 00:00:00', |
|
32
|
|
|
`status` int(11) NOT NULL DEFAULT '0', |
|
33
|
|
|
`device` int(11) NOT NULL DEFAULT '0', |
|
34
|
|
|
PRIMARY KEY (`guid`), |
|
35
|
|
|
UNIQUE KEY `login_log_id_unique` (`guid`,`id`), |
|
36
|
|
|
KEY `login_log_creator_fk` (`user_guid`), |
|
37
|
|
|
CONSTRAINT `login_log_creator_fk` FOREIGN KEY (`user_guid`) REFERENCES `user` (`guid`) ON DELETE CASCADE ON UPDATE CASCADE |
|
38
|
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |
|
39
|
|
|
``` |
|
40
|
|
|
* |
|
41
|
|
|
* @property integer $status Login status. |
|
42
|
|
|
* @property integer $device Login device. |
|
43
|
|
|
* |
|
44
|
|
|
* @version 1.0 |
|
45
|
|
|
* @author vistart <[email protected]> |
|
46
|
|
|
*/ |
|
47
|
|
|
class Login extends BaseBlameableModel |
|
48
|
|
|
{ |
|
49
|
|
|
public $contentAttribute = false; |
|
50
|
|
|
public $updatedAtAttribute = false; |
|
51
|
|
|
public $updatedByAttribute = false; |
|
52
|
|
|
|
|
53
|
|
|
const LIMIT_NO_LIMIT = 0x0; |
|
54
|
|
|
const LIMIT_MAX = 0x1; |
|
55
|
|
|
const LIMIT_DURATION = 0x2; |
|
56
|
|
|
|
|
57
|
|
|
public $limitType = 0x3; |
|
58
|
|
|
public $limitMax = 100; |
|
59
|
|
|
public $limitDuration = 90 * 86400; |
|
60
|
|
|
|
|
61
|
|
|
/** |
|
62
|
|
|
* |
|
63
|
|
|
*/ |
|
64
|
|
|
public function init() |
|
65
|
|
|
{ |
|
66
|
|
|
if (($this->limitType & static::LIMIT_MAX) && ($this->limitMax < 2 || !is_int($this->limitMax))) { // at least 2 records. |
|
67
|
|
|
$this->limitMax = 100; // 100 records. |
|
68
|
|
|
} |
|
69
|
|
|
if (($this->limitType & static::LIMIT_DURATION) && ($this->limitDuration < 86400 || !is_int($this->limitDuration))) { // at least one day. |
|
70
|
|
|
$this->limitDuration = 90 * 86400; // 90 Days. |
|
71
|
|
|
} |
|
72
|
|
|
if ($this->limitType > 0) { |
|
73
|
|
|
$this->on(static::EVENT_AFTER_INSERT, [$this, 'onDeleteExtraRecords']); |
|
74
|
|
|
} |
|
75
|
|
|
parent::init(); |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
/** |
|
79
|
|
|
* |
|
80
|
|
|
* @param ModelEvent $event |
|
81
|
|
|
* @return integer |
|
82
|
|
|
*/ |
|
83
|
|
|
public function onDeleteExtraRecords($event) |
|
84
|
|
|
{ |
|
85
|
|
|
$sender = $event->sender; |
|
86
|
|
|
/* @var $sender static */ |
|
87
|
|
|
Yii::info('Login Log limit type:' . $sender->limitType, __METHOD__); |
|
88
|
|
|
$result = 0; |
|
89
|
|
|
if ($sender->limitType & static::LIMIT_MAX) { |
|
90
|
|
|
$result += $sender->deleteExtraRecords(); |
|
91
|
|
|
} |
|
92
|
|
|
if ($sender->limitType & static::LIMIT_DURATION) { |
|
93
|
|
|
$result += $sender->deleteExpiredRecords(); |
|
94
|
|
|
} |
|
95
|
|
|
return $result; |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
/** |
|
99
|
|
|
* Delete extra records. |
|
100
|
|
|
* @return integer The total of rows deleted. |
|
101
|
|
|
*/ |
|
102
|
|
|
protected function deleteExtraRecords() |
|
103
|
|
|
{ |
|
104
|
|
|
try { |
|
105
|
|
|
$limit = (int)($this->limitMax); |
|
106
|
|
|
} catch (\Exception $ex) { |
|
|
|
|
|
|
107
|
|
|
Yii::error($ex->getMessage(), __METHOD__); |
|
108
|
|
|
return 0; |
|
109
|
|
|
} |
|
110
|
|
|
$host = $this->host; |
|
111
|
|
|
/* @var $host \rhosocial\user\User */ |
|
112
|
|
|
$count = static::find()->createdBy($host)->count(); |
|
113
|
|
|
Yii::info($host->getReadableGUID() . " has $count login logs.", __METHOD__); |
|
114
|
|
|
if ($count > $limit) { |
|
115
|
|
|
foreach (static::find()->createdBy($host)->orderByCreatedAt()->limit($count - $limit)->all() as $login) { |
|
116
|
|
|
/* @var $login static */ |
|
117
|
|
|
$result = $login->delete(); |
|
118
|
|
|
if (YII_ENV_DEV) { |
|
119
|
|
|
Yii::info($host->getReadableGUID() . ": ($result) login record created at (" . $login->getCreatedAt() . ") was just deleted.", __METHOD__); |
|
120
|
|
|
} |
|
121
|
|
|
} |
|
122
|
|
|
} |
|
123
|
|
|
return $count - $limit; |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
/** |
|
127
|
|
|
* Delete expired records. |
|
128
|
|
|
* @return integer The total of rows deleted. |
|
129
|
|
|
*/ |
|
130
|
|
|
protected function deleteExpiredRecords() |
|
131
|
|
|
{ |
|
132
|
|
|
try { |
|
133
|
|
|
$limit = (int)($this->limitDuration); |
|
134
|
|
|
} catch (\Exception $ex) { |
|
|
|
|
|
|
135
|
|
|
Yii::error($ex->getMessage(), __METHOD__); |
|
136
|
|
|
return 0; |
|
137
|
|
|
} |
|
138
|
|
|
$count = 0; |
|
139
|
|
|
$host = $this->host; |
|
140
|
|
|
/* @var $host \rhosocial\user\User */ |
|
141
|
|
|
foreach (static::find() |
|
142
|
|
|
->createdBy($host) |
|
143
|
|
|
->andWhere(['<=', $this->createdAtAttribute, $this->offsetDatetime($this->currentUtcDatetime(), -$limit)]) |
|
144
|
|
|
->all() as $login) { |
|
145
|
|
|
/* @var $login static */ |
|
146
|
|
|
$result = $login->delete(); |
|
147
|
|
|
$count += $result; |
|
148
|
|
|
if (YII_ENV_DEV) { |
|
149
|
|
|
Yii::info($host->getReadableGUID() . ": ($result) login record created at (" . $login->getCreatedAt() . ") was just deleted.", __METHOD__); |
|
150
|
|
|
} |
|
151
|
|
|
} |
|
152
|
|
|
return $count; |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
const STATUS_NORMAL = 0x000; |
|
156
|
|
|
const STATUS_ABNORMAL_UNUSUAL_LOCATION = 0x001; |
|
157
|
|
|
const STATUS_ABNORMAL_TOO_MANY_WRONG_PASSWORD_ATTEMPTS = 0x002; |
|
158
|
|
|
const STATUS_ABNORMAL_UNUSUAL_DEVICE = 0x003; |
|
159
|
|
|
|
|
160
|
|
|
public static $statuses = [ |
|
161
|
|
|
self::STATUS_NORMAL => 'Normal', |
|
162
|
|
|
self::STATUS_ABNORMAL_UNUSUAL_LOCATION => 'Abnormal (Unusual location)', |
|
163
|
|
|
self::STATUS_ABNORMAL_TOO_MANY_WRONG_PASSWORD_ATTEMPTS => 'Abnormal (Too many wrong password attempts)', |
|
164
|
|
|
self::STATUS_ABNORMAL_UNUSUAL_DEVICE => 'Abnormal (Unusual device)', |
|
165
|
|
|
]; |
|
166
|
|
|
|
|
167
|
|
|
const DEVICE_UNKNOWN = 0x000; |
|
168
|
|
|
const DEVICE_PC_NO_CLASSIFICATION = 0x010; |
|
169
|
|
|
const DEVICE_PC_WINDOWS_BROWSER = 0x011; |
|
170
|
|
|
const DEVICE_PC_LINUX_BROWSER = 0x012; |
|
171
|
|
|
const DEVICE_PC_OSX_BROWSER = 0x013; |
|
172
|
|
|
|
|
173
|
|
|
const DEVICE_MOBILE_NO_CLASSICATION = 0x020; |
|
174
|
|
|
const DEVICE_MOBILE_ANDROID_BROWSER = 0x021; |
|
175
|
|
|
const DEVICE_MOBILE_WINDOWSPHONE_BROWSER = 0x022; |
|
176
|
|
|
const DEVICE_MOBILE_IOS_BROWSER = 0x023; |
|
177
|
|
|
|
|
178
|
|
|
const DEVICE_PC_WINDOWS_APPLICATION = 0x031; |
|
179
|
|
|
const DEVICE_PC_LINUX_APPLICATION = 0x032; |
|
180
|
|
|
const DEVICE_PC_OSX_APPLICATION = 0x033; |
|
181
|
|
|
|
|
182
|
|
|
const DEVICE_MOBILE_ANDROID_APPLICATION = 0x041; |
|
183
|
|
|
const DEVICE_MOBILE_WINDOWSPHONE_APPLICATION = 0x042; |
|
184
|
|
|
const DEVICE_MOBILE_IOS_APPLICATION = 0x043; |
|
185
|
|
|
|
|
186
|
|
|
const DEVICE_3PA_NO_CLASSIFICATION = 0x050; |
|
187
|
|
|
const DEVICE_3PA_PC = 0x051; |
|
188
|
|
|
const DEVICE_3PA_MOBILE = 0x052; |
|
189
|
|
|
const DEVICE_3PA_BROWSER = 0x053; |
|
190
|
|
|
|
|
191
|
|
|
public static $devices = [ |
|
192
|
|
|
self::DEVICE_UNKNOWN => 'Unknown', |
|
193
|
|
|
self::DEVICE_PC_NO_CLASSIFICATION => 'PC (No classification)', |
|
194
|
|
|
self::DEVICE_PC_WINDOWS_BROWSER => 'PC (Windows, Browser)', |
|
195
|
|
|
self::DEVICE_PC_LINUX_BROWSER => 'PC (Linux, Browser)', |
|
196
|
|
|
self::DEVICE_PC_OSX_BROWSER => 'PC (OS X, Browser)', |
|
197
|
|
|
self::DEVICE_MOBILE_NO_CLASSICATION => 'Mobile (No classification)', |
|
198
|
|
|
self::DEVICE_MOBILE_ANDROID_BROWSER => 'Mobile (Android, Browser)', |
|
199
|
|
|
self::DEVICE_MOBILE_WINDOWSPHONE_BROWSER => 'Mobile (Windows Phone, Browser)', |
|
200
|
|
|
self::DEVICE_MOBILE_IOS_BROWSER => 'Mobile (iOS, Browser)', |
|
201
|
|
|
self::DEVICE_PC_WINDOWS_BROWSER => 'PC (Windows, Application)', |
|
202
|
|
|
self::DEVICE_PC_LINUX_APPLICATION => 'PC (Linux, Application)', |
|
203
|
|
|
self::DEVICE_PC_OSX_APPLICATION => 'PC (OS X, Application)', |
|
204
|
|
|
self::DEVICE_MOBILE_ANDROID_APPLICATION => 'Mobile (Android, Application)', |
|
205
|
|
|
self::DEVICE_MOBILE_WINDOWSPHONE_APPLICATION => 'Mobile (Windows Phone, Application)', |
|
206
|
|
|
self::DEVICE_MOBILE_IOS_APPLICATION => 'Mobile (iOS, Application)', |
|
207
|
|
|
self::DEVICE_3PA_NO_CLASSIFICATION => 'Third party authorization (No classification)', |
|
208
|
|
|
self::DEVICE_3PA_PC => 'Third party authorization (PC)', |
|
209
|
|
|
self::DEVICE_3PA_MOBILE => 'Third party authorization (Mobile)', |
|
210
|
|
|
self::DEVICE_3PA_BROWSER => 'Third party authorization (Browser)', |
|
211
|
|
|
]; |
|
212
|
|
|
|
|
213
|
|
|
/** |
|
214
|
|
|
* @return array |
|
215
|
|
|
*/ |
|
216
|
|
|
public function getLoginRules() |
|
217
|
|
|
{ |
|
218
|
|
|
return [ |
|
219
|
|
|
['status', 'in', 'range' => array_keys(static::$statuses)], |
|
220
|
|
|
['status', 'default', 'value' => self::STATUS_NORMAL], |
|
221
|
|
|
['device', 'in', 'range' => array_keys(static::$devices)], |
|
222
|
|
|
['device', 'default', 'value' => self::DEVICE_UNKNOWN], |
|
223
|
|
|
]; |
|
224
|
|
|
} |
|
225
|
|
|
|
|
226
|
|
|
/** |
|
227
|
|
|
* @return array |
|
228
|
|
|
*/ |
|
229
|
|
|
public function rules() |
|
230
|
|
|
{ |
|
231
|
|
|
return array_merge($this->getLoginRules(), parent::rules()); |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
/** |
|
235
|
|
|
* @return mixed|null |
|
236
|
|
|
*/ |
|
237
|
|
|
public function getStatusDesc() |
|
238
|
|
|
{ |
|
239
|
|
|
if (array_key_exists($this->status, static::$statuses)) { |
|
240
|
|
|
return static::$statuses[$this->status]; |
|
241
|
|
|
} |
|
242
|
|
|
return null; |
|
243
|
|
|
} |
|
244
|
|
|
|
|
245
|
|
|
/** |
|
246
|
|
|
* @return mixed|null |
|
247
|
|
|
*/ |
|
248
|
|
|
public function getDeviceDesc() |
|
249
|
|
|
{ |
|
250
|
|
|
if (array_key_exists($this->device, static::$devices)) { |
|
251
|
|
|
return static::$devices[$this->device]; |
|
252
|
|
|
} |
|
253
|
|
|
return null; |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
/** |
|
257
|
|
|
* @return string |
|
258
|
|
|
*/ |
|
259
|
|
|
public static function tableName() |
|
260
|
|
|
{ |
|
261
|
|
|
return '{{%log_login}}'; |
|
262
|
|
|
} |
|
263
|
|
|
|
|
264
|
|
|
const GET_ALL_LATESTS = 'all'; |
|
265
|
|
|
|
|
266
|
|
|
/** |
|
267
|
|
|
* Get latest ones. |
|
268
|
|
|
* @param User $user |
|
269
|
|
|
* @param integer $limit |
|
270
|
|
|
* @return static[] |
|
271
|
|
|
*/ |
|
272
|
|
|
public static function getLatests(User $user, $limit = 1) |
|
273
|
|
|
{ |
|
274
|
|
|
$query = static::find()->createdBy($user)->orderByCreatedAt(SORT_DESC); |
|
275
|
|
|
if ($limit == self::GET_ALL_LATESTS || !is_int($limit) || $limit < 1) { |
|
276
|
|
|
return $query->all(); |
|
277
|
|
|
} |
|
278
|
|
|
return $query->limit($limit)->all(); |
|
279
|
|
|
} |
|
280
|
|
|
} |
|
281
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return,dieorexitstatements that have been added for debug purposes.In the above example, the last
return falsewill never be executed, because a return statement has already been met in every possible execution path.