File indexing completed on 2024-04-28 16:13:25

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