File indexing completed on 2024-04-28 05:02:31

0001 /*
0002     SPDX-FileCopyrightText: 2004 Ace Jones acejones @users.sourceforge.net
0003     SPDX-FileCopyrightText: 2019 Thomas Baumgart tbaumgart @kde.org
0004     SPDX-FileCopyrightText: 2024 Ralf Habacker ralf.habacker @freenet.de
0005 
0006     This file is part of libalkimia.
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "alkonlinequote_p.h"
0012 
0013 #include "alkdateformat.h"
0014 #include "alkexception.h"
0015 #ifdef ENABLE_FINANCEQUOTE
0016 #include "alkfinancequoteprocess.h"
0017 #include "alkonlinequoteprocess.h"
0018 #endif
0019 #include "alkonlinequotesprofile.h"
0020 #include "alkonlinequotesprofilemanager.h"
0021 #include "alkonlinequotesource.h"
0022 #include "alkimia/alkversion.h"
0023 #include "alkwebpage.h"
0024 
0025 
0026 #ifdef BUILD_WITH_QTNETWORK
0027     #include <QNetworkAccessManager>
0028     #include <QNetworkRequest>
0029     #include <QNetworkReply>
0030     #include <QNetworkProxyFactory>
0031 #else
0032     #include <KIO/Scheduler>
0033 #endif
0034 
0035 #if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
0036     #include <KLocalizedString>
0037 #ifndef BUILD_WITH_QTNETWORK
0038     #include <KIO/Job>
0039 #endif
0040     #include <QDebug>
0041     #include <QTemporaryFile>
0042     #define kDebug(a) qDebug()
0043     #define KIcon QIcon
0044 #else
0045     #include <KDebug>
0046     #include <KGlobal>
0047     #include <KLocale>
0048 #ifndef BUILD_WITH_QTNETWORK
0049     #include <kio/netaccess.h>
0050 #endif
0051 #endif
0052 
0053 #include <KConfigGroup>
0054 #include <KEncodingProber>
0055 #include <KProcess>
0056 #include <KShell>
0057 
0058 #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
0059     #include <QRegExp>
0060     using Regex = QRegExp;
0061     #define hasRegexMatch(a) indexIn(a) != -1
0062     #define capturedText(txt, index) cap(index)
0063 #else
0064     #include <QRegularExpression>
0065     using Regex = QRegularExpression;
0066     #define hasRegexMatch(a) match(a).hasMatch()
0067     #define capturedText(txt, index) match(txt).captured(index)
0068 #endif
0069 
0070 #ifndef I18N_NOOP
0071 #include <KLazyLocalizedString>
0072 #endif
0073 
0074 AlkOnlineQuote::Private::Private(AlkOnlineQuote *parent)
0075     : m_p(parent)
0076     , m_eventLoop(nullptr)
0077     , m_ownProfile(false)
0078     , m_timeout(-1)
0079 {
0080 #ifdef ENABLE_FINANCEQUOTE
0081     connect(&m_filter, SIGNAL(processExited(QString)), this, SLOT(slotParseQuote(QString)));
0082 #endif
0083 }
0084 
0085 AlkOnlineQuote::Private::~Private()
0086 {
0087     if (m_ownProfile)
0088         delete m_profile;
0089 }
0090 
0091 #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
0092 int AlkOnlineQuote::Private::dbgArea()
0093 {
0094     static int s_area = KDebug::registerArea("Alkimia (AlkOnlineQuote)");
0095     return s_area;
0096 }
0097 #endif
0098 
0099 bool AlkOnlineQuote::Private::initLaunch(const QString &_symbol, const QString &_id, const QString &_source)
0100 {
0101     m_symbol = _symbol;
0102     m_id = _id;
0103     m_errors = Errors::None;
0104 
0105     Q_EMIT m_p->status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol, _id));
0106 
0107     // Get sources from the config file
0108     QString source = _source;
0109     if (source.isEmpty()) {
0110         source = "KMyMoney Currency";
0111     }
0112 
0113     if (!m_profile->quoteSources().contains(source)) {
0114         Q_EMIT m_p->error(i18n("Source <placeholder>%1</placeholder> does not exist.", source));
0115         m_errors |= Errors::Source;
0116         return false;
0117     }
0118 
0119     //m_profile->createSource(source);
0120     m_source = AlkOnlineQuoteSource(source, m_profile);
0121 
0122     KUrl url;
0123 
0124     // if the source has room for TWO symbols..
0125     if (m_source.url().contains("%2")) {
0126         // this is a two-symbol quote.  split the symbol into two.  valid symbol
0127         // characters are: 0-9, A-Z and the dot.  anything else is a separator
0128 #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
0129         QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive);
0130         // if we've truly found 2 symbols delimited this way...
0131         if (splitrx.indexIn(m_symbol) != -1) {
0132             url = KUrl(m_source.url().arg(splitrx.cap(1), splitrx.cap(2)));
0133         } else {
0134             kDebug(Private::dbgArea()) << QString("AlkOnlineQuote::Private::initLaunch() did not find 2 symbols in '%1'").arg(m_symbol);
0135         }
0136 #else
0137         QRegularExpression splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", QRegularExpression::CaseInsensitiveOption);
0138         const auto match = splitrx.match(m_symbol);
0139         // if we've truly found 2 symbols delimited this way...
0140         if (match.hasMatch()) {
0141             url = KUrl(m_source.url().arg(match.captured(1), match.captured(2)));
0142         } else {
0143             kDebug(Private::dbgArea()) << QString("AlkOnlineQuote::Private::initLaunch() did not find 2 symbols in '%1'").arg(m_symbol);
0144         }
0145 #endif
0146     } else {
0147         // a regular one-symbol quote
0148         url = KUrl(m_source.url().arg(m_symbol));
0149     }
0150 
0151     m_url = url;
0152 
0153     return true;
0154 }
0155 
0156 void AlkOnlineQuote::Private::slotLoadFinishedHtmlParser(bool ok)
0157 {
0158     if (!ok) {
0159         Q_EMIT m_p->error(i18n("Unable to fetch url for %1", m_symbol));
0160         m_errors |= Errors::URL;
0161         Q_EMIT m_p->failed(m_id, m_symbol);
0162     } else {
0163         // parse symbol
0164         slotParseQuote(AlkOnlineQuotesProfileManager::instance().webPage()->toHtml());
0165     }
0166     if (m_eventLoop)
0167         m_eventLoop->exit();
0168 }
0169 
0170 void AlkOnlineQuote::Private::slotLoadFinishedCssSelector(bool ok)
0171 {
0172     if (!ok) {
0173         Q_EMIT m_p->error(i18n("Unable to fetch url for %1", m_symbol));
0174         m_errors |= Errors::URL;
0175         Q_EMIT m_p->failed(m_id, m_symbol);
0176     } else {
0177         AlkWebPage *webPage = AlkOnlineQuotesProfileManager::instance().webPage();
0178         // parse symbol
0179         QString identifier = webPage->getFirstElement(m_source.idRegex());
0180         if (!identifier.isEmpty()) {
0181             Q_EMIT m_p->status(i18n("Symbol found: '%1'", identifier));
0182         } else {
0183             m_errors |= Errors::Symbol;
0184             Q_EMIT m_p->error(i18n("Unable to parse symbol for %1", m_symbol));
0185         }
0186 
0187         // parse price
0188         QString price = webPage->getFirstElement(m_source.priceRegex());
0189         bool gotprice = parsePrice(price);
0190 
0191         // parse date
0192         QString date = webPage->getFirstElement(m_source.dateRegex());
0193         bool gotdate = parseDate(date);
0194 
0195         if (gotprice && gotdate) {
0196             Q_EMIT m_p->quote(m_id, m_symbol, m_date, m_price);
0197         } else {
0198             Q_EMIT m_p->failed(m_id, m_symbol);
0199         }
0200     }
0201     if (m_eventLoop)
0202         m_eventLoop->exit();
0203 }
0204 
0205 void AlkOnlineQuote::Private::slotLoadStarted()
0206 {
0207     Q_EMIT m_p->status(i18n("Fetching URL %1...", m_url.prettyUrl()));
0208 }
0209 
0210 bool AlkOnlineQuote::Private::launchWebKitCssSelector(const QString &_symbol, const QString &_id,
0211                                              const QString &_source)
0212 {
0213     if (!initLaunch(_symbol, _id, _source)) {
0214         return false;
0215     }
0216     AlkWebPage *webPage = AlkOnlineQuotesProfileManager::instance().webPage();
0217     connect(webPage, SIGNAL(loadStarted()), this, SLOT(slotLoadStarted()));
0218     connect(webPage, SIGNAL(loadFinished(bool)), this,
0219             SLOT(slotLoadFinishedCssSelector(bool)));
0220     if (m_timeout != -1)
0221         QTimer::singleShot(m_timeout, this, SLOT(slotLoadTimeout()));
0222     webPage->setUrl(m_url);
0223     m_eventLoop = new QEventLoop;
0224     m_eventLoop->exec();
0225     delete m_eventLoop;
0226     m_eventLoop = nullptr;
0227     disconnect(webPage, SIGNAL(loadStarted()), this, SLOT(slotLoadStarted()));
0228     disconnect(webPage, SIGNAL(loadFinished(bool)), this,
0229                SLOT(slotLoadFinishedCssSelector(bool)));
0230 
0231     return !(m_errors & Errors::URL || m_errors & Errors::Price
0232              || m_errors & Errors::Date || m_errors & Errors::Data);
0233 }
0234 
0235 bool AlkOnlineQuote::Private::launchWebKitHtmlParser(const QString &_symbol, const QString &_id,
0236                                             const QString &_source)
0237 {
0238     if (!initLaunch(_symbol, _id, _source)) {
0239         return false;
0240     }
0241     AlkWebPage *webPage = AlkOnlineQuotesProfileManager::instance().webPage();
0242     connect(webPage, SIGNAL(loadStarted()), this, SLOT(slotLoadStarted()));
0243     connect(webPage, SIGNAL(loadFinished(bool)), this, SLOT(slotLoadFinishedHtmlParser(bool)));
0244     if (m_timeout != -1)
0245         QTimer::singleShot(m_timeout, this, SLOT(slotLoadTimeout()));
0246     webPage->load(m_url, m_acceptLanguage);
0247     m_eventLoop = new QEventLoop;
0248     m_eventLoop->exec();
0249     delete m_eventLoop;
0250     m_eventLoop = nullptr;
0251     disconnect(webPage, SIGNAL(loadStarted()), this, SLOT(slotLoadStarted()));
0252     disconnect(webPage, SIGNAL(loadFinished(bool)), this, SLOT(slotLoadFinishedHtmlParser(bool)));
0253 
0254     return !(m_errors & Errors::URL || m_errors & Errors::Price
0255              || m_errors & Errors::Date || m_errors & Errors::Data);
0256 }
0257 
0258 bool AlkOnlineQuote::Private::launchNative(const QString &_symbol, const QString &_id,
0259                                const QString &_source)
0260 {
0261     bool result = true;
0262     if (!initLaunch(_symbol, _id, _source)) {
0263         return false;
0264     }
0265 
0266     KUrl url = m_url;
0267     if (url.isLocalFile()) {
0268 #ifdef ENABLE_FINANCEQUOTE
0269         result = processLocalScript(url);
0270 #endif
0271     } else {
0272         slotLoadStarted();
0273         result = downloadUrl(url);
0274     }
0275     return result;
0276 }
0277 
0278 #ifdef ENABLE_FINANCEQUOTE
0279 bool AlkOnlineQuote::Private::processLocalScript(const KUrl& url)
0280 {
0281     Q_EMIT m_p->status(i18nc("The process x is executing", "Executing %1...", url.toLocalFile()));
0282 
0283     bool result = true;
0284 
0285     m_filter.clearProgram();
0286 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
0287     m_filter << url.toLocalFile().split(' ', QString::SkipEmptyParts);
0288 #else
0289     m_filter << url.toLocalFile().split(' ', Qt::SkipEmptyParts);
0290 #endif
0291     m_filter.setSymbol(m_symbol);
0292 
0293     m_filter.setOutputChannelMode(KProcess::MergedChannels);
0294     m_filter.start();
0295 
0296     // This seems to work best if we just block until done.
0297     if (!m_filter.waitForFinished()) {
0298         Q_EMIT m_p->error(i18n("Unable to launch: %1", url.toLocalFile()));
0299         m_errors |= Errors::Script;
0300         result = slotParseQuote(QString());
0301     }
0302     return result;
0303 }
0304 #endif
0305 
0306 bool AlkOnlineQuote::Private::processDownloadedFile(const KUrl& url, const QString& tmpFile)
0307 {
0308   bool result = false;
0309 
0310   QFile f(tmpFile);
0311   if (f.open(QIODevice::ReadOnly)) {
0312     // Find out the page encoding and convert it to unicode
0313     QByteArray page = f.readAll();
0314     result = processDownloadedPage(url, page);
0315     f.close();
0316   } else {
0317     Q_EMIT m_p->error(i18n("Failed to open downloaded file"));
0318     m_errors |= Errors::URL;
0319     result = slotParseQuote(QString());
0320   }
0321   return result;
0322 }
0323 
0324 bool AlkOnlineQuote::Private::processDownloadedPage(const KUrl& url, const QByteArray& page)
0325 {
0326     bool result = false;
0327     KEncodingProber prober(KEncodingProber::Universal);
0328     prober.feed(page);
0329     QTextCodec *codec = QTextCodec::codecForName(prober.encoding());
0330     if (!codec) {
0331       codec = QTextCodec::codecForLocale();
0332     }
0333     QString quote = codec->toUnicode(page);
0334     Q_EMIT m_p->status(i18n("URL found: %1...", url.prettyUrl()));
0335     if (AlkOnlineQuotesProfileManager::instance().webPageEnabled())
0336       AlkOnlineQuotesProfileManager::instance().webPage()->setContent(quote.toLocal8Bit());
0337     result = slotParseQuote(quote);
0338     return result;
0339 }
0340 
0341 #ifndef BUILD_WITH_QTNETWORK
0342 #if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
0343 
0344 bool AlkOnlineQuote::Private::downloadUrl(const QUrl& url)
0345 {
0346     // Create a temporary filename (w/o leaving the file on the filesystem)
0347     // In case the file is still present, the KIO::file_copy operation cannot
0348     // be performed on some operating systems (Windows).
0349     auto tmpFile = new QTemporaryFile;
0350     tmpFile->open();
0351     auto tmpFileName = QUrl::fromLocalFile(tmpFile->fileName());
0352     delete tmpFile;
0353 
0354     m_eventLoop = new QEventLoop;
0355     KJob *job = KIO::file_copy(url, tmpFileName, -1, KIO::HideProgressInfo);
0356     connect(job, SIGNAL(result(KJob*)), this, SLOT(downloadUrlDone(KJob*)));
0357 
0358     if (m_timeout != -1)
0359         QTimer::singleShot(m_timeout, this, SLOT(slotLoadTimeout()));
0360     auto result = m_eventLoop->exec(QEventLoop::ExcludeUserInputEvents);
0361     delete m_eventLoop;
0362     m_eventLoop = nullptr;
0363 
0364     return result;
0365 }
0366 
0367 void AlkOnlineQuote::Private::downloadUrlDone(KJob* job)
0368 {
0369   QString tmpFileName = dynamic_cast<KIO::FileCopyJob*>(job)->destUrl().toLocalFile();
0370   QUrl url = dynamic_cast<KIO::FileCopyJob*>(job)->srcUrl();
0371 
0372   bool result;
0373   if (!job->error()) {
0374     qDebug() << "Downloaded" << tmpFileName << "from" << url;
0375     result = processDownloadedFile(url, tmpFileName);
0376   } else {
0377     Q_EMIT m_p->error(job->errorString());
0378     m_errors |= Errors::URL;
0379     result = slotParseQuote(QString());
0380   }
0381   m_eventLoop->exit(result);
0382 }
0383 
0384 #else // QT_VERSION
0385 
0386 // This is simply a placeholder. It is unused but needs to be present
0387 // to make the linker happy (since the declaration of the slot cannot
0388 // be made dependendant on QT_VERSION with the Qt4 moc compiler.
0389 void AlkOnlineQuote::Private::downloadUrlDone(KJob* job)
0390 {
0391     Q_UNUSED(job);
0392 }
0393 
0394 bool AlkOnlineQuote::Private::downloadUrl(const KUrl& url)
0395 {
0396     bool result = false;
0397 
0398     QString tmpFile;
0399     if (KIO::NetAccess::download(url, tmpFile, nullptr)) {
0400         // kDebug(Private::dbgArea()) << "Downloaded " << tmpFile;
0401         kDebug(Private::dbgArea()) << "Downloaded" << tmpFile << "from" << url;
0402         result = processDownloadedFile(url, tmpFile);
0403         KIO::NetAccess::removeTempFile(tmpFile);
0404     } else {
0405         Q_EMIT m_p->error(KIO::NetAccess::lastErrorString());
0406         m_errors |= Errors::URL;
0407         result = slotParseQuote(QString());
0408     }
0409     return result;
0410 }
0411 
0412 #endif // QT_VERSION
0413 #else // BUILD_WITH_QTNETWORK
0414 
0415 void AlkOnlineQuote::Private::downloadUrlDone(QNetworkReply *reply)
0416 {
0417     int result = 0;
0418     if (reply->error() == QNetworkReply::NoError) {
0419         QUrl newUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0420         if (!newUrl.isEmpty() && newUrl != reply->url()) {
0421             m_url = reply->url().resolved(newUrl);
0422             // TODO migrate to i18n()
0423             Q_EMIT m_p->status(QString("<font color=\"orange\">%1</font>")
0424             #ifdef I18N_NOOP
0425                             .arg(I18N_NOOP("The URL has been redirected; check an update of the online quote URL")));
0426             #else
0427                             .arg(kli18n("The URL has been redirected; check an update of the online quote URL").untranslatedText()));
0428             #endif
0429             result = 2;
0430         } else {
0431             kDebug(Private::dbgArea()) << "Downloaded data from" << reply->url();
0432             result = processDownloadedPage(KUrl(reply->url()), reply->readAll()) ? 0 : 1;
0433         }
0434     } else {
0435         Q_EMIT m_p->error(reply->errorString());
0436         m_errors |= Errors::URL;
0437         result = slotParseQuote(QString()) ? 0 : 1;
0438     }
0439     m_eventLoop->exit(result);
0440 }
0441 
0442 bool AlkOnlineQuote::Private::downloadUrl(const KUrl &url)
0443 {
0444     QNetworkAccessManager manager(this);
0445     connect(&manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadUrlDone(QNetworkReply*)));
0446 
0447     QNetworkRequest request;
0448     request.setUrl(url);
0449     request.setRawHeader("User-Agent", "alkimia " ALK_VERSION_STRING);
0450     manager.get(request);
0451 
0452     if (m_timeout != -1)
0453         QTimer::singleShot(m_timeout, this, SLOT(slotLoadTimeout()));
0454     m_eventLoop = new QEventLoop;
0455     int result = m_eventLoop->exec(QEventLoop::ExcludeUserInputEvents);
0456     delete m_eventLoop;
0457     m_eventLoop = nullptr;
0458     if (result == 2) {
0459         QNetworkRequest req;
0460         req.setUrl(m_url);
0461         req.setRawHeader("User-Agent", "alkimia " ALK_VERSION_STRING);
0462         manager.get(req);
0463 
0464         if (m_timeout != -1)
0465             QTimer::singleShot(m_timeout, this, SLOT(slotLoadTimeout()));
0466         m_eventLoop = new QEventLoop;
0467         result = m_eventLoop->exec(QEventLoop::ExcludeUserInputEvents);
0468         delete m_eventLoop;
0469         m_eventLoop = nullptr;
0470     }
0471     return result == 0;
0472 }
0473 #endif // BUILD_WITH_QTNETWORK
0474 
0475 #ifdef ENABLE_FINANCEQUOTE
0476 bool AlkOnlineQuote::Private::launchFinanceQuote(const QString &_symbol, const QString &_id,
0477                                         const QString &_sourcename)
0478 {
0479     bool result = true;
0480     m_symbol = _symbol;
0481     m_id = _id;
0482     m_errors = Errors::None;
0483     m_source = AlkOnlineQuoteSource(_sourcename,
0484                                     m_profile->scriptPath(),
0485                                     "\"([^,\"]*)\",.*", // symbol regexp
0486                                     AlkOnlineQuoteSource::IdSelector::Symbol,
0487                                     "[^,]*,[^,]*,\"([^\"]*)\"", // price regexp
0488                                     "[^,]*,([^,]*),.*", // date regexp
0489                                     "%y-%m-%d"); // date format
0490 
0491     //Q_EMIT status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id));
0492     AlkFinanceQuoteProcess tmp;
0493     QString fQSource = m_profile->type() == AlkOnlineQuotesProfile::Type::Script ?
0494                 tmp.crypticName(_sourcename) : _sourcename.section(' ', 1);
0495 
0496     QStringList args;
0497     args << "perl" << m_profile->scriptPath() << fQSource << m_symbol;
0498     m_filter.clearProgram();
0499     m_filter << args;
0500     Q_EMIT m_p->status(i18nc("Executing 'script' 'online source' 'investment symbol' ",
0501                       "Executing %1 %2 %3...", args.join(" "), QString(), QString()));
0502 
0503     m_filter.setOutputChannelMode(KProcess::MergedChannels);
0504     m_filter.start();
0505 
0506     // This seems to work best if we just block until done.
0507     if (m_filter.waitForFinished()) {
0508     } else {
0509         Q_EMIT m_p->error(i18n("Unable to launch: %1", m_profile->scriptPath()));
0510         m_errors |= Errors::Script;
0511         result = slotParseQuote(QString());
0512     }
0513     return result;
0514 }
0515 #endif
0516 
0517 bool AlkOnlineQuote::Private::parsePrice(const QString &_pricestr)
0518 {
0519     bool result = true;
0520     // not made static due to QRegExp
0521     const Regex nonDigitChar("\\D");
0522     const Regex validChars("^\\s*([0-9,.\\s]*[0-9,.])");
0523 
0524     if (validChars.hasRegexMatch(_pricestr)) {
0525         // Deal with european quotes that come back as X.XXX,XX or XX,XXX
0526         //
0527         // We will make the assumption that ALL prices have a decimal separator.
0528         // So "1,000" always means 1.0, not 1000.0.
0529         //
0530 
0531         // Remove all non-digits from the price string except the last one, and
0532         // set the last one to a period.
0533         QString pricestr = validChars.capturedText(_pricestr, 1);
0534 
0535         int pos = pricestr.lastIndexOf(Regex("\\D"));
0536         if (pos > 0) {
0537             pricestr[pos] = '.';
0538             pos = pricestr.lastIndexOf(Regex("\\D"), pos - 1);
0539         }
0540         while (pos > 0) {
0541             pricestr.remove(pos, 1);
0542             pos = pricestr.lastIndexOf(Regex("\\D"), pos);
0543         }
0544 
0545         bool ok;
0546         m_price = pricestr.toDouble(&ok);
0547         if (ok) {
0548             kDebug(Private::dbgArea()) << "Price" << pricestr;
0549             Q_EMIT m_p->status(i18n("Price found: '%1' (%2)", pricestr, m_price));
0550         } else {
0551             m_errors |= Errors::Price;
0552             Q_EMIT m_p->error(i18n("Price '%1' cannot be converted to a number for '%2'", pricestr, m_symbol));
0553             result = false;
0554         }
0555     } else {
0556         m_errors |= Errors::Price;
0557         Q_EMIT m_p->error(i18n("Unable to parse price for '%1'", m_symbol));
0558         result = false;
0559         m_price = 0.0;
0560     }
0561     return result;
0562 }
0563 
0564 bool AlkOnlineQuote::Private::parseDate(const QString &datestr)
0565 {
0566     if (!datestr.isEmpty()) {
0567         Q_EMIT m_p->status(i18n("Date found: '%1'", datestr));
0568 
0569         AlkDateFormat dateparse(m_source.dateFormat());
0570         try {
0571             m_date = dateparse.convertString(datestr, false /*strict*/);
0572             kDebug(Private::dbgArea()) << "Date" << datestr;
0573             Q_EMIT m_p->status(i18n("Date format found: '%1' -> '%2'", datestr, m_date.toString()));
0574         } catch (const AlkException &e) {
0575             m_errors |= Errors::DateFormat;
0576             Q_EMIT m_p->error(i18n("Unable to parse date '%1' using format '%2': %3", datestr,
0577                                                                                dateparse.format(),
0578                                                                                e.what()));
0579             m_date = QDate::currentDate();
0580             Q_EMIT m_p->status(i18n("Using current date for '%1'", m_symbol));
0581         }
0582     } else {
0583         if (m_source.dateRegex().isEmpty()) {
0584             Q_EMIT m_p->status(i18n("Parsing date is disabled for '%1'", m_symbol));
0585         } else {
0586             m_errors |= Errors::Date;
0587             Q_EMIT m_p->error(i18n("Unable to parse date for '%1'", m_symbol));
0588         }
0589         m_date = QDate::currentDate();
0590         Q_EMIT m_p->status(i18n("Using current date for '%1'", m_symbol));
0591     }
0592     return true;
0593 }
0594 
0595 /**
0596  * Parse quote data expected as stripped html
0597  *
0598  * @param quotedata quote data to parse
0599  * @return true parsing successful
0600  * @return false parsing unsuccessful
0601  */
0602 bool AlkOnlineQuote::Private::parseQuoteStripHTML(const QString &_quotedata)
0603 {
0604     QString quotedata = _quotedata;
0605 
0606     //
0607     // First, remove extraneous non-data elements
0608     //
0609 
0610     // HTML tags
0611     quotedata.remove(Regex("<[^>]*>"));
0612 
0613     // &...;'s
0614     quotedata.replace(Regex("&\\w+;"), " ");
0615 
0616     // Extra white space
0617     quotedata = quotedata.simplified();
0618     kDebug(Private::dbgArea()) << "stripped text" << quotedata;
0619 
0620     return parseQuoteHTML(quotedata);
0621 }
0622 
0623 /**
0624  * Parse quote data in html format
0625  *
0626  * @param quotedata quote data to parse
0627  * @return true parsing successful
0628  * @return false parsing unsuccessful
0629  */
0630 bool AlkOnlineQuote::Private::parseQuoteHTML(const QString &quotedata)
0631 {
0632     bool gotprice = false;
0633     bool gotdate = false;
0634     bool result = true;
0635 
0636     Regex identifierRegExp(m_source.idRegex());
0637     Regex dateRegExp(m_source.dateRegex());
0638     Regex priceRegExp(m_source.priceRegex());
0639 
0640 #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
0641 
0642     if (identifierRegExp.indexIn(quotedata) > -1) {
0643         kDebug(Private::dbgArea()) << "Symbol" << identifierRegExp.cap(1);
0644         Q_EMIT m_p->status(i18n("Symbol found: '%1'", identifierRegExp.cap(1)));
0645     } else {
0646         m_errors |= Errors::Symbol;
0647         Q_EMIT m_p->error(i18n("Unable to parse symbol for %1", m_symbol));
0648     }
0649 
0650     if (priceRegExp.indexIn(quotedata) > -1) {
0651         QString pricestr = priceRegExp.cap(1);
0652         gotprice = parsePrice(pricestr);
0653     } else {
0654         gotprice = parsePrice(QString());
0655     }
0656 
0657     if (dateRegExp.indexIn(quotedata) > -1) {
0658         QString datestr = dateRegExp.cap(1);
0659         gotdate = parseDate(datestr);
0660     } else {
0661         gotdate = parseDate(QString());
0662     }
0663 
0664 #else
0665 
0666     QRegularExpressionMatch match;
0667     match = identifierRegExp.match(quotedata);
0668     if (match.hasMatch()) {
0669         kDebug(Private::dbgArea()) << "Symbol" << match.captured(1);
0670         Q_EMIT m_p->status(i18n("Symbol found: '%1'", match.captured(1)));
0671     } else {
0672         m_errors |= Errors::Symbol;
0673         Q_EMIT m_p->error(i18n("Unable to parse symbol for %1", m_symbol));
0674     }
0675 
0676     match = priceRegExp.match(quotedata);
0677     if (match.hasMatch()) {
0678         QString pricestr = match.captured(1);
0679         gotprice = parsePrice(pricestr);
0680     } else {
0681         gotprice = parsePrice(QString());
0682     }
0683 
0684     match = dateRegExp.match(quotedata);
0685     if (match.hasMatch()) {
0686         QString datestr = match.captured(1);
0687         gotdate = parseDate(datestr);
0688     } else {
0689         gotdate = parseDate(QString());
0690     }
0691 
0692 #endif
0693 
0694     if (gotprice && gotdate) {
0695         Q_EMIT m_p->quote(m_id, m_symbol, m_date, m_price);
0696     } else {
0697         Q_EMIT m_p->failed(m_id, m_symbol);
0698         result = false;
0699     }
0700     return result;
0701 }
0702 
0703 /**
0704  * Parse quote data in csv format
0705  *
0706  * @param quotedata quote data to parse
0707  * @return true parsing successful
0708  * @return false parsing unsuccessful
0709  */
0710 bool AlkOnlineQuote::Private::parseQuoteCSV(const QString &quotedata)
0711 {
0712     QString dateColumn(m_source.dateRegex());
0713     QString priceColumn(m_source.priceRegex());
0714     QStringList lines = quotedata.split(QRegExp("\r?\n"));
0715     QString header = lines.first();
0716     QString columnSeparator;
0717     Regex rx("([,;\t])");
0718 
0719 #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
0720 
0721     if (rx.indexIn(header) != -1) {
0722         columnSeparator = rx.cap(1);
0723     }
0724 
0725 #else
0726     const auto match = rx.match(header);
0727     if (match.hasMatch()) {
0728         columnSeparator = match.captured(1);
0729     }
0730 
0731 #endif
0732 
0733     if (columnSeparator.isEmpty()) {
0734         m_errors |= Errors::Source;
0735         Q_EMIT m_p->error(i18n("Unable to detect field delimiter in first line (header line) of quote data."));
0736         Q_EMIT m_p->failed(m_id, m_symbol);
0737         return false;
0738     }
0739     const QChar decimalSeparator = (columnSeparator.at(0) == ';') ? QLatin1Char(',') : QLatin1Char('.');
0740 
0741     // detect column index
0742     int dateCol = -1;
0743     int priceCol = -1;
0744     // check if column numbers are given
0745     if (dateColumn.startsWith(QLatin1Char('#')) && priceColumn.startsWith(QLatin1Char('#'))) {
0746         dateColumn.remove(0,1);
0747         priceColumn.remove(0,1);
0748         dateCol = dateColumn.toInt() - 1;
0749         priceCol = priceColumn.toInt() - 1;
0750     } else { // find columns
0751         QStringList headerColumns = header.split(columnSeparator);
0752         for (int i = 0; i < headerColumns.size(); i++) {
0753             if (headerColumns[i].contains(dateColumn))
0754                 dateCol = i;
0755             else if (headerColumns[i].contains(priceColumn))
0756                 priceCol = i;
0757         }
0758         lines.takeFirst();
0759     }
0760     if (dateCol == -1) {
0761         m_errors |= Errors::Date;
0762         Q_EMIT m_p->error(i18n("Unable to find date column '%1' in quote data", dateColumn));
0763         Q_EMIT m_p->failed(m_id, m_symbol);
0764         return false;
0765     }
0766     if (priceCol == -1) {
0767         m_errors |= Errors::Price;
0768         Q_EMIT m_p->error(i18n("Unable to find price column '%1' in quote data", priceColumn));
0769         Q_EMIT m_p->failed(m_id, m_symbol);
0770         return false;
0771     }
0772     AlkDatePriceMap prices;
0773     for (const auto &line : lines) {
0774         if (line.trimmed().isEmpty())
0775             continue;
0776         QStringList cols = line.split(columnSeparator);
0777         QString dateValue = cols[dateCol].trimmed();
0778         QString priceValue = cols[priceCol].trimmed();
0779         if (dateValue.isEmpty() || priceValue.isEmpty())
0780             continue;
0781         // @todo: auto detect format
0782         AlkDateFormat dateFormat(m_source.dateFormat());
0783         QDate date = dateFormat.convertString(dateValue, false);
0784         if (!date.isValid()) {
0785             m_errors |= Errors::DateFormat;
0786             Q_EMIT m_p->error(i18n("Unable to convert date '%1' with '%2' in quote data", dateValue, m_source.dateFormat()));
0787             Q_EMIT m_p->failed(m_id, m_symbol);
0788             return false;
0789         }
0790         if (!m_startDate.isNull() && date < m_startDate)
0791             continue;
0792         if (!m_endDate.isNull() && date > m_endDate)
0793             continue;
0794         prices[date] = AlkValue(priceValue, decimalSeparator);
0795     }
0796     if (prices.isEmpty()) {
0797         m_errors |= Errors::Price;
0798         Q_EMIT m_p->error(i18n("Unable to find date/price pairs in quote data"));
0799         Q_EMIT m_p->failed(m_id, m_symbol);
0800         return false;
0801     }
0802 
0803     if (m_useSingleQuoteSignal) {
0804         for (auto &key : prices.keys()) {
0805             Q_EMIT m_p->quote(m_id, m_symbol, key, prices[key].toDouble());
0806         }
0807     } else {
0808         Q_EMIT m_p->quotes(m_id, m_symbol, prices);
0809     }
0810 
0811     return true;
0812 }
0813 
0814 /**
0815  * Parse quote data according to currently selected web price quote source
0816  *
0817  * @param _quotedata quote data to parse
0818  * @return true parsing successful
0819  * @return false parsing unsuccessful
0820  */
0821 bool AlkOnlineQuote::Private::slotParseQuote(const QString &quotedata)
0822 {
0823     m_quoteData = quotedata;
0824 
0825     kDebug(Private::dbgArea()) << "quotedata" << quotedata;
0826 
0827     if (quotedata.isEmpty()) {
0828         m_errors |= Errors::Data;
0829         Q_EMIT m_p->error(i18n("Unable to update price for %1 (empty quote data)", m_symbol));
0830         Q_EMIT m_p->failed(m_id, m_symbol);
0831         return false;
0832     }
0833     switch (m_source.dataFormat()) {
0834     case AlkOnlineQuoteSource::StrippedHTML:
0835         return parseQuoteStripHTML(quotedata);
0836     case AlkOnlineQuoteSource::HTML:
0837         return parseQuoteHTML(quotedata);
0838     case AlkOnlineQuoteSource::CSV:
0839         return parseQuoteCSV(quotedata);
0840     default:
0841         return false;
0842     }
0843 }
0844 
0845 void AlkOnlineQuote::Private::slotLoadTimeout()
0846 {
0847     Q_EMIT m_p->error(i18n("Timeout exceeded on fetching url for %1", m_symbol));
0848     m_errors |= Errors::Timeout;
0849     Q_EMIT m_p->failed(m_id, m_symbol);
0850     m_eventLoop->exit(Errors::Timeout);
0851 }