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 :
|