LCOV - code coverage report
Current view: top level - src/utilities - WebPageGrabber.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 74.1 % 147 109
Test Date: 2026-03-23 10:19:47 Functions: 100.0 % 12 12

            Line data    Source code
       1              : #include "WebPageGrabber.h"
       2              : #include <QXmlStreamReader>
       3              : #include <memory>
       4              : 
       5              : // TidyLib
       6              : #include <tidy.h>
       7              : #include <buffio.h>
       8              : 
       9              : #include "FangLogging.h"
      10              : 
      11              : namespace {
      12              :     // RAII wrapper for TidyDoc to ensure cleanup
      13              :     struct TidyDocDeleter {
      14           74 :         void operator()(TidyDoc doc) const {
      15           74 :             if (doc) {
      16           74 :                 tidyRelease(doc);
      17              :             }
      18           74 :         }
      19              :     };
      20              :     using TidyDocPtr = std::unique_ptr<std::remove_pointer<TidyDoc>::type, TidyDocDeleter>;
      21              : }
      22              : 
      23          114 : WebPageGrabber::WebPageGrabber(bool handleMetaRefresh, int timeoutMS, QObject *parent, QNetworkAccessManager* networkManager) :
      24              :     FangObject(parent),
      25          114 :     core(nullptr),
      26          114 :     handleMetaRefresh(handleMetaRefresh),
      27          114 :     redirectAttempts(0),
      28          114 :     error(true),
      29          114 :     done(false)
      30              : {
      31          114 :     NetworkDownloadConfig config;
      32          114 :     config.timeoutMs = timeoutMS;
      33          114 :     config.maxRedirects = 10;
      34          114 :     config.useInactivityTimeout = true;
      35          114 :     core = new NetworkDownloadCore(config, this, networkManager);
      36          114 :     init();
      37          114 : }
      38              : 
      39           40 : WebPageGrabber::WebPageGrabber(QObject *parent) :
      40              :     FangObject(parent),
      41           40 :     core(nullptr),
      42           40 :     handleMetaRefresh(DEFAULT_HANDLE_META_REFRESH),
      43           40 :     redirectAttempts(0),
      44           40 :     error(true),
      45           40 :     done(false)
      46              : {
      47           40 :     NetworkDownloadConfig config;
      48           40 :     config.timeoutMs = DEFAULT_TIMEOUT_MS;
      49           40 :     config.maxRedirects = 10;
      50           40 :     config.useInactivityTimeout = true;
      51           40 :     core = new NetworkDownloadCore(config, this, nullptr);
      52           40 :     init();
      53           40 : }
      54              : 
      55              : 
      56            7 : void WebPageGrabber::load(const QUrl& url)
      57              : {
      58              :     // Reset!
      59            7 :     redirectAttempts = 0;
      60            7 :     error = true;
      61              : 
      62              :     // Now GO!
      63            7 :     loadInternal(url);
      64            7 : }
      65              : 
      66           69 : QString *WebPageGrabber::load(const QString& htmlString)
      67              : {
      68              :     // Reset!
      69           69 :     error = true;
      70              : 
      71           69 :     return loadInternal(htmlString, false);
      72              : }
      73              : 
      74            7 : void WebPageGrabber::loadInternal(const QUrl& url)
      75              : {
      76            7 :     originalUrl = url;
      77            7 :     core->download(url);
      78            7 : }
      79              : 
      80           74 : QString *WebPageGrabber::loadInternal(const QString& htmlString, bool handleRefresh)
      81              : {
      82           74 :     document.clear();
      83              : 
      84              :     // Tidy up the string!
      85              :     TidyBuffer output;
      86              :     TidyBuffer errbuf;
      87           74 :     tidyBufInit(&output);
      88           74 :     tidyBufInit(&errbuf);
      89              : 
      90           74 :     TidyDocPtr tdoc(tidyCreate());
      91           74 :     if (!tdoc) {
      92            0 :         emitReadySignal(nullptr);
      93            0 :         return nullptr;
      94              :     }
      95              : 
      96              :     // QString can convert to/from utf8
      97           74 :     tidySetInCharEncoding(tdoc.get(), "utf8");
      98           74 :     tidySetOutCharEncoding(tdoc.get(), "utf8");
      99              : 
     100              :     // Configure and process the HTML
     101           74 :     if (!tidyOptSetBool(tdoc.get(), TidyXhtmlOut, yes)) {
     102            0 :         tidyBufFree(&output);
     103            0 :         tidyBufFree(&errbuf);
     104            0 :         emitReadySignal(nullptr);
     105            0 :         return nullptr;
     106              :     }
     107              : 
     108           74 :     if (!tidyOptSetInt(tdoc.get(), TidyIndentContent, TidyNoState)) {
     109            0 :         tidyBufFree(&output);
     110            0 :         tidyBufFree(&errbuf);
     111            0 :         emitReadySignal(nullptr);
     112            0 :         return nullptr;
     113              :     }
     114              : 
     115           74 :     int rc = tidySetErrorBuffer(tdoc.get(), &errbuf);
     116           74 :     if (rc >= 0) {
     117           74 :         rc = tidyParseString(tdoc.get(), htmlString.toUtf8().constData());
     118              :     }
     119           74 :     if (rc >= 0) {
     120           74 :         rc = tidyCleanAndRepair(tdoc.get());
     121              :     }
     122           74 :     if (rc >= 0) {
     123           74 :         rc = tidyRunDiagnostics(tdoc.get());
     124              :     }
     125           74 :     if (rc > 1) {
     126              :         // If error, force output
     127           31 :         rc = tidyOptSetBool(tdoc.get(), TidyForceOutput, yes) ? rc : -1;
     128              :     }
     129           74 :     if (rc >= 0) {
     130           74 :         rc = tidySaveBuffer(tdoc.get(), &output);
     131              :     }
     132              : 
     133           74 :     QString result;
     134          148 :     qCDebug(logWebPage) << "TidyLib rc:" << rc << "output.bp:" << (output.bp != nullptr);
     135              : 
     136           74 :     if (rc >= 0 && output.bp) {
     137           74 :         result = QString::fromUtf8(reinterpret_cast<const char*>(output.bp));
     138              :     } else {
     139            0 :         qCDebug(logWebPage) << "WebPageGrabber error!";
     140            0 :         tidyBufFree(&output);
     141            0 :         tidyBufFree(&errbuf);
     142            0 :         emitReadySignal(nullptr);
     143            0 :         return nullptr;
     144              :     }
     145              : 
     146              :     // Free memory (tdoc is automatically freed by unique_ptr)
     147           74 :     tidyBufFree(&output);
     148           74 :     tidyBufFree(&errbuf);
     149              : 
     150           74 :     document = result;
     151              : 
     152              :     // Check for an HTML meta refresh if requested.
     153           74 :     if (handleRefresh) {
     154            5 :         QString redirectURL = searchForRedirect(document);
     155            5 :         if (redirectAttempts > MAX_REDIRECTS) {
     156            0 :             qCDebug(logWebPage) << "Error: Maximum HTML redirects";
     157            0 :             emitReadySignal(nullptr);
     158            0 :             return nullptr;
     159            5 :         } else if (!redirectURL.isEmpty()) {
     160            0 :             QUrl url(redirectURL);
     161            0 :             if (url.isValid()) {
     162              :                 // Bump counter and call our internal load method that doesn't reset it.
     163            0 :                 redirectAttempts++;
     164            0 :                 loadInternal(url);
     165            0 :                 return nullptr;
     166              :             }
     167            0 :         }
     168            5 :     }
     169              : 
     170              :     // Woo-hoo! We have a document!
     171           74 :     error = false;
     172           74 :     emitReadySignal(&document);
     173           74 :     return &document;
     174           74 : }
     175              : 
     176            2 : void WebPageGrabber::onDownloadError(const QUrl& url, const QString& errorString)
     177              : {
     178              :     Q_UNUSED(url);
     179              :     Q_UNUSED(errorString);
     180              : 
     181              :     // Crap. :(
     182            2 :     emitReadySignal(nullptr);
     183            2 : }
     184              : 
     185            5 : void WebPageGrabber::onDownloadFinished(const QUrl& url, const QByteArray& data)
     186              : {
     187              :     Q_UNUSED(url);
     188            5 :     loadInternal(data, handleMetaRefresh);
     189            5 : }
     190              : 
     191            5 : QString WebPageGrabber::searchForRedirect(const QString& document)
     192              : {
     193              :     // Examples of what we're looking for:
     194              :     //     <meta http-equiv="refresh" content="0; url=http://example.com/">
     195              :     //     <meta http-equiv="refresh" content="0;URL='http://thetudors.example.com/'" />
     196              :     //     <meta http-equiv="refresh" content="0;URL=http://www.mrericsir.com/blog/" />
     197              : 
     198            5 :     QXmlStreamReader xml;
     199            5 :     xml.addData(document);
     200              : 
     201           97 :     while (!xml.atEnd()) {
     202           92 :         xml.readNext();
     203              : 
     204           92 :         if (xml.isStartElement()) {
     205           29 :             QString tagName = xml.name().toString().toLower();
     206           29 :             if (tagName == "body") {
     207            5 :                 return QString();
     208              :             }
     209              : 
     210           24 :             if (tagName == "meta") {
     211            5 :                 QXmlStreamAttributes attributes = xml.attributes();
     212              : 
     213           10 :                 if (attributes.hasAttribute("http-equiv") && attributes.hasAttribute("content") &&
     214            5 :                         attributes.value("", "http-equiv").toString().toLower() == "refresh") {
     215              : 
     216              :                     // For this method we're assuming that URL is always the last parameter in
     217              :                     // the content attribute.
     218            0 :                     QString content = attributes.value("", "content").toString();
     219            0 :                     int index = content.indexOf("url=", 0, Qt::CaseInsensitive);
     220            0 :                     if (index >= 0) {
     221              :                         // URLs are allowed to be in quotes, so we have to check for that.
     222            0 :                         QString url = content.mid(index + 4).trimmed();  // "url=" is 4 chars
     223            0 :                         if (!url.isEmpty()) {
     224            0 :                             QChar firstChar = url.at(0);
     225            0 :                             if (firstChar == '\'' || firstChar == '\"') {
     226            0 :                                 url = url.mid(1);
     227            0 :                                 if (url.endsWith('\'') || url.endsWith('\"')) {
     228            0 :                                     url.chop(1);
     229              :                                 }
     230              :                             }
     231            0 :                             return url;
     232              :                         }
     233            0 :                     }
     234            0 :                 }
     235            5 :             }
     236           29 :         }
     237              :     }
     238              : 
     239            0 :     return QString();
     240            5 : }
     241              : 
     242           76 : void WebPageGrabber::emitReadySignal(QString* document)
     243              : {
     244              :     // Remember that we're done.
     245           76 :     done = true;
     246              : 
     247              :     // Now emit the signal.
     248           76 :     emit ready(this, document);
     249           76 : }
     250              : 
     251          154 : void WebPageGrabber::init()
     252              : {
     253          154 :     connect(core, &NetworkDownloadCore::error, this, &WebPageGrabber::onDownloadError);
     254          154 :     connect(core, &NetworkDownloadCore::finished, this, &WebPageGrabber::onDownloadFinished);
     255          154 : }
     256              : 
     257              : 
        

Generated by: LCOV version 2.0-1