1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace rdx\fuelly; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use rdx\fuelly\FuelUp; |
7
|
|
|
use rdx\fuelly\Vehicle; |
8
|
|
|
use rdx\fuelly\WebAuth; |
9
|
|
|
use rdx\http\HTTP; |
10
|
|
|
use rdx\jsdom\Node; |
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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('https://www.fuelly.com/fuelups/create?usercar_id=' . $data['usercar_id']); |
187
|
|
|
|
188
|
|
|
$token = $this->extractFormToken($response->body); |
189
|
|
|
if ( $token ) { |
190
|
|
|
$data['_token'] = $token; |
191
|
|
|
|
192
|
|
|
// POST /fuelups/create |
193
|
|
|
$response = $this->_post('https://www.fuelly.com/fuelups', array( |
194
|
|
|
'data' => $data, |
195
|
|
|
)); |
196
|
|
|
if ( $response->code == 302 ) { |
197
|
|
|
$response = $this->_get(str_replace('https:', 'http:', $response->headers['location'][0])); |
198
|
|
|
|
199
|
|
|
// Take new fuelup ID from response and add it |
200
|
|
|
if ( $response->code == 200 ) { |
201
|
|
|
$regex = '#' . preg_quote($this->base, '#') . 'fuelups/(\d+)/edit#'; |
202
|
|
|
if ( preg_match($regex, $response->body, $match) ) { |
203
|
|
|
$response->fuelup_id = $match[1]; |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
return $response; |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* |
214
|
|
|
*/ |
215
|
|
|
public function getVehicle( $id ) { |
216
|
|
|
$vehicles = $this->getVehicles(); |
217
|
|
|
return @$vehicles[$id]; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* |
222
|
|
|
*/ |
223
|
|
|
public function getVehicles() { |
224
|
|
|
// Must exist, because session must be valid, so we did a `GET /dashboard` |
225
|
|
|
return $this->vehicles; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* |
230
|
|
|
*/ |
231
|
|
|
protected function extractVehicles( $page_html ) { |
232
|
|
|
$regex = '#<ul class="dashboard-vehicle" data-clickable="([^"]+)">[\w\W]+?</ul>#'; |
233
|
|
|
$vehicles = array(); |
234
|
|
|
if ( preg_match_all($regex, $page_html, $matches) ) { |
235
|
|
|
foreach ( $matches[0] as $i => $car_html ) { |
236
|
|
|
$url = $matches[1][$i]; |
237
|
|
|
|
238
|
|
|
preg_match('#/(\d+)$#', $url, $match); |
239
|
|
|
$id = $match[1]; |
240
|
|
|
|
241
|
|
|
preg_match('#<h3[^>]*>(.+?)</h3>#', $car_html, $match); |
242
|
|
|
$name = htmlspecialchars_decode($match[1]); |
243
|
|
|
|
244
|
|
|
preg_match("#background\-image:\s*url\(([^\)]+)\)#", $car_html, $match); |
245
|
|
|
$image = $this->base . trim($match[1], "/'"); |
246
|
|
|
|
247
|
|
|
preg_match("#data-trend='([^']+)'#", $car_html, $match); |
248
|
|
|
$trend = @json_decode($match[1], true) ?: false; |
249
|
|
|
|
250
|
|
|
$vehicles[$id] = new Vehicle($this, compact('url', 'id', 'name', 'image', 'trend')); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
return $vehicles; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* |
259
|
|
|
*/ |
260
|
|
|
public function logIn() { |
261
|
|
|
if ( !$this->auth->mail || !$this->auth->pass ) { |
262
|
|
|
return false; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
// GET /login |
266
|
|
|
$response = $this->_get('login', array('login' => true)); |
267
|
|
|
|
268
|
|
|
if ( $token = $this->extractFormToken($response->body) ) { |
269
|
|
|
// POST /login |
270
|
|
|
$response = $this->_post('login', array( |
271
|
|
|
'login' => true, |
272
|
|
|
'cookies' => $response->cookies, |
273
|
|
|
'data' => array( |
274
|
|
|
'_token' => $token, |
275
|
|
|
'email' => $this->auth->mail, |
276
|
|
|
'password' => $this->auth->pass, |
277
|
|
|
), |
278
|
|
|
)); |
279
|
|
|
$this->auth->session = $response->cookies_by_name['fuelly_session'][0]; |
280
|
|
|
return $this->checkSession(); |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
return false; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* |
288
|
|
|
*/ |
289
|
|
|
public function checkSession() { |
290
|
|
|
if ( !$this->auth->session ) { |
291
|
|
|
return false; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
$response = $this->_get('dashboard'); |
295
|
|
|
if ( $response->code == 200 ) { |
296
|
|
|
$regex = '#<a href="' . preg_quote($this->base, '#') . 'driver/([\w\d]+)/edit">Settings</a>#'; |
297
|
|
|
if ( preg_match($regex, $response->body, $match) ) { |
298
|
|
|
$this->username = $match[1]; |
299
|
|
|
|
300
|
|
|
// Since we're downloading /dashboard anyway, let's extract our vehicles from it |
301
|
|
|
$this->vehicles = $this->extractVehicles($response->body); |
302
|
|
|
|
303
|
|
|
return true; |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
return false; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* |
312
|
|
|
*/ |
313
|
|
|
public function ensureSession() { |
314
|
|
|
if ( !$this->checkSession() ) { |
315
|
|
|
return $this->logIn(); |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
return true; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* |
323
|
|
|
*/ |
324
|
|
|
protected function extractFormToken( $html ) { |
325
|
|
|
$doc = Node::create($html); |
326
|
|
|
$el = $doc->query('input[name="_token"]'); |
327
|
|
|
return $el['value']; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
|
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* HTTP GET |
334
|
|
|
*/ |
335
|
|
|
public function _get( $uri, $options = array() ) { |
336
|
|
|
return $this->_http($uri, $options + array('method' => 'GET')); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* HTTP POST |
341
|
|
|
*/ |
342
|
|
|
public function _post( $uri, $options = array() ) { |
343
|
|
|
return $this->_http($uri, $options + array('method' => 'POST')); |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* HTTP URL |
348
|
|
|
*/ |
349
|
|
|
public function _url( $uri, $options = array() ) { |
350
|
|
|
$base = !empty($options['login']) ? $this->loginBase : $this->base; |
351
|
|
|
$url = strpos($uri, '://') ? $uri : $base . $uri; |
352
|
|
|
return $url; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* HTTP REQUEST |
357
|
|
|
*/ |
358
|
|
|
public function _http( $uri, $options = array() ) { |
359
|
|
|
if ( $this->auth->session ) { |
360
|
|
|
$options['cookies'][] = array('fuelly_session', $this->auth->session); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
$url = $this->_url($uri, $options); |
364
|
|
|
|
365
|
|
|
$log = array(); |
366
|
|
|
$log['req'] = $options['method'] . ' ' . $url; |
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
|
|
|
|
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
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:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.