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