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

Generated by: LCOV version 2.0-1