506 lines
16 KiB
C++
506 lines
16 KiB
C++
/*
|
|
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;
|
|
}
|