Completed
Push — master ( cb4e34...0670fd )
by Rudie
03:57
created

Client::createTrendInputConversion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 4
rs 10
1
<?php
2
3
namespace rdx\fuelly;
4
5
use InvalidArgumentException;
6
use rdx\fuelly\FuelUp;
7
use rdx\fuelly\UnitConversion;
8
use rdx\fuelly\Vehicle;
9
use rdx\fuelly\WebAuth;
10
use rdx\http\HTTP;
11
12
class Client {
13
14
	public $base = 'http://www.fuelly.com/';
15
	public $loginBase = 'https://m.fuelly.com/';
16
17
	public $dateFormat = 'd/m/Y';
18
	public $timeFormat = 'g:i a';
19
20
	public $auth; // rdx\fuelly\WebAuth
21
	public $input; // rdx\fuelly\InputConversion
22
	public $username = '';
23
	public $vehicles = array();
24
25
	public $log = array();
26
27
	/**
28
	 * Dependency constructor
29
	 */
30
	public function __construct( WebAuth $auth, InputConversion $input ) {
31
		$this->auth = $auth;
32
		$this->input = $input;
33
	}
34
35
	/**
36
	 *
37
	 */
38
	public function createTrendInputConversion() {
39
		// Trend is always in real numbers, and only its natives are reliable so use those
40
		return new InputConversion('ml', 'usg', $this->input->mileage, ',', '.');
41
	}
42
43
	/**
44
	 *
45
	 */
46
	public function getFuelUp( $id ) {
47
		$fuelup = compact('id');
48
49
		$response = $this->_get('fuelups/' . $id . '/edit');
50
51
		preg_match_all('#<input[\s\S]+?name="([^"]+)"[\s\S]+?>#', $response->body, $matches, PREG_SET_ORDER);
52
		foreach ($matches as $match) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
53
			if ( in_array($match[1], array('_token', 'miles_last_fuelup', 'price_per_unit', 'amount', 'fuelup_date')) ) {
54
				preg_match('#value="([^"]+)"#', $match[0], $match2);
55
				$fuelup[ $match[1] ] = $match2[1];
56
			}
57
		}
58
59
		preg_match('#<textarea[^>]+name="note"[^>]*>([^>]+)</textarea>#', $response->body, $match);
60
		$fuelup['note'] = trim(@$match[1]);
61
62
		return $fuelup;
63
	}
64
65
	/**
66
	 *
67
	 */
68
	public function updateFuelUp( $id, $data ) {
69
		$data = $this->_validateFuelUpData($data);
70
		unset($data['id']);
71
72
		if ( !isset($data['_token']) ) {
73
			$response = $this->_get('fuelups/' . $id . '/edit');
74
			$token = $this->extractFormToken($response->body);
0 ignored issues
show
Unused Code introduced by
$token is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
75
		}
76
77
		$data['_method'] = 'PUT';
78
		$response = $this->_post('fuelups/' . $id, array(
79
			'data' => $data,
80
		));
81
		return $response->code == 302;
82
	}
83
84
	/**
85
	 *
86
	 */
87
	public function _validateFuelUpData( $data ) {
88
		$data += array(
89
			'errorlevel' => 2,
90
			'price_per_unit' => '',
91
			'cost' => '',
92
			'city_pct' => '0',
93
			'fueltype_id' => '',
94
			'fuelup_date' => date($this->dateFormat),
95
			'time' => date($this->timeFormat),
96
			'paymenttype_id' => '',
97
			'fuelbrand' => '',
98
			'tirepsi' => '',
99
			'note' => '',
100
		);
101
102
		$required = array('usercar_id', 'miles_last_fuelup', 'amount');
103
		$missing = array_diff($required, array_keys(array_filter($data)));
104
		if ( $missing ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $missing of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
105
			throw new InvalidArgumentException('Missing params: ' . implode(', ', $missing));
106
		}
107
108
		return $data;
109
	}
110
111
	/**
112
	 *
113
	 */
