1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* NewYnabJobHandler.php |
4
|
|
|
* Copyright (c) 2019 [email protected] |
5
|
|
|
* |
6
|
|
|
* This file is part of Firefly III (https://github.com/firefly-iii). |
7
|
|
|
* |
8
|
|
|
* This program is free software: you can redistribute it and/or modify |
9
|
|
|
* it under the terms of the GNU Affero General Public License as |
10
|
|
|
* published by the Free Software Foundation, either version 3 of the |
11
|
|
|
* License, or (at your option) any later version. |
12
|
|
|
* |
13
|
|
|
* This program is distributed in the hope that it will be useful, |
14
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
15
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16
|
|
|
* GNU Affero General Public License for more details. |
17
|
|
|
* |
18
|
|
|
* You should have received a copy of the GNU Affero General Public License |
19
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>. |
20
|
|
|
*/ |
21
|
|
|
|
22
|
|
|
declare(strict_types=1); |
23
|
|
|
|
24
|
|
|
namespace FireflyIII\Support\Import\JobConfiguration\Ynab; |
25
|
|
|
|
26
|
|
|
use FireflyIII\Exceptions\FireflyException; |
27
|
|
|
use FireflyIII\Models\ImportJob; |
28
|
|
|
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; |
29
|
|
|
use GuzzleHttp\Client; |
30
|
|
|
use GuzzleHttp\Exception\GuzzleException; |
31
|
|
|
use Illuminate\Support\MessageBag; |
32
|
|
|
use Log; |
33
|
|
|
use RuntimeException; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Class NewYnabJobHandler |
37
|
|
|
* @deprecated |
38
|
|
|
* @codeCoverageIgnore |
39
|
|
|
*/ |
40
|
|
|
class NewYnabJobHandler implements YnabJobConfigurationInterface |
|
|
|
|
41
|
|
|
{ |
42
|
|
|
/** @var ImportJob */ |
43
|
|
|
private $importJob; |
44
|
|
|
/** @var ImportJobRepositoryInterface */ |
45
|
|
|
private $repository; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Return true when this stage is complete. |
49
|
|
|
* |
50
|
|
|
* @return bool |
51
|
|
|
* @throws FireflyException |
52
|
|
|
*/ |
53
|
|
|
public function configurationComplete(): bool |
54
|
|
|
{ |
55
|
|
|
if (!$this->hasRefreshToken()) { |
56
|
|
|
Log::debug('YNAB NewYnabJobHandler configurationComplete: stage is new, no refresh token, return false'); |
57
|
|
|
|
58
|
|
|
return false; |
59
|
|
|
} |
60
|
|
|
if ($this->hasRefreshToken() && $this->hasClientId() && $this->hasClientSecret()) { |
61
|
|
|
Log::debug('YNAB NewYnabJobHandler configurationComplete: stage is new, has a refresh token, return true'); |
62
|
|
|
// need to grab access token using refresh token |
63
|
|
|
$this->getAccessToken(); |
64
|
|
|
$this->repository->setStatus($this->importJob, 'ready_to_run'); |
65
|
|
|
$this->repository->setStage($this->importJob, 'get_budgets'); |
66
|
|
|
|
67
|
|
|
return true; |
68
|
|
|
} |
69
|
|
|
Log::error('YNAB NewYnabJobHandler configurationComplete: something broke, return true'); |
70
|
|
|
|
71
|
|
|
return true; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Store the job configuration. There is never anything to store for this stage. |
76
|
|
|
* |
77
|
|
|
* @param array $data |
78
|
|
|
* |
79
|
|
|
* @return MessageBag |
80
|
|
|
*/ |
81
|
|
|
public function configureJob(array $data): MessageBag |
82
|
|
|
{ |
83
|
|
|
Log::debug('YNAB NewYnabJobHandler configureJob: nothing to do.'); |
84
|
|
|
|
85
|
|
|
return new MessageBag; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* Get data for config view. |
90
|
|
|
* |
91
|
|
|
* @return array |
92
|
|
|
* @throws \Psr\Container\NotFoundExceptionInterface |
93
|
|
|
* @throws \Psr\Container\ContainerExceptionInterface |
94
|
|
|
*/ |
95
|
|
|
public function getNextData(): array |
96
|
|
|
{ |
97
|
|
|
$data = []; |
98
|
|
|
// here we update the job so it can redirect properly to YNAB |
99
|
|
|
if (!$this->hasRefreshToken() && $this->hasClientSecret() && $this->hasClientId()) { |
100
|
|
|
// update stage to make sure we catch the token. |
101
|
|
|
$this->repository->setStage($this->importJob, 'catch-auth-code'); |
102
|
|
|
$clientId = app('preferences')->get('ynab_client_id')->data; |
103
|
|
|
$callBackUri = route('import.callback.ynab'); |
104
|
|
|
$uri = sprintf( |
105
|
|
|
'https://app.youneedabudget.com/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s', $clientId, $callBackUri, |
106
|
|
|
$this->importJob->key |
107
|
|
|
); |
108
|
|
|
$data['token-url'] = $uri; |
109
|
|
|
Log::debug(sprintf('YNAB getNextData: URI to redirect to is %s', $uri)); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
return $data; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Get the view for this stage. |
117
|
|
|
* |
118
|
|
|
* @return string |
119
|
|
|
*/ |
120
|
|
|
public function getNextView(): string |
121
|
|
|
{ |
122
|
|
|
Log::debug('Return YNAB redirect view.'); |
123
|
|
|
|
124
|
|
|
return 'import.ynab.redirect'; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Set the import job. |
129
|
|
|
* |
130
|
|
|
* @param ImportJob $importJob |
131
|
|
|
*/ |
132
|
|
|
public function setImportJob(ImportJob $importJob): void |
133
|
|
|
{ |
134
|
|
|
$this->importJob = $importJob; |
135
|
|
|
$this->repository = app(ImportJobRepositoryInterface::class); |
136
|
|
|
$this->repository->setUser($importJob->user); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* @throws \Psr\Container\NotFoundExceptionInterface |
142
|
|
|
* @throws \Psr\Container\ContainerExceptionInterface |
143
|
|
|
* @throws FireflyException |
144
|
|
|
*/ |
145
|
|
|
private function getAccessToken(): void |
146
|
|
|
{ |
147
|
|
|
$clientId = app('preferences')->get('ynab_client_id')->data; |
148
|
|
|
$clientSecret = app('preferences')->get('ynab_client_secret')->data; |
149
|
|
|
$refreshToken = app('preferences')->get('ynab_refresh_token')->data; |
150
|
|
|
$uri = sprintf( |
151
|
|
|
'https://app.youneedabudget.com/oauth/token?client_id=%s&client_secret=%s&grant_type=refresh_token&refresh_token=%s', $clientId, $clientSecret, |
152
|
|
|
$refreshToken |
153
|
|
|
); |
154
|
|
|
|
155
|
|
|
$client = new Client(); |
156
|
|
|
try { |
157
|
|
|
$res = $client->request('post', $uri); |
158
|
|
|
} catch (GuzzleException $e) { |
159
|
|
|
Log::error($e->getMessage()); |
160
|
|
|
Log::error($e->getTraceAsString()); |
161
|
|
|
throw new FireflyException($e->getMessage()); |
162
|
|
|
} |
163
|
|
|
$statusCode = $res->getStatusCode(); |
164
|
|
|
try { |
165
|
|
|
$content = trim($res->getBody()->getContents()); |
166
|
|
|
} catch (RuntimeException $e) { |
167
|
|
|
Log::error($e->getMessage()); |
168
|
|
|
Log::error($e->getTraceAsString()); |
169
|
|
|
throw new FireflyException($e->getMessage()); |
170
|
|
|
} |
171
|
|
|
$json = json_decode($content, true) ?? []; |
172
|
|
|
Log::debug(sprintf('Status code from YNAB is %d', $statusCode)); |
173
|
|
|
Log::debug(sprintf('Body of result is %s', $content), $json); |
174
|
|
|
|
175
|
|
|
// store refresh token (if present?) as preference |
176
|
|
|
// store token in job: |
177
|
|
|
$configuration = $this->repository->getConfiguration($this->importJob); |
178
|
|
|
$configuration['access_token'] = $json['access_token']; |
179
|
|
|
$configuration['access_token_expires'] = (int)$json['created_at'] + (int)$json['expires_in']; |
180
|
|
|
$this->repository->setConfiguration($this->importJob, $configuration); |
181
|
|
|
|
182
|
|
|
// also store new refresh token: |
183
|
|
|
$refreshToken = (string)($json['refresh_token'] ?? ''); |
184
|
|
|
if ('' !== $refreshToken) { |
185
|
|
|
app('preferences')->set('ynab_refresh_token', $refreshToken); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
|
189
|
|
|
Log::debug('end of NewYnabJobHandler::getAccessToken()'); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Check if we have the client ID. |
194
|
|
|
* |
195
|
|
|
* @return bool |
196
|
|
|
*/ |
197
|
|
|
private function hasClientId(): bool |
198
|
|
|
{ |
199
|
|
|
$clientId = app('preferences')->getForUser($this->importJob->user, 'ynab_client_id', null); |
200
|
|
|
if (null === $clientId) { |
201
|
|
|
Log::debug('user has no YNAB client ID'); |
202
|
|
|
|
203
|
|
|
return false; |
204
|
|
|
} |
205
|
|
|
if ('' === (string)$clientId->data) { |
206
|
|
|
Log::debug('user has no YNAB client ID (empty)'); |
207
|
|
|
|
208
|
|
|
return false; |
209
|
|
|
} |
210
|
|
|
Log::debug('user has a YNAB client ID'); |
211
|
|
|
|
212
|
|
|
return true; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Check if we have the client secret |
217
|
|
|
* |
218
|
|
|
* @return bool |
219
|
|
|
*/ |
220
|
|
|
private function hasClientSecret(): bool |
221
|
|
|
{ |
222
|
|
|
$clientSecret = app('preferences')->getForUser($this->importJob->user, 'ynab_client_secret', null); |
223
|
|
|
if (null === $clientSecret) { |
224
|
|
|
Log::debug('user has no YNAB client secret'); |
225
|
|
|
|
226
|
|
|
return false; |
227
|
|
|
} |
228
|
|
|
if ('' === (string)$clientSecret->data) { |
229
|
|
|
Log::debug('user has no YNAB client secret (empty)'); |
230
|
|
|
|
231
|
|
|
return false; |
232
|
|
|
} |
233
|
|
|
Log::debug('user has a YNAB client secret'); |
234
|
|
|
|
235
|
|
|
return true; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* @return bool |
240
|
|
|
* @throws \Psr\Container\NotFoundExceptionInterface |
241
|
|
|
* @throws \Psr\Container\ContainerExceptionInterface |
242
|
|
|
*/ |
243
|
|
|
private function hasRefreshToken(): bool |
244
|
|
|
{ |
245
|
|
|
$preference = app('preferences')->get('ynab_refresh_token'); |
246
|
|
|
if (null === $preference) { |
247
|
|
|
Log::debug('user has no YNAB refresh token.'); |
248
|
|
|
|
249
|
|
|
return false; |
250
|
|
|
} |
251
|
|
|
if ('' === (string)$preference->data) { |
252
|
|
|
Log::debug('user has no YNAB refresh token (empty).'); |
253
|
|
|
|
254
|
|
|
return false; |
255
|
|
|
} |
256
|
|
|
Log::debug(sprintf('user has YNAB refresh token: %s', $preference->data)); |
257
|
|
|
|
258
|
|
|
return true; |
259
|
|
|
} |
260
|
|
|
} |
261
|
|
|
|
This interface has been deprecated. The supplier of the interface has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.