/* * * (c) Copyright Ascensio System Limited 2010-2020 * * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. * * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html * * You can contact Ascensio System SIA by email at sales@onlyoffice.com * * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. * * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains * relevant author attributions when distributing the software. If the display of the logo in its graphic * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" * in every copy of the program you distribute. * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. * */ var url = require('url') , fs = require('graceful-fs') , tmp = require('tmp') , stream = require('stream') , crossSpawn = require('cross-spawn') , optUtils = require('./options') , phantomScript = __dirname + '/webshot.phantom.js' , extensions = ['jpeg', 'jpg', 'png', 'pdf'] , siteTypes = ['url', 'html', 'file']; module.exports = function() { // Process arguments var args = Array.prototype.slice.call(arguments, 0); var cb = null; var options = {}; var path = null; /* Possible valid arguments: SITE, , CB SITE, SITE, PATH, , CB */ var site = args.shift(); /* , CB PATH, , CB */ var last = args[args.length - 1]; if (Object.prototype.toString.call(last) == '[object Function]') { cb = args.pop(); } /* PATH, */ switch (args.length) { case 1: var arg = args.pop(); if (toString.call(arg) === '[object String]') { path = arg; } else { options = arg; } break; case 2: path = args.shift(); options = args.shift(); break; } var streaming = !path; var defaults = optUtils.mergeObjects(optUtils.caller, optUtils.phantom); // Apply the compiled phantomjs path only if it compiled successfully try { defaults.phantomPath = require('phantomjs-prebuilt').path; } catch (ex) {} options = processOptions(options, defaults); // Check that a valid fileType was given for the output image var extension = (path) ? path.substring(~(~path.lastIndexOf('.') || ~path.length) + 1) : options.streamType; if (!~extensions.indexOf(extension.toLowerCase())) { return cb( new Error('All files must end with one of the following extensions: ' + extensions.join(', '))); } // Check that a valid siteType was provided if (!~siteTypes.indexOf(options.siteType)) { var err = new Error(args.siteType + ' is not a valid sitetype.'); if (cb) return cb(err); throw err; } // Add protocol to the site url if not present if (options.siteType === 'url') { site = url.parse(site).protocol ? site : 'http://' + site; } // Remove the given file if it already exists, then call phantom var spawn = function() { if (options.siteType === 'html') { var obj = tmp.fileSync(); var tmpPath = obj.name; fs.writeSync(obj.fd, site, null, 'utf-8'); fs.close(obj.fd); options.siteType = 'file'; site = tmpPath; return spawn(); } else { return spawnPhantom(site, path, streaming, options, cb); } }; if (path) { fs.exists(path, function(exists) { if (exists) { fs.unlink(path, function(err) { if (err) return cb(err); return spawn(); }); } else { return spawn(); } }); } else { return spawn(); } }; /* * Process the options object into the values to be exposed to phantom * * @param (Object) options * @param (Object) defaults * @return (Object) */ function processOptions(options, defaults) { // Alias 'screenSize' to 'windowSize' options.windowSize = options.windowSize || options.screenSize; // Alias 'userAgent' to 'settings.userAgent' if (options.userAgent) { options.settings = options.settings || {}; options.settings.userAgent = options.userAgent; } // Alias 'script' to 'onLoadFinished' if (options.script) { options.onLoadFinished = options.onLoadFinished || options.script; } // Fill in defaults for undefined options var withDefaults = optUtils.mergeObjects(options, defaults); // Convert function options to strings for later JSON serialization optUtils.phantomCallback.forEach(function(optionName) { var fnArg = withDefaults[optionName]; if (fnArg) { if (toString.call(fnArg) === '[object Function]') { withDefaults[optionName] = { fn: fnArg.toString() , context: {} }; } else { fnArg.fn = fnArg.fn.toString(); } } }); return withDefaults; } /* * Spawn a phantom instance to take the screenshot * * @param (String) site * @param (String) path * @param (Boolean) streaming * @param (Object) options * @param (Function) cb */ function spawnPhantom(site, path, streaming, options, cb) { // Filter out options that shouldn't be passed to the phantom process var filteredOptions = optUtils.filterObject(options, Object.keys(optUtils.phantom) .concat(optUtils.phantomPage) .concat(optUtils.phantomCallback)); filteredOptions.streaming = streaming; var phantomArgs = [phantomScript, JSON.stringify(filteredOptions), site, path]; if (options.phantomConfig) { phantomArgs = Object.keys(options.phantomConfig).map(function (key) { return '--' + key + '=' + options.phantomConfig[key]; }).concat(phantomArgs); } var phantomProc = crossSpawn.spawn(options.phantomPath, phantomArgs); // This variable will contain our timeout ID. var timeoutID = null; // Whether or not we've called our callback already. var calledCallback = false; // Only set the timer if the timeout has been specified (by default it's not) if (options.timeout) { timeoutID = setTimeout(function() { // The phantomjs process didn't exit in time. // Double-check we didn't already call the callback already as that would // happen when the process has already exited. Sending a SIGKILL to a PID // that might be handed out to another process could be potentially very // dangerous. if (!calledCallback) { calledCallback = true; // Send the kill signal phantomProc.kill('SIGKILL'); // Call our callback. var err = new Error('PhantomJS did not respond within the given ' + 'timeout setting.'); if (cb) return cb(err); s.emit('error', err); } }, options.timeout); } if (!streaming) { phantomProc.stderr.on('data', function(data) { if (options.errorIfJSException) { calledCallback = true; clearTimeout(timeoutID); cb(new Error('' + data)) } }); phantomProc.on('exit', function(code) { if (!calledCallback) { calledCallback = true; // No need to run the timeout anymore. clearTimeout(timeoutID); cb(code ? new Error('PhantomJS exited with return value ' + code) : null); } }); } else { var s = new stream.Stream(); s.readable = true; phantomProc.stdout.on('data', function(data) { clearTimeout(timeoutID); s.emit('data', new Buffer(''+data, 'base64')); }); phantomProc.stderr.on('data', function(data) { if (options.errorIfJSException) { s.emit('error', ''+data); } }); phantomProc.on('exit', function() { s.emit('end'); }); if (cb) { cb(null, s); } else { return s; } } }