1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This is the model class for table "backend". |
5
|
|
|
* |
6
|
|
|
* The followings are the available columns in table 'backends': |
7
|
|
|
* @property int $id |
8
|
|
|
* @property string $name |
9
|
|
|
* @property string $hostname |
10
|
|
|
* @property int $port |
11
|
|
|
* @property int $tcp_port |
12
|
|
|
* @property string $username |
13
|
|
|
* @property string $password |
14
|
|
|
* @property string $proxyLocation |
15
|
|
|
* @property int $default |
16
|
|
|
* @property string $macAddress |
17
|
|
|
* @property string $subnetMask |
18
|
|
|
* |
19
|
|
|
* @author Sam Stenvall <[email protected]> |
20
|
|
|
* @copyright Copyright © Sam Stenvall 2014- |
21
|
|
|
* @license https://www.gnu.org/licenses/gpl.html The GNU General Public License v3.0 |
22
|
|
|
* |
23
|
|
|
* @method Backend default() applies the "default" scope |
24
|
|
|
*/ |
25
|
|
|
class Backend extends CActiveRecord |
26
|
|
|
{ |
27
|
|
|
|
28
|
|
|
const DEFAULT_HOSTNAME = 'localhost'; |
29
|
|
|
const DEFAULT_PORT = 8080; |
30
|
|
|
const DEFAULT_TCP_PORT = 9090; |
31
|
|
|
const DEFAULT_USERNAME = 'kodi'; |
32
|
|
|
const DEFAULT_PASSWORD = 'kodi'; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Timeout (in seconds) limit while checking if a backend is connectable |
36
|
|
|
*/ |
37
|
|
|
const SOCKET_TIMEOUT = 5; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Returns the static model of the specified AR class. |
41
|
|
|
* @param string $className active record class name. |
42
|
|
|
* @return Backend the static model class |
43
|
|
|
*/ |
44
|
|
|
public static function model($className = __CLASS__) |
45
|
|
|
{ |
46
|
|
|
return parent::model($className); |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @return string the associated database table name |
51
|
|
|
*/ |
52
|
|
|
public function tableName() |
53
|
|
|
{ |
54
|
|
|
return 'backend'; |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* @return array validation rules for model attributes. |
59
|
|
|
*/ |
60
|
|
|
public function rules() |
61
|
|
|
{ |
62
|
|
|
return array( |
63
|
|
|
array('name, hostname, port, tcp_port, username, password', 'required'), |
64
|
|
|
array('default', 'requireDefaultBackend'), |
65
|
|
|
array('default', 'numerical', 'integerOnly'=>true), |
66
|
|
|
array('port, tcp_port', 'numerical', 'integerOnly'=>true, 'max'=>65535), |
67
|
|
|
array('proxyLocation', 'safe'), |
68
|
|
|
// the following rules depend on each other so they must come in this order |
69
|
|
|
array('hostname', 'checkConnectivity'), |
70
|
|
|
array('hostname', 'checkServerType'), |
71
|
|
|
array('username', 'checkCredentials'), |
72
|
|
|
array('macAddress', 'validateMacAddress'), |
73
|
|
|
array('subnetMask', 'validateSubnetMask'), |
74
|
|
|
); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @return array the scopes for this model |
79
|
|
|
*/ |
80
|
|
|
public function scopes() |
81
|
|
|
{ |
82
|
|
|
return array( |
83
|
|
|
'default'=>array( |
84
|
|
|
'condition'=>'`default` = 1', |
85
|
|
|
) |
86
|
|
|
); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @return array customized attribute labels (name=>label) |
91
|
|
|
*/ |
92
|
|
|
public function attributeLabels() |
93
|
|
|
{ |
94
|
|
|
return array( |
95
|
|
|
'name'=>Yii::t('Backend', 'Backend name'), |
96
|
|
|
'hostname'=>Yii::t('Backend', 'Hostname'), |
97
|
|
|
'port'=>Yii::t('Backend', 'Port'), |
98
|
|
|
'tcp_port'=>Yii::t('Backend', 'TCP port'), |
99
|
|
|
'username'=>Yii::t('Backend', 'Username'), |
100
|
|
|
'password'=>Yii::t('Backend', 'Password'), |
101
|
|
|
'proxyLocation'=>Yii::t('Backend', 'Proxy location'), |
102
|
|
|
'default'=>Yii::t('Backend', 'Set as default'), |
103
|
|
|
'macAddress'=>Yii::t('Backend', 'MAC address'), |
104
|
|
|
'subnetMask'=>Yii::t('Backend', 'Subnet mask'), |
105
|
|
|
); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Checks that there is actually something listening on the specified |
110
|
|
|
* hostname and port. |
111
|
|
|
* @param string $attribute the attribute being validated ("hostname" in |
112
|
|
|
* this case) |
113
|
|
|
*/ |
114
|
|
|
public function checkConnectivity($attribute) |
115
|
|
|
{ |
116
|
|
|
if (!$this->areAttributesValid(array('hostname', 'port'))) |
117
|
|
|
return; |
118
|
|
|
|
119
|
|
|
if (!$this->isConnectable()) |
120
|
|
|
$this->addError($attribute, Yii::t('Backend', 'Unable to connect to {hostname}:{port}, make sure XBMC is running and has its web server enabled', |
121
|
|
|
array('{hostname}'=>$this->hostname, '{port}'=>$this->port))); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Checks that the credentials entered are valid |
126
|
|
|
* @param string $attribute the attribute being validated ("username" in |
127
|
|
|
* this case) |
128
|
|
|
*/ |
129
|
|
|
public function checkCredentials($attribute) |
130
|
|
|
{ |
131
|
|
|
if (!$this->areAttributesValid(array('hostname', 'port', 'username', 'password'))) |
132
|
|
|
return; |
133
|
|
|
|
134
|
|
|
$webserver = new WebServer($this->hostname, $this->port); |
135
|
|
|
|
136
|
|
|
if (!$webserver->checkCredentials($this->username, $this->password)) |
137
|
|
|
$this->addError($attribute, Yii::t('Backend', 'Invalid credentials')); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Checks that the server running on hostname:port is actually XBMC and not |
142
|
|
|
* some other software. We do this by looking at the authentication realm |
143
|
|
|
* string. |
144
|
|
|
* @param string $attribute the attribute being validated ("username" in |
145
|
|
|
* this case) |
146
|
|
|
*/ |
147
|
|
|
public function checkServerType($attribute) |
148
|
|
|
{ |
149
|
|
|
if (!$this->areAttributesValid(array('hostname', 'port'))) |
150
|
|
|
return; |
151
|
|
|
|
152
|
|
|
$webserver = new WebServer($this->hostname, $this->port); |
153
|
|
|
|
154
|
|
|
// Check that the server requires authentication |
155
|
|
|
if (!$webserver->requiresAuthentication()) |
156
|
|
|
$this->addError($attribute, Yii::t('Backend', 'The server does not ask for authentication')); |
157
|
|
|
else |
158
|
|
|
{ |
159
|
|
|
// Check the authentication realm |
160
|
|
|
$realm = $webserver->getAuthenticationRealm(); |
161
|
|
|
|
162
|
|
|
if (strtolower($realm) !== 'xbmc' && strtolower($realm) !== 'kodi') |
163
|
|
|
{ |
164
|
|
|
$message = 'The server at '.$webserver->getHostInfo()." doesn't seem to be an XBMC instance"; |
165
|
|
|
|
166
|
|
|
// Log whatever string the server identified as for debugging purposes |
167
|
|
|
Yii::log($message.' (the server identified as "'.$realm.'")', CLogger::LEVEL_ERROR, 'Backend'); |
168
|
|
|
$this->addError($attribute, $message); |
169
|
|
|
} |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Checks that there is another backend set as default if the default |
175
|
|
|
* checkbox is unchecked for this one |
176
|
|
|
* @param string $attribute the attribute being validated |
177
|
|
|
*/ |
178
|
|
|
public function requireDefaultBackend($attribute) |
179
|
|
|
{ |
180
|
|
|
if (!$this->{$attribute}) |
181
|
|
|
{ |
182
|
|
|
$error = Yii::t('Backend', 'There must be a default backend'); |
183
|
|
|
|
184
|
|
|
// If this backend is currently the default one it must remain so |
185
|
|
|
if (!$this->isNewRecord) |
186
|
|
|
{ |
187
|
|
|
$model = $this->findByPk($this->id); |
188
|
|
|
|
189
|
|
|
if ($model->default) |
190
|
|
|
$this->addError($attribute, $error); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
// If there are no other backends then this must be the default one |
194
|
|
|
if (count(Backend::model()->findAll()) === 0) |
195
|
|
|
$this->addError($attribute, $error); |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Validates the MAC address attribute |
201
|
|
|
* @param string $attribute the attribute being validated |
202
|
|
|
*/ |
203
|
|
|
public function validateMacAddress($attribute) |
204
|
|
|
{ |
205
|
|
|
if (!empty($this->macAddress) && !preg_match('/^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$/i', $this->macAddress)) |
206
|
|
|
$this->addError($attribute, Yii::t('Backend', 'Invalid MAC address')); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Validates the subnet mask attribute |
211
|
|
|
* @param string $attribute the attribute being validated |
212
|
|
|
*/ |
213
|
|
|
public function validateSubnetMask($attribute) |
214
|
|
|
{ |
215
|
|
|
if (!empty($this->subnetMask) && !filter_var($this->subnetMask, FILTER_VALIDATE_IP)) |
216
|
|
|
$this->addError($attribute, Yii::t('Backend', 'Invalid subnet mask')); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Formats the MAC address and sets the default subnet mask before saving |
221
|
|
|
* the model |
222
|
|
|
* @return boolean whether the save should happen or not |
223
|
|
|
*/ |
224
|
|
|
protected function beforeSave() |
225
|
|
|
{ |
226
|
|
|
$this->macAddress = strtolower(str_replace('-', ':', $this->macAddress)); |
227
|
|
|
|
228
|
|
|
if (!empty($this->macAddress) && empty($this->subnetMask)) |
229
|
|
|
$this->subnetMask = '255.255.255.0'; |
230
|
|
|
|
231
|
|
|
return parent::beforeSave(); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Makes sure that no other backend is set as default if this one is |
236
|
|
|
*/ |
237
|
|
|
protected function afterSave() |
238
|
|
|
{ |
239
|
|
|
if ($this->default) |
240
|
|
|
{ |
241
|
|
|
Yii::app()->db->createCommand()->update($this->tableName(), |
242
|
|
|
array('default'=>0), 'id != :id', array(':id'=>$this->id)); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
parent::afterSave(); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Resets some attributes to their default values |
250
|
|
|
*/ |
251
|
|
|
public function setDefaultValues() |
252
|
|
|
{ |
253
|
|
|
$this->hostname = self::DEFAULT_HOSTNAME; |
254
|
|
|
$this->port = self::DEFAULT_PORT; |
255
|
|
|
$this->tcp_port = self::DEFAULT_TCP_PORT; |
256
|
|
|
$this->username = self::DEFAULT_USERNAME; |
257
|
|
|
$this->password = self::DEFAULT_PASSWORD; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* @return boolean whether this backend is connectable |
262
|
|
|
* @param int $port the port to try to connect to. Defaults to null, meaning |
263
|
|
|
* the HTTP port configured for this backend |
264
|
|
|
* @param boolean $logFailure whether unsuccessful attempts should be logged. Defaults |
265
|
|
|
* to true. |
266
|
|
|
*/ |
267
|
|
|
public function isConnectable($port = null, $logFailure = true) |
268
|
|
|
{ |
269
|
|
|
$errno = 0; |
270
|
|
|
$errStr = ''; |
271
|
|
|
|
272
|
|
|
if ($port === null) |
273
|
|
|
$port = $this->port; |
274
|
|
|
|
275
|
|
|
if (@fsockopen(Backend::normalizeAddress($this->hostname), $port, $errno, $errStr, |
276
|
|
|
self::SOCKET_TIMEOUT) === false || $errno !== 0) |
277
|
|
|
{ |
278
|
|
|
if ($logFailure) |
279
|
|
|
Yii::log('Failed to connect to '.$this->hostname.':'.$this->port.'. The exact error was: '.$errStr.' ('.$errno.')', CLogger::LEVEL_ERROR, 'Backend'); |
280
|
|
|
|
281
|
|
|
return false; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
return true; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* @return boolean whether the backend can be contacted over a WebSocket |
289
|
|
|
*/ |
290
|
|
|
public function hasWebSocketConnectivity() |
291
|
|
|
{ |
292
|
|
|
return $this->isConnectable($this->tcp_port, false); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Returns a data provider for this model |
297
|
|
|
* @return \CActiveDataProvider |
298
|
|
|
*/ |
299
|
|
|
public function getDataProvider() |
300
|
|
|
{ |
301
|
|
|
return new CActiveDataProvider(__CLASS__, array( |
302
|
|
|
'pagination'=>false |
303
|
|
|
)); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Optionally mangles the specified address so it can be used properly, e.g. |
308
|
|
|
* by adding braces around IPv6 addresses. |
309
|
|
|
* @param string $address a hostname or IP address |
310
|
|
|
* @return string the normalized address |
311
|
|
|
*/ |
312
|
|
|
public static function normalizeAddress($address) |
313
|
|
|
{ |
314
|
|
|
// If the address is an IPv6 address we need to wrap it in square brackets |
315
|
|
|
if (strpos($address, ':') !== false) |
316
|
|
|
return '[' . $address . ']'; |
317
|
|
|
else |
318
|
|
|
return $address; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* Checks whether any of the specified attributes have errors and returns |
323
|
|
|
* true if they don't |
324
|
|
|
* @param string[] $attributes the attributes to check |
325
|
|
|
* @return boolean whether any of the attributes have errors |
326
|
|
|
*/ |
327
|
|
|
private function areAttributesValid($attributes) |
328
|
|
|
{ |
329
|
|
|
foreach ($attributes as $attribute) |
330
|
|
|
if ($this->hasErrors($attribute)) |
331
|
|
|
return false; |
332
|
|
|
|
333
|
|
|
return true; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
} |
337
|
|
|
|