File indexing completed on 2025-04-27 07:52:00
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 "edata) 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 "edata) 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 "edata) 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 }