/*
 * Copyright (C) 2012-2014, Gaetan Bisson <bisson@archlinux.org>.
 *
 * 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-qt5 -o squaw.moc squaw.cpp
 *
 *   c++ -O2 -lQt5WebKitWidgets -lQt5PrintSupport -lQt5Widgets -lQt5WebKit -lQt5Gui -lQt5Network -lQt5Core -I/usr/include/qt -fPIE -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:
 * - "-p host port", which forwards connections through the given HTTP proxy.
 * - "-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 <sys/stat.h>
#include <QtWebKit/QtWebKit>
#include <QtWebKitWidgets/QWebView>
#include <QtWebKitWidgets/QWebFrame>
#include <QtPrintSupport/QPrintDialog>
#include <QtWidgets/QShortcut>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QStatusBar>
#include <QtWidgets/QApplication>

/* For spawning new instances. */
QString I;           /* argv[0]    */
QStringList A;       /* argv+1     */

/* Directories. */
QString S;           /* storage    */
QString T;           /* temporary  */
QString D;           /* download   */

/* Parameters. */
QNetworkProxy P;     /* http-proxy */
QList<QByteArray> H; /* headers    */
QString U;           /* user-agent */


/* ****************************************************************************
 *
 * NETWORK COOKIE JAR
 *
 * Make cookie jar persistent.
 */


class NetworkCookieJar: public QNetworkCookieJar {
	public:
	QList<QNetworkCookie> c;
	QFile f;

	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<QNetworkCookie> &l, const QUrl &u) {
		bool r = QNetworkCookieJar::setCookiesFromUrl(l, u);
		c = allCookies();
		f.open(QIODevice::WriteOnly);
		for (int i=0;i<c.size();i++) f.write(c[i].toRawForm()+"\n");
		f.close();
		return r;
	}
};


/* ****************************************************************************
 *
 * NETWORK ACCESS MANAGER
 *
 * Block selected hosts. Customize HTTP request headers.
 */


class NetworkAccessManager: public QNetworkAccessManager {
	public:
	QHash<QByteArray, bool> b;
	QNetworkDiskCache d;
	NetworkCookieJar c;

	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);
	}
	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<H.size();i+=2) q.setRawHeader(H[i], H[i+1]);
		return QNetworkAccessManager::createRequest(o, q, d);
	}
};


/* ****************************************************************************
 *
 * WEB PAGE
 *
 * Prefill forms and reply to authentication requests. Show useful error pages.
 * Customize User-Agent. Use single window. Track downloads.
 */


class WebPage: public QWebPage {
	public:
	QHash<QByteArray, QHash<QString, QString> > c;
	QList<QNetworkReply*> d;
	QList<QNetworkReply*> e;
	NetworkAccessManager n;

	WebPage () {
		get_creds();
		setNetworkAccessManager(&n);
		setForwardUnsupportedContent(true);
		connect(this, &WebPage::loadFinished,                        this, &WebPage::a_prefill);
		connect(this, &WebPage::downloadRequested,                   this, &WebPage::a_download);
		connect(this, &WebPage::unsupportedContent,                  this, &WebPage::a_unsupported);
		connect(&n,   &NetworkAccessManager::finished,               this, &WebPage::a_finished);
		connect(&n,   &NetworkAccessManager::authenticationRequired, this, &WebPage::a_auth);
	}
	bool supportsExtension(Extension e) const {
		if (e!=QWebPage::ErrorPageExtension) return QWebPage::supportsExtension(e);
		return true;
	}
	bool extension(Extension e, const ExtensionOption *oo, ExtensionReturn *rr) {
		if (e!=QWebPage::ErrorPageExtension) return QWebPage::extension(e,oo,rr);
		const ErrorPageExtensionOption *o = static_cast<const ErrorPageExtensionOption *>(oo);
		ErrorPageExtensionReturn *r = static_cast<ErrorPageExtensionReturn *>(rr);
		QString h = "<html><body><h1>Error</h1><h2>";
		switch (o->domain) {
			case QWebPage::QtNetwork: h += "QtNetwork "; break;
			case QWebPage::WebKit: h += "WebKit "; break;
			case QWebPage::Http: h += "HTTP "; break;
			default: h+= "Unknown "; break;
		}
		h += QString::number(o->error);
		h += "</h2><h3>";
		h += o->errorString;
		h += "</h3></body></html>";
		r->baseUrl = o->url;
		r->content = h.toUtf8();
		return true;
	}
	QString userAgentForUrl (const QUrl &u) const { return U.isEmpty() ? QWebPage::userAgentForUrl(u) : U; }
	QWebPage *createWindow (WebWindowType t) { return this; }

	void get_creds () {
		QFile f (S+"credentials.txt");
		f.open(QIODevice::ReadOnly);
		QList<QByteArray> 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.size();i++)
				c[w[i]].insert(x.left(y), x.right(x.size()-y-1));
			else {
				c.insert(x, QHash<QString, QString>());
				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;
	}
	void a_prefill (bool o) {
		QWebFrame *f = currentFrame();
		QByteArray s = f->url().toEncoded(QUrl::RemoveQuery);
		QList<QString> k = c[s].keys();
		for (int i=0;i<k.size();i++)
			f->documentElement().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 {
	Q_OBJECT

	public:
	QWebSettings *o;
	WebPage p;

	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);
		new QShortcut(QKeySequence::Back,    this, SLOT(back()));
		new QShortcut(QKeySequence::Forward, this, SLOT(forward()));
		new QShortcut(QKeySequence::Refresh, this, SLOT(reload()));
		new QShortcut(QKeySequence::Print,   this, SLOT(a_print()));
		new QShortcut(QKeySequence::ZoomIn,  this, SLOT(a_zoom_in()));
		new QShortcut(QKeySequence::ZoomOut, this, 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. Warn of
 * untrusted SSL.
 */


class MainWindow: public QMainWindow {
	Q_OBJECT

	public:
	QLineEdit a;
	QLineEdit s;
	WebView v;

	MainWindow (QUrl u) {
		setCentralWidget(&v);
		v.setFocus();
		v.load(u);
		a.setReadOnly(true);
		a.setFont(QFont("monospace", 9));
		s.setFont(QFont("monospace", 9));
		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, &QLineEdit::textChanged, this, &MainWindow::a_search_change);
		connect(&v, &WebView::titleChanged,  this, &MainWindow::a_title);
		connect(&v, &WebView::urlChanged,    this, &MainWindow::a_url);
		connect(&v.p.n, &NetworkAccessManager::sslErrors, this, &MainWindow::a_ssl);
	}

	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.toDisplayString()); }
	void a_ssl (QNetworkReply *r, QList<QSslError> e) {
		for (int i=0;i<e.size();i++) printf("SSL: %s\n", qPrintable(e[i].errorString()));
		a.setStyleSheet("QLineEdit{background:red;}");
		r->ignoreSslErrors();
	}

	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(); }
};


/* ****************************************************************************
 *
 * 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 if (v[0]=="-p") {
			P.setHostName(v[1]);
			P.setPort(v[2].toUInt());
			P.setType(QNetworkProxy::HttpProxy);
			A.append(v.takeAt(0));
			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;i<v.size();i++) s += "+"+v[i].toLocal8Bit();
	QByteArray d ("https://duckduckgo.com/?q=");
	QFile f (S+"keys.txt");
	f.open(QIODevice::ReadOnly);
	while (f.bytesAvailable()) {
		QByteArray x = f.readLine().trimmed();
		int y = x.indexOf('=');
		if (y>0 && 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("2.2");
	QStringList y = x.arguments();
	I = y.takeFirst();
	y = arg(y);

	S = QStandardPaths::writableLocation(QStandardPaths::HomeLocation)+"/.squaw/";
	T = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)+"/";
	D = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)+"/";
	QDir().mkpath(S);
	QDir().mkpath(T);
	QDir().mkpath(D);

	if (P.type()) QNetworkProxy::setApplicationProxy(P);
	MainWindow w (QUrl::fromEncoded(res(y)));
	w.show();
	return x.exec();
}

#include "squaw.moc"
