/*
 * Copyright (C) 2012, 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 -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. Two options are currently supported by arg():
 * - "-h foo bar", which defines a "foo: bar" HTTP header.
 * - "-u foo", which sets the User-Agent string to "foo".
 *
 * Resource descriptions are processed in res() and can either be URLs or make
 * use of keys (see below).
 */

/*
 * 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 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.
 *
 * 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 <QtGui/QtGui>
#include <QtNetwork/QtNetwork>
#include <QtWebKit/QtWebKit>

QString S, T, D, U; // storage, temporary, download, user-agent
QList<QPair<QByteArray, QByteArray> > H; // custom HTTP headers


/* ****************************************************************************
 *
 * FEATURES DERIVED FROM QTWEBKIT
 *
 */


/* Use single window, customize user-agent string */
class Page: public QWebPage {
	public:
	QWebPage *createWindow (WebWindowType t) { return this; }
	QString userAgentForUrl (const QUrl &u) const { return U.isEmpty() ? QWebPage::userAgentForUrl(u) : U; }
};

/* Block selected domains, customize HTTP request headers */
class Network: public QNetworkAccessManager {
	QHash<QString, bool> b;
	public:
	Network() {
		QFile f (S+"block.txt");
		f.open(QIODevice::ReadOnly);
		while (f.bytesAvailable()) b.insert(QString(f.readLine().trimmed()), true);
		f.close();
	}
	QNetworkReply *createRequest (Operation o, const QNetworkRequest &r, QIODevice *d=0) {
		if (b.value(r.url().host(), false)) return QNetworkAccessManager::head(QNetworkRequest());
		QNetworkRequest q (r);
		for (int i=0;i<H.size();i++) q.setRawHeader(H[i].first, H[i].second);
		return QNetworkAccessManager::createRequest(o, q, d);
	}
};

/* Load and store cookies */
class Cookies: public QNetworkCookieJar {
	QList<QNetworkCookie> c;
	QFile f;
	public:
	Cookies () {
		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;
	}
};


/* ****************************************************************************
 *
 * INTERFACE LAYOUT
 *
 */


class Squaw: public QMainWindow {

	Q_OBJECT
	Page *p;
	Network *n;
	QWebView *v;
	QLineEdit *a;
	QLineEdit *s;
	QStatusBar *b;
	QPrintDialog *l;
	QList<QNetworkReply*> d;
	QList<QNetworkReply*> e;
	QHash<QString, QHash<QString, QString> > c;

	/* Load credentials */
	void get_creds () {
		QFile f (S+"credentials.txt");
		f.open(QIODevice::ReadOnly);
		QList<QString> w;
		while (f.bytesAvailable()) {
			QString 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();
	}

	public:

	Squaw (QUrl u) {
		get_creds();

		/* Global settings for QtWebKit */
		QWebSettings *o = QWebSettings::globalSettings();
		o->setAttribute(QWebSettings::PluginsEnabled, true);
		o->setAttribute(QWebSettings::DnsPrefetchEnabled, true);
		o->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);
		o->setAttribute(QWebSettings::PrintElementBackgrounds, false);
		o->setMaximumPagesInCache(5);
		o->setUserStyleSheetUrl(QUrl(
			"data:text/css;charset=utf-8;base64,"+QByteArray(
"body{text-rendering:optimizeLegibility;font-family:serif!important;background-color:#ddeeee;}"
"code,pre,samp,textarea,tt{font-family:monospace!important;}"
"*{font-family:inherit!important;}"
			).toBase64()));

		/* Create core objects */
		n = new Network;
		n->setCookieJar(new Cookies);
		n->setCache(new QNetworkDiskCache);
		qobject_cast<QNetworkDiskCache*>(n->cache())->setCacheDirectory(T);
		p = new Page;
		p->setNetworkAccessManager(n);
		p->setForwardUnsupportedContent(true);
		v = new QWebView;
		v->setPage(p);
		v->setFocus();
		v->load(u);
		setCentralWidget(v);

		/* Create widgets */
		s = new QLineEdit;
		a = new QLineEdit;
		a->setReadOnly(true);
		a->setFont(QFont("monospace"));
		b = statusBar();
		b->setSizeGripEnabled(false);
		b->addPermanentWidget(a, 4);
		b->addPermanentWidget(s, 1);
		l = new QPrintDialog;

		/* Listen for events */
		connect(new QShortcut(QKeySequence::Back,     v), SIGNAL(activated()), SLOT(a_back()));
		connect(new QShortcut(QKeySequence::Forward,  v), SIGNAL(activated()), SLOT(a_forward()));
		connect(new QShortcut(QKeySequence::Refresh,  v), SIGNAL(activated()), SLOT(a_reload()));
		connect(new QShortcut(QKeySequence::Print,    v), SIGNAL(activated()), SLOT(a_print()));
		connect(new QShortcut(QKeySequence::ZoomIn,   v), SIGNAL(activated()), SLOT(a_zoom_in()));
		connect(new QShortcut(QKeySequence::ZoomOut,  v), SIGNAL(activated()), SLOT(a_zoom_out()));
		connect(new QShortcut(QKeySequence("Ctrl+="), v), SIGNAL(activated()), SLOT(a_zoom_none()));
		connect(new QShortcut(QKeySequence::Find,     v), SIGNAL(activated()), SLOT(a_search_focus()));
		connect(new QShortcut(QKeySequence("Escape"), v), SIGNAL(activated()), SLOT(a_search_unfocus()));
		connect(new QShortcut(QKeySequence("Return"), v), SIGNAL(activated()), SLOT(a_search_next()));
		connect(new QShortcut(QKeySequence::FindNext, v), SIGNAL(activated()), SLOT(a_search_next()));
		connect(s, SIGNAL(textChanged(QString)),                               SLOT(a_search_change(QString)));
		connect(v, SIGNAL(urlChanged(QUrl)),                                   SLOT(a_url(QUrl)));
		connect(v, SIGNAL(titleChanged(QString)),                              SLOT(a_title(QString)));
		connect(p, SIGNAL(downloadRequested(QNetworkRequest)),                 SLOT(a_download(QNetworkRequest)));
		connect(p, SIGNAL(unsupportedContent(QNetworkReply*)),                 SLOT(a_unsupported(QNetworkReply*)));
		connect(n, SIGNAL(finished(QNetworkReply*)),                           SLOT(a_finished(QNetworkReply*)));
		connect(n, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)),        SLOT(a_ssl(QNetworkReply*, QList<QSslError>)));
		connect(p, SIGNAL(loadFinished(bool)),                                 SLOT(a_prefill(bool)));
		connect(n, SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), SLOT(a_auth(QNetworkReply*, QAuthenticator*)));
	}

	/* Save downloaded data permanently */
	QString save (QNetworkReply *r) {
		QString s = D+r->url().toString().section(QChar('?'), 0, 0).section(QChar('/'), -1, -1);
		while (QFileInfo(s).exists()) s += "+";
		QFile f (s);
		f.open(QIODevice::WriteOnly);
		f.write(r->readAll());
		f.close();
		return s;
	}

	protected slots:

	void a_back () { v->back(); }
	void a_forward () { v->forward(); }
	void a_reload () { v->reload(); }
	void a_print () { if (l->exec()==QDialog::Accepted) p->currentFrame()->print(l->printer()); }
	void a_zoom_in () { v->setZoomFactor(v->zoomFactor()*1.1); }
	void a_zoom_out () { v->setZoomFactor(v->zoomFactor()/1.1); }
	void a_zoom_none () { v->setZoomFactor(1); }
	void a_search_focus () { s->setFocus(); s->selectAll(); }
	void a_search_unfocus () { v->setFocus(); }
	void a_search_next () { p->findText(s->text(), QWebPage::FindWrapsAroundDocument); }
	void a_search_change (QString s) { p->findText("", QWebPage::HighlightAllOccurrences); p->findText(s, QWebPage::HighlightAllOccurrences); }
	void a_url (QUrl u) { a->setText(u.toString()); }
	void a_title (QString t) { setWindowTitle(t); }

	/* Handle downloads and external/unsupported resources */
	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(QString("xdg-open"), QStringList(s));
		}
	}

	/* Mark broken SSL red, let it through */
	void a_ssl (QNetworkReply *r, QList<QSslError> e) {
		QPalette t = a->palette();
		t.setColor(QPalette::Base, Qt::red);
		a->setPalette(t);
		r->ignoreSslErrors();
	}

	/* Use credentials when possible */
	void a_prefill (bool o) {
		QWebFrame *f = p->currentFrame();
		QString s = f->url().toString().section(QChar('?'), 0, 0);
		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_auth (QNetworkReply *r, QAuthenticator *a) {
		QString s = r->url().toString().section(QChar('?'), 0, 0);
		a->setUser(c[s]["user"]);
		a->setPassword(c[s]["password"]);
	}

};


/* ****************************************************************************
 *
 * STARTUP
 *
 */


/* Process optional arguments */
void arg (int *c, char **v[]) {
	while (*c>0)
		if (!strcmp((*v)[0], "-h")) {
			H.append(QPair<QByteArray, QByteArray>((*v)[1], (*v)[2]));
			*c -= 3; *v += 3;
		} else if (!strcmp((*v)[0], "-u")) {
			U = QString::fromUtf8((*v)[1]);
			*c -= 2; *v += 2;
		} else break;
}

/* Process resource description */
QUrl res (int c, char *v[]) {
	QString s = QString::fromUtf8(v[0]);
	QString t = QString::fromUtf8(v[1]);
	if (s.contains("://")) return s;
	if (s.contains(".")) return "http://"+s;
	for (int i=2;i<c;i++) t += "+"+QString::fromUtf8(v[i]);
	QFile f (S+"keys.txt");
	f.open(QIODevice::ReadOnly);
	while (f.bytesAvailable()) {
		QString x = f.readLine().trimmed();
		int y = x.indexOf('=');
		if (x.left(y)==s) { f.close(); return x.right(x.size()-y-1)+t; }
	}
	f.close(); return "https://duckduckgo.com/?q="+s+"+"+t;
}

/* Initialize and run */
int main (int argc, char *argv[]) {
	QApplication x (argc, argv);
	x.setApplicationName("Squaw");
	x.setApplicationVersion("0.9");

	umask(S_IRWXG|S_IRWXO);
	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);

	int c = argc-1;
	char **v = argv+1;
	arg(&c, &v);

	if (c<1) {
		printf("\
Squaw. The simplistic Qt-based user agent for the Web.\n\
Copyright (C) 2012 Gaetan Bisson. All rights reserved.\n\
Version 0.9; compiled on "__DATE__".\n\
\n\
Squaw does not support wandering the Web: a description of\n\
the resource to be accessed must be provided as argument.\n\
See the source code for details.\n\
");
		return 1;
	}

	(new Squaw(res(c, v)))->show();
	return x.exec();
}

#include "squaw.moc"