114
	public function getFuelUpsWithIds( Vehicle $vehicle, $limit = 15 ) {
115
		$query = http_build_query(array(
116
			'iDisplayStart' => 0,
117
			'iDisplayLength' => $limit,
118
			'sSortDir_0' => 'desc',
119
			'usercar_id' => $vehicle->id,
120
		));
121
		$response = $this->_get('ajax/fuelup-log?' . $query);
122
		if ( $response->code == 200 ) {
123
			if ( $response->response ) {
124
				$fuelups = array();
125
				foreach ( $response->response['aaData'] as $fuelup ) {
126
					if ( preg_match('#fuelups/(\d+)/edit#', $fuelup[0], $match) ) {
127
						$fuelup = array(
128
							'id' => $match[1],
129
							'usercar_id' => $vehicle->id,
130
							'fuelup_date' => $fuelup[2][0],
131
							'miles_last_fuelup' => $fuelup[3][0],
132
							'amount' => $fuelup[4][0],
133
						);
134
135
						$fuelups[ $fuelup['id'] ] = FuelUp::createFromDetail($vehicle, $fuelup);
136
					}
137
				}
138
139
				// Sort by date DESC
140
				uasort($fuelups, array(FuelUp::class, 'dateCmp'));
141
142
				return $fuelups;
143
			}
144
		}
145
146
		return array();
147
	}
148
149
	/**
150
	 *
151
	 */
152
	public function getAllFuelups( Vehicle $vehicle ) {
153
		$response = $this->_get('car/make/model/2001/username/' . $vehicle->id . '/export');
154
		if ( $token = $this->extractFormToken($response->body) ) {
155
			$response = $this->_post('exportfuelups', array(
156
				'data' => array(
157
					'_token' => $token,
158
					'usercar_id' => $vehicle->id,
159
				),
160
			));
161
			if ( $response->code == 200 ) {
162
				$lines = array_filter(preg_split('#[\r\n]+#', $response->body));
163
				$header = $rows = array();
164
				foreach ($lines as $line) {
165
					$row = array_map('trim', str_getcsv($line));
166
					if ( !$header ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $header of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
167
						$header = $row;
168
					}
169
					else {
170
						$rows[] = array_combine($header, $row);
171
					}
172
				}
173
174
				return $rows;
175
			}
176
		}
177
	}
178
179
	/**
180
	 *
181
	 */
182
	public function addFuelUp( $data ) {
183
		$data = $this->_validateFuelUpData($data);
184
185
		// GET /fuelups/create
186
		$response = $this->_get('fuelups/create');
187
188
		if ( $token = $this->extractFormToken($response->body) ) {
189
			$data['_token'] = $token;
190
191
			// POST /fuelups/create
192
			$response = $this->_post('fuelups', array(
193
				'data' => $data,
194
			));
195
			if ( $response->code == 302 ) {
196
				$response = $this->_get($response->headers['location'][0]);
197
198
				// Take new fuelup ID from response and add it
199
				if ( $response->code == 200 ) {
200
					$regex = '#' . preg_quote($this->base, '#') . 'fuelups/(\d+)/edit#';
201
					if ( preg_match($regex, $response->body, $match) ) {
202
						$response->fuelup_id = $match[1];
203
					}
204
				}
205
206
				return $response;
207
			}
208
		}
209
	}
210
211
	/**
212
	 *
213
	 */
214
	public function getVehicle( $id ) {
215
		$vehicles = $this->getVehicles();
216
		return @$vehicles[$id];
217
	}
218
219
	/**
220
	 *
221
	 */
222
	public function getVehicles() {
223
		// Must exist, because session must be valid, so we did a `GET /dashboard`
224
		return $this->vehicles;
225
	}
226
227
	/**
228
	 *
229
	 */
230
	protected function extractVehicles( $html ) {
231
		$regex = '#<ul class="dashboard-vehicle" data-clickable="([^"]+)">[\w\W]+?</ul>#';
232
		$vehicles = array();
233
		if ( preg_match_all($regex, $html, $matches) ) {
234
			foreach ( $matches[0] as $i => $html ) {
235
				$url = $matches[1][$i];
236
237
				preg_match('#/(\d+)$#', $url, $match);
238
				$id = $match[1];
239
240
				preg_match('#<h3[^>]*>(.+?)</h3>#', $html, $match);
241
				$name = htmlspecialchars_decode($match[1]);
242
243
				preg_match("#:\s*url\('/([^']+)'\)#", $html, $match);
244
				$image = $this->base . $match[1];
245
246
				preg_match("#data-trend='([^']+)'#", $html, $match);
247
				$trend = @json_decode($match[1], true) ?: false;
248
249
				$vehicles[$id] = new Vehicle($this, compact('url', 'id', 'name', 'image', 'trend'));
250
			}
251
		}
252
253
		return $vehicles;
254
	}
255
256
	/**
257
	 *
258
	 */
259
	public function logIn() {
260
		if ( !$this->auth->mail || !$this->auth->pass ) {
261
			return false;
262
		}
263
264
		// GET /login
265
		$response = $this->_get('login', array('login' => true));
266
267
		if ( $token = $this->extractFormToken($response->body) ) {
268
			// POST /login
269
			$response = $this->_post('login', array(
270
				'login' => true,
271
				'cookies' => $response->cookies,
272
				'data' => array(
273
					'_token' => $token,
274
					'email' => $this->auth->mail,
275
					'password' => $this->auth->pass,
276
				),
277
			));
278
			$this->auth->session = $response->cookies_by_name['fuelly_session'][0];
279
			return $this->checkSession();
280
		}
281
282
		return false;
283
	}
284
285
	/**
286
	 *
287
	 */
288
	public function checkSession() {
289
		if ( !$this->auth->session ) {
290
			return false;
291
		}
292
293
		$response = $this->_get('dashboard');
294
		if ( $response->code == 200 ) {
295
			$regex = '#<a href="' . preg_quote($this->base, '#') . 'driver/([\w\d]+)/edit">Settings</a>#';
296
			if ( preg_match($regex, $response->body, $match) ) {
297
				$this->username = $match[1];
298
299
				// Since we're downloading /dashboard anyway, let's extract our vehicles from it
300
				$this->vehicles = $this->extractVehicles($response->body);
301
302
				return true;
303
			}
304
		}
305
306
		return false;
307
	}
308
309
	/**
310
	 *
311
	 */
312
	public function ensureSession() {
313
		if ( !$this->checkSession() ) {
314
			return $this->logIn();
315
		}
316
317
		return true;
318
	}
319
320
	/**
321
	 *
322
	 */
323
	protected function extractFormToken( $html ) {
324
		if ( preg_match('#<input.+?name="_token".+?>#i', $html, $match) ) {
325
			if ( preg_match('#value="([^"]+)"#', $match[0], $match) ) {
326
				return $match[1];
327
			}
328
		}
329
	}
330
331
332
333
	/**
334
	 * HTTP GET
335
	 */
336
	public function _get( $uri, $options = array() ) {
337
		return $this->_http($uri, $options + array('method' => 'GET'));
338
	}
339
340
	/**
341
	 * HTTP POST
342
	 */
343
	public function _post( $uri, $options = array() ) {
344
		return $this->_http($uri, $options + array('method' => 'POST'));
345
	}
346
347
	/**
348
	 * HTTP URL
349
	 */
350
	public function _url( $uri, $options = array() ) {
351
		$base = !empty($options['login']) ? $this->loginBase : $this->base;
352
		$url = strpos($uri, '://') ? $uri : $base . $uri;
353
		return $url;
354
	}
355
356
	/**
357
	 * HTTP REQUEST
358
	 */
359
	public function _http( $uri, $options = array() ) {
360
		if ($this->auth->session) {
361
			$options['cookies'][] = array('fuelly_session', $this->auth->session);
362
		}
363
364
		$url = $this->_url($uri, $options);
365
366
		$log['req'] = $options['method'] . ' ' . $url;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$log was never initialized. Although not strictly required by PHP, it is generally a good practice to add $log = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
367
		$this->log[] = &$log;
368
369
		$_start = microtime(1);
370
		$request = HTTP::create($url, $options);
371
372
		$response = $request->request();
373
		$_time = microtime(1) - $_start;
374
375
		$log['rsp'] = $response->code . ' ' . $response->status;
376
		$log['time'] = $_time;
377
378
		return $response;
379
	}
380
381
}
382