File indexing completed on 2024-05-12 16:41:59

0001 /*
0002     SPDX-FileCopyrightText: 2004 Ace Jones <acejones@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0004     SPDX-FileCopyrightText: 2021 Dawid Wróbel <me@dawidwrobel.com>
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "webpricequote.h"
0009 
0010 // ----------------------------------------------------------------------------
0011 // QT Headers
0012 
0013 #include <QFile>
0014 #include <QTextCodec>
0015 #include <QByteArray>
0016 #include <QString>
0017 #include <QTemporaryFile>
0018 #include <QUrl>
0019 #include <QRegularExpression>
0020 #include <QDebug>
0021 #include <QLoggingCategory>
0022 #include <QLocale>
0023 
0024 // ----------------------------------------------------------------------------
0025 // KDE Headers
0026 
0027 #include <KLocalizedString>
0028 #include <KConfig>
0029 #include <KShell>
0030 #include <KConfigGroup>
0031 #include <KEncodingProber>
0032 #include <KIO/Scheduler>
0033 #include <KIO/Job>
0034 #include <KJobWidgets>
0035 
0036 // ----------------------------------------------------------------------------
0037 // Project Headers
0038 
0039 #include "mymoneyexception.h"
0040 #include "mymoneyfile.h"
0041 #include "mymoneysecurity.h"
0042 
0043 Q_DECLARE_LOGGING_CATEGORY(WEBPRICEQUOTE)
0044 Q_LOGGING_CATEGORY(WEBPRICEQUOTE, "kmymoney_webpricequote")
0045 
0046 // define static members
0047 QString WebPriceQuote::m_financeQuoteScriptPath;
0048 QStringList WebPriceQuote::m_financeQuoteSources;
0049 
0050 class WebPriceQuote::Private
0051 {
0052 public:
0053     WebPriceQuoteProcess m_filter;
0054     QString m_quoteData;
0055     QString m_webID;
0056     QString m_kmmID;
0057     QDate m_date;
0058     QDate m_fromDate;
0059     QDate m_toDate;
0060     double m_price;
0061     WebPriceQuoteSource m_source;
0062     PricesProfile    m_CSVSource;
0063 };
0064 
0065 WebPriceQuote::WebPriceQuote(QObject* _parent):
0066     QObject(_parent),
0067     d(new Private)
0068 {
0069     // only do this once (I know, it is not thread safe, but it should
0070     // always yield the same result so we don't do any semaphore foo here)
0071     if (m_financeQuoteScriptPath.isEmpty()) {
0072         m_financeQuoteScriptPath = QStandardPaths::locate(QStandardPaths::DataLocation, QString("misc/financequote.pl"));
0073     }
0074     connect(&d->m_filter, SIGNAL(processExited(QString)), this, SLOT(slotParseQuote(QString)));
0075 }
0076 
0077 WebPriceQuote::~WebPriceQuote()
0078 {
0079     delete d;
0080 }
0081 
0082 void WebPriceQuote::setDate(const QDate& _from, const QDate& _to)
0083 {
0084     d->m_fromDate = _from;
0085     d->m_toDate = _to;
0086 }
0087 
0088 bool WebPriceQuote::launch(const QString& _webID, const QString& _kmmID, const QString& _sourcename)
0089 {
0090     if (_sourcename.contains("Finance::Quote"))
0091         return (launchFinanceQuote(_webID, _kmmID, _sourcename));
0092     else if ((!d->m_fromDate.isValid() || !d->m_toDate.isValid()) ||
0093              (d->m_fromDate == d->m_toDate && d->m_toDate == QDate::currentDate()))
0094         return (launchNative(_webID, _kmmID, _sourcename));
0095     else
0096         return launchCSV(_webID, _kmmID, _sourcename);
0097 }
0098 
0099 bool WebPriceQuote::launchCSV(const QString& _webID, const QString& _kmmID, const QString& _sourcename)
0100 {
0101     d->m_webID = _webID;
0102     d->m_kmmID = _kmmID;
0103 
0104 //   emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id));
0105 
0106     // Get sources from the config file
0107     QString sourcename = _sourcename;
0108     if (sourcename.isEmpty())
0109         return false;
0110 
0111     // for historical exchange rates we switch to Stooq
0112     if (sourcename == QLatin1String("KMyMoney Currency"))
0113         sourcename = QLatin1String("Stooq Currency");
0114 
0115     if (quoteSources().contains(sourcename))
0116         d->m_source = WebPriceQuoteSource(sourcename);
0117     else {
0118         emit error(i18n("Source <placeholder>%1</placeholder> does not exist.", sourcename));
0119         emit failed(d->m_kmmID, d->m_webID);
0120         return false;
0121     }
0122 
0123     int monthOffset = 0;
0124     if (sourcename.contains(QLatin1String("Yahoo"), Qt::CaseInsensitive))
0125         monthOffset = -1;
0126 
0127     QUrl url;
0128     QString urlStr = d->m_source.m_csvUrl;
0129     int i = urlStr.indexOf(QLatin1String("%y"));
0130     if (i != -1)
0131         urlStr.replace(i, 2, QString().setNum(d->m_fromDate.year()));
0132     i = urlStr.indexOf(QLatin1String("%y"));
0133     if (i != -1)
0134         urlStr.replace(i, 2, QString().setNum(d->m_toDate.year()));
0135 
0136     i = urlStr.indexOf(QLatin1String("%m"));
0137     if (i != -1)
0138         urlStr.replace(i, 2, QString().setNum(d->m_fromDate.month() + monthOffset).rightJustified(2, QLatin1Char('0')));
0139     i = urlStr.indexOf(QLatin1String("%m"));
0140     if (i != -1)
0141         urlStr.replace(i, 2, QString().setNum(d->m_toDate.month() + monthOffset).rightJustified(2, QLatin1Char('0')));
0142 
0143     i = urlStr.indexOf(QLatin1String("%d"));
0144     if (i != -1)
0145         urlStr.replace(i, 2, QString().setNum(d->m_fromDate.day()).rightJustified(2, QLatin1Char('0')));
0146     i = urlStr.indexOf(QLatin1String("%d"));
0147     if (i != -1)
0148         urlStr.replace(i, 2, QString().setNum(d->m_toDate.day()).rightJustified(2, QLatin1Char('0')));
0149 
0150     if (urlStr.contains(QLatin1String("%y")) || urlStr.contains(QLatin1String("%m")) || urlStr.contains(QLatin1String("%d"))) {
0151         emit error(i18n("Cannot resolve input date."));
0152         emit failed(d->m_kmmID, d->m_webID);
0153         return false;
0154     }
0155 
0156     bool isCurrency = false;
0157     if (urlStr.contains(QLatin1String("%2"))) {
0158         d->m_CSVSource.m_profileType = Profile::CurrencyPrices;
0159         isCurrency = true;
0160     } else
0161         d->m_CSVSource.m_profileType = Profile::StockPrices;
0162 
0163     d->m_CSVSource.m_profileName = sourcename;
0164     if (!d->m_CSVSource.readSettings(CSVImporterCore::configFile())) {
0165         QMap<QString, PricesProfile> result = defaultCSVQuoteSources();
0166         d->m_CSVSource = result.value(sourcename);
0167         if (d->m_CSVSource.m_profileName.isEmpty()) {
0168             emit error(i18n("CSV source <placeholder>%1</placeholder> does not exist.", sourcename));
0169             emit failed(d->m_kmmID, d->m_webID);
0170             return false;
0171         }
0172     }
0173 
0174     if (isCurrency) {
0175         // this is a two-symbol quote.  split the symbol into two.  valid symbol
0176         // characters are: 0-9, A-Z and the dot.  anything else is a separator
0177         QRegularExpression splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", QRegularExpression::CaseInsensitiveOption);
0178         QRegularExpressionMatch match;
0179         // if we've truly found 2 symbols delimited this way...
0180         if (d->m_webID.indexOf(splitrx, 0, &match) != -1) {
0181             url = QUrl(urlStr.arg(match.captured(1), match.captured(2)));
0182             d->m_CSVSource.m_currencySymbol = match.captured(2);
0183             d->m_CSVSource.m_securitySymbol = match.captured(1);
0184         } else {
0185             qCDebug(WEBPRICEQUOTE) << "WebPriceQuote::launch() did not find 2 symbols";
0186             emit error(i18n("Cannot find from and to currency."));
0187             emit failed(d->m_kmmID, d->m_webID);
0188             return false;
0189         }
0190 
0191     } else {
0192         // a regular one-symbol quote
0193         url = QUrl(urlStr.arg(d->m_webID));
0194         d->m_CSVSource.m_securityName = MyMoneyFile::instance()->security(d->m_kmmID).name();
0195         d->m_CSVSource.m_securitySymbol = MyMoneyFile::instance()->security(d->m_kmmID).tradingSymbol();
0196     }
0197 
0198     if (url.isLocalFile()) {
0199         emit error(i18n("Local quote sources aren't supported."));
0200         emit failed(d->m_kmmID, d->m_webID);
0201         return false;
0202     } else {
0203         //silent download
0204         emit status(i18n("Fetching URL %1...", url.toDisplayString()));
0205         QString tmpFile;
0206         {
0207             QTemporaryFile tmpFileFile;
0208             tmpFileFile.setAutoRemove(false);
0209             if (tmpFileFile.open())
0210                 qDebug() << "created tmpfile";
0211 
0212             tmpFile = tmpFileFile.fileName();
0213         }
0214         QFile::remove(tmpFile);
0215         const QUrl dest = QUrl::fromLocalFile(tmpFile);
0216         KIO::Scheduler::checkSlaveOnHold(true);
0217         KIO::Job *job = KIO::file_copy(url, dest, -1, KIO::HideProgressInfo);
0218         connect(job, SIGNAL(result(KJob*)),
0219                 this, SLOT(downloadCSV(KJob*)));
0220     }
0221     return true;
0222 }
0223 
0224 bool WebPriceQuote::launchNative(const QString& _webID, const QString& _kmmID, const QString& _sourcename)
0225 {
0226     d->m_webID = _webID;
0227     d->m_kmmID = _kmmID;
0228 
0229     if (_webID == i18n("[No identifier]")) {
0230         emit error(i18n("<placeholder>%1</placeholder> skipped because it doesn't have identification number.", _kmmID));
0231         emit failed(d->m_kmmID, d->m_webID);
0232         return false;
0233     }
0234 //   emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id));
0235 
0236     // Get sources from the config file
0237     QString sourcename = _sourcename;
0238     if (sourcename.isEmpty())
0239         sourcename = "Yahoo";
0240 
0241     if (quoteSources().contains(sourcename))
0242         d->m_source = WebPriceQuoteSource(sourcename);
0243     else {
0244         emit error(i18n("Source <placeholder>%1</placeholder> does not exist.", sourcename));
0245         emit failed(d->m_kmmID, d->m_webID);
0246         return false;
0247     }
0248 
0249     QUrl url;
0250 
0251     // if the source has room for TWO symbols..
0252     if (d->m_source.m_url.contains("%2")) {
0253         // this is a two-symbol quote.  split the symbol into two.  valid symbol
0254         // characters are: 0-9, A-Z and the dot.  anything else is a separator
0255         QRegularExpression splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", QRegularExpression::CaseInsensitiveOption);
0256         QRegularExpressionMatch match;
0257         // if we've truly found 2 symbols delimited this way...
0258         if (d->m_webID.indexOf(splitrx, 0, &match) != -1) {
0259             url = QUrl(d->m_source.m_url.arg(match.captured(1), match.captured(2)));
0260         } else {
0261             qCDebug(WEBPRICEQUOTE) << "WebPriceQuote::launch() did not find 2 symbols";
0262         }
0263     } else {
0264         // a regular one-symbol quote
0265         url = QUrl(d->m_source.m_url.arg(d->m_webID));
0266     }
0267 
0268     if (url.isLocalFile()) {
0269         emit status(i18nc("The process x is executing", "Executing %1...", url.toLocalFile()));
0270 
0271         QString program;
0272         QStringList arguments = url.toLocalFile().split(' ', QString::SkipEmptyParts);
0273         if (!arguments.isEmpty()) {
0274             program = arguments.first();
0275             arguments.removeFirst();
0276 
0277             // in case we are running as AppImage, we need
0278             // to prepend "/bin/sh -c " here
0279             if (qEnvironmentVariableIsSet("APPDIR")) {
0280                 program = QStringLiteral("/bin/sh");
0281                 arguments.clear();
0282                 arguments << QStringLiteral("-c");
0283                 arguments << url.toLocalFile();
0284 
0285                 /*
0286                  * Adjust the LD_LIBRARY_PATH environment variable to exclude
0287                  * the AppImage mount point so that external tools do not try
0288                  * to use libs contained inside the AppImage
0289                  */
0290                 auto environment = QProcessEnvironment::systemEnvironment();
0291                 auto ld_library_path = environment.value(QLatin1String("LD_LIBRARY_PATH"));
0292                 qDebug() << "WebPriceQuote::launchNative";
0293                 qDebug() << "LD_LIBRARY_PATH" << ld_library_path;
0294                 if (!ld_library_path.isEmpty()) {
0295                     const auto appdir = environment.value(QLatin1String("APPDIR"));
0296                     qDebug() << "APPDIR" << appdir;
0297                     auto path_list = ld_library_path.split(QLatin1Char(':'));
0298                     while (!path_list.isEmpty() && path_list.at(0).startsWith(appdir)) {
0299                         path_list.removeAt(0);
0300                         ld_library_path.clear();
0301                         if (!path_list.isEmpty()) {
0302                             ld_library_path = path_list.join(QLatin1Char(':'));
0303                         }
0304                         if (!ld_library_path.isEmpty()) {
0305                             environment.insert(QLatin1String("LD_LIBRARY_PATH"), ld_library_path);
0306                             qDebug() << "LD_LIBRARY_PATH" << ld_library_path;
0307                         } else {
0308                             environment.remove(QLatin1String("LD_LIBRARY_PATH"));
0309                             qDebug() << "LD_LIBRARY_PATH removed";
0310                         }
0311                         d->m_filter.setProcessEnvironment(environment);
0312                     }
0313                 }
0314             }
0315         }
0316         d->m_filter.setWebID(d->m_webID);
0317 
0318         d->m_filter.setProcessChannelMode(QProcess::MergedChannels);
0319         d->m_filter.start(program, arguments);
0320 
0321         if (!d->m_filter.waitForStarted()) {
0322             emit error(i18n("Unable to launch: %1", url.toLocalFile()));
0323             slotParseQuote(QString());
0324         }
0325     } else {
0326         //silent download
0327         emit status(i18n("Fetching URL %1...", url.toDisplayString()));
0328         QString tmpFile;
0329         {
0330             QTemporaryFile tmpFileFile;
0331             tmpFileFile.setAutoRemove(false);
0332             if (tmpFileFile.open())
0333                 qDebug() << "created tmpfile";
0334 
0335             tmpFile = tmpFileFile.fileName();
0336         }
0337         QFile::remove(tmpFile);
0338         const QUrl dest = QUrl::fromLocalFile(tmpFile);
0339         KIO::Scheduler::checkSlaveOnHold(true);
0340         KIO::Job *job = KIO::file_copy(url, dest, -1, KIO::HideProgressInfo);
0341         connect(job, SIGNAL(result(KJob*)),
0342                 this, SLOT(downloadResult(KJob*)));
0343     }
0344     return true;
0345 }
0346 
0347 void WebPriceQuote::downloadCSV(KJob* job)
0348 {
0349     QString tmpFile = dynamic_cast<KIO::FileCopyJob*>(job)->destUrl().toLocalFile();
0350     QUrl url = dynamic_cast<KIO::FileCopyJob*>(job)->srcUrl();
0351     if (!job->error())
0352     {
0353         qDebug() << "Downloaded" << tmpFile << "from" << url;
0354         QFile f(tmpFile);
0355         if (f.open(QIODevice::ReadOnly)) {
0356             f.close();
0357             slotParseCSVQuote(tmpFile);
0358         } else {
0359             emit error(i18n("Failed to open downloaded file"));
0360             slotParseCSVQuote(QString());
0361         }
0362     } else {
0363         emit error(job->errorString());
0364         slotParseCSVQuote(QString());
0365     }
0366 }
0367 
0368 void WebPriceQuote::downloadResult(KJob* job)
0369 {
0370     QString tmpFile = dynamic_cast<KIO::FileCopyJob*>(job)->destUrl().toLocalFile();
0371     QUrl url = dynamic_cast<KIO::FileCopyJob*>(job)->srcUrl();
0372     if (!job->error())
0373     {
0374         qDebug() << "Downloaded" << tmpFile << "from" << url;
0375         QFile f(tmpFile);
0376         if (f.open(QIODevice::ReadOnly)) {
0377             // Find out the page encoding and convert it to unicode
0378             QByteArray page = f.readAll();
0379             KEncodingProber prober(KEncodingProber::Universal);
0380             prober.feed(page);
0381             QTextCodec* codec = QTextCodec::codecForName(prober.encoding());
0382             if (!codec)
0383                 codec = QTextCodec::codecForLocale();
0384             QString quote = codec->toUnicode(page);
0385             f.close();
0386             slotParseQuote(quote);
0387         } else {
0388             emit error(i18n("Failed to open downloaded file"));
0389             slotParseQuote(QString());
0390         }
0391         QFile::remove(tmpFile);
0392     } else {
0393         emit error(job->errorString());
0394         slotParseQuote(QString());
0395     }
0396 }
0397 
0398 bool WebPriceQuote::launchFinanceQuote(const QString& _webID, const QString& _kmmID,
0399                                        const QString& _sourcename)
0400 {
0401     bool result = true;
0402     d->m_webID = _webID;
0403     d->m_kmmID = _kmmID;
0404     QString FQSource = _sourcename.section(' ', 1);
0405     d->m_source = WebPriceQuoteSource(_sourcename, m_financeQuoteScriptPath, m_financeQuoteScriptPath,
0406                                       "\"([^,\"]*)\",.*",  // webIDRegExp
0407                                       WebPriceQuoteSource::identifyBy::Symbol,
0408                                       "[^,]*,[^,]*,\"([^\"]*)\"", // price regexp
0409                                       "[^,]*,([^,]*),.*", // date regexp
0410                                       "%y-%m-%d"); // date format
0411 
0412     //emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id));
0413 
0414     QStringList arguments;
0415     arguments << m_financeQuoteScriptPath << FQSource << KShell::quoteArg(_webID);
0416     d->m_filter.setWebID(d->m_webID);
0417     emit status(i18nc("Executing 'script' 'online source' 'investment symbol' ", "Executing %1 %2 %3...", m_financeQuoteScriptPath, FQSource, _webID));
0418 
0419     d->m_filter.setProcessChannelMode(QProcess::MergedChannels);
0420     d->m_filter.start(QLatin1Literal("perl"), arguments);
0421 
0422     // This seems to work best if we just block until done.
0423     if (d->m_filter.waitForFinished()) {
0424         result = true;
0425     } else {
0426         emit error(i18n("Unable to launch: %1", m_financeQuoteScriptPath));
0427         slotParseQuote(QString());
0428     }
0429 
0430     return result;
0431 }
0432 
0433 void WebPriceQuote::slotParseCSVQuote(const QString& filename)
0434 {
0435     bool isOK = true;
0436     if (filename.isEmpty())
0437         isOK = false;
0438     else {
0439         MyMoneyStatement st;
0440         CSVImporterCore* csvImporter = new CSVImporterCore;
0441         st = csvImporter->unattendedImport(filename, &d->m_CSVSource);
0442         if (!st.m_listPrices.isEmpty())
0443             emit csvquote(d->m_kmmID, d->m_webID, st);
0444         else
0445             isOK = false;
0446         delete csvImporter;
0447         QFile::remove(filename);
0448     }
0449 
0450     if (!isOK) {
0451         emit error(i18n("Unable to update price for %1", d->m_webID));
0452         emit failed(d->m_kmmID, d->m_webID);
0453     }
0454 }
0455 
0456 void WebPriceQuote::slotParseQuote(const QString& _quotedata)
0457 {
0458     QString quotedata = _quotedata;
0459     d->m_quoteData = quotedata;
0460 
0461     qCDebug(WEBPRICEQUOTE) << "quotedata" << _quotedata;
0462 
0463     if (! quotedata.isEmpty()) {
0464         if (!d->m_source.m_skipStripping) {
0465             // First, remove extraneous non-data elements
0466 
0467             // HTML tags
0468             quotedata.remove(QRegularExpression("<[^>]*>"));
0469 
0470             // &...;'s
0471             quotedata.replace(QRegularExpression("&\\w+;"), QLatin1String(" "));
0472 
0473             // Extra white space
0474             quotedata = quotedata.simplified();
0475             qCDebug(WEBPRICEQUOTE) << "stripped text" << quotedata;
0476         }
0477 
0478         QRegularExpression webIDRegExp(d->m_source.m_webID);
0479         QRegularExpression dateRegExp(d->m_source.m_date);
0480         QRegularExpression priceRegExp(d->m_source.m_price);
0481         QRegularExpressionMatch match;
0482 
0483         if (quotedata.indexOf(webIDRegExp, 0, &match) > -1) {
0484             qCDebug(WEBPRICEQUOTE) << "Identifier" << match.captured(1);
0485             emit status(i18n("Identifier found: '%1'", match.captured(1)));
0486         }
0487 
0488         bool gotprice = false;
0489         bool gotdate = false;
0490 
0491         if (quotedata.indexOf(priceRegExp, 0, &match) > -1) {
0492             gotprice = true;
0493             QString pricestr = match.captured(1);
0494 
0495             // Deal with exponential prices
0496             // we extract the exponent and add it again before we convert to a double
0497             QRegularExpression expRegExp("[eE][+-]?\\D+");
0498             int pos;
0499             QString exponent;
0500             if ((pos = pricestr.indexOf(expRegExp, 0, &match)) > -1) {
0501                 exponent = pricestr.mid(pos);
0502                 pricestr = pricestr.left(pos);
0503             }
0504 
0505             // Deal with european quotes that come back as X.XXX,XX or XX,XXX
0506             //
0507             // We will make the assumption that ALL prices have a decimal separator.
0508             // So "1,000" always means 1.0, not 1000.0.
0509             //
0510             // Remove all non-digits from the price string except the last one, and
0511             // set the last one to a period.
0512 
0513             pos = pricestr.lastIndexOf(QRegularExpression("\\D"));
0514             if (pos > 0) {
0515                 pricestr[pos] = QLatin1Char('.');
0516                 pos = pricestr.lastIndexOf(QRegularExpression("\\D"), pos - 1);
0517             }
0518             while (pos > 0) {
0519                 pricestr.remove(pos, 1);
0520                 pos = pricestr.lastIndexOf(QRegularExpression("\\D"), pos);
0521             }
0522             pricestr.append(exponent);
0523 
0524             d->m_price = pricestr.toDouble();
0525             qCDebug(WEBPRICEQUOTE) << "Price" << pricestr;
0526             emit status(i18n("Price found: '%1' (%2)", pricestr, d->m_price));
0527         }
0528 
0529         if (quotedata.indexOf(dateRegExp, 0, &match) > -1) {
0530             QString datestr = match.captured(1);
0531 
0532             MyMoneyDateFormat dateparse(d->m_source.m_dateformat);
0533             try {
0534                 d->m_date = dateparse.convertString(datestr, false /*strict*/);
0535                 gotdate = true;
0536                 qCDebug(WEBPRICEQUOTE) << "Date" << datestr;
0537                 emit status(i18n("Date found: '%1'", d->m_date.toString()));
0538             } catch (const MyMoneyException &) {
0539                 // emit error(i18n("Unable to parse date %1 using format %2: %3").arg(datestr,dateparse.format(),e.what()));
0540                 d->m_date = QDate::currentDate();
0541                 gotdate = true;
0542             }
0543         }
0544 
0545         if (gotprice && gotdate) {
0546             emit quote(d->m_kmmID, d->m_webID, d->m_date, d->m_price);
0547         } else {
0548             emit error(i18n("Unable to update price for %1 (no price or no date)", d->m_webID));
0549             emit failed(d->m_kmmID, d->m_webID);
0550         }
0551     } else {
0552         emit error(i18n("Unable to update price for %1 (empty quote data)", d->m_webID));
0553         emit failed(d->m_kmmID, d->m_webID);
0554     }
0555 }
0556 
0557 const QMap<QString, PricesProfile> WebPriceQuote::defaultCSVQuoteSources()
0558 {
0559     QMap<QString, PricesProfile> result;
0560 
0561     // tip: possible delimiter indexes are in csvenums.h
0562 
0563     result[QLatin1String("Stooq")] = PricesProfile(QLatin1String("Stooq"),
0564                                      106, 1, 0, DateFormat::YearMonthDay, FieldDelimiter::Semicolon,
0565                                      TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0566     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 4}},
0567     2, Profile::StockPrices);
0568 
0569     result[QLatin1String("Stooq Currency")] = PricesProfile(QLatin1String("Stooq Currency"),
0570             106, 1, 0, DateFormat::YearMonthDay, FieldDelimiter::Semicolon,
0571             TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0572     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 4}},
0573     2, Profile::CurrencyPrices);
0574 
0575     result[QLatin1String("Yahoo")] = PricesProfile(QLatin1String("Yahoo"),
0576                                      106, 1, 0, DateFormat::YearMonthDay, FieldDelimiter::Comma,
0577                                      TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0578     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 4}},
0579     2, Profile::StockPrices);
0580 
0581     result[QLatin1String("Nasdaq Baltic - Shares")] = PricesProfile(QLatin1String("Nasdaq Baltic - Shares"),
0582             106, 1, 0, DateFormat::DayMonthYear, FieldDelimiter::Tab,
0583             TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0584     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 5}},
0585     2, Profile::StockPrices);
0586 
0587     result[QLatin1String("Nasdaq Baltic - Funds")] = PricesProfile(QLatin1String("Nasdaq Baltic - Funds"),
0588             106, 1, 0, DateFormat::DayMonthYear, FieldDelimiter::Tab,
0589             TextDelimiter::DoubleQuote, DecimalSymbol::Dot,
0590     QMap<Column, int> {{Column::Date, 0}, {Column::Price, 5}},
0591     2, Profile::StockPrices);
0592     return result;
0593 }
0594 
0595 const QMap<QString, WebPriceQuoteSource> WebPriceQuote::defaultQuoteSources()
0596 {
0597     QMap<QString, WebPriceQuoteSource> result;
0598 
0599     // Use fx-rate.net as the standard currency exchange rate source until
0600     // we have the capability to use more than one source. Use a neutral
0601     // name for the source.
0602     result["KMyMoney Currency"] = WebPriceQuoteSource("KMyMoney Currency",
0603                                   "https://fx-rate.net/%1/%2",
0604                                   QString(),
0605                                   "https://fx-rate.net/([^/]+/[^/]+)",
0606                                   WebPriceQuoteSource::identifyBy::Symbol,
0607                                   "1\\s[^=]+\\s=</span><br\\s/>\\s([^\\s]+)",
0608                                   "updated\\s\\d+:\\d+:\\d+\\(\\w+\\)\\s+(\\d{1,2}/\\d{2}/\\d{4})",
0609                                   "%d/%m/%y",
0610                                   true // skip HTML stripping
0611                                                      );
0612 
0613     // Update on 2017-06 by Łukasz Wojniłowicz
0614     result["Globe & Mail"] = WebPriceQuoteSource("Globe & Mail",
0615                              "http://globefunddb.theglobeandmail.com/gishome/plsql/gis.price_history?pi_fund_id=%1",
0616                              QString(),
0617                              QString(),  // webIDRegExp
0618                              WebPriceQuoteSource::identifyBy::IdentificationNumber,
0619                              "Fund Price:\\D+(\\d+\\.\\d+)", // priceregexp
0620                              "Fund Price:.+as at (\\w+ \\d+, \\d+)\\)", // dateregexp
0621                              "%m %d %y" // dateformat
0622                                                 );
0623 
0624     // Update on 2017-06 by Łukasz Wojniłowicz
0625     result["MSN"] = WebPriceQuoteSource("MSN",
0626                                         "http://www.msn.com/en-us/money/stockdetails/%1",
0627                                         QString(),
0628                                         QString(),  // webIDRegExp
0629                                         WebPriceQuoteSource::identifyBy::Symbol,
0630                                         "(\\d+\\.\\d+) [+-]\\d+.\\d+", // priceregexp
0631                                         "(\\d+/\\d+/\\d+)", //dateregexp
0632                                         "%y %m %d" // dateformat
0633                                        );
0634 
0635     // Finanztreff (replaces VWD.DE) supplied by Michael Zimmerman
0636     result["Finanztreff"] = WebPriceQuoteSource("Finanztreff",
0637                             "http://finanztreff.de/kurse_einzelkurs_detail.htn?u=100&i=%1",
0638                             "",
0639                             QString(),  // webIDRegExp
0640                             WebPriceQuoteSource::identifyBy::IdentificationNumber,
0641                             "([0-9]+,\\d+).+Gattung:Fonds", // priceregexp
0642                             "\\).(\\d+\\D+\\d+\\D+\\d+)", // dateregexp (doesn't work; date in chart
0643                             "%d.%m.%y" // dateformat
0644                                                );
0645 
0646     // First revision by Michael Zimmerman
0647     // Update on 2017-06 by Łukasz Wojniłowicz
0648     result["boerseonlineaktien"] = WebPriceQuoteSource("Börse Online - Aktien",
0649                                    "http://www.boerse-online.de/aktie/%1-Aktie",
0650                                    QString(),
0651                                    QString(),  // webIDRegExp
0652                                    WebPriceQuoteSource::identifyBy::Name,
0653                                    "Aktienkurs\\D+(\\d+,\\d{2})", // priceregexp
0654                                    "Datum (\\d{2}\\.\\d{2}\\.\\d{4})", // dateregexp
0655                                    "%d.%m.%y" // dateformat
0656                                                       );
0657 
0658     // This quote source provided by e-mail and should replace the previous one:
0659     // From: David Houlden <djhoulden@gmail.com>
0660     // To: kmymoney@kde.org
0661     // Date: Sat, 6 Apr 2013 13:22:45 +0100
0662     // Updated on 2017-06 by Łukasz Wojniłowicz
0663     result["Financial Times - UK Funds"] = WebPriceQuoteSource("Financial Times",
0664                                            "http://funds.ft.com/uk/Tearsheet/Summary?s=%1",
0665                                            QString(),
0666                                            QString(), // webIDRegExp
0667                                            WebPriceQuoteSource::identifyBy::IdentificationNumber,
0668                                            "Price\\D+([\\d,]*\\d+\\.\\d+)", // price regexp
0669                                            "Data delayed at least 15 minutes, as of\\ (.*)\\.", // date regexp
0670                                            "%m %d %y", // date format
0671                                            true // skip HTML stripping
0672                                                               );
0673 
0674     // The following two price sources were contributed by
0675     // Marc Zahnlecker <tf2k@users.sourceforge.net>
0676 
0677     result["Wallstreet-Online.DE (Default)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Default)",
0678             "http://www.wallstreet-online.de/si/?k=%1&spid=ws",
0679             "",
0680             "Symbol:(\\w+)",  // webIDRegExp
0681             WebPriceQuoteSource::identifyBy::Symbol,
0682             "Letzter Kurs: ([0-9.]+,\\d+)", // priceregexp
0683             ", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp
0684             "%d %m %y" // dateformat
0685                                                                   );
0686 
0687     // (tf2k) The "mpid" is I think the market place id. In this case five
0688     // stands for Hamburg.
0689     //
0690     // Here the id for several market places: 2 Frankfurt, 3 Berlin, 4
0691     // Düsseldorf, 5 Hamburg, 6 München/Munich, 7 Hannover, 9 Stuttgart, 10
0692     // Xetra, 32 NASDAQ, 36 NYSE
0693 
0694     result["Wallstreet-Online.DE (Hamburg)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Hamburg)",
0695             "http://fonds.wallstreet-online.de/si/?k=%1&spid=ws&mpid=5",
0696             "",
0697             "Symbol:(\\w+)",  // webIDRegExp
0698             WebPriceQuoteSource::identifyBy::Symbol,
0699             "Fonds \\(EUR\\) ([0-9.]+,\\d+)", // priceregexp
0700             ", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp
0701             "%d %m %y" // dateformat
0702                                                                   );
0703     // First revision on 2017-06 by Łukasz Wojniłowicz
0704     result["Puls Biznesu"] = WebPriceQuoteSource("Puls Biznesu",
0705                              "http://notowania.pb.pl/instrument/%1",
0706                              QString(),
0707                              QString(),                   // webIDRegExp
0708                              WebPriceQuoteSource::identifyBy::IdentificationNumber,
0709                              "(\\d+,\\d{2})\\D+\\d+,\\d{2}%",    // price regexp
0710                              "(\\d{4}-\\d{2}-\\d{2}) \\d{2}:\\d{2}:\\d{2}", // date regexp
0711                              "%y %m %d"                   // date format
0712                                                 );
0713 
0714     result["Puls Biznesu - Funds"] = WebPriceQuoteSource("Puls Biznesu - Funds",
0715                                                          "http://notowania.pb.pl/instrument/%1",
0716                                                          QString(),
0717                                                          QString(), // webIDRegExp
0718                                                          WebPriceQuoteSource::identifyBy::IdentificationNumber,
0719                                                          "(\\d+,\\d{2})\\D+\\(\\d{4}-\\d{2}-\\d{2}\\)", // price regexp
0720                                                          "\\d+,\\d{2}\\D+\\((\\d{4}-\\d{2}-\\d{2})\\)", // date regexp
0721                                                          "%y-%m-%d" // date format
0722     );
0723 
0724     // The following price quote was contributed by
0725     // Piotr Adacha <piotr.adacha@googlemail.com>
0726 
0727     // I would like to post new Online Query Settings for KMyMoney. This set is
0728     // suitable to query stooq.com service, providing quotes for stocks, futures,
0729     // mutual funds and other financial instruments from Polish Gielda Papierow
0730     // Wartosciowych (GPW). Unfortunately, none of well-known international
0731     // services provide quotes for this market (biggest one in central and eastern
0732     // Europe), thus, I think it could be helpful for Polish users of KMyMoney (and
0733     // I am one of them for almost a year).
0734 
0735     // Update on 2017-06 by Łukasz Wojniłowicz
0736     result["Stooq"] = WebPriceQuoteSource("Stooq",
0737                                           "http://stooq.com/q/?s=%1",
0738                                           "http://stooq.pl/q/d/l/?s=%1&d1=%y%m%d&d2=%y%m%d&i=d&c=1",
0739                                           QString(),                   // webIDRegExp
0740                                           WebPriceQuoteSource::identifyBy::Symbol,
0741                                           "Last(\\d+\\.\\d+).*Date",    // price regexp
0742                                           "(\\d{4,4}-\\d{2,2}-\\d{2,2})", // date regexp
0743                                           "%y %m %d"                   // date format
0744                                          );
0745 
0746     // First revision on 2017-06 by Łukasz Wojniłowicz
0747     result[QLatin1String("Stooq Currency")] = WebPriceQuoteSource("Stooq Currency",
0748             "http://stooq.com/q/?s=%1%2",
0749             "http://stooq.pl/q/d/l/?s=%1%2&d1=%y%m%d&d2=%y%m%d&i=d&c=1",
0750             QString(),                   // webIDRegExp
0751             WebPriceQuoteSource::identifyBy::Symbol,
0752             "Last.*(\\d+\\.\\d+).*Date",    // price regexp
0753             "(\\d{4,4}-\\d{2,2}-\\d{2,2})", // date regexp
0754             "%y %m %d"                   // date format
0755                                                                  );
0756 
0757     // First revision on 2017-06 by Łukasz Wojniłowicz
0758     result["Nasdaq Baltic - Shares"] = WebPriceQuoteSource("Nasdaq Baltic - Shares",
0759                                        "http://www.nasdaqbaltic.com/market/?pg=details&instrument=%1&lang=en",
0760                                        "http://www.nasdaqbaltic.com/market/?instrument=%1&pg=details&tab=historical&lang=en&date=&start=%d.%m.%y&end=%d.%m.%y&pg=details&pg2=equity&downloadcsv=1&csv_style=english",
0761                                        QString(),  // webIDRegExp
0762                                        WebPriceQuoteSource::identifyBy::IdentificationNumber,
0763                                        "lastPrice\\D+(\\d+,\\d+)",  // priceregexp
0764                                        "as of: (\\d{2}.\\d{2}.\\d{4})",  // dateregexp
0765                                        "%d.%m.%y",   // dateformat
0766                                        true
0767                                                           );
0768 
0769     // First revision on 2017-06 by Łukasz Wojniłowicz
0770     result["Nasdaq Baltic - Funds"] = WebPriceQuoteSource("Nasdaq Baltic - Funds",
0771                                       "http://www.nasdaqbaltic.com/market/?pg=details&instrument=%1&lang=en",
0772                                       "http://www.nasdaqbaltic.com/market/?instrument=%1&pg=details&tab=historical&lang=en&date=&start=%d.%m.%y&end=%d.%m.%y&pg=details&pg2=equity&downloadcsv=1&csv_style=english",
0773                                       QString(),  // webIDRegExp
0774                                       WebPriceQuoteSource::identifyBy::IdentificationNumber,
0775                                       "marketShareDetailTable(.+\\n){21}\\D+(\\d+)",  // priceregexp
0776                                       "as of: (\\d{2}.\\d{2}.\\d{4})",  // dateregexp
0777                                       "%d.%m.%y",   // dateformat
0778                                       true
0779                                                          );
0780     // The following was contributed by Brendan Coupe <Brendan@CoupeWare.com>
0781     result["Yahoo Finance"] = WebPriceQuoteSource("Yahoo Finance",
0782                               "https://query1.finance.yahoo.com/v7/finance/quote?fields=regularMarketPrice&symbols=%1",
0783                               QString(),
0784                               "%1",  // webIDRegExp
0785                               WebPriceQuoteSource::identifyBy::Symbol,
0786                               "\"regularMarketPrice\":((\\d+|\\d{1,3}(?:[,]\\d{3})).\\d+)",  // priceregexp
0787                               "\"regularMarketTime\":([\\d]+)",  // dateregexp
0788                               QString(),
0789                               true
0790                                                  );
0791 
0792     return result;
0793 }
0794 
0795 const QStringList WebPriceQuote::quoteSources(const _quoteSystemE _system)
0796 {
0797     if (_system == Native)
0798         return (quoteSourcesNative());
0799     else
0800         return (quoteSourcesFinanceQuote());
0801 }
0802 
0803 const QStringList WebPriceQuote::quoteSourcesNative()
0804 {
0805     KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0806     QStringList groups = kconfig->groupList();
0807 
0808     QStringList::Iterator it;
0809     QRegularExpression onlineQuoteSource(QString("^Online-Quote-Source-(.*)$"));
0810     QRegularExpressionMatch match;
0811 
0812     // get rid of all 'non online quote source' entries
0813     // and only keep the names of the quote sources in the list
0814     // (i.e. remove the leading "Online-Quote-Source-")
0815     for (it = groups.begin(); it != groups.end(); it = groups.erase(it)) {
0816         if ((*it).indexOf(onlineQuoteSource, 0, &match) >= 0) {
0817             // Insert the name part
0818             it = groups.insert(it, match.captured(1));
0819             ++it;
0820         }
0821     }
0822 
0823     // if the user has the OLD quote source defined, now is the
0824     // time to remove that entry and convert it to the new system.
0825     if (! groups.count() && kconfig->hasGroup("Online Quotes Options")) {
0826         KConfigGroup grp = kconfig->group("Online Quotes Options");
0827         QString url(grp.readEntry("URL", "http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1"));
0828         QString webIDRegExp(grp.readEntry("SymbolRegex", "\"([^,\"]*)\",.*"));
0829         QString priceRegExp(grp.readEntry("PriceRegex", "[^,]*,([^,]*),.*"));
0830         QString dateRegExp(grp.readEntry("DateRegex", "[^,]*,[^,]*,\"([^\"]*)\""));
0831         kconfig->deleteGroup("Online Quotes Options");
0832 
0833         groups += "Old Source";
0834         grp = kconfig->group(QString(QLatin1String("Online-Quote-Source-%1")).arg("Old Source"));
0835         grp.writeEntry("URL", url);
0836         grp.writeEntry("CSVURL", "http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1");
0837         grp.writeEntry("IDRegex", webIDRegExp);
0838         grp.writeEntry("PriceRegex", priceRegExp);
0839         grp.writeEntry("DateRegex", dateRegExp);
0840         grp.writeEntry("DateFormatRegex", "%m %d %y");
0841         grp.sync();
0842     }
0843 
0844     // if the user has OLD quote source based only on symbols (and not ISIN)
0845     // now is the time to convert it to the new system.
0846     for (const auto& group : groups) {
0847         KConfigGroup grp = kconfig->group(QString(QLatin1String("Online-Quote-Source-%1")).arg(group));
0848         if (grp.hasKey("SymbolRegex")) {
0849             grp.writeEntry("IDRegex", grp.readEntry("SymbolRegex"));
0850             grp.deleteEntry("SymbolRegex");
0851         } else
0852             break;
0853     }
0854 
0855     // Set up each of the default sources.  These are done piecemeal so that
0856     // when we add a new source, it's automatically picked up. And any changes
0857     // are also picked up.
0858     QMap<QString, WebPriceQuoteSource> defaults = defaultQuoteSources();
0859     QMap<QString, WebPriceQuoteSource>::const_iterator it_source = defaults.constBegin();
0860     while (it_source != defaults.constEnd()) {
0861         if (! groups.contains((*it_source).m_name)) {
0862             groups += (*it_source).m_name;
0863             (*it_source).write();
0864             kconfig->sync();
0865         }
0866         ++it_source;
0867     }
0868 
0869     return groups;
0870 }
0871 
0872 const QStringList WebPriceQuote::quoteSourcesFinanceQuote()
0873 {
0874     if (m_financeQuoteSources.empty()) { // run the process one time only
0875         // since this is a static function it can be called without constructing an object
0876         // so we need to make sure that m_financeQuoteScriptPath is properly initialized
0877         if (m_financeQuoteScriptPath.isEmpty()) {
0878             m_financeQuoteScriptPath = QStandardPaths::locate(QStandardPaths::DataLocation, QString("misc/financequote.pl"));
0879         }
0880         FinanceQuoteProcess getList;
0881         getList.launch(m_financeQuoteScriptPath);
0882         while (!getList.isFinished()) {
0883             QCoreApplication::processEvents();
0884         }
0885         m_financeQuoteSources = getList.getSourceList();
0886     }
0887     return (m_financeQuoteSources);
0888 }
0889 
0890 //
0891 // Helper class to load/save an individual source
0892 //
0893 
0894 WebPriceQuoteSource::WebPriceQuoteSource(const QString& name, const QString& url, const QString &csvUrl, const QString& id, const identifyBy idBy, const QString& price, const QString& date, const QString& dateformat, bool skipStripping):
0895     m_name(name),
0896     m_url(url),
0897     m_csvUrl(csvUrl),
0898     m_webID(id),
0899     m_webIDBy(idBy),
0900     m_price(price),
0901     m_date(date),
0902     m_dateformat(dateformat),
0903     m_skipStripping(skipStripping)
0904 { }
0905 
0906 WebPriceQuoteSource::WebPriceQuoteSource(const QString& name)
0907 {
0908     m_name = name;
0909     KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0910     KConfigGroup grp = kconfig->group(QString("Online-Quote-Source-%1").arg(m_name));
0911     m_webID = grp.readEntry("IDRegex");
0912     m_webIDBy = static_cast<WebPriceQuoteSource::identifyBy>(grp.readEntry("IDBy", "0").toInt());
0913     m_date = grp.readEntry("DateRegex");
0914     m_dateformat = grp.readEntry("DateFormatRegex", "%m %d %y");
0915     m_price = grp.readEntry("PriceRegex");
0916     m_url = grp.readEntry("URL");
0917     m_csvUrl = grp.readEntry("CSVURL");
0918     m_skipStripping = grp.readEntry("SkipStripping", false);
0919 }
0920 
0921 void WebPriceQuoteSource::write() const
0922 {
0923     KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0924     KConfigGroup grp = kconfig->group(QString("Online-Quote-Source-%1").arg(m_name));
0925     grp.writeEntry("URL", m_url);
0926     grp.writeEntry("CSVURL", m_csvUrl);
0927     grp.writeEntry("PriceRegex", m_price);
0928     grp.writeEntry("DateRegex", m_date);
0929     grp.writeEntry("DateFormatRegex", m_dateformat);
0930     grp.writeEntry("IDRegex", m_webID);
0931     grp.writeEntry("IDBy", static_cast<int>(m_webIDBy));
0932     if (m_skipStripping)
0933         grp.writeEntry("SkipStripping", m_skipStripping);
0934     else
0935         grp.deleteEntry("SkipStripping");
0936     kconfig->sync();
0937     kconfig->reparseConfiguration();
0938 }
0939 
0940 void WebPriceQuoteSource::rename(const QString& name)
0941 {
0942     remove();
0943     m_name = name;
0944     write();
0945 }
0946 
0947 void WebPriceQuoteSource::remove() const
0948 {
0949     KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0950     kconfig->deleteGroup(QString("Online-Quote-Source-%1").arg(m_name));
0951     kconfig->sync();
0952     kconfig->reparseConfiguration();
0953 }
0954 
0955 //
0956 // Helper class to babysit the KProcess used for running the local script in that case
0957 //
0958 
0959 WebPriceQuoteProcess::WebPriceQuoteProcess()
0960 {
0961     connect(this, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter()));
0962     connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotProcessExited(int,QProcess::ExitStatus)));
0963 }
0964 
0965 void WebPriceQuoteProcess::slotReceivedDataFromFilter()
0966 {
0967 //   qDebug() << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << QString(data);
0968     m_string += QString(readAllStandardOutput());
0969 }
0970 
0971 void WebPriceQuoteProcess::slotProcessExited(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/)
0972 {
0973 //   qDebug() << "WebPriceQuoteProcess::slotProcessExited()";
0974     emit processExited(m_string);
0975     m_string.truncate(0);
0976 }
0977 
0978 //
0979 // Helper class to babysit the KProcess used for running the Finance Quote sources script
0980 //
0981 
0982 FinanceQuoteProcess::FinanceQuoteProcess()
0983 {
0984     m_isDone = false;
0985     m_string = "";
0986     m_fqNames["aex"] = "AEX";
0987     m_fqNames["aex_futures"] = "AEX Futures";
0988     m_fqNames["aex_options"] = "AEX Options";
0989     m_fqNames["amfiindia"] = "AMFI India";
0990     m_fqNames["asegr"] = "ASE";
0991     m_fqNames["asia"] = "Asia (Yahoo, ...)";
0992     m_fqNames["asx"] = "ASX";
0993     m_fqNames["australia"] = "Australia (ASX, Yahoo, ...)";
0994     m_fqNames["bmonesbittburns"] = "BMO NesbittBurns";
0995     m_fqNames["brasil"] = "Brasil (Yahoo, ...)";
0996     m_fqNames["canada"] = "Canada (Yahoo, ...)";
0997     m_fqNames["canadamutual"] = "Canada Mutual (Fund Library, ...)";
0998     m_fqNames["deka"] = "Deka Investments";
0999     m_fqNames["dutch"] = "Dutch (AEX, ...)";
1000     m_fqNames["dwsfunds"] = "DWS";
1001     m_fqNames["europe"] = "Europe (Yahoo, ...)";
1002     m_fqNames["fidelity"] = "Fidelity (Fidelity, ...)";
1003     m_fqNames["fidelity_direct"] = "Fidelity Direct";
1004     m_fqNames["financecanada"] = "Finance Canada";
1005     m_fqNames["ftportfolios"] = "First Trust (First Trust, ...)";
1006     m_fqNames["ftportfolios_direct"] = "First Trust Portfolios";
1007     m_fqNames["fundlibrary"] = "Fund Library";
1008     m_fqNames["greece"] = "Greece (ASE, ...)";
1009     m_fqNames["indiamutual"] = "India Mutual (AMFI, ...)";
1010     m_fqNames["maninv"] = "Man Investments";
1011     m_fqNames["fool"] = "Motley Fool";
1012     m_fqNames["nasdaq"] = "Nasdaq (Yahoo, ...)";
1013     m_fqNames["nz"] = "New Zealand (Yahoo, ...)";
1014     m_fqNames["nyse"] = "NYSE (Yahoo, ...)";
1015     m_fqNames["nzx"] = "NZX";
1016     m_fqNames["platinum"] = "Platinum Asset Management";
1017     m_fqNames["seb_funds"] = "SEB";
1018     m_fqNames["sharenet"] = "Sharenet";
1019     m_fqNames["za"] = "South Africa (Sharenet, ...)";
1020     m_fqNames["troweprice_direct"] = "T. Rowe Price";
1021     m_fqNames["troweprice"] = "T. Rowe Price";
1022     m_fqNames["tdefunds"] = "TD Efunds";
1023     m_fqNames["tdwaterhouse"] = "TD Waterhouse Canada";
1024     m_fqNames["tiaacref"] = "TIAA-CREF";
1025     m_fqNames["trustnet"] = "Trustnet";
1026     m_fqNames["uk_unit_trusts"] = "U.K. Unit Trusts";
1027     m_fqNames["unionfunds"] = "Union Investments";
1028     m_fqNames["tsp"] = "US Govt. Thrift Savings Plan";
1029     m_fqNames["usfedbonds"] = "US Treasury Bonds";
1030     m_fqNames["usa"] = "USA (Yahoo, Fool ...)";
1031     m_fqNames["vanguard"] = "Vanguard";
1032     m_fqNames["vwd"] = "VWD";
1033     m_fqNames["yahoo"] = "Yahoo";
1034     m_fqNames["yahoo_asia"] = "Yahoo Asia";
1035     m_fqNames["yahoo_australia"] = "Yahoo Australia";
1036     m_fqNames["yahoo_brasil"] = "Yahoo Brasil";
1037     m_fqNames["yahoo_europe"] = "Yahoo Europe";
1038     m_fqNames["yahoo_nz"] = "Yahoo New Zealand";
1039     m_fqNames["zifunds"] = "Zuerich Investments";
1040     connect(this, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter()));
1041     connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotProcessExited()));
1042     connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(slotProcessExited()));
1043 }
1044 
1045 void FinanceQuoteProcess::slotReceivedDataFromFilter()
1046 {
1047     auto data = QString::fromUtf8(readAllStandardOutput());
1048 
1049 #ifdef Q_OS_WIN
1050     // on Windows we need to change the CR-LF sequence into a simple LF
1051     // Otherwise, the CR will be appended to the name which is then
1052     // confusing the rest of the logic.
1053     data.remove(QLatin1Char('\r'));
1054 #endif
1055 
1056     // qDebug() << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << QString(data);
1057     m_string.append(data);
1058 }
1059 
1060 void FinanceQuoteProcess::slotProcessExited()
1061 {
1062     // qDebug() << "WebPriceQuoteProcess::slotProcessExited()";
1063     m_isDone = true;
1064 }
1065 
1066 void FinanceQuoteProcess::launch(const QString& scriptPath)
1067 {
1068     QStringList arguments;
1069     arguments << scriptPath << QLatin1Literal("-l");
1070     setProcessChannelMode(QProcess::SeparateChannels);
1071     start(QLatin1Literal("perl"), arguments);
1072     if (! waitForStarted()) qWarning("Unable to start FQ script");
1073     return;
1074 }
1075 
1076 const QStringList FinanceQuoteProcess::getSourceList() const
1077 {
1078     QStringList raw = m_string.split(QLatin1Char('\n'), QString::SkipEmptyParts);
1079     QStringList sources;
1080     QStringList::iterator it;
1081     for (it = raw.begin(); it != raw.end(); ++it) {
1082         if (m_fqNames[*it].isEmpty()) sources.append(*it);
1083         else sources.append(m_fqNames[*it]);
1084     }
1085     sources.sort();
1086     return (sources);
1087 }
1088 
1089 const QString FinanceQuoteProcess::crypticName(const QString& niceName) const
1090 {
1091     QString ret(niceName);
1092     fqNameMap::const_iterator it;
1093     for (it = m_fqNames.begin(); it != m_fqNames.end(); ++it) {
1094         if (niceName == it.value()) {
1095             ret = it.key();
1096             break;
1097         }
1098     }
1099     return (ret);
1100 }
1101 
1102 const QString FinanceQuoteProcess::niceName(const QString& crypticName) const
1103 {
1104     QString ret(m_fqNames[crypticName]);
1105     if (ret.isEmpty()) ret = crypticName;
1106     return (ret);
1107 }
1108 //
1109 // Universal date converter
1110 //
1111 
1112 // In 'strict' mode, this is designed to be compatible with the QIF profile date
1113 // converter.  However, that converter deals with the concept of an apostrophe
1114 // format in a way I don't understand.  So for the moment, they are 99%
1115 // compatible, waiting on that issue. (acejones)
1116 
1117 const QDate MyMoneyDateFormat::convertString(const QString& _in, bool _strict, unsigned _centurymidpoint) const
1118 {
1119     //
1120     // Break date format string into component parts
1121     //
1122 
1123     QRegularExpression formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)", QRegularExpression::CaseInsensitiveOption);
1124     QRegularExpressionMatch match;
1125     if (m_format.indexOf(formatrex, 0, &match) == -1) {
1126         throw MYMONEYEXCEPTION_CSTRING("Invalid format string");
1127     }
1128 
1129     QStringList formatParts;
1130     formatParts += match.captured(1);
1131     formatParts += match.captured(3);
1132     formatParts += match.captured(5);
1133 
1134     QStringList formatDelimiters;
1135     formatDelimiters += match.captured(2);
1136     formatDelimiters += match.captured(4);
1137     match = QRegularExpressionMatch();
1138 
1139     //
1140     // Break input string up into component parts,
1141     // using the delimiters found in the format string
1142     //
1143 
1144     QRegularExpression inputrex;
1145     inputrex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
1146 
1147     // strict mode means we must enforce the delimiters as specified in the
1148     // format.  non-strict allows any delimiters
1149     if (_strict)
1150         inputrex.setPattern(QString("(\\w+)%1(\\w+)%2(\\w+)").arg(formatDelimiters[0], formatDelimiters[1]));
1151     else
1152         inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)");
1153 
1154     if (_in.indexOf(inputrex, 0, &match) == -1) {
1155         throw MYMONEYEXCEPTION_CSTRING("Invalid input string");
1156     }
1157 
1158     QStringList scannedParts;
1159     scannedParts += match.captured(1).toLower();
1160     scannedParts += match.captured(2).toLower();
1161     scannedParts += match.captured(3).toLower();
1162     match = QRegularExpressionMatch();
1163 
1164     //
1165     // Convert the scanned parts into actual date components
1166     //
1167     unsigned day = 0, month = 0, year = 0;
1168     bool ok;
1169     QRegularExpression digitrex("(\\d+)");
1170     QStringList::const_iterator it_scanned = scannedParts.constBegin();
1171     QStringList::const_iterator it_format = formatParts.constBegin();
1172     while (it_scanned != scannedParts.constEnd()) {
1173         // decide upon the first character of the part
1174         switch ((*it_format).at(0).cell()) {
1175         case 'd':
1176             // remove any extraneous non-digits (e.g. read "3rd" as 3)
1177             ok = false;
1178             if ((*it_scanned).indexOf(digitrex, 0, &match) != -1)
1179                 day = match.captured(1).toUInt(&ok);
1180             if (!ok || day > 31)
1181                 throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid day entry: %1").arg(*it_scanned));
1182             break;
1183         case 'm':
1184             month = (*it_scanned).toUInt(&ok);
1185             if (!ok) {
1186                 month = 0;
1187                 // maybe it's a textual date
1188                 unsigned i = 1;
1189                 // search the name in the current selected locale
1190                 QLocale locale;
1191                 while (i <= 12) {
1192                     if (locale.standaloneMonthName(i).toLower() == *it_scanned
1193                             || locale.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) {
1194                         month = i;
1195                         break;
1196                     }
1197                     ++i;
1198                 }
1199                 // in case we did not find the month in the current locale,
1200                 // we look for it in the C locale
1201                 if(month == 0) {
1202                     QLocale localeC(QLocale::C);
1203                     if( !(locale == localeC)) {
1204                         i = 1;
1205                         while (i <= 12) {
1206                             if (localeC.standaloneMonthName(i).toLower() == *it_scanned
1207                                     || localeC.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) {
1208                                 month = i;
1209                                 break;
1210                             }
1211                             ++i;
1212                         }
1213                     }
1214                 }
1215             }
1216 
1217             if (month < 1 || month > 12)
1218                 throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid month entry: %1").arg(*it_scanned));
1219 
1220             break;
1221         case 'y':
1222             if (_strict && (*it_scanned).length() != (*it_format).length())
1223                 throw MYMONEYEXCEPTION(QString::fromLatin1("Length of year (%1) does not match expected length (%2).")
1224                                        .arg(*it_scanned, *it_format));
1225 
1226             year = (*it_scanned).toUInt(&ok);
1227 
1228             if (!ok)
1229                 throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid year entry: %1").arg(*it_scanned));
1230 
1231             //
1232             // 2-digit year case
1233             //
1234             // this algorithm will pick a year within +/- 50 years of the
1235             // centurymidpoint parameter.  i.e. if the midpoint is 2000,
1236             // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999
1237             if (year < 100) {
1238                 unsigned centuryend = _centurymidpoint + 50;
1239                 unsigned centurybegin = _centurymidpoint - 50;
1240 
1241                 if (year < centuryend % 100)
1242                     year += 100;
1243                 year += centurybegin - centurybegin % 100;
1244             }
1245 
1246             if (year < 1900)
1247                 throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid year (%1)").arg(year));
1248 
1249             break;
1250         default:
1251             throw MYMONEYEXCEPTION_CSTRING("Invalid format character");
1252         }
1253 
1254         ++it_scanned;
1255         ++it_format;
1256     }
1257     QDate result(year, month, day);
1258     if (! result.isValid())
1259         throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid date (yr%1 mo%2 dy%3)").arg(year).arg(month).arg(day));
1260 
1261     return result;
1262 }
1263 
1264 //
1265 // Unit test helpers
1266 //
1267 
1268 convertertest::QuoteReceiver::QuoteReceiver(WebPriceQuote* q, QObject* parent) :
1269     QObject(parent)
1270 {
1271     connect(q, SIGNAL(quote(QString,QString,QDate,double)),
1272             this, SLOT(slotGetQuote(QString,QString,QDate,double)));
1273     connect(q, SIGNAL(status(QString)),
1274             this, SLOT(slotStatus(QString)));
1275     connect(q, SIGNAL(error(QString)),
1276             this, SLOT(slotError(QString)));
1277 }
1278 
1279 convertertest::QuoteReceiver::~QuoteReceiver()
1280 {
1281 }
1282 
1283 void convertertest::QuoteReceiver::slotGetQuote(const QString&, const QString&, const QDate& d, const double& m)
1284 {
1285 //   qDebug() << "test::QuoteReceiver::slotGetQuote( , " << d << " , " << m.toString() << " )";
1286 
1287     m_price = MyMoneyMoney(m);
1288     m_date = d;
1289 }
1290 
1291 void convertertest::QuoteReceiver::slotStatus(const QString& msg)
1292 {
1293 //   qDebug() << "test::QuoteReceiver::slotStatus( " << msg << " )";
1294 
1295     m_statuses += msg;
1296 }
1297 
1298 void convertertest::QuoteReceiver::slotError(const QString& msg)
1299 {
1300 //   qDebug() << "test::QuoteReceiver::slotError( " << msg << " )";
1301 
1302     m_errors += msg;
1303 }
1304 
1305 // leave this moc until we will have resolved our dependency issues
1306 // now 'converter' depends on 'kmymoney' a pointer to the application
1307 // defined in main.cpp, which makes this static library unusable without
1308 // the --as-needed linker flag;
1309 // otherwise the 'moc' methods of this object will be linked into the automoc
1310 // object file which contains methods from all the other objects from this
1311 // library, thus even if the --as-needed option is used all objects will be
1312 // pulled in while linking 'convertertest' which only needs the WebPriceQuote
1313 // object - spent a whole day investigating this
1314 #include "moc_webpricequote.cpp"