diff --git a/examples/injectme.js b/examples/injectme.js new file mode 100644 index 00000000..8d93c9ce --- /dev/null +++ b/examples/injectme.js @@ -0,0 +1,25 @@ +// Use 'page.loadJsFile()' to load the script itself in the Page context + +if ( typeof(phantom) !== "undefined" ) { + var page = new WebPage(); + + // Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") + page.onConsoleMessage = function(msg) { + console.log(msg); + }; + + page.onAlert = function(msg) { + console.log(msg); + }; + + console.log("* Script running in the Phantom context."); + console.log("* Script will 'inject' itself in a page..."); + page.open("about:blank", function(status) { + if ( status === "success" ) { + console.log(page.injectJs("injectme.js") ? "... done injecting itself!" : "... fail! Check the $PWD?!"); + } + phantom.exit(); + }); +} else { + alert("* Script running in the Page context."); +} diff --git a/examples/phantomwebintro.js b/examples/phantomwebintro.js new file mode 100644 index 00000000..062a51dd --- /dev/null +++ b/examples/phantomwebintro.js @@ -0,0 +1,19 @@ +// Read the Phantom webpage '#intro' element text using jQuery and "includeJs" + +var page = new WebPage(); + +page.onConsoleMessage = function(msg) { + console.log(msg); +}; + +page.open("http://www.phantomjs.org", function(status) { + if ( status === "success" ) { + page.includeJs("http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js", function() { + page.evaluate(function() { + console.log("$(\"#intro\").text() -> " + $("#intro").text()); + }); + phantom.exit(); + }); + } +}); + diff --git a/src/bootstrap.js b/src/bootstrap.js index 347a9c42..daa5f46d 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -7,36 +7,36 @@ window.WebPage = function() { page.settings = JSON.parse(JSON.stringify(phantom.defaultPageSettings)); // private, don't touch this - page.handlers = {}; + page._handlers = {}; page.__defineSetter__("onLoadStarted", function(f) { - if (this.handlers && typeof this.handlers.loadStarted === 'function') { + if (this._handlers && typeof this._handlers.loadStarted === 'function') { try { - this.loadStarted.disconnect(this.handlers.loadStarted); + this.loadStarted.disconnect(this._handlers.loadStarted); } catch (e) {} } - this.handlers.loadStarted = f; - this.loadStarted.connect(this.handlers.loadStarted); + this._handlers.loadStarted = f; + this.loadStarted.connect(this._handlers.loadStarted); }); page.__defineSetter__("onLoadFinished", function(f) { - if (this.handlers && typeof this.handlers.loadFinished === 'function') { + if (this._handlers && typeof this._handlers.loadFinished === 'function') { try { - this.loadFinished.disconnect(this.handlers.loadFinished); + this.loadFinished.disconnect(this._handlers.loadFinished); } catch (e) {} } - this.handlers.loadFinished = f; - this.loadFinished.connect(this.handlers.loadFinished); + this._handlers.loadFinished = f; + this.loadFinished.connect(this._handlers.loadFinished); }); page.__defineSetter__("onResourceRequested", function(f) { - if (this.handlers && typeof this.handlers.resourceRequested === 'function') { + if (this._handlers && typeof this._handlers.resourceRequested === 'function') { try { - this.resourceRequested.disconnect(this.handlers.resourceRequested); + this.resourceRequested.disconnect(this._handlers.resourceRequested); } catch (e) {} } - this.handlers.resourceRequested = f; - this.resourceRequested.connect(this.handlers.resourceRequested); + this._handlers.resourceRequested = f; + this.resourceRequested.connect(this._handlers.resourceRequested); }); page.__defineSetter__("onResourceReceived", function(f) { @@ -83,5 +83,22 @@ window.WebPage = function() { throw "Wrong use of WebPage#open"; }; + page.includeJs = function(scriptUrl, onScriptLoaded) { + // Register temporary signal handler for 'alert()' + this.javaScriptAlertSent.connect(function(msgFromAlert) { + if ( msgFromAlert === scriptUrl ) { + // Resource loaded, time to fire the callback + onScriptLoaded(scriptUrl); + // And disconnect the signal handler + try { + this.javaScriptAlertSent.disconnect(this); + } catch (e) {} + } + }); + + // Append the script tag to the body + this._appendScriptElement(scriptUrl); + }; + return page; } diff --git a/src/consts.h b/src/consts.h index 4e51d30f..3d2cfd2e 100644 --- a/src/consts.h +++ b/src/consts.h @@ -35,5 +35,17 @@ #define PHANTOMJS_VERSION_MINOR 2 #define PHANTOMJS_VERSION_PATCH 0 #define PHANTOMJS_VERSION_STRING "1.2.0" +#define COFFEE_SCRIPT_EXTENSION ".coffee" + +#define JS_ELEMENT_CLICK "(function (el) { " \ + "var ev = document.createEvent('MouseEvents');" \ + "ev.initEvent(\"click\", true, true);" \ + "el.dispatchEvent(ev);" \ + "})(this);" + +#define JS_APPEND_SCRIPT_ELEMENT "var el = document.createElement('script');" \ + "el.onload = function() { alert('%1'); };" \ + "el.src = '%1';" \ + "document.body.appendChild(el);" #endif // CONSTS_H diff --git a/src/csconverter.cpp b/src/csconverter.cpp index ab84e612..68983392 100644 --- a/src/csconverter.cpp +++ b/src/csconverter.cpp @@ -52,8 +52,7 @@ CSConverter::CSConverter(QObject *parent) QString CSConverter::convert(const QString &script) { setProperty("source", script); - QWebFrame *frame = m_webPage.mainFrame(); - QVariant result = frame->evaluateJavaScript("this.CoffeeScript.compile(converter.source)"); + QVariant result = m_webPage.mainFrame()->evaluateJavaScript("this.CoffeeScript.compile(converter.source)"); if (result.type() == QVariant::String) return result.toString(); return QString(); diff --git a/src/main.cpp b/src/main.cpp index f2ec00f2..48333eae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -58,6 +58,8 @@ int main(int argc, char** argv) Phantom phantom; if (phantom.execute()) { app.exec(); + } else { + exit(EXIT_FAILURE); } return phantom.returnValue(); } diff --git a/src/phantom.cpp b/src/phantom.cpp index e486f741..5dcc2c42 100644 --- a/src/phantom.cpp +++ b/src/phantom.cpp @@ -34,6 +34,8 @@ #include #include +#include +#include #include #include "consts.h" @@ -184,31 +186,9 @@ QVariantMap Phantom::defaultPageSettings() const bool Phantom::execute() { - if (m_scriptFile.isEmpty()) - return false; - - QFile file; - file.setFileName(m_scriptFile); - if (!file.open(QFile::ReadOnly)) { - std::cerr << "Can't open '" << qPrintable(m_scriptFile) << "'" << std::endl << std::endl; - exit(1); - return false; - } - m_script = QString::fromUtf8(file.readAll()); - file.close(); - - if (m_scriptFile.endsWith(".coffee")) { - if (!m_converter) - m_converter = new CSConverter(this); - m_script = m_converter->convert(m_script); - } - - if (m_script.startsWith("#!")) { - m_script.prepend("//"); - } - - m_page->mainFrame()->evaluateJavaScript(m_script); - return !m_terminated; + return !m_scriptFile.isEmpty() && //< script filename provided + Utils::injectJsInFrame(m_scriptFile, QDir::currentPath(), m_page->mainFrame()) && //< script injected + !m_terminated; //< not terminated } int Phantom::returnValue() const @@ -230,6 +210,7 @@ QObject *Phantom::createWebPage() WebPage *page = new WebPage(this); page->applySettings(m_defaultPageSettings); page->setNetworkAccessManager(m_netAccessMan); + page->setScriptLookupDir(QFileInfo(m_scriptFile).dir().absolutePath()); return page; } @@ -240,6 +221,10 @@ void Phantom::exit(int code) QApplication::instance()->exit(code); } +bool Phantom::injectJs(const QString &jsFilePath) { + return Utils::injectJsInFrame(jsFilePath, QDir::currentPath(), m_page->mainFrame()); +} + void Phantom::printConsoleMessage(const QString &msg) { std::cout << qPrintable(msg) << std::endl; diff --git a/src/phantom.h b/src/phantom.h index 07f68225..3393a80a 100644 --- a/src/phantom.h +++ b/src/phantom.h @@ -58,6 +58,7 @@ public: public slots: QObject *createWebPage(); + bool injectJs(const QString &jsFilePath); void exit(int code = 0); private slots: diff --git a/src/utils.cpp b/src/utils.cpp index bde5a419..5920c363 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -31,7 +31,9 @@ #include #include #include +#include +#include "consts.h" #include "utils.h" // public: @@ -67,6 +69,50 @@ void Utils::messageHandler(QtMsgType type, const char *msg) } } +QString Utils::coffee2js(const QString &script) +{ + // We need only one instance of the CSConverter to survive for the whole life of PhantomJS + static CSConverter *coffeeScriptConverter = NULL; + if ( !coffeeScriptConverter ) { + coffeeScriptConverter = new CSConverter(); + } + + return coffeeScriptConverter->convert(script); +} + +bool Utils::injectJsInFrame(const QString &jsFilePath, const QString &scriptLookupDir, QWebFrame *targetFrame) +{ + // Don't do anything if an empty string is passed + if (!jsFilePath.isEmpty()) { + QFile jsFile; + + // Is file in the PWD? + jsFile.setFileName(QDir::fromNativeSeparators(jsFilePath)); //< Normalise User-provided path + if (!jsFile.exists()) { + // File is not in the PWD. Is it in the lookup directory? + jsFile.setFileName( scriptLookupDir + '/' + QDir::fromNativeSeparators(jsFilePath) ); + } + + if ( jsFile.open(QFile::ReadOnly) ) { + QString scriptBody = QString::fromUtf8(jsFile.readAll()); + // Remove CLI script heading + if (scriptBody.startsWith("#!")) { + scriptBody.prepend("//"); + } + + // Execute JS code in the context of the document + targetFrame->evaluateJavaScript(jsFile.fileName().endsWith(COFFEE_SCRIPT_EXTENSION) ? + Utils::coffee2js(scriptBody) : //< convert from Coffee Script + scriptBody); + jsFile.close(); + return true; + } else { + std::cerr << "Can't open '" << qPrintable(jsFilePath) << "'" << std::endl << std::endl; + } + } + return false; +} + // private: Utils::Utils() { diff --git a/src/utils.h b/src/utils.h index 4ff9a5cf..65548fde 100644 --- a/src/utils.h +++ b/src/utils.h @@ -31,6 +31,9 @@ #define UTILS_H #include +#include + +#include "csconverter.h" /** * Aggregate common utility functions. @@ -42,6 +45,8 @@ class Utils public: static void showUsage(); static void messageHandler(QtMsgType type, const char *msg); + static QString coffee2js(const QString &script); + static bool injectJsInFrame(const QString &jsFilePath, const QString &scriptLookupDir, QWebFrame *targetFrame); private: Utils(); //< This class shouldn't be instantiated diff --git a/src/webpage.cpp b/src/webpage.cpp index a4447a73..ebccafa4 100644 --- a/src/webpage.cpp +++ b/src/webpage.cpp @@ -43,12 +43,16 @@ #include #include +#include "utils.h" + #if QT_VERSION >= QT_VERSION_CHECK(4, 6, 0) #include #endif #include +#include "consts.h" + class CustomPage: public QWebPage { public: @@ -160,6 +164,17 @@ void WebPage::setContent(const QString &content) m_mainFrame->setHtml(content); } + +QString WebPage::scriptLookupDir() const +{ + return m_scriptLookupDir; +} + +void WebPage::setScriptLookupDir(const QString &dirPath) +{ + m_scriptLookupDir = dirPath; +} + void WebPage::applySettings(const QVariantMap &def) { QWebSettings *opt = m_webPage->settings(); @@ -459,12 +474,6 @@ bool WebPage::renderPdf(const QString &fileName) #if QT_VERSION >= QT_VERSION_CHECK(4, 6, 0) -#define ELEMENT_CLICK "(function (el) { " \ - "var ev = document.createEvent('MouseEvents');" \ - "ev.initEvent('click', true, true);" \ - "el.dispatchEvent(ev);" \ - "})(this)" - void WebPage::uploadFile(const QString &selector, const QString &fileName) { QWebElement el = m_mainFrame->findFirstElement(selector); @@ -472,6 +481,15 @@ void WebPage::uploadFile(const QString &selector, const QString &fileName) return; m_webPage->m_uploadFile = fileName; - el.evaluateJavaScript(ELEMENT_CLICK); + el.evaluateJavaScript(JS_ELEMENT_CLICK); } + #endif + +bool WebPage::injectJs(const QString &jsFilePath) { + return Utils::injectJsInFrame(jsFilePath, m_scriptLookupDir, m_mainFrame); +} + +void WebPage::_appendScriptElement(const QString &scriptUrl) { + m_mainFrame->evaluateJavaScript( QString(JS_APPEND_SCRIPT_ELEMENT).arg(scriptUrl) ); +} diff --git a/src/webpage.h b/src/webpage.h index 87eb59c0..19eec472 100644 --- a/src/webpage.h +++ b/src/webpage.h @@ -42,6 +42,7 @@ class WebPage: public QObject { Q_OBJECT Q_PROPERTY(QString content READ content WRITE setContent) + Q_PROPERTY(QString scriptLookupDir READ scriptLookupDir WRITE setScriptLookupDir) Q_PROPERTY(QVariantMap viewportSize READ viewportSize WRITE setViewportSize) Q_PROPERTY(QVariantMap paperSize READ paperSize WRITE setPaperSize) Q_PROPERTY(QVariantMap clipRect READ clipRect WRITE setClipRect) @@ -55,6 +56,9 @@ public: QString content() const; void setContent(const QString &content); + QString scriptLookupDir() const; + void setScriptLookupDir(const QString &dirPath); + void setViewportSize(const QVariantMap &size); QVariantMap viewportSize() const; @@ -64,11 +68,12 @@ public: void setPaperSize(const QVariantMap &size); QVariantMap paperSize() const; - public slots: void openUrl(const QString &address, const QVariant &op, const QVariantMap &settings); QVariant evaluate(const QString &code); bool render(const QString &fileName); + bool injectJs(const QString &jsFilePath); + void _appendScriptElement(const QString &scriptUrl); // moc does not understand QT_VERSION_CHECK and hence the encoded hex #if QT_VERSION >= 0x040600 @@ -91,6 +96,7 @@ private: QWebFrame *m_mainFrame; QRect m_clipRect; QVariantMap m_paperSize; // For PDF output via render() + QString m_scriptLookupDir; QImage renderImage(); bool renderPdf(const QString &fileName);