1 /*
2 * Copyright 2009-2020 Thomas Baumgart <tbaumgart@kde.org>
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License as
6 * published by the Free Software Foundation; either version 2 of
7 * the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include <config-kmymoney.h>
19
20 #include "ofxpartner.h"
21 #include "kmymoneysettings.h"
22
23 // ----------------------------------------------------------------------------
24 // QT Includes
25
26 #include <QDateTime>
27 #include <QEventLoop>
28 #include <QFileInfo>
29 #include <QApplication>
30 #include <QRegExp>
31 #include <QDir>
32 #include <QFile>
33 #include <QTextStream>
34 #include <QDomDocument>
35 #include <QDebug>
36 #include <QRegularExpression>
37 #include <QRegularExpressionMatch>
38
39 // ----------------------------------------------------------------------------
40 // KDE Includes
41
42 #include <KIO/Job>
43 #include <KIO/TransferJob>
44 #include <KIO/CopyJob>
45 #include <KJobUiDelegate>
46 #include <KLocalizedString>
47 #include <KMessageBox>
48
49 // ----------------------------------------------------------------------------
50 // Some standard defined stuff collides with libofx.h
51 #ifdef Q_CC_MSVC
52 #undef ERROR
53 #undef DELETE
54 #endif
55
56 // ----------------------------------------------------------------------------
57 // Project Includes
58
59 namespace OfxPartner
60 {
61 bool post(const QString& request, const QMap<QString, QString>& attr, const QUrl &url, const QUrl& filename);
62 bool get(const QString& request, const QMap<QString, QString>& attr, const QUrl &url, const QUrl& filename);
63
64 const QString kBankFilename = "ofx-bank-index.xml";
65 const QString kCcFilename = "ofx-cc-index.xml";
66 const QString kInvFilename = "ofx-inv-index.xml";
67
68 #define VER "9"
69
70 static QString directory;
71
setDirectory(const QString & dir)72 void setDirectory(const QString& dir)
73 {
74 directory = dir;
75 }
76
needReload(const QFileInfo & i)77 bool needReload(const QFileInfo& i)
78 {
79 return ((!i.isReadable())
80 || (i.lastModified().addDays(7) < QDateTime::currentDateTime())
81 || (i.size() < 1024));
82 }
83
ValidateIndexCache()84 void ValidateIndexCache()
85 {
86 // TODO (Ace) Check whether these files exist and are recent enough before getting them again
87
88 QUrl fname;
89
90 QMap<QString, QString> attr;
91
92 fname = QUrl("file://" + directory + kBankFilename);
93 QDir dir;
94 dir.mkpath(directory);
95
96 QFileInfo i(fname.toLocalFile());
97 if (needReload(i))
98 get("", attr, QUrl(QStringLiteral("https://www.ofxhome.com/api.php?all=yes")), fname);
99 }
100
ParseFile(QMap<QString,QString> & result,const QString & fileName,const QString & bankName)101 static void ParseFile(QMap<QString, QString>& result, const QString& fileName, const QString& bankName)
102 {
103 QFile f(fileName);
104 if (f.open(QIODevice::ReadOnly)) {
105 QTextStream stream(&f);
106 stream.setCodec("UTF-8");
107 QString msg;
108 int errl, errc;
109 QDomDocument doc;
110 if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) {
111 QDomNodeList olist = doc.elementsByTagName("institutionid");
112 for (int i = 0; i < olist.count(); ++i) {
113 QDomNode onode = olist.item(i);
114 if (onode.isElement()) {
115 QDomElement elo = onode.toElement();
116 QString name = elo.attribute("name");
117
118 if (bankName.isEmpty())
119 result[name].clear();
120
121 else if (name == bankName) {
122 result[elo.attribute("id")].clear();
123 }
124 }
125 }
126 }
127 f.close();
128 }
129 }
130
BankNames()131 QStringList BankNames()
132 {
133 QMap<QString, QString> result;
134
135 // Make sure the index files are up to date
136 ValidateIndexCache();
137
138 ParseFile(result, directory + kBankFilename, QString());
139
140 // Add Innovision
141 result["Innovision"].clear();
142
143 return QStringList() << result.keys();
144 }
145
FipidForBank(const QString & bank)146 QStringList FipidForBank(const QString& bank)
147 {
148 QMap<QString, QString> result;
149
150 ParseFile(result, directory + kBankFilename, bank);
151
152 // the fipid for Innovision is 1.
153 if (bank == "Innovision")
154 result["1"].clear();
155
156 return QStringList() << result.keys();
157 }
158
extractNodeText(QDomElement & node,const QString & name)159 QString extractNodeText(QDomElement& node, const QString& name)
160 {
161 QString res;
162 QRegExp exp("([^/]+)/?([^/].*)?");
163 if (exp.indexIn(name) != -1) {
164 QDomNodeList olist = node.elementsByTagName(exp.cap(1));
165 if (olist.count()) {
166 QDomNode onode = olist.item(0);
167 if (onode.isElement()) {
168 QDomElement elo = onode.toElement();
169 if (exp.cap(2).isEmpty()) {
170 res = elo.text();
171 } else {
172 res = extractNodeText(elo, exp.cap(2));
173 }
174 }
175 }
176 }
177 return res;
178 }
179
extractNodeText(QDomDocument & doc,const QString & name)180 QString extractNodeText(QDomDocument& doc, const QString& name)
181 {
182 QString res;
183 QRegExp exp("([^/]+)/?([^/].*)?");
184 if (exp.indexIn(name) != -1) {
185 QDomNodeList olist = doc.elementsByTagName(exp.cap(1));
186 if (olist.count()) {
187 QDomNode onode = olist.item(0);
188 if (onode.isElement()) {
189 QDomElement elo = onode.toElement();
190 if (exp.cap(2).isEmpty()) {
191 res = elo.text();
192 } else {
193 res = extractNodeText(elo, exp.cap(2));
194 }
195 }
196 }
197 }
198 return res;
199 }
200
ServiceInfo(const QString & fipid)201 OfxHomeServiceInfo ServiceInfo(const QString& fipid)
202 {
203 OfxHomeServiceInfo result;
204 memset(&result.ofxInfo, 0, sizeof(result.ofxInfo));
205 result.ofxValidated = true;
206 result.sslValidated = true;
207 result.lastOfxValidated = QDate::currentDate().toString();
208 result.lastSslValidated = result.lastOfxValidated;
209
210 // Hard-coded values for Innovision test server
211 if (fipid == "1") {
212 strncpy(result.ofxInfo.fid, "00000", OFX_FID_LENGTH - 1);
213 strncpy(result.ofxInfo.org, "ReferenceFI", OFX_ORG_LENGTH - 1);
214 strncpy(result.ofxInfo.url, "https://ofx.innovision.com", OFX_URL_LENGTH - 1);
215 result.ofxInfo.accountlist = 1;
216 result.ofxInfo.statements = 1;
217 result.ofxInfo.billpay = 1;
218 result.ofxInfo.investments = 1;
219
220 return result;
221 }
222
223 QMap<QString, QString> attr;
224
225 QUrl guidFile(QString("file://%1fipid-%2.xml").arg(directory).arg(fipid));
226
227 QFileInfo i(guidFile.toLocalFile());
228
229 if (!i.isReadable() || i.lastModified().addDays(7) < QDateTime::currentDateTime())
230 get("", attr, QUrl(QString("https://www.ofxhome.com/api.php?lookup=%1").arg(fipid)), guidFile);
231
232 QFile f(guidFile.toLocalFile());
233 if (f.open(QIODevice::ReadOnly)) {
234 QTextStream stream(&f);
235 stream.setCodec("UTF-8");
236 QString msg;
237 int errl, errc;
238 QDomDocument doc;
239 if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) {
240 const auto fid = extractNodeText(doc, "institution/fid");
241 const auto org = extractNodeText(doc, "institution/org");
242 const auto url = extractNodeText(doc, "institution/url");
243 result.ofxValidated = (extractNodeText(doc, "institution/ofxfail").toUInt() == 0);
244 result.sslValidated = (extractNodeText(doc, "institution/sslfail").toUInt() == 0);
245 result.lastOfxValidated = extractNodeText(doc, "institution/lastofxvalidation");
246 result.lastSslValidated = extractNodeText(doc, "institution/lastsslvalidation");
247
248 strncpy(result.ofxInfo.fid, fid.toLatin1(), OFX_FID_LENGTH - 1);
249 strncpy(result.ofxInfo.org, org.toLatin1(), OFX_ORG_LENGTH - 1);
250 strncpy(result.ofxInfo.url, url.toLatin1(), OFX_URL_LENGTH - 1);
251
252 result.ofxInfo.accountlist = true;
253 result.ofxInfo.statements = true;
254 result.ofxInfo.billpay = false;
255 result.ofxInfo.investments = true;
256 }
257 }
258 else
259 {
260 memset(&result.ofxInfo, 0, sizeof(result.ofxInfo));
261 result.ofxValidated = false;
262 result.sslValidated = false;
263 result.lastOfxValidated.clear();
264 result.lastSslValidated.clear();
265 qDebug() << "OFX ServiceInfo:" << f.errorString();
266 }
267 return result;
268 }
269
get(const QString & request,const QMap<QString,QString> & attr,const QUrl & url,const QUrl & filename)270 bool get(const QString& request, const QMap<QString, QString>& attr, const QUrl &url, const QUrl& filename)
271 {
272 Q_UNUSED(request);
273 QByteArray req;
274 OfxHttpRequest job("GET", url, req, attr, filename, false);
275
276 return job.error() == 0;
277 }
278
post(const QString & request,const QMap<QString,QString> & attr,const QUrl & url,const QUrl & filename)279 bool post(const QString& request, const QMap<QString, QString>& attr, const QUrl &url, const QUrl& filename)
280 {
281 QByteArray req(request.toUtf8());
282
283 OfxHttpRequest job("POST", url, req, attr, filename, false);
284 return job.error() == 0;
285 }
286
287 } // namespace OfxPartner
288
289 class OfxHttpRequest::Private
290 {
291 public:
292 QFile m_fpTrace;
293 };
294
OfxHttpRequest(const QString & type,const QUrl & url,const QByteArray & postData,const QMap<QString,QString> & metaData,const QUrl & dst,bool showProgressInfo)295 OfxHttpRequest::OfxHttpRequest(const QString& type, const QUrl &url, const QByteArray &postData, const QMap<QString, QString>& metaData, const QUrl& dst, bool showProgressInfo)
296 : d(new Private)
297 , m_dst(dst.toLocalFile())
298 , m_error(-1)
299 , m_postJob(0)
300 , m_getJob(0)
301 {
302 #if defined(Q_OS_WIN)
303 // on MS windows, the local file could be presented as
304 //
305 // "//<drive-letter>/<path-name>"
306 //
307 // which needs to be converted to
308 //
309 // "<drive-letter>:/<path-name>"
310 //
311 // see https://bugs.kde.org/show_bug.cgi?id=396286 for details of the analysis.
312 QRegularExpression re(QStringLiteral("^//(?<drive>[a-z])/(?<path>.+)$"), QRegularExpression::CaseInsensitiveOption);
313 const auto match = re.match(m_dst);
314 if (match.hasMatch()) {
315 m_dst = QString("/%1:/%2").arg(match.captured(QStringLiteral("drive")), match.captured(QStringLiteral("path")));
316 qDebug() << "destination changed to" << m_dst;
317 }
318 #endif
319
320 m_eventLoop = new QEventLoop(qApp->activeWindow());
321
322 if (KMyMoneySettings::logOfxTransactions()) {
323 QString logPath = KMyMoneySettings::logPath();
324 d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(logPath));
325 d->m_fpTrace.open(QIODevice::WriteOnly | QIODevice::Append);
326 }
327
328 KIO::JobFlag jobFlags = KIO::DefaultFlags;
329 if (!showProgressInfo)
330 jobFlags = KIO::HideProgressInfo;
331
332 KIO::Job* job;
333 if(type.toLower() == QStringLiteral("get")) {
334 job = m_getJob = KIO::copy(url, QUrl(QString("file://%1").arg(m_dst)), jobFlags);
335 } else {
336 job = m_postJob = KIO::http_post(url, postData, jobFlags);
337 m_postJob->addMetaData("content-type", "Content-type: application/x-ofx");
338 m_postJob->addMetaData(metaData);
339 connect(job, SIGNAL(data(KIO::Job*,QByteArray)), this, SLOT(slotOfxData(KIO::Job*,QByteArray)));
340 connect(job, SIGNAL(connected(KIO::Job*)), this, SLOT(slotOfxConnected(KIO::Job*)));
341 }
342
343 if (d->m_fpTrace.isOpen()) {
344 QTextStream ts(&d->m_fpTrace);
345 ts << "url: " << url.toDisplayString() << "\n";
346 ts << "request:\n" << QString(postData) << "\n" << "response:\n";
347 }
348
349 connect(job, SIGNAL(result(KJob*)), this, SLOT(slotOfxFinished(KJob*)));
350
351 job->start();
352
353 qDebug("Starting eventloop");
354 if (m_eventLoop)
355 m_eventLoop->exec();
356 qDebug("Ending eventloop");
357 }
358
~OfxHttpRequest()359 OfxHttpRequest::~OfxHttpRequest()
360 {
361 delete m_eventLoop;
362
363 if (d->m_fpTrace.isOpen()) {
364 d->m_fpTrace.close();
365 }
366 delete d;
367 }
368
slotOfxConnected(KIO::Job *)369 void OfxHttpRequest::slotOfxConnected(KIO::Job*)
370 {
371 qDebug() << "OfxHttpRequest::slotOfxConnected" << m_dst;
372 m_file.setFileName(m_dst);
373 m_file.open(QIODevice::WriteOnly);
374 }
375
slotOfxData(KIO::Job *,const QByteArray & _ba)376 void OfxHttpRequest::slotOfxData(KIO::Job*, const QByteArray& _ba)
377 {
378 if (m_file.isOpen()) {
379 m_file.write(_ba);
380
381 if (d->m_fpTrace.isOpen()) {
382 d->m_fpTrace.write(_ba);
383 }
384 }
385 }
386
slotOfxFinished(KJob *)387 void OfxHttpRequest::slotOfxFinished(KJob* /* e */)
388 {
389 if (m_file.isOpen()) {
390 m_file.close();
391 if (d->m_fpTrace.isOpen()) {
392 d->m_fpTrace.write("\nCompleted\n\n\n\n", 14);
393 }
394 }
395
396 if(m_postJob) {
397 m_error = m_postJob->error();
398 if (m_error) {
399 m_postJob->uiDelegate()->showErrorMessage();
400 QFile::remove(m_dst);
401
402 } else if (m_postJob->isErrorPage()) {
403 QString details;
404 QFile f(m_dst);
405 if (f.open(QIODevice::ReadOnly)) {
406 QTextStream stream(&f);
407 while (!stream.atEnd()) {
408 details += stream.readLine(); // line of text excluding '\n'
409 }
410 f.close();
411 }
412 KMessageBox::detailedSorry(0, i18n("The HTTP request failed."), details, i18nc("The HTTP request failed", "Failed"));
413 QFile::remove(m_dst);
414 }
415
416 } else if(m_getJob) {
417 m_error = m_getJob->error();
418 if (m_error) {
419 m_getJob->uiDelegate()->showErrorMessage();
420 QFile::remove(m_dst);
421 }
422 }
423
424 qDebug("Finishing eventloop");
425 if (m_eventLoop)
426 m_eventLoop->exit();
427 }
428