Passed
Push — master ( d021a5...a13070 )
by Sam
03:47 queued 12s
created

Backend::validateMacAddress()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 2
c 1
b 0
f 1
nc 2
nop 1
dl 0
loc 4
rs 10
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 &copy; 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