Completed
Branch master (082d55)
by Rudie
02:26
created

Client::extractFormToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 3
eloc 4
nc 3
nop 1
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
			$data['_token'] = $this->extractFormToken($response->body);
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 = array();
367
		$log['req'] = $options['method'] . ' ' . $url;
368
		$this->log[] = &$log;
369
370
		$_start = microtime(1);
371
		$request = HTTP::create($url, $options);
372
373
		$response = $request->request();
374
		$_time = microtime(1) - $_start;
375
376
		$log['rsp'] = $response->code . ' ' . $response->status;
377
		$log['time'] = $_time;
378
379
		return $response;
380
	}
381
382
}
383