Source

profile/profile.js

const fs = require("fs");
const fse = require("fs-extra");
const path = require("path");
const { promisify } = require("util");
const utils = require("../utils");
const { assert } = utils;
const crypto = require("crypto");
const os = require("os");
const get_chrome_extension = require("./chrome");
const get_firefox_extension = require("./firefox");

/**
 * @typedef {object} ProfileOptions
 * @property {('firefox'|'chromium')} browser
 * @property {Array<string>} extensions - paths of the extensions to load
 * 	Please note that the extensions will only be loaded temporarly
 * 	and will NOT be installed in the profile. This is only possible for firefox
 * 	with `browser.install`, since chromium do not provide an API for this
 * @property {string} path - path where the profile will be created or loaded from
 * @property {string} name - name of the profile
 * @property {string} binary - browser's binary path
 * @property {object} options - puppteer options
 * @property {object} env - Environment variables
 */

/**
 * Allow to manipulate a browser session
 */
class Profile {
	/**
	 *	Create a Profile
	 * @param {ProfileOptions} options
	 */
	constructor({
		browser,
		name,
		path,
		tmp = false,
		extensions = [],
		binary,
		env,
		options
	}) {
		this.tmp = !name || tmp;

		if (this.tmp) {
			let uuid = crypto.randomBytes(12).toString("hex");
			name = `${browser}_${uuid}`;
		}

		assert(
			Profile.validBrowser(browser),
			`'${browser}' is not a valid browser name! 'firefox' or 'chromium' accepted`
		);

		this.browser = browser;
		this.name = name;
		this.path = path || Profile.path(browser, name, this.tmp);
		this.fullname = this.browser + "/" + this.name;
		this.binary = binary;
		this.options = { ...options };
		this.extensions = [...extensions];
		this.env = env;

		if (!name && !tmp) {
			console.error(
				`Warning : Profile : tmp = false but name undefined.` +
					`Creating a temporary profile at ${this.path}`
			);
		}
	}

	/**
	 * Return true if profile exists in file system
	 * @returns {Promise<boolean>}
	 */
	async exists() {
		return await Profile.exists(this.path);
	}

	/**
	 * Init the profile - ie create a folder for the profile
	 */
	async init() {
		if (await this.exists())
			throw new Error(
				`The profile '${this.fullname}' already exists in file system!`
			);

		await promisify(fs.mkdir)(this.path, { recursive: true });
	}

	/**
	 * Rename the profile
	 * @param {string} newName
	 */
	async rename({ name, path } = {}) {
		assert(await this.exists(), "Profile does not exists!");
		let newProfileOptions = {
			tmp,
			path,
			name,
			browser: this.browser,
			extensions: this.extensions,
			binary: this.binary,
			options: this.options
		};
		let newProfile = new Profile(newProfileOptions);
		assertNot(await newProfile.exists(), "Profile already exists!");
		await newProfile.assertDoesntExists();

		await fse.move(this.path, newProfile.path);
		await utils.replaceInFolder(newProfile.path, this.path, newProfile.path);

		this.name = newProfile.name;
		this.path = newProfile.path;
		this.fullname = newProfile.fullname;
	}

	/**
	 * copy the profile
	 * @param {string} newName
	 */
	async copy({ path, name } = {}) {
		assert(await this.exists(), "Profile does not exists!");
		let newProfileOptions = {
			path,
			name,
			env: this.env,
			browser: this.browser,
			extensions: this.extensions,
			binary: this.binary,
			options: this.options
		};
		let newProfile = new Profile(newProfileOptions);
		assert(!(await newProfile.exists()), "Profile already exists!");

		await fse.copy(this.path, newProfile.path);
		await utils.replaceInFolder(newProfile.path, this.path, newProfile.path);
		return newProfile;
	}

	/**
	 * Remove the profile
	 */
	async remove() {
		await fse.remove(this.path);
	}

	/**
	 * Clear the profile : delete it if exist, then init it
	 */
	async clear() {
		await this.remove();
		await this.init();
	}

	/**
	 * Throw an error if the profile doesnt exist
	 */
	async assertExists() {
		if (!(await this.exists()))
			throw new Error(`Profile '${this.fullname}' does not exist !`);
	}

	/**
	 * Throw an error if the profile exists
	 */
	async assertDoesntExists() {
		if (!(await this.exists()))
			throw new Error(`Profile '${this.fullname}' already exist.`);
	}

	/**
	 * Path of the profile on the file system
	 * @param {('firefox'|'chromium')} browser - the browser type:
	 * @param {*} name - name of the profile
	 * @returns {string} the path of the profile
	 */
	static path(browser, name, tmp = false) {
		if (tmp) {
			return path.resolve(os.tmpdir(), name);
		} else {
			return path.resolve(__dirname, "../profiles", browser, name);
		}
	}

	/**
	 * Path of the profile on the file system
	 * @param {string} path - path of the profile
	 * @returns {Promise<boolean>} true is profile exists
	 */
	static async exists(path) {
		return await promisify(fs.exists)(path);
	}

	/**
	 * Return true if {browser} is a valid browser name
	 * @param {string} browser - The type/name of the browser
	 */
	static validBrowser(browser) {
		return ["firefox", "chromium"].includes(browser);
	}

	/**
	 * throw an exception if {browser} is not valid
	 * @param {string} browser
	 */
	static assertValidBrowser(browser) {
		if (!Profile.validBrowser(browser))
			throw new Error(
				`Browser '${browser}' is not a valid Browser.` +
					` Browser must be either 'firefox' or 'chromium'`
			);
	}

	/**
	 * Load a profile or create it if it doesn't exist
	 * @param {ProfileOptions} options
	 * @returns {Promise<Profile>}
	 */
	static async profile(options) {
		Profile.assertValidBrowser(options.browser);

		let profile = new Profile({
			...options,
			tmp: typeof options.tmp === "undefined" ? !options.name : options.tmp
		});

		if (!(await profile.exists())) {
			await profile.init();
		}

		return profile;
	}

	/**
	 * Get extension for chrome from url and save it to {cache}
	 * @param {string} params.id - the id of the chrome extension you can also use the {url} param (see examples)
	 * @param {string} params.url - the url of the chrome extension (see examples)
	 * @param {string} params.name - the name of the extension. Will be used as foldername of the extension,
	 * 	so avoid special characters, especially "/" !
	 * @param {string} params.cache - path of the cache folder where the extension will be stored (created if don't exist)
	 * @param {boolean} params.verbose - should the function display debug info ? (download progress bar, etc...)
	 * @returns {Promise<String>} - Return a promise of the path of the extension
	 * @example
	 * let extensionPath = await Profile.getChromeExtension({
	 * 	// This is the url of the extension on google Webstore
	 * 	url: "https://chrome.google.com/webstore/detail/ublock/epcnnfbjfcgphgdmggkamkmgojdagdnn?hl=en",
	 * 	name: "ublock",
	 * 	cache: "./.cache", //default value
	 * 	verbose: true
	 * });
	 * // You can make it shorter by extracting the extension id from the url:
	 * let extensionPath = await Chrome.getExtension({
	 * 	// https://..../detail/ublock/{id}?hl=en
	 * 	id: "epcnnfbjfcgphgdmggkamkmgojdagdnn",
	 * 	name: "ublock",
	 * 	verbose: true
	 * });
	 * // You can also put the name before the id like this:
	 * let extensionPath = await Chrome.getExtension({
	 * 	id: "ublock/epcnnfbjfcgphgdmggkamkmgojdagdnn",
	 * 	verbose: true
	 * });
	 */
	static async getChromeExtension(params) {
		return await get_chrome_extension(params);
	}

	static async getFirefoxExtension(params) {
		return await get_firefox_extension(params);
	}
}

module.exports = Profile;