/* Copyright (c) 2006-2010, Linden Research, Inc.
 * 
 * LLQtWebKit Source Code
 * The source code in this file ("Source Code") is provided by Linden Lab
 * to you under the terms of the GNU General Public License, version 2.0
 * ("GPL"), unless you have obtained a separate licensing agreement
 * ("Other License"), formally executed by you and Linden Lab.  Terms of
 * the GPL can be found in GPL-license.txt in this distribution, or online at
 * http://secondlifegrid.net/technology-programs/license-virtual-world/viewerlicensing/gplv2
 * 
 * There are special exceptions to the terms and conditions of the GPL as
 * it is applied to this Source Code. View the full text of the exception
 * in the file FLOSS-exception.txt in this software distribution, or
 * online at
 * http://secondlifegrid.net/technology-programs/license-virtual-world/viewerlicensing/flossexception
 * 
 * By copying, modifying or distributing this software, you acknowledge
 * that you have read and understood your obligations described above,
 * and agree to abide by those obligations.
 * 
 * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
 * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
 * COMPLETENESS OR PERFORMANCE.
 */

#include "networkcookiejar.h"
#include "networkcookiejar_p.h"
#include "twoleveldomains_p.h"

//#define NETWORKCOOKIEJAR_DEBUG

#ifndef QT_NO_DEBUG
// ^ Prevent being left on in a released product by accident
// qDebug any cookies that are rejected for further inspection
#define NETWORKCOOKIEJAR_LOGREJECTEDCOOKIES
#include <qdebug.h>
#endif

#include <qurl.h>
#include <qdatetime.h>

#if defined(NETWORKCOOKIEJAR_DEBUG)
#include <qdebug.h>
#endif


NetworkCookieJar::NetworkCookieJar(QObject *parent)
        : QNetworkCookieJar(parent)
{
    d = new NetworkCookieJarPrivate;
}

NetworkCookieJar::~NetworkCookieJar()
{
    delete d;
}

static QStringList splitHost(const QString &host) {
    QStringList parts = host.split(QLatin1Char('.'), QString::KeepEmptyParts);
    // Remove empty components that are on the start and end
    while (!parts.isEmpty() && parts.last().isEmpty())
        parts.removeLast();
    while (!parts.isEmpty() && parts.first().isEmpty())
        parts.removeFirst();
    return parts;
}

inline static bool shorterPaths(const QNetworkCookie &c1, const QNetworkCookie &c2)
{
    return c2.path().length() < c1.path().length();
}

QList<QNetworkCookie> NetworkCookieJar::cookiesForUrl(const QUrl &url) const
{
#if defined(NETWORKCOOKIEJAR_DEBUG)
    qDebug() << "NetworkCookieJar::" << __FUNCTION__ << url;
#endif
    // Generate split host
    QString host = url.host();
    if (url.scheme().toLower() == QLatin1String("file"))
        host = QLatin1String("localhost");
    QStringList urlHost = splitHost(host);

    // Get all the cookies for url
    QList<QNetworkCookie> cookies = d->tree.find(urlHost);
    if (urlHost.count() > 2) {
        int top = 2;
        if (d->matchesBlacklist(urlHost.last()))
            top = 3;

        urlHost.removeFirst();
        while (urlHost.count() >= top) {
            cookies += d->tree.find(urlHost);
            urlHost.removeFirst();
        }
    }

    // Prevent doing anything expensive in the common case where
    // there are no cookies to check
    if (cookies.isEmpty())
        return cookies;

    QDateTime now = QDateTime::currentDateTime().toTimeSpec(Qt::UTC);
    const QString urlPath = d->urlPath(url);
    const bool isSecure = url.scheme().toLower() == QLatin1String("https");
    QList<QNetworkCookie>::iterator i = cookies.begin();
    for (; i != cookies.end();) {
        if (!d->matchingPath(*i, urlPath)) {
#if defined(NETWORKCOOKIEJAR_DEBUG)
            qDebug() << __FUNCTION__ << "Ignoring cookie, path does not match" << *i << urlPath;
#endif
            i = cookies.erase(i);
            continue;
        }
        if (!isSecure && i->isSecure()) {
            i = cookies.erase(i);
#if defined(NETWORKCOOKIEJAR_DEBUG)
            qDebug() << __FUNCTION__ << "Ignoring cookie, security mismatch"
                     << *i << !isSecure;
#endif
            continue;
        }
        if (!i->isSessionCookie() && now > i->expirationDate()) {
            // remove now (expensive short term) because there will
            // probably be many more cookiesForUrl calls for this host
            d->tree.remove(splitHost(i->domain()), *i);
#if defined(NETWORKCOOKIEJAR_DEBUG)
            qDebug() << __FUNCTION__ << "Ignoring cookie, expiration issue"
                     << *i << now;
#endif
            i = cookies.erase(i);
            continue;
        }
        ++i;
    }

    // shorter paths should go first
    qSort(cookies.begin(), cookies.end(), shorterPaths);
#if defined(NETWORKCOOKIEJAR_DEBUG)
    qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "returning" << cookies.count();
    qDebug() << cookies;
#endif
    return cookies;
}

static const qint32 NetworkCookieJarMagic = 0xae;

QByteArray NetworkCookieJar::saveState () const
{
    int version = 1;
    QByteArray data;
    QDataStream stream(&data, QIODevice::WriteOnly);

    stream << qint32(NetworkCookieJarMagic);
    stream << qint32(version);
    stream << d->tree;
    return data;
}

bool NetworkCookieJar::restoreState(const QByteArray &state)
{
    int version = 1;
    QByteArray sd = state;
    QDataStream stream(&sd, QIODevice::ReadOnly);
    if (stream.atEnd())
        return false;
    qint32 marker;
    qint32 v;
    stream >> marker;
    stream >> v;
    if (marker != NetworkCookieJarMagic || v != version)
        return false;
    stream >> d->tree;
    if (stream.status() != QDataStream::Ok) {
        d->tree.clear();
        return false;
    }
    return true;
}

/*!
    Remove any session cookies or cookies that have expired.
  */
void NetworkCookieJar::endSession()
{
    const QList<QNetworkCookie> cookies = d->tree.all();
    QDateTime now = QDateTime::currentDateTime().toTimeSpec(Qt::UTC);
    QList<QNetworkCookie>::const_iterator i = cookies.constBegin();
    for (; i != cookies.constEnd();) {
        if (i->isSessionCookie()
            || (!i->isSessionCookie() && now > i->expirationDate())) {
                d->tree.remove(splitHost(i->domain()), *i);
        }
        ++i;
    }
}

static const int maxCookiePathLength = 1024;

bool NetworkCookieJar::setCookiesFromUrl(const QList<QNetworkCookie> &cookieList, const QUrl &url)
{
#if defined(NETWORKCOOKIEJAR_DEBUG)
    qDebug() << "NetworkCookieJar::" << __FUNCTION__ << url;
    qDebug() << cookieList;
#endif
    QDateTime now = QDateTime::currentDateTime().toTimeSpec(Qt::UTC);
    bool changed = false;
    QString fullUrlPath = url.path();
    QString defaultPath = fullUrlPath.mid(0, fullUrlPath.lastIndexOf(QLatin1Char('/')) + 1);
    if (defaultPath.isEmpty())
        defaultPath = QLatin1Char('/');

    QString urlPath = d->urlPath(url);
    foreach (QNetworkCookie cookie, cookieList) {
        if (cookie.path().length() > maxCookiePathLength)
            continue;

        bool alreadyDead = !cookie.isSessionCookie() && cookie.expirationDate() < now;

        if (cookie.path().isEmpty()) {
            cookie.setPath(defaultPath);
        }
        // Matching the behavior of Firefox, no path checking is done when setting cookies
        // Safari does something even odder, when that paths don't match it keeps
        // the cookie, but changes the paths to the default path
#if 0
        else if (!d->matchingPath(cookie, urlPath)) {
#ifdef NETWORKCOOKIEJAR_LOGREJECTEDCOOKIES
            qDebug() << "NetworkCookieJar::" << __FUNCTION__
                     << "Blocked cookie because: path doesn't match: " << cookie << url;
#endif
            continue;
        }
#endif

        if (cookie.domain().isEmpty()) {
            QString host = url.host().toLower();
            if (host.isEmpty())
                continue;
            cookie.setDomain(host);
        } else if (!d->matchingDomain(cookie, url)) {
#ifdef NETWORKCOOKIEJAR_LOGREJECTEDCOOKIES
            qDebug() << "NetworkCookieJar::" << __FUNCTION__
                     << "Blocked cookie because: domain doesn't match: " << cookie << url;
#endif
            continue;
        }

        // replace/remove existing cookies
		removeCookie(cookie);
		
		// Notify derived class
		onCookieSetFromURL(cookie, url, alreadyDead);
		
        if (alreadyDead)
            continue;

        changed = true;
        d->tree.insert(splitHost(cookie.domain()), cookie);
    }

    return changed;
}

QList<QNetworkCookie> NetworkCookieJar::allCookies() const
{
#if defined(NETWORKCOOKIEJAR_DEBUG)
    qDebug() << "NetworkCookieJar::" << __FUNCTION__;
#endif
    return d->tree.all();
}

void NetworkCookieJar::clearCookies()
{
#if defined(NETWORKCOOKIEJAR_DEBUG)
    qDebug() << "NetworkCookieJar::" << __FUNCTION__;
#endif
    d->tree.clear();
}

void NetworkCookieJar::setCookies(const QList<QNetworkCookie> &cookieList)
{
#if defined(NETWORKCOOKIEJAR_DEBUG)
    qDebug() << "NetworkCookieJar::" << __FUNCTION__ << cookieList.count();
#endif

    QDateTime now = QDateTime::currentDateTime().toTimeSpec(Qt::UTC);

    foreach (const QNetworkCookie &cookie, cookieList) 
	{
		// If a matching cookie is already in the list, remove it.
		removeCookie(cookie);
		
		if(!cookie.isSessionCookie() && cookie.expirationDate() < now)
		{
#if defined(NETWORKCOOKIEJAR_DEBUG)
			qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "removing cookie: " << cookie;
#endif
			// This cookie has expired -- don't re-add it
		}
		else
		{
#if defined(NETWORKCOOKIEJAR_DEBUG)
			qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "adding cookie: " << cookie;
#endif
			// this cookie has not expired -- save it
    	    d->tree.insert(splitHost(cookie.domain()), cookie);
		}

    }
}

void NetworkCookieJar::setAllCookies(const QList<QNetworkCookie> &cookieList)
{
#if defined(NETWORKCOOKIEJAR_DEBUG)
    qDebug() << "NetworkCookieJar::" << __FUNCTION__ << cookieList.count();
#endif
	clearCookies();
	setCookies(cookieList);
}

void NetworkCookieJar::removeCookie(const QNetworkCookie &cookie)
{
#if defined(NETWORKCOOKIEJAR_DEBUG)
	qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "removing cookie: " << cookie;
#endif
	
	// If a cookie with the matching domain, path, and name exists in the cookiejar, remove it.
	QString domain = cookie.domain();
	Q_ASSERT(!domain.isEmpty());
	QStringList urlHost = splitHost(domain);

	QList<QNetworkCookie> cookies = d->tree.find(urlHost);
	QList<QNetworkCookie>::const_iterator it = cookies.constBegin();
	for (; it != cookies.constEnd(); ++it) 
	{
		if (cookie.name() == it->name() &&
			domain == it->domain() &&
			cookie.path() == it->path()) 
		{
#if defined(NETWORKCOOKIEJAR_DEBUG)
			qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "found matching cookie: " << *it;
#endif
			d->tree.remove(urlHost, *it);
			break;
		}
	}
}

void NetworkCookieJar::dump()
{
#if defined(NETWORKCOOKIEJAR_DEBUG)
	qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "dumping all cookies: ";
	QList<QNetworkCookie> cookies = allCookies();
	foreach (const QNetworkCookie &cookie, cookies)
	{
		qDebug() << "    " << cookie;
	}
#endif	
}

QString NetworkCookieJarPrivate::urlPath(const QUrl &url) const
{
    QString urlPath = url.path();
    if (!urlPath.endsWith(QLatin1Char('/')))
        urlPath += QLatin1Char('/');
    return urlPath;
}

bool NetworkCookieJarPrivate::matchingPath(const QNetworkCookie &cookie, const QString &urlPath) const
{
    QString cookiePath = cookie.path();
    if (!cookiePath.endsWith(QLatin1Char('/')))
        cookiePath += QLatin1Char('/');

    return urlPath.startsWith(cookiePath);
}

bool NetworkCookieJarPrivate::matchesBlacklist(const QString &string) const
{
    if (!setSecondLevelDomain) {
        // Alternatively to save a little bit of ram we could just
        // use bsearch on twoLevelDomains in place
        for (int j = 0; twoLevelDomains[j]; ++j)
            secondLevelDomains += QLatin1String(twoLevelDomains[j]);
        setSecondLevelDomain = true;
    }
    QStringList::const_iterator i =
         qBinaryFind(secondLevelDomains.constBegin(), secondLevelDomains.constEnd(), string);
        return (i != secondLevelDomains.constEnd());
}

bool NetworkCookieJarPrivate::matchingDomain(const QNetworkCookie &cookie, const QUrl &url) const
{
    QString domain = cookie.domain().simplified().toLower();
    domain.remove(QLatin1Char(' '));
    QStringList parts = splitHost(domain);
    if (parts.isEmpty())
        return false;

    // When there is only one part only file://localhost/ is accepted
    if (parts.count() == 1) {
        QString s = parts.first();
        if (parts.first() != QLatin1String("localhost"))
            return false;
        if (url.scheme().toLower() == QLatin1String("file"))
            return true;
    }

    // Check for blacklist
    if (parts.count() == 2 && matchesBlacklist(parts.last()))
        return false;

    QStringList urlParts = url.host().toLower().split(QLatin1Char('.'), QString::SkipEmptyParts);
    if (urlParts.isEmpty())
        return false;
    while (urlParts.count() > parts.count())
        urlParts.removeFirst();

    for (int j = 0; j < urlParts.count(); ++j) {
        if (urlParts.at(j) != parts.at(j)) {
            return false;
        }
    }

    return true;
}

void NetworkCookieJar::setSecondLevelDomains(const QStringList &secondLevelDomains)
{
    d->setSecondLevelDomain = true;
    d->secondLevelDomains = secondLevelDomains;
    qSort(d->secondLevelDomains);
}


void NetworkCookieJar::onCookieSetFromURL(const QNetworkCookie &cookie, const QUrl &url, bool already_dead)
{
	Q_UNUSED(cookie);
	Q_UNUSED(url);
	Q_UNUSED(already_dead);
	
	// Derived classes can use this to track cookie changes.
}
