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