From 244cf251cd767db3ca72d1f2ba9432bda0b0ba7d Mon Sep 17 00:00:00 2001 From: Joseph Rollinson Date: Tue, 18 Jun 2013 14:09:36 -0500 Subject: [PATCH] Adds support for multiple Cookie Jars. Previously, there was a single global cookie jar shared between all web pages. Now, one can have separate cookie jars for different web pages. Makes CookieJar a normal class, not a singleton. Moves many public CookieJar methods to public slots. Adds default cookie jar to Phantom. Adds the CookieJar module that provides access to cookie jars in javascript. Adds cookie jar module tests. Usage: var jar = require('cookiejar').create(); var webpage = require('webpage').create(); webpage.cookieJar = jar; ... webpage.close(); jar.close(); JS API changes: Webpage: var jar = page.cookieJar; -- assigns 'jar' the given webpage's cookie jar. page.cookiejar = jar; -- sets 'jar' as the given webpage's cookie jar. CookieJar: var jar = require('cookiejar').create(path) creates a cookie jar with persistent storage at the given file path (path not mandatory). var cookies = jar.cookies; -- assign's 'jar' the list of cookies in the cookie jar. jar.cookies = [c1, c2]; -- sets the cookie jar's cookies as the ones in the list. jar.addCookie(cookie) -- adds cookie 'cookie' to the cookie jar. https://github.com/ariya/phantomjs/issues/11417 --- src/cookiejar.cpp | 41 +++++++------- src/cookiejar.h | 19 +++---- src/modules/cookiejar.js | 68 ++++++++++++++++++++++++ src/modules/webpage.js | 14 +++++ src/networkaccessmanager.cpp | 7 ++- src/phantom.cpp | 26 +++++---- src/phantom.h | 3 ++ src/phantomjs.pro | 1 + src/phantomjs.qrc | 1 + src/webpage.cpp | 33 +++++++++--- src/webpage.h | 18 +++++++ test/cookiejar-spec.js | 100 +++++++++++++++++++++++++++++++++++ test/require/require_spec.js | 4 ++ test/run-tests.js | 1 + 14 files changed, 286 insertions(+), 50 deletions(-) create mode 100644 src/modules/cookiejar.js create mode 100644 test/cookiejar-spec.js diff --git a/src/cookiejar.cpp b/src/cookiejar.cpp index 34babd08..a953d4b4 100644 --- a/src/cookiejar.cpp +++ b/src/cookiejar.cpp @@ -78,32 +78,22 @@ QDataStream &operator>>(QDataStream &stream, QList &list) } QT_END_NAMESPACE -// private: +// public: CookieJar::CookieJar(QString cookiesFile, QObject *parent) : QNetworkCookieJar(parent) - , m_cookieStorage(new QSettings(cookiesFile, QSettings::IniFormat, this)) , m_enabled(true) { - load(); -} - -// public: -CookieJar *CookieJar::instance(QString cookiesFile) -{ - static CookieJar *singleton = NULL; - if (!singleton) { - if (cookiesFile.isEmpty()) { - qDebug() << "CookieJar - Created but will not store cookies (use option '--cookies-file=' to enable persistent cookie storage)"; - } else { - qDebug() << "CookieJar - Created and will store cookies in:" << cookiesFile; - } - // Create singleton and assign ownershipt to the Phantom singleton object - // NOTE: First time this is done is when we set "once and for all" the Cookies' File - singleton = new CookieJar(cookiesFile, Phantom::instance()); + if (cookiesFile == "") { + m_cookieStorage = 0; + qDebug() << "CookieJar - Created but will not store cookies (use option '--cookies-file=' to enable persistent cookie storage)"; + } else { + m_cookieStorage = new QSettings(cookiesFile, QSettings::IniFormat, this); + load(); + qDebug() << "CookieJar - Created and will store cookies in:" << cookiesFile; } - return singleton; } +// private: CookieJar::~CookieJar() { // On destruction, before saving, clear all the session cookies @@ -390,6 +380,11 @@ bool CookieJar::isEnabled() const return m_enabled; } +void CookieJar::close() +{ + deleteLater(); +} + // private: bool CookieJar::purgeExpiredCookies() { @@ -457,7 +452,9 @@ void CookieJar::save() #endif // Store cookies - m_cookieStorage->setValue(QLatin1String("cookies"), QVariant::fromValue >(allCookies())); + if (m_cookieStorage) { + m_cookieStorage->setValue(QLatin1String("cookies"), QVariant::fromValue >(allCookies())); + } } } @@ -468,7 +465,9 @@ void CookieJar::load() qRegisterMetaTypeStreamOperators >("QList"); // Load all the cookies - setAllCookies(qvariant_cast >(m_cookieStorage->value(QLatin1String("cookies")))); + if (m_cookieStorage) { + setAllCookies(qvariant_cast >(m_cookieStorage->value(QLatin1String("cookies")))); + } // If any cookie has expired since last execution, purge and save before going any further if (purgeExpiredCookies()) { diff --git a/src/cookiejar.h b/src/cookiejar.h index b13c4b7f..d25f1a06 100644 --- a/src/cookiejar.h +++ b/src/cookiejar.h @@ -40,35 +40,36 @@ class CookieJar: public QNetworkCookieJar { Q_OBJECT -private: - CookieJar(QString cookiesFile, QObject *parent = NULL); public: - static CookieJar *instance(QString cookiesFile = QString()); + CookieJar(QString cookiesFile, QObject *parent = NULL); virtual ~CookieJar(); bool setCookiesFromUrl(const QList &cookieList, const QUrl & url); QList cookiesForUrl (const QUrl & url) const; bool addCookie(const QNetworkCookie &cookie, const QString &url = QString()); - bool addCookieFromMap(const QVariantMap &cookie, const QString &url = QString()); bool addCookies(const QList &cookiesList, const QString &url = QString()); - bool addCookiesFromMap(const QVariantList &cookiesList, const QString &url = QString()); QList cookies(const QString &url = QString()) const; - QVariantList cookiesToMap(const QString &url = QString()) const; QNetworkCookie cookie(const QString &name, const QString &url = QString()) const; - QVariantMap cookieToMap(const QString &name, const QString &url = QString()) const; - bool deleteCookie(const QString &name, const QString &url = QString()); bool deleteCookies(const QString &url = QString()); - void clearCookies(); void enable(); void disable(); bool isEnabled() const; +public slots: + bool addCookieFromMap(const QVariantMap &cookie, const QString &url = QString()); + bool addCookiesFromMap(const QVariantList &cookiesList, const QString &url = QString()); + QVariantList cookiesToMap(const QString &url = QString()) const; + QVariantMap cookieToMap(const QString &name, const QString &url = QString()) const; + bool deleteCookie(const QString &name, const QString &url = QString()); + void clearCookies(); + void close(); + private slots: bool purgeExpiredCookies(); bool purgeSessionCookies(); diff --git a/src/modules/cookiejar.js b/src/modules/cookiejar.js new file mode 100644 index 00000000..8d418b5f --- /dev/null +++ b/src/modules/cookiejar.js @@ -0,0 +1,68 @@ +/*jslint sloppy: true, nomen: true */ +/*global exports:true */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2013 Joseph Rollinson, jtrollinson@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 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 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. +*/ + +/* Takes in a QtCookieJar and decorates it with useful functions. */ +function decorateCookieJar(jar) { + + /* Allows one to add a cookie to the cookie jar from inside JavaScript */ + jar.addCookie = function(cookie) { + jar.addCookieFromMap(cookie); + }; + + /* Getting and Setting jar.cookies gets and sets all the cookies in the + * cookie jar. + */ + jar.__defineGetter__('cookies', function() { + return this.cookiesToMap(); + }); + + jar.__defineSetter__('cookies', function(cookies) { + this.addCookiesFromMap(cookies); + }); + + return jar; + +} + +/* Creates and decorates a new cookie jar. + * path is the file path where Phantomjs will store the cookie jar persistently. + * path is not mandatory. + */ +exports.create = function (path) { + if (arguments.length < 1) { + path = ""; + } + return decorateCookieJar(phantom.createCookieJar(path)); +}; + +/* Exports the decorateCookieJar function */ +exports.decorate = decorateCookieJar; diff --git a/src/modules/webpage.js b/src/modules/webpage.js index 095e186b..55031099 100644 --- a/src/modules/webpage.js +++ b/src/modules/webpage.js @@ -414,6 +414,20 @@ function decorateNewPage(opts, page) { this.evaluate.apply(this, args); }; + /** + * get cookie jar for the page + */ + page.__defineGetter__("cookieJar", function() { + return require("cookiejar").decorate(this.cookieJar()); + }); + + /** + * set cookie jar for the page + */ + page.__defineSetter__("cookieJar", function(cookieJar) { + this.setCookieJarFromQObject(cookieJar); + }); + /** * get cookies of the page */ diff --git a/src/networkaccessmanager.cpp b/src/networkaccessmanager.cpp index 17112b34..39a01e8e 100644 --- a/src/networkaccessmanager.cpp +++ b/src/networkaccessmanager.cpp @@ -120,8 +120,6 @@ NetworkAccessManager::NetworkAccessManager(QObject *parent, const Config *config , m_networkDiskCache(0) , m_sslConfiguration(QSslConfiguration::defaultConfiguration()) { - setCookieJar(CookieJar::instance()); - if (config->diskCacheEnabled()) { m_networkDiskCache = new QNetworkDiskCache(this); m_networkDiskCache->setCacheDirectory(QDesktopServices::storageLocation(QDesktopServices::CacheLocation)); @@ -195,8 +193,9 @@ void NetworkAccessManager::setCookieJar(QNetworkCookieJar *cookieJar) QNetworkAccessManager::setCookieJar(cookieJar); // Remove NetworkAccessManager's ownership of this CookieJar and // pass it to the PhantomJS Singleton object. - // CookieJar is a SINGLETON, shouldn't be deleted when - // the NetworkAccessManager is deleted but only when we shutdown. + // CookieJar is shared between multiple instances of NetworkAccessManager. + // It shouldn't be deleted when the NetworkAccessManager is deleted, but + // only when close is called on the cookie jar. cookieJar->setParent(Phantom::instance()); } diff --git a/src/phantom.cpp b/src/phantom.cpp index a3492b15..519e56ed 100644 --- a/src/phantom.cpp +++ b/src/phantom.cpp @@ -97,9 +97,10 @@ void Phantom::init() } // Initialize the CookieJar - CookieJar::instance(m_config.cookiesFile()); + m_defaultCookieJar = new CookieJar(m_config.cookiesFile()); m_page = new WebPage(this, QUrl::fromLocalFile(m_config.scriptFile())); + m_page->setCookieJar(m_defaultCookieJar); m_pages.append(m_page); QString proxyType = m_config.proxyType(); @@ -290,15 +291,15 @@ bool Phantom::printDebugMessages() const bool Phantom::areCookiesEnabled() const { - return CookieJar::instance()->isEnabled(); + return m_defaultCookieJar->isEnabled(); } void Phantom::setCookiesEnabled(const bool value) { if (value) { - CookieJar::instance()->enable(); + m_defaultCookieJar->enable(); } else { - CookieJar::instance()->disable(); + m_defaultCookieJar->disable(); } } @@ -308,9 +309,14 @@ bool Phantom::webdriverMode() const } // public slots: +QObject *Phantom::createCookieJar(const QString& filePath) { + return new CookieJar(filePath, this); +} + QObject *Phantom::createWebPage() { WebPage *page = new WebPage(this); + page->setCookieJar(m_defaultCookieJar); // Store pointer to the page for later cleanup m_pages.append(page); @@ -436,33 +442,33 @@ void Phantom::onInitialized() bool Phantom::setCookies(const QVariantList &cookies) { // Delete all the cookies from the CookieJar - CookieJar::instance()->clearCookies(); + m_defaultCookieJar->clearCookies(); // Add a new set of cookies - return CookieJar::instance()->addCookiesFromMap(cookies); + return m_defaultCookieJar->addCookiesFromMap(cookies); } QVariantList Phantom::cookies() const { // Return all the Cookies in the CookieJar, as a list of Maps (aka JSON in JS space) - return CookieJar::instance()->cookiesToMap(); + return m_defaultCookieJar->cookiesToMap(); } bool Phantom::addCookie(const QVariantMap &cookie) { - return CookieJar::instance()->addCookieFromMap(cookie); + return m_defaultCookieJar->addCookieFromMap(cookie); } bool Phantom::deleteCookie(const QString &cookieName) { if (!cookieName.isEmpty()) { - return CookieJar::instance()->deleteCookie(cookieName); + return m_defaultCookieJar->deleteCookie(cookieName); } return false; } void Phantom::clearCookies() { - CookieJar::instance()->clearCookies(); + m_defaultCookieJar->clearCookies(); } diff --git a/src/phantom.h b/src/phantom.h index 49bed7ff..d515d537 100644 --- a/src/phantom.h +++ b/src/phantom.h @@ -39,6 +39,7 @@ #include "config.h" #include "system.h" #include "childprocess.h" +#include "cookiejar.h" class WebPage; class CustomPage; @@ -108,6 +109,7 @@ public: Q_INVOKABLE QObject *_createChildProcess(); public slots: + QObject *createCookieJar(const QString &filePath); QObject *createWebPage(); QObject *createWebServer(); QObject *createFilesystem(); @@ -193,6 +195,7 @@ private: QList > m_pages; QList > m_servers; Config m_config; + CookieJar *m_defaultCookieJar; friend class CustomPage; }; diff --git a/src/phantomjs.pro b/src/phantomjs.pro index 7cf77dbd..ffd1449c 100644 --- a/src/phantomjs.pro +++ b/src/phantomjs.pro @@ -53,6 +53,7 @@ OTHER_FILES += \ modules/webpage.js \ modules/webserver.js \ modules/child_process.js \ + modules/cookiejar.js \ repl.js include(gif/gif.pri) diff --git a/src/phantomjs.qrc b/src/phantomjs.qrc index 31839b5a..56933ccf 100644 --- a/src/phantomjs.qrc +++ b/src/phantomjs.qrc @@ -10,6 +10,7 @@ modules/system.js modules/child_process.js modules/_coffee-script.js + modules/cookiejar.js repl.js coffee-script/package.json diff --git a/src/webpage.cpp b/src/webpage.cpp index b29c4053..59e10168 100644 --- a/src/webpage.cpp +++ b/src/webpage.cpp @@ -110,6 +110,10 @@ public: } } + void setCookieJar(CookieJar *cookieJar) { + m_cookieJar = cookieJar; + } + public slots: bool shouldInterruptJavaScript() { m_webPage->javascriptInterrupt(); @@ -219,6 +223,7 @@ protected: newPage = new WebPage(Phantom::instance()); Phantom::instance()->m_pages.append(newPage); } + newPage->setCookieJar(m_cookieJar); // Apply default settings newPage->applySettings(Phantom::instance()->defaultPageSettings()); @@ -235,6 +240,7 @@ private: QString m_userAgent; QStringList m_uploadFiles; friend class WebPage; + CookieJar *m_cookieJar; }; @@ -775,36 +781,51 @@ QVariantMap WebPage::customHeaders() const return m_networkAccessManager->customHeaders(); } +void WebPage::setCookieJar(CookieJar *cookieJar) { + m_cookieJar = cookieJar; + m_customWebPage->setCookieJar(m_cookieJar); + m_networkAccessManager->setCookieJar(m_cookieJar); +} + +void WebPage::setCookieJarFromQObject(QObject *cookieJar) { + setCookieJar(qobject_cast(cookieJar)); +} + +CookieJar *WebPage::cookieJar() +{ + return m_cookieJar; +} + bool WebPage::setCookies(const QVariantList &cookies) { // Delete all the cookies for this URL - CookieJar::instance()->deleteCookies(this->url()); + m_cookieJar->deleteCookies(this->url()); // Add a new set of cookies foor this URL - return CookieJar::instance()->addCookiesFromMap(cookies, this->url()); + return m_cookieJar->addCookiesFromMap(cookies, this->url()); } QVariantList WebPage::cookies() const { // Return all the Cookies visible to this Page, as a list of Maps (aka JSON in JS space) - return CookieJar::instance()->cookiesToMap(this->url()); + return m_cookieJar->cookiesToMap(this->url()); } bool WebPage::addCookie(const QVariantMap &cookie) { - return CookieJar::instance()->addCookieFromMap(cookie, this->url()); + return m_cookieJar->addCookieFromMap(cookie, this->url()); } bool WebPage::deleteCookie(const QString &cookieName) { if (!cookieName.isEmpty()) { - return CookieJar::instance()->deleteCookie(cookieName, this->url()); + return m_cookieJar->deleteCookie(cookieName, this->url()); } return false; } bool WebPage::clearCookies() { - return CookieJar::instance()->deleteCookies(this->url()); + return m_cookieJar->deleteCookies(this->url()); } void WebPage::openUrl(const QString &address, const QVariant &op, const QVariantMap &settings) diff --git a/src/webpage.h b/src/webpage.h index c1ffbedf..df2b83a0 100644 --- a/src/webpage.h +++ b/src/webpage.h @@ -36,6 +36,8 @@ #include #include +#include "cookiejar.h" + class Config; class CustomPage; class WebpageCallbacks; @@ -357,6 +359,21 @@ public slots: */ QString currentFrameName() const; + /** + * Allows to set cookie jar for this page. + */ + void setCookieJar(CookieJar *cookieJar); + + /** + * Allows to set cookie jar in through QtWebKit Bridge + */ + void setCookieJarFromQObject(QObject *cookieJar); + + /** + * Returns the CookieJar object + */ + CookieJar *cookieJar(); + /** * Allows to set cookies by this Page, at the current URL. * This means that loading new URLs, causes the cookies to change dynamically @@ -520,6 +537,7 @@ private: bool m_ownsPages; int m_loadingProgress; bool m_shouldInterruptJs; + CookieJar *m_cookieJar; friend class Phantom; friend class CustomPage; diff --git a/test/cookiejar-spec.js b/test/cookiejar-spec.js new file mode 100644 index 00000000..0b41e109 --- /dev/null +++ b/test/cookiejar-spec.js @@ -0,0 +1,100 @@ +describe("CookieJar object", function() { + var jar = require('cookiejar').create(); + + it("should be creatable", function() { + expect(typeof jar).toEqual('object'); + expect(jar).toNotEqual(null); + }); + + expectHasProperty(jar, 'cookies'); + + expectHasFunction(jar, 'addCookie'); + expectHasFunction(jar, 'deleteCookie'); + expectHasFunction(jar, 'clearCookies'); + + it("should add a cookie and then remove it", function() { + var cookie = { + 'name' : 'Valid-Cookie-Name', + 'value' : 'Valid-Cookie-Value', + 'domain' : 'localhost', + 'path' : '/foo', + 'httponly' : true, + 'secure' : false + }; + + jar.addCookie(cookie); + var cookies = jar.cookies; + + expect(cookies.length).toEqual(1); + + expect(jar.deleteCookie('Valid-Cookie-Name')).toBe(true); + + expect(jar.cookies.length).toBe(0); + }); + + it("should set and get cookies with .cookies", function() { + var cookies = [{ + 'name' : 'Valid-Cookie-Name', + 'value' : 'Valid-Cookie-Value', + 'domain' : 'localhost', + 'path' : '/foo', + 'httponly' : true, + 'secure' : false + },{ + 'name' : 'Valid-Cookie-Name-Sec', + 'value' : 'Valid-Cookie-Value-Sec', + 'domain' : 'localhost', + 'path' : '/foo', + 'httponly' : true, + 'secure' : false, + 'expires' : new Date().getTime() + 3600 //< expires in 1h + }]; + + jar.cookies = cookies; + expect(jar.cookies.length).toBe(2); + + jar.clearCookies(); + expect(jar.cookies.length).toEqual(0); + + }); + + it("should be separate cookie jars", function() { + var jar1 = require('cookiejar').create(); + var jar2 = require('cookiejar').create(); + + var cookie1 = { + 'name' : 'Valid-Cookie-Name-1', + 'value' : 'Valid-Cookie-Value', + 'domain' : 'localhost', + 'path' : '/foo', + 'httponly' : true, + 'secure' : false + }; + + var cookie2 = { + 'name' : 'Valid-Cookie-Name-2', + 'value' : 'Valid-Cookie-Value', + 'domain' : 'localhost', + 'path' : '/foo', + 'httponly' : true, + 'secure' : false + }; + + jar1.addCookie(cookie1); + + expect(jar1.cookies.length).toBe(1); + expect(jar2.cookies.length).toBe(0); + + jar2.addCookie(cookie2); + expect(jar1.deleteCookie('Valid-Cookie-Name-1')).toBe(true); + + expect(jar1.cookies.length).toBe(0); + expect(jar2.cookies.length).toBe(1); + + jar1.close(); + jar2.close(); + + + }); + +}); diff --git a/test/require/require_spec.js b/test/require/require_spec.js index 2f006699..c7c5d175 100644 --- a/test/require/require_spec.js +++ b/test/require/require_spec.js @@ -11,6 +11,10 @@ describe("require()", function() { should.exist(require('webserver').create); }); + it("loads 'cookiejar' native module", function() { + should.exist(require('cookiejar').create); + }); + it("loads 'system' native module", function() { require('system').platform.should.equal('phantomjs'); }); diff --git a/test/run-tests.js b/test/run-tests.js index 83ae13d7..d508d783 100644 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -70,6 +70,7 @@ phantom.injectJs("./fs-spec-03.js"); //< Filesystem Specs 03 (Paths) phantom.injectJs("./fs-spec-04.js"); //< Filesystem Specs 04 (Tests) phantom.injectJs("./system-spec.js"); phantom.injectJs("./webkit-spec.js"); +phantom.injectJs("./cookiejar-spec.js"); require("./module_spec.js"); require("./require/require_spec.js"); require("./cjk-text-codecs.js");