/* * Copyright (C) 2012-2013, Gaetan Bisson . * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* * Squaw. The simplistic Qt-based user agent for the Web. * * Squaw is a Web browser based on the Qt port of WebKit which strives to be * minimalistic and flexible; it achieves both by merely consisting of this * short, easy-to-hack C++ file. * * Compile with: * * moc -o squaw.moc squaw.cpp * * c++ -O2 -lQtWebKit -lQtGui -lQtNetwork -lQtCore -o squaw squaw.cpp */ /* * ARGUMENTS * * Squaw expects an argument array consisting of zero or more options followed * by a resource description. The latter, processed in res(), can be a URL or * make use of keys (see below). The options currently supported by arg() are: * - "-h foo bar", which defines a "foo: bar" HTTP request header. * - "-u foo", which sets the User-Agent string to "foo". */ /* * CONFIGURATION * * Squaw uses three directories, all created in main(): * - CacheLocation, typically "~/.cache/Squaw", as network cache directory. * - DesktopLocation, typically "~/Desktop", as download target directory. * - "~/.squaw" to store the following (all optional) files: * * - "block.txt", a read-only list of hosts to block. * - "cookies.txt", a read-write persistent list of cookies. * - "credentials.txt", a read-only list of authentication values. * - "keys.txt", a read-only list of keys pointing to resources. * - "user.css", a read-only user CSS style. * * Lines of the keys file of the type "key=url" have the effect that the * resource description "key foo bar" points to URL "urlfoo+bar"; see res(). * * Lines of the credentials file of the type "attr=val" located between a "url" * line and the next blank line have the effect that the first input element * with attribute "attr" of URL "url" will be prefilled with the value "val". * Entries with attribute "user" or "password" are also used for HTTP * authentication. See get_creds(), a_prefill(), and a_auth(). * * To generate an ad-blocking list of hosts, use for instance: * * curl http://someonewhocares.org/hosts/hosts | * awk '/hijack/{a=1}a&&/^127/{print $2}' > block.txt */ /* **************************************************************************** * * HEADERS AND GLOBAL VARIABLES * */ #include #include #include #include QStringList A; QString S, T, U, D, I; QList H; /* **************************************************************************** * * NETWORK COOKIE JAR * * Make cookie jar persistent. */ class NetworkCookieJar: public QNetworkCookieJar { QList c; QFile f; public: NetworkCookieJar () { f.setFileName(S+"cookies.txt"); f.open(QIODevice::ReadOnly); while (f.bytesAvailable()) c.append(QNetworkCookie::parseCookies(f.readLine())); setAllCookies(c); f.close(); } bool setCookiesFromUrl (const QList &l, const QUrl &u) { bool r = QNetworkCookieJar::setCookiesFromUrl(l, u); c = allCookies(); f.open(QIODevice::WriteOnly); for (int i=0;i b; QNetworkDiskCache d; NetworkCookieJar c; Q_OBJECT public: NetworkAccessManager () { QFile f (S+"block.txt"); f.open(QIODevice::ReadOnly); while (f.bytesAvailable()) b.insert(f.readLine().trimmed(), true); f.close(); setCache(&d); setCookieJar(&c); d.setCacheDirectory(T); connect(this, SIGNAL(sslErrors(QNetworkReply*, QList)), SLOT(a_ssl(QNetworkReply*, QList))); } QNetworkReply *createRequest (Operation o, const QNetworkRequest &r, QIODevice *d=0) { QNetworkRequest q (r); if (b.value(r.url().host().toLocal8Bit(), false)) q.setUrl(QUrl()); for (int i=0;i e) { for (int i=0;iignoreSslErrors(); } }; /* **************************************************************************** * * WEB PAGE * * Prefill forms and reply to HTTP authentication requests. Track downloads. * Customize User-Agent string. */ class WebPage: public QWebPage { QHash > c; QList d; QList e; NetworkAccessManager n; Q_OBJECT public: WebPage () { get_creds(); setNetworkAccessManager(&n); setForwardUnsupportedContent(true); connect(this, SIGNAL(loadFinished(bool)), SLOT(a_prefill(bool))); connect(this, SIGNAL(downloadRequested(QNetworkRequest)), SLOT(a_download(QNetworkRequest))); connect(this, SIGNAL(unsupportedContent(QNetworkReply*)), SLOT(a_unsupported(QNetworkReply*))); connect(&n, SIGNAL(finished(QNetworkReply*)), SLOT(a_finished(QNetworkReply*))); connect(&n, SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), SLOT(a_auth(QNetworkReply*, QAuthenticator*))); } void get_creds () { QFile f (S+"credentials.txt"); f.open(QIODevice::ReadOnly); QList w; while (f.bytesAvailable()) { QByteArray x = f.readLine().trimmed(); if (x.isEmpty() || x[0]=='#') { w.clear(); continue; } int y = x.indexOf('='); if (y>0) for (int i=0;i()); w.append(x); } } f.close(); } QString save (QNetworkReply *r) { QString s = D+r->url().toString(QUrl::RemoveQuery|QUrl::StripTrailingSlash).section('/', -1, -1); while (QFileInfo(s).exists()) s += "+"; QFile f (s); f.open(QIODevice::WriteOnly); f.write(r->readAll()); f.close(); return s; } QString userAgentForUrl (const QUrl &u) const { return U.isEmpty() ? QWebPage::userAgentForUrl(u) : U; } protected slots: void a_prefill (bool o) { QWebFrame *f = currentFrame(); QByteArray s = f->url().toEncoded(QUrl::RemoveQuery); QList k = c[s].keys(); for (int i=0;idocumentElement().findFirst("input[name=\""+k[i]+"\"]").setAttribute("value", c[s][k[i]]); } void a_download (QNetworkRequest r) { d.append(n.get(r)); } void a_unsupported (QNetworkReply *r) { e.append(r); } void a_finished (QNetworkReply *r) { if (d.removeAll(r)) save(r); if (e.removeAll(r)) { QString s = save(r); QProcess().startDetached("xdg-open", QStringList(s)); } } void a_auth (QNetworkReply *r, QAuthenticator *a) { QByteArray s = r->url().toEncoded(QUrl::RemoveQuery); a->setUser(c[s]["user"]); a->setPassword(c[s]["password"]); } }; /* **************************************************************************** * * WEB VIEW * * Set sane defaults. Bind mouse events to zooming and spawning new instances. * Add print dialog. */ class WebView: public QWebView { QWebSettings *o; WebPage p; Q_OBJECT public: WebView () { setPage(&p); o = QWebSettings::globalSettings(); o->setMaximumPagesInCache(5); o->setAttribute(QWebSettings::PluginsEnabled, true); o->setAttribute(QWebSettings::DnsPrefetchEnabled, true); o->setAttribute(QWebSettings::DeveloperExtrasEnabled, true); o->setAttribute(QWebSettings::PrintElementBackgrounds, false); o->setUserStyleSheetUrl(QUrl("file://"+S+"user.css")); o->setFontFamily(QWebSettings::SerifFont, "serif"); o->setFontFamily(QWebSettings::CursiveFont, "serif"); o->setFontFamily(QWebSettings::StandardFont, "serif"); o->setFontFamily(QWebSettings::FixedFont, "monospace"); o->setFontFamily(QWebSettings::FantasyFont, "sans-serif"); o->setFontFamily(QWebSettings::SansSerifFont, "sans-serif"); o->setThirdPartyCookiePolicy(QWebSettings::AlwaysBlockThirdPartyCookies); connect(new QShortcut(QKeySequence::Back, this), SIGNAL(activated()), SLOT(back())); connect(new QShortcut(QKeySequence::Forward, this), SIGNAL(activated()), SLOT(forward())); connect(new QShortcut(QKeySequence::Refresh, this), SIGNAL(activated()), SLOT(reload())); connect(new QShortcut(QKeySequence::Print, this), SIGNAL(activated()), SLOT(a_print())); connect(new QShortcut(QKeySequence::ZoomIn, this), SIGNAL(activated()), SLOT(a_zoom_in())); connect(new QShortcut(QKeySequence::ZoomOut, this), SIGNAL(activated()), SLOT(a_zoom_out())); } void mouseReleaseEvent (QMouseEvent *e) { if (e->button()==Qt::MidButton) { QUrl u = p.frameAt(e->pos())->hitTestContent(e->pos()).linkUrl(); if (!u.isEmpty()) { QProcess().startDetached(I, A+QStringList(u.toEncoded())); return e->accept(); } } QWebView::mouseReleaseEvent(e); } void wheelEvent (QWheelEvent *e) { if (e->modifiers()==Qt::ControlModifier) { setZoomFactor(zoomFactor()*pow(1.0008, e->delta())); return e->accept(); } QWebView::wheelEvent(e); } protected slots: void a_zoom_in () { setZoomFactor(zoomFactor()*1.1); } void a_zoom_out () { setZoomFactor(zoomFactor()/1.1); } void a_print () { QPrintDialog l (this); if (l.exec()==QDialog::Accepted) page()->currentFrame()->print(l.printer()); } }; /* **************************************************************************** * * MAIN WINDOW * * Populate status bar with read-only URL line and search line. */ class MainWindow: public QMainWindow { QLineEdit a; QLineEdit s; WebView v; Q_OBJECT public: MainWindow (QUrl u) { setCentralWidget(&v); v.setFocus(); v.load(u); a.setReadOnly(true); a.setFont(QFont("monospace")); statusBar()->setSizeGripEnabled(false); statusBar()->addPermanentWidget(&a, 4); statusBar()->addPermanentWidget(&s, 1); connect(new QShortcut(QKeySequence::Find, &v), SIGNAL(activated()), SLOT(a_search_focus())); connect(new QShortcut(QKeySequence::FindNext, &v), SIGNAL(activated()), SLOT(a_search_next())); connect(new QShortcut(QKeySequence("Return"), &v), SIGNAL(activated()), SLOT(a_search_next())); connect(new QShortcut(QKeySequence("Escape"), &v), SIGNAL(activated()), SLOT(a_search_unfocus())); connect(&s, SIGNAL(textChanged(QString)), SLOT(a_search_change(QString))); connect(&v, SIGNAL(titleChanged(QString)), SLOT(a_title(QString))); connect(&v, SIGNAL(urlChanged(QUrl)), SLOT(a_url(QUrl))); } protected slots: void a_search_focus () { s.selectAll(); s.setFocus(); } void a_search_next () { v.page()->findText(s.text(), QWebPage::FindWrapsAroundDocument); } void a_search_unfocus () { v.setFocus(); } void a_search_change (QString s) { v.page()->findText("", QWebPage::HighlightAllOccurrences); v.page()->findText(s, QWebPage::HighlightAllOccurrences); } void a_title (QString t) { setWindowTitle(t); } void a_url (QUrl u) { a.setText(u.toString()); } }; /* **************************************************************************** * * MAIN * * Process options and resource description. Run by Qt sugar. */ QStringList arg (QStringList v) { while (!v.isEmpty()) { if (v[0]=="-h") { H.append(v[1].toLocal8Bit()); H.append(v[2].toLocal8Bit()); A.append(v.takeAt(0)); A.append(v.takeAt(0)); A.append(v.takeAt(0)); } else if (v[0]=="-u") { U = v[1]; A.append(v.takeAt(0)); A.append(v.takeAt(0)); } else break; } return v; } QByteArray res (QStringList v) { QByteArray t (v.value(0,"").toLocal8Bit()); if (t.contains("://")) return t; if (t.contains(".")) return "http://"+t; QByteArray s (v.value(1,"").toLocal8Bit()); for (int i=2;i0 && x.left(y)==t) { f.close(); return x.right(x.size()-y-1)+s; } else if (y==0) d = x.right(x.size()-1); } f.close(); return d+t+"+"+s; } int main (int argc, char *argv[]) { umask(S_IRWXG|S_IRWXO); QApplication x (argc, argv); x.setApplicationName("Squaw"); x.setApplicationVersion("1.2"); QStringList y = x.arguments(); I = y.takeFirst(); y = arg(y); S = QDesktopServices::storageLocation(QDesktopServices::HomeLocation)+"/.squaw/"; T = QDesktopServices::storageLocation(QDesktopServices::CacheLocation)+"/"; D = QDesktopServices::storageLocation(QDesktopServices::DesktopLocation)+"/"; QDir().mkpath(S); QDir().mkpath(T); QDir().mkpath(D); MainWindow w (QUrl::fromEncoded(res(y))); w.show(); return x.exec(); } #include "squaw.moc"