/*
  This file is part of the PhantomJS project from Ofi Labs.

  Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com>
  Copyright (C) 2012 Ivan De Marino <ivan.de.marino@gmail.com>

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include "phantom.h"
#include "config.h"
#include "cookiejar.h"

#include <QDateTime>
#include <QDataStream>
#include <QSettings>
#include <QTimer>

#define COOKIE_JAR_VERSION      1

// Operators needed for Cookie Serialization
QT_BEGIN_NAMESPACE
QDataStream& operator<<(QDataStream& stream, const QList<QNetworkCookie>& list)
{
    stream << COOKIE_JAR_VERSION;
    stream << quint32(list.size());
    for (int i = 0; i < list.size(); ++i) {
        stream << list.at(i).toRawForm();
    }
    return stream;
}

QDataStream& operator>>(QDataStream& stream, QList<QNetworkCookie>& list)
{
    list.clear();

    quint32 version;
    stream >> version;

    if (version != COOKIE_JAR_VERSION) {
        return stream;
    }

    quint32 count;
    stream >> count;
    for (quint32 i = 0; i < count; ++i) {
        QByteArray value;
        stream >> value;
        QList<QNetworkCookie> newCookies = QNetworkCookie::parseCookies(value);
        if (newCookies.count() == 0 && value.length() != 0) {
            qWarning() << "CookieJar: Unable to parse saved cookie:" << value;
        }
        for (int j = 0; j < newCookies.count(); ++j) {
            list.append(newCookies.at(j));
        }
        if (stream.atEnd()) {
            break;
        }
    }
    return stream;
}
QT_END_NAMESPACE

// public:
CookieJar::CookieJar(QString cookiesFile, QObject* parent)
    : QNetworkCookieJar(parent)
    , m_enabled(true)
{
    if (cookiesFile == "") {
        m_cookieStorage = 0;
        qDebug() << "CookieJar - Created but will not store cookies (use option '--cookies-file=<filename>' to enable persistent cookie storage)";
    } else {
        m_cookieStorage = new QSettings(cookiesFile, QSettings::IniFormat, this);
        load();
        qDebug() << "CookieJar - Created and will store cookies in:" << cookiesFile;
    }
}

// private:
CookieJar::~CookieJar()
{
    // On destruction, before saving, clear all the session cookies
    purgeSessionCookies();
    save();
}

bool CookieJar::setCookiesFromUrl(const QList<QNetworkCookie>& cookieList, const QUrl& url)
{
    // Update cookies in memory
    if (isEnabled()) {
        QNetworkCookieJar::setCookiesFromUrl(cookieList, url);
        save();
    }
    // No changes occurred
    return false;
}

QList<QNetworkCookie> CookieJar::cookiesForUrl(const QUrl& url) const
{
    if (isEnabled()) {
        return QNetworkCookieJar::cookiesForUrl(url);
    }
    // The CookieJar is disabled: don't return any cookie
    return QList<QNetworkCookie>();
}

bool CookieJar::addCookie(const QNetworkCookie& cookie, const QString& url)
{
    if (isEnabled() && (!url.isEmpty() || !cookie.domain().isEmpty())) {
        // Save a single cookie
        setCookiesFromUrl(
            QList<QNetworkCookie>() << cookie, //< unfortunately, "setCookiesFromUrl" requires a list
            !url.isEmpty() ?
            url :           //< use given URL
            QString(        //< mock-up a URL
                (cookie.isSecure() ? "https://" : "http://") +                              //< URL protocol
                QString(cookie.domain().startsWith('.') ? "www" : "") + cookie.domain() +   //< URL domain
                (cookie.path().isEmpty() ? "/" : cookie.path())));                          //< URL path

        // Return "true" if the cookie was really set
        if (contains(cookie)) {
            return true;
        }
        qDebug() << "CookieJar - Rejected Cookie" << cookie.toRawForm();
        return false;
    }
    return false;
}

void CookieJar::addCookie(const QVariantMap& cookie)
{
    addCookieFromMap(cookie);
}

bool CookieJar::addCookieFromMap(const QVariantMap& cookie, const QString& url)
{
    QNetworkCookie newCookie;

    // The cookie must have a non-empty "name" and a "value"
    if (!cookie["name"].isNull() && !cookie["name"].toString().isEmpty() && !cookie["value"].isNull()) {
        // Name & Value
        newCookie.setName(cookie["name"].toByteArray());
        newCookie.setValue(cookie["value"].toByteArray());

        // Domain, if provided
        if (!cookie["domain"].isNull() && !cookie["domain"].toString().isEmpty()) {
            newCookie.setDomain(cookie["domain"].toString());
        }

        // Path, if provided
        if (!cookie["path"].isNull() || !cookie["path"].toString().isEmpty()) {
            newCookie.setPath(cookie["path"].toString());
        }

        // HttpOnly, false by default
        newCookie.setHttpOnly(cookie["httponly"].isNull() ? false : cookie["httponly"].toBool());
        // Secure, false by default
        newCookie.setSecure(cookie["secure"].isNull() ? false : cookie["secure"].toBool());

        // Expiration Date, if provided, giving priority to "expires" over "expiry"
        QVariant expiresVar;
        if (!cookie["expires"].isNull()) {
            expiresVar = cookie["expires"];
        } else if (!cookie["expiry"].isNull()) {
            expiresVar = cookie["expiry"];
        }

        if (expiresVar.isValid()) {
            QDateTime expirationDate;
            if (expiresVar.type() == QVariant::String) {
                // Set cookie expire date via "classic" string format
                QString datetime = expiresVar.toString().replace(" GMT", "");
                expirationDate = QDateTime::fromString(datetime, "ddd, dd MMM yyyy hh:mm:ss");
            } else if (expiresVar.type() == QVariant::Double) {
                // Set cookie expire date via "number of seconds since epoch"
                // NOTE: Every JS number is a Double.
                // @see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf
                expirationDate = QDateTime::fromMSecsSinceEpoch(expiresVar.toLongLong());
            }

            if (expirationDate.isValid()) {
                newCookie.setExpirationDate(expirationDate);
            }
        }

        return addCookie(newCookie, url);
    }
    return false;
}

bool CookieJar::addCookies(const QList<QNetworkCookie>& cookiesList, const QString& url)
{
    bool added = false;
    for (int i = cookiesList.length() - 1; i >= 0; --i) {
        if (addCookie(cookiesList.at(i), url)) {
            // change it to "true" if at least 1 cookie was set
            added = true;
        }
    }
    return added;
}

bool CookieJar::addCookiesFromMap(const QVariantList& cookiesList, const QString& url)
{
    bool added = false;
    for (int i = cookiesList.length() - 1; i >= 0; --i) {
        if (addCookieFromMap(cookiesList.at(i).toMap(), url)) {
            // change it to "true" if at least 1 cookie was set
            added = true;
        }
    }
    return added;
}

QList<QNetworkCookie> CookieJar::cookies(const QString& url) const
{
    if (url.isEmpty()) {
        // No url provided: return all the cookies in this CookieJar
        return allCookies();
    } else {
        // Return ONLY the cookies that match this URL
        return cookiesForUrl(url);
    }
}

QVariantList CookieJar::cookiesToMap(const QString& url) const
{
    QVariantList result;
    QNetworkCookie c;
    QVariantMap cookie;

    QList<QNetworkCookie> cookiesList = cookies(url);
    for (int i = cookiesList.length() - 1; i >= 0; --i) {
        c = cookiesList.at(i);

        cookie.clear();
        cookie["domain"] = QVariant(c.domain());
        cookie["name"] = QVariant(QString(c.name()));
        cookie["value"] = QVariant(QString(c.value()));
        cookie["path"] = (c.path().isNull() || c.path().isEmpty()) ? QVariant("/") : QVariant(c.path());
        cookie["httponly"] = QVariant(c.isHttpOnly());
        cookie["secure"] = QVariant(c.isSecure());
        if (c.expirationDate().isValid()) {
            cookie["expires"] = QVariant(QString(c.expirationDate().toString("ddd, dd MMM yyyy hh:mm:ss")).append(" GMT"));
            cookie["expiry"] = QVariant(c.expirationDate().toMSecsSinceEpoch() / 1000);
        }

        result.append(cookie);
    }

    return result;
}

QNetworkCookie CookieJar::cookie(const QString& name, const QString& url) const
{
    QList<QNetworkCookie> cookiesList = cookies(url);
    for (int i = cookiesList.length() - 1; i >= 0; --i) {
        if (cookiesList.at(i).name() == name) {
            return cookiesList.at(i);
        }
    }
    return QNetworkCookie();
}

QVariantMap CookieJar::cookieToMap(const QString& name, const QString& url) const
{
    QVariantMap cookie;

    QVariantList cookiesList = cookiesToMap(url);
    for (int i = cookiesList.length() - 1; i >= 0; --i) {
        cookie = cookiesList.at(i).toMap();
        if (cookie["name"].toString() == name) {
            return cookie;
        }
    }
    return QVariantMap();
}

bool CookieJar::deleteCookie(const QString& name, const QString& url)
{
    bool deleted = false;
    if (isEnabled()) {

        // NOTE: This code has been written in an "extended form" to make it
        // easy to understand. Surely this could be "shrinked", but it
        // would probably look uglier.

        QList<QNetworkCookie> cookiesListAll;

        if (url.isEmpty()) {
            if (name.isEmpty()) {           //< Neither "name" or "url" provided
                // This method has been used wrong:
                // "redirecting" to the right method for the job
                clearCookies();
            } else {                        //< Only "name" provided
                // Delete all cookies with the given name from the CookieJar
                cookiesListAll = allCookies();
                for (int i = cookiesListAll.length() - 1; i >= 0; --i) {
                    if (cookiesListAll.at(i).name() == name) {
                        // Remove this cookie
                        qDebug() << "CookieJar - Deleted" << cookiesListAll.at(i).toRawForm();
                        cookiesListAll.removeAt(i);
                        deleted = true;
                    }
                }
            }
        } else {
            // Delete cookie(s) from the ones visible to the given "url".
            // Use the "name" to delete only the right one, otherwise all of them.
            QList<QNetworkCookie> cookiesListUrl = cookies(url);
            cookiesListAll = allCookies();
            for (int i = cookiesListAll.length() - 1; i >= 0; --i) {
                if (cookiesListUrl.contains(cookiesListAll.at(i)) &&            //< if it part of the set of cookies visible at URL
                        (cookiesListAll.at(i).name() == name || name.isEmpty())) {  //< and if the name matches, or no name provided
                    // Remove this cookie
                    qDebug() << "CookieJar - Deleted" << cookiesListAll.at(i).toRawForm();
                    cookiesListAll.removeAt(i);
                    deleted = true;

                    if (!name.isEmpty()) {
                        // Only one cookie was supposed to be deleted: we are done here!
                        break;
                    }
                }
            }
        }

        // Put back the remaining cookies
        setAllCookies(cookiesListAll);
    }
    return deleted;
}

bool CookieJar::deleteCookies(const QString& url)
{
    if (isEnabled()) {
        if (url.isEmpty()) {
            // No URL provided: delete ALL the cookies in the CookieJar
            clearCookies();
            return true;
        }

        // No cookie name provided: delete all the cookies visible by this URL
        qDebug() << "Delete all cookies for URL:" << url;
        return deleteCookie("", url);
    }
    return false;
}

void CookieJar::clearCookies()
{
    if (isEnabled()) {
        setAllCookies(QList<QNetworkCookie>());
    }
}

void CookieJar::enable()
{
    m_enabled = true;
}

void CookieJar::disable()
{
    m_enabled = false;
}

bool CookieJar::isEnabled() const
{
    return m_enabled;
}

void CookieJar::close()
{
    deleteLater();
}

// private:
bool CookieJar::purgeExpiredCookies()
{
    QList<QNetworkCookie> cookiesList = allCookies();

    // If empty, there is nothing to purge
    if (cookiesList.isEmpty()) {
        return false;
    }

    // Check if any cookie has expired
    int prePurgeCookiesCount = cookiesList.count();
    QDateTime now = QDateTime::currentDateTime();
    for (int i = cookiesList.count() - 1; i >= 0; --i) {
        if (!cookiesList.at(i).isSessionCookie() && cookiesList.at(i).expirationDate() < now) {
            qDebug() << "CookieJar - Purged (expired)" << cookiesList.at(i).toRawForm();
            cookiesList.removeAt(i);
        }
    }

    // Set cookies and returns "true" if at least 1 cookie expired and has been removed
    if (prePurgeCookiesCount != cookiesList.count()) {
        setAllCookies(cookiesList);
        return true;
    }
    return false;
}

bool CookieJar::purgeSessionCookies()
{
    QList<QNetworkCookie> cookiesList = allCookies();

    // If empty, there is nothing to purge
    if (cookiesList.isEmpty()) {
        return false;
    }

    // Check if any cookie has expired
    int prePurgeCookiesCount = cookiesList.count();
    for (int i = cookiesList.count() - 1; i >= 0; --i) {
        if (cookiesList.at(i).isSessionCookie()) {
            qDebug() << "CookieJar - Purged (session)" << cookiesList.at(i).toRawForm();
            cookiesList.removeAt(i);
        }
    }

    // Set cookies and returns "true" if at least 1 session cookie was found and removed
    if (prePurgeCookiesCount != cookiesList.count()) {
        setAllCookies(cookiesList);
        return true;
    }
    return false;
}

void CookieJar::save()
{
    if (isEnabled()) {
        // Get rid of all the Cookies that have expired
        purgeExpiredCookies();

#ifndef QT_NO_DEBUG_OUTPUT
        foreach(QNetworkCookie cookie, allCookies()) {
            qDebug() << "CookieJar - Saved" << cookie.toRawForm();
        }
#endif

        // Store cookies
        if (m_cookieStorage) {
            m_cookieStorage->setValue(QLatin1String("cookies"), QVariant::fromValue<QList<QNetworkCookie> >(allCookies()));
        }
    }
}

void CookieJar::load()
{
    if (isEnabled()) {
        // Register a "StreamOperator" for this Meta Type, so we can easily serialize/deserialize the cookies
        qRegisterMetaTypeStreamOperators<QList<QNetworkCookie> >("QList<QNetworkCookie>");

        // Load all the cookies
        if (m_cookieStorage) {
            setAllCookies(qvariant_cast<QList<QNetworkCookie> >(m_cookieStorage->value(QLatin1String("cookies"))));
        }

        // If any cookie has expired since last execution, purge and save before going any further
        if (purgeExpiredCookies()) {
            save();
        }

#ifndef QT_NO_DEBUG_OUTPUT
        foreach(QNetworkCookie cookie, allCookies()) {
            qDebug() << "CookieJar - Loaded" << cookie.toRawForm();
        }
#endif
    }
}

bool CookieJar::contains(const QNetworkCookie& cookieToFind) const
{
    QList<QNetworkCookie> cookiesList = allCookies();
    foreach(QNetworkCookie cookie, cookiesList) {
        if (cookieToFind == cookie) {
            return true;
        }
    }

    return false;
}
