/*jslint sloppy: true, nomen: true */ /*global exports:true,phantom:true */ /* This file is part of the PhantomJS project from Ofi Labs. Copyright (C) 2011 Ariya Hidayat Copyright (C) 2011 Ivan De Marino Copyright (C) 2011 James Roe Copyright (C) 2011 execjosh, http://execjosh.blogspot.com Copyright (C) 2012 James M. Greene 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. */ function checkType(o, type) { return typeof o === type; } function isObject(o) { return checkType(o, 'object'); } function isUndefined(o) { return checkType(o, 'undefined'); } function isUndefinedOrNull(o) { return isUndefined(o) || null === o; } function copyInto(target, source) { if (target === source || isUndefinedOrNull(source)) { return target; } target = target || {}; // Copy into objects only if (isObject(target)) { // Make sure source exists source = source || {}; if (isObject(source)) { var i, newTarget, newSource; for (i in source) { if (source.hasOwnProperty(i)) { newTarget = target[i]; newSource = source[i]; if (newTarget && isObject(newSource)) { // Deep copy newTarget = copyInto(target[i], newSource); } else { newTarget = newSource; } if (!isUndefined(newTarget)) { target[i] = newTarget; } } } } else { target = source; } } return target; } function definePageSignalHandler(page, handlers, handlerName, signalName) { Object.defineProperty(page, handlerName, { set: function (f) { // Disconnect previous handler (if any) if (!!handlers[handlerName] && typeof handlers[handlerName].callback === "function") { try { this[signalName].disconnect(handlers[handlerName].callback); } catch (e) {} } // Delete the previous handler delete handlers[handlerName]; // Connect the new handler iff it's a function if (typeof f === "function") { // Store the new handler for reference handlers[handlerName] = { callback: f } this[signalName].connect(f); } }, get: function() { return !!handlers[handlerName] && typeof handlers[handlerName].callback === "function" ? handlers[handlerName].callback : undefined; } }); } function definePageCallbackHandler(page, handlers, handlerName, callbackConstructor) { Object.defineProperty(page, handlerName, { set: function(f) { // Fetch the right callback object var callbackObj = page[callbackConstructor](); // Disconnect previous handler (if any) var handlerObj = handlers[handlerName]; if (!!handlerObj && typeof handlerObj.callback === "function" && typeof handlerObj.connector === "function") { try { callbackObj.called.disconnect(handlerObj.connector); } catch (e) { console.log(e); } } // Delete the previous handler delete handlers[handlerName]; // Connect the new handler iff it's a function if (typeof f === "function") { var connector = function() { // Callback will receive a "deserialized", normal "arguments" array callbackObj.returnValue = f.apply(this, arguments[0]); }; // Store the new handler for reference handlers[handlerName] = { callback: f, connector: connector }; // Connect a new handler callbackObj.called.connect(connector); } }, get: function() { var handlerObj = handlers[handlerName]; return (!!handlerObj && typeof handlerObj.callback === "function" && typeof handlerObj.connector === "function") ? handlers[handlerName].callback : undefined; } }); } // Inspired by Douglas Crockford's remedies: proper String quoting. // @see http://javascript.crockford.com/remedial.html function quoteString(str) { var c, i, l = str.length, o = '"'; for (i = 0; i < l; i += 1) { c = str.charAt(i); if (c >= ' ') { if (c === '\\' || c === '"') { o += '\\'; } o += c; } else { switch (c) { case '\b': o += '\\b'; break; case '\f': o += '\\f'; break; case '\n': o += '\\n'; break; case '\r': o += '\\r'; break; case '\t': o += '\\t'; break; default: c = c.charCodeAt(); o += '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); } } } return o + '"'; } // Inspired by Douglas Crockford's remedies: a better Type Detection. // @see http://javascript.crockford.com/remedial.html function detectType(value) { var s = typeof value; if (s === 'object') { if (value) { if (value instanceof Array) { s = 'array'; } else if (value instanceof RegExp) { s = 'regexp'; } else if (value instanceof Date) { s = 'date'; } } else { s = 'null'; } } return s; } function decorateNewPage(opts, page) { var handlers = {}; try { page.rawPageCreated.connect(function(newPage) { // Decorate the new raw page appropriately newPage = decorateNewPage(opts, newPage); // Notify via callback, if a callback was provided if (page.onPageCreated && typeof(page.onPageCreated) === "function") { page.onPageCreated(newPage); } }); } catch (e) {} // deep copy page.settings = JSON.parse(JSON.stringify(phantom.defaultPageSettings)); definePageSignalHandler(page, handlers, "onInitialized", "initialized"); definePageSignalHandler(page, handlers, "onLoadStarted", "loadStarted"); definePageSignalHandler(page, handlers, "onLoadFinished", "loadFinished"); definePageSignalHandler(page, handlers, "onUrlChanged", "urlChanged"); definePageSignalHandler(page, handlers, "onNavigationRequested", "navigationRequested"); definePageSignalHandler(page, handlers, "onRepaintRequested", "repaintRequested"); definePageSignalHandler(page, handlers, "onResourceRequested", "resourceRequested"); definePageSignalHandler(page, handlers, "onResourceReceived", "resourceReceived"); definePageSignalHandler(page, handlers, "onResourceError", "resourceError"); definePageSignalHandler(page, handlers, "onResourceTimeout", "resourceTimeout"); definePageSignalHandler(page, handlers, "onAlert", "javaScriptAlertSent"); definePageSignalHandler(page, handlers, "onConsoleMessage", "javaScriptConsoleMessageSent"); definePageSignalHandler(page, handlers, "onClosing", "closing"); // Private callback for "page.open()" definePageSignalHandler(page, handlers, "_onPageOpenFinished", "loadFinished"); phantom.__defineErrorSignalHandler__(page, page, handlers); page.onError = phantom.defaultErrorHandler; page.open = function (url, arg1, arg2, arg3, arg4) { var thisPage = this; if (arguments.length === 1) { this.openUrl(url, 'get', this.settings); return; } else if (arguments.length === 2 && typeof arg1 === 'function') { this._onPageOpenFinished = function() { thisPage._onPageOpenFinished = null; //< Disconnect callback (should fire only once) arg1.apply(thisPage, arguments); //< Invoke the actual callback } this.openUrl(url, 'get', this.settings); return; } else if (arguments.length === 2) { this.openUrl(url, arg1, this.settings); return; } else if (arguments.length === 3 && typeof arg2 === 'function') { this._onPageOpenFinished = function() { thisPage._onPageOpenFinished = null; //< Disconnect callback (should fire only once) arg2.apply(thisPage, arguments); //< Invoke the actual callback } this.openUrl(url, arg1, this.settings); return; } else if (arguments.length === 3) { this.openUrl(url, { operation: arg1, data: arg2 }, this.settings); return; } else if (arguments.length === 4) { this._onPageOpenFinished = function() { thisPage._onPageOpenFinished = null; //< Disconnect callback (should fire only once) arg3.apply(thisPage, arguments); //< Invoke the actual callback } this.openUrl(url, { operation: arg1, data: arg2 }, this.settings); return; } else if (arguments.length === 5) { this._onPageOpenFinished = function() { thisPage._onPageOpenFinished = null; //< Disconnect callback (should fire only once) arg4.apply(thisPage, arguments); //< Invoke the actual callback } this.openUrl(url, { operation: arg1, data: arg2, headers : arg3 }, this.settings); return; } throw "Wrong use of WebPage#open"; }; /** * Include an external JavaScript file and notify when done. * @param scriptUrl URL to the Script to include * @param onScriptLoaded If provided, this call back is executed when the inclusion is done */ 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 (if any) if (onScriptLoaded && typeof(onScriptLoaded) === "function") { onScriptLoaded(scriptUrl); } // And disconnect the signal handler try { this.javaScriptAlertSent.disconnect(arguments.callee); } catch (e) {} } }); // Append the script tag to the body this._appendScriptElement(scriptUrl); }; /** * evaluate a function in the page * @param {function} func the function to evaluate * @param {...} args function arguments * @return {*} the function call result */ page.evaluate = function (func, args) { var str, arg, argType, i, l; if (!(func instanceof Function || typeof func === 'string' || func instanceof String)) { throw "Wrong use of WebPage#evaluate"; } str = 'function() { return (' + func.toString() + ')('; for (i = 1, l = arguments.length; i < l; i++) { arg = arguments[i]; argType = detectType(arg); switch (argType) { case "object": //< for type "object" case "array": //< for type "array" str += JSON.stringify(arg) + "," break; case "date": //< for type "date" str += "new Date(" + JSON.stringify(arg) + ")," break; case "string": //< for type "string" str += quoteString(arg) + ','; break; default: // for types: "null", "number", "function", "regexp", "undefined" str += arg + ','; break; } } str = str.replace(/,$/, '') + '); }'; return this.evaluateJavaScript(str); }; /** * evaluate a function in the page, asynchronously * NOTE: it won't return anything: the execution is asynchronous respect to the call. * NOTE: the execution stack starts from within the page object * @param {function} func the function to evaluate * @param {number} timeMs time to wait before execution * @param {...} args function arguments */ page.evaluateAsync = function (func, timeMs, args) { // Remove the first 2 arguments because we are going to consume them var args = Array.prototype.slice.call(arguments, 2), numArgsToAppend = args.length, funcTimeoutWrapper; if (!(func instanceof Function || typeof func === 'string' || func instanceof String)) { throw "Wrong use of WebPage#evaluateAsync"; } // Wrapping the "func" argument into a setTimeout funcTimeoutWrapper = "function() { setTimeout(" + func.toString() + ", " + timeMs; while(numArgsToAppend > 0) { --numArgsToAppend; funcTimeoutWrapper += ", arguments[" + numArgsToAppend + "]"; } funcTimeoutWrapper += "); }"; // Augment the "args" array args.splice(0, 0, funcTimeoutWrapper); this.evaluate.apply(this, args); }; /** * upload a file * @param {string} selector css selector for the file input element * @param {string,array} fileNames the name(s) of the file(s) to upload */ page.uploadFile = function(selector, fileNames) { if (typeof fileNames == "string") { fileNames = [fileNames]; } this._uploadFile(selector, fileNames); }; // Copy options into page if (opts) { page = copyInto(page, opts); } // Calls from within the page to "phantomCallback()" arrive to this handler definePageCallbackHandler(page, handlers, "onCallback", "_getGenericCallback"); // Calls arrive to this handler when the user is asked to pick a file definePageCallbackHandler(page, handlers, "onFilePicker", "_getFilePickerCallback"); // Calls from within the page to "window.confirm(message)" arrive to this handler // @see https://developer.mozilla.org/en/DOM/window.confirm definePageCallbackHandler(page, handlers, "onConfirm", "_getJsConfirmCallback"); // Calls from within the page to "window.prompt(message, defaultValue)" arrive to this handler // @see https://developer.mozilla.org/en/DOM/window.prompt definePageCallbackHandler(page, handlers, "onPrompt", "_getJsPromptCallback"); // Calls from within the page when some javascript code running to long definePageCallbackHandler(page, handlers, "onLongRunningScript", "_getJsInterruptCallback"); page.event = {}; page.event.modifier = { shift: 0x02000000, ctrl: 0x04000000, alt: 0x08000000, meta: 0x10000000, keypad: 0x20000000 }; page.event.key = { '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57, 'A': 65, 'AE': 198, 'Aacute': 193, 'Acircumflex': 194, 'AddFavorite': 16777408, 'Adiaeresis': 196, 'Agrave': 192, 'Alt': 16777251, 'AltGr': 16781571, 'Ampersand': 38, 'Any': 32, 'Apostrophe': 39, 'ApplicationLeft': 16777415, 'ApplicationRight': 16777416, 'Aring': 197, 'AsciiCircum': 94, 'AsciiTilde': 126, 'Asterisk': 42, 'At': 64, 'Atilde': 195, 'AudioCycleTrack': 16777478, 'AudioForward': 16777474, 'AudioRandomPlay': 16777476, 'AudioRepeat': 16777475, 'AudioRewind': 16777413, 'Away': 16777464, 'B': 66, 'Back': 16777313, 'BackForward': 16777414, 'Backslash': 92, 'Backspace': 16777219, 'Backtab': 16777218, 'Bar': 124, 'BassBoost': 16777331, 'BassDown': 16777333, 'BassUp': 16777332, 'Battery': 16777470, 'Bluetooth': 16777471, 'Book': 16777417, 'BraceLeft': 123, 'BraceRight': 125, 'BracketLeft': 91, 'BracketRight': 93, 'BrightnessAdjust': 16777410, 'C': 67, 'CD': 16777418, 'Calculator': 16777419, 'Calendar': 16777444, 'Call': 17825796, 'Camera': 17825824, 'CameraFocus': 17825825, 'Cancel': 16908289, 'CapsLock': 16777252, 'Ccedilla': 199, 'Clear': 16777227, 'ClearGrab': 16777421, 'Close': 16777422, 'Codeinput': 16781623, 'Colon': 58, 'Comma': 44, 'Community': 16777412, 'Context1': 17825792, 'Context2': 17825793, 'Context3': 17825794, 'Context4': 17825795, 'ContrastAdjust': 16777485, 'Control': 16777249, 'Copy': 16777423, 'Cut': 16777424, 'D': 68, 'DOS': 16777426, 'Dead_Abovedot': 16781910, 'Dead_Abovering': 16781912, 'Dead_Acute': 16781905, 'Dead_Belowdot': 16781920, 'Dead_Breve': 16781909, 'Dead_Caron': 16781914, 'Dead_Cedilla': 16781915, 'Dead_Circumflex': 16781906, 'Dead_Diaeresis': 16781911, 'Dead_Doubleacute': 16781913, 'Dead_Grave': 16781904, 'Dead_Hook': 16781921, 'Dead_Horn': 16781922, 'Dead_Iota': 16781917, 'Dead_Macron': 16781908, 'Dead_Ogonek': 16781916, 'Dead_Semivoiced_Sound': 16781919, 'Dead_Tilde': 16781907, 'Dead_Voiced_Sound': 16781918, 'Delete': 16777223, 'Direction_L': 16777305, 'Direction_R': 16777312, 'Display': 16777425, 'Documents': 16777427, 'Dollar': 36, 'Down': 16777237, 'E': 69, 'ETH': 208, 'Eacute': 201, 'Ecircumflex': 202, 'Ediaeresis': 203, 'Egrave': 200, 'Eisu_Shift': 16781615, 'Eisu_toggle': 16781616, 'Eject': 16777401, 'End': 16777233, 'Enter': 16777221, 'Equal': 61, 'Escape': 16777216, 'Excel': 16777428, 'Exclam': 33, 'Execute': 16908291, 'Explorer': 16777429, 'F': 70, 'F1': 16777264, 'F10': 16777273, 'F11': 16777274, 'F12': 16777275, 'F13': 16777276, 'F14': 16777277, 'F15': 16777278, 'F16': 16777279, 'F17': 16777280, 'F18': 16777281, 'F19': 16777282, 'F2': 16777265, 'F20': 16777283, 'F21': 16777284, 'F22': 16777285, 'F23': 16777286, 'F24': 16777287, 'F25': 16777288, 'F26': 16777289, 'F27': 16777290, 'F28': 16777291, 'F29': 16777292, 'F3': 16777266, 'F30': 16777293, 'F31': 16777294, 'F32': 16777295, 'F33': 16777296, 'F34': 16777297, 'F35': 16777298, 'F4': 16777267, 'F5': 16777268, 'F6': 16777269, 'F7': 16777270, 'F8': 16777271, 'F9': 16777272, 'Favorites': 16777361, 'Finance': 16777411, 'Flip': 17825798, 'Forward': 16777314, 'G': 71, 'Game': 16777430, 'Go': 16777431, 'Greater': 62, 'H': 72, 'Hangul': 16781617, 'Hangul_Banja': 16781625, 'Hangul_End': 16781619, 'Hangul_Hanja': 16781620, 'Hangul_Jamo': 16781621, 'Hangul_Jeonja': 16781624, 'Hangul_PostHanja': 16781627, 'Hangul_PreHanja': 16781626, 'Hangul_Romaja': 16781622, 'Hangul_Special': 16781631, 'Hangul_Start': 16781618, 'Hangup': 17825797, 'Hankaku': 16781609, 'Help': 16777304, 'Henkan': 16781603, 'Hibernate': 16777480, 'Hiragana': 16781605, 'Hiragana_Katakana': 16781607, 'History': 16777407, 'Home': 16777232, 'HomePage': 16777360, 'HotLinks': 16777409, 'Hyper_L': 16777302, 'Hyper_R': 16777303, 'I': 73, 'Iacute': 205, 'Icircumflex': 206, 'Idiaeresis': 207, 'Igrave': 204, 'Insert': 16777222, 'J': 74, 'K': 75, 'Kana_Lock': 16781613, 'Kana_Shift': 16781614, 'Kanji': 16781601, 'Katakana': 16781606, 'KeyboardBrightnessDown': 16777398, 'KeyboardBrightnessUp': 16777397, 'KeyboardLightOnOff': 16777396, 'L': 76, 'LastNumberRedial': 17825801, 'Launch0': 16777378, 'Launch1': 16777379, 'Launch2': 16777380, 'Launch3': 16777381, 'Launch4': 16777382, 'Launch5': 16777383, 'Launch6': 16777384, 'Launch7': 16777385, 'Launch8': 16777386, 'Launch9': 16777387, 'LaunchA': 16777388, 'LaunchB': 16777389, 'LaunchC': 16777390, 'LaunchD': 16777391, 'LaunchE': 16777392, 'LaunchF': 16777393, 'LaunchG': 16777486, 'LaunchH': 16777487, 'LaunchMail': 16777376, 'LaunchMedia': 16777377, 'Left': 16777234, 'Less': 60, 'LightBulb': 16777405, 'LogOff': 16777433, 'M': 77, 'MailForward': 16777467, 'Market': 16777434, 'Massyo': 16781612, 'MediaLast': 16842751, 'MediaNext': 16777347, 'MediaPause': 16777349, 'MediaPlay': 16777344, 'MediaPrevious': 16777346, 'MediaRecord': 16777348, 'MediaStop': 16777345, 'MediaTogglePlayPause': 16777350, 'Meeting': 16777435, 'Memo': 16777404, 'Menu': 16777301, 'MenuKB': 16777436, 'MenuPB': 16777437, 'Messenger': 16777465, 'Meta': 16777250, 'Minus': 45, 'Mode_switch': 16781694, 'MonBrightnessDown': 16777395, 'MonBrightnessUp': 16777394, 'Muhenkan': 16781602, 'Multi_key': 16781600, 'MultipleCandidate': 16781629, 'Music': 16777469, 'MySites': 16777438, 'N': 78, 'News': 16777439, 'No': 16842754, 'Ntilde': 209, 'NumLock': 16777253, 'NumberSign': 35, 'O': 79, 'Oacute': 211, 'Ocircumflex': 212, 'Odiaeresis': 214, 'OfficeHome': 16777440, 'Ograve': 210, 'Ooblique': 216, 'OpenUrl': 16777364, 'Option': 16777441, 'Otilde': 213, 'P': 80, 'PageDown': 16777239, 'PageUp': 16777238, 'ParenLeft': 40, 'ParenRight': 41, 'Paste': 16777442, 'Pause': 16777224, 'Percent': 37, 'Period': 46, 'Phone': 16777443, 'Pictures': 16777468, 'Play': 16908293, 'Plus': 43, 'PowerDown': 16777483, 'PowerOff': 16777399, 'PreviousCandidate': 16781630, 'Print': 16777225, 'Printer': 16908290, 'Q': 81, 'Question': 63, 'QuoteDbl': 34, 'QuoteLeft': 96, 'R': 82, 'Refresh': 16777316, 'Reload': 16777446, 'Reply': 16777445, 'Return': 16777220, 'Right': 16777236, 'Romaji': 16781604, 'RotateWindows': 16777447, 'RotationKB': 16777449, 'RotationPB': 16777448, 'S': 83, 'Save': 16777450, 'ScreenSaver': 16777402, 'ScrollLock': 16777254, 'Search': 16777362, 'Select': 16842752, 'Semicolon': 59, 'Send': 16777451, 'Shift': 16777248, 'Shop': 16777406, 'SingleCandidate': 16781628, 'Slash': 47, 'Sleep': 16908292, 'Space': 32, 'Spell': 16777452, 'SplitScreen': 16777453, 'Standby': 16777363, 'Stop': 16777315, 'Subtitle': 16777477, 'Super_L': 16777299, 'Super_R': 16777300, 'Support': 16777454, 'Suspend': 16777484, 'SysReq': 16777226, 'T': 84, 'THORN': 222, 'Tab': 16777217, 'TaskPane': 16777455, 'Terminal': 16777456, 'Time': 16777479, 'ToDoList': 16777420, 'ToggleCallHangup': 17825799, 'Tools': 16777457, 'TopMenu': 16777482, 'Touroku': 16781611, 'Travel': 16777458, 'TrebleDown': 16777335, 'TrebleUp': 16777334, 'U': 85, 'UWB': 16777473, 'Uacute': 218, 'Ucircumflex': 219, 'Udiaeresis': 220, 'Ugrave': 217, 'Underscore': 95, 'Up': 16777235, 'V': 86, 'Video': 16777459, 'View': 16777481, 'VoiceDial': 17825800, 'VolumeDown': 16777328, 'VolumeMute': 16777329, 'VolumeUp': 16777330, 'W': 87, 'WLAN': 16777472, 'WWW': 16777403, 'WakeUp': 16777400, 'WebCam': 16777466, 'Word': 16777460, 'X': 88, 'Xfer': 16777461, 'Y': 89, 'Yacute': 221, 'Yes': 16842753, 'Z': 90, 'Zenkaku': 16781608, 'Zenkaku_Hankaku': 16781610, 'Zoom': 16908294, 'ZoomIn': 16777462, 'ZoomOut': 16777463, 'acute': 180, 'brokenbar': 166, 'cedilla': 184, 'cent': 162, 'copyright': 169, 'currency': 164, 'degree': 176, 'diaeresis': 168, 'division': 247, 'exclamdown': 161, 'guillemotleft': 171, 'guillemotright': 187, 'hyphen': 173, 'iTouch': 16777432, 'macron': 175, 'masculine': 186, 'mu': 181, 'multiply': 215, 'nobreakspace': 160, 'notsign': 172, 'onehalf': 189, 'onequarter': 188, 'onesuperior': 185, 'ordfeminine': 170, 'paragraph': 182, 'periodcentered': 183, 'plusminus': 177, 'questiondown': 191, 'registered': 174, 'section': 167, 'ssharp': 223, 'sterling': 163, 'threequarters': 190, 'threesuperior': 179, 'twosuperior': 178, 'unknown': 33554431, 'ydiaeresis': 255, 'yen': 165 }; return page; } exports.create = function (opts) { return decorateNewPage(opts, phantom.createWebPage()); };