<commit_before>import Promise from 'bluebird';
import contentTypeParser from 'content-type';
import os from 'os';
import fs from 'fs';
import fsExtra from 'fs-extra';
import ApifyClient from 'apify-client';
import psTree from 'ps-tree';
import requestPromise from 'request-promise';
import XRegExp from 'xregexp';
import { delayPromise, getRandomInt } from 'apify-shared/utilities';
import { checkParamOrThrow } from 'apify-client/build/utils';
import { ENV_VARS, USER_AGENT_LIST } from './constants';

/* globals process */

/**
 * Default regular expression to match URLs in a string that may be plain text, JSON, CSV or other. It supports common URL characters
 * and does not support URLs containing commas or spaces. The URLs also may contain Unicode letters (not symbols).
 * @memberOf utils
 */
const URL_NO_COMMAS_REGEX = XRegExp('https?://(www\\.)?[\\p{L}0-9][-\\p{L}0-9@:%._\\+~#=]{0,254}[\\p{L}0-9]\\.[a-z]{2,63}(:\\d{1,5})?(/[-\\p{L}0-9@:%_\\+.~#?&//=\\(\\)]*)?', 'gi'); // eslint-disable-line
/**
 * Regular expression that, in addition to the default regular expression URL_NO_COMMAS_REGEX, supports matching commas in URL path and query.
 * Note, however, that this may prevent parsing URLs from comma delimited lists, or the URLs may become malformed.
 * @memberOf utils
 */
const URL_WITH_COMMAS_REGEX = XRegExp('https?://(www\\.)?[\\p{L}0-9][-\\p{L}0-9@:%._\\+~#=]{0,254}[\\p{L}0-9]\\.[a-z]{2,63}(:\\d{1,5})?(/[-\\p{L}0-9@:%_\\+,.~#?&//=\\(\\)]*)?', 'gi'); // eslint-disable-line

const ensureDirPromised = Promise.promisify(fsExtra.ensureDir);
const psTreePromised = Promise.promisify(psTree);

/**
 * Creates an instance of ApifyClient using options as defined in the environment variables.
 * This function is exported to enable unit testing.
 *
 * @returns {*}
 * @ignore
 */
export const newClient = () => {
    const opts = {
        userId: process.env[ENV_VARS.USER_ID] || null,
        token: process.env[ENV_VARS.TOKEN] || null,
    };

    // Only set baseUrl if overridden by env var, so that 'https://api.apify.com' is used by default.
    // This simplifies local development, which should run against production unless user wants otherwise.
    const apiBaseUrl = process.env[ENV_VARS.API_BASE_URL];
    if (apiBaseUrl) opts.baseUrl = apiBaseUrl;

    return new ApifyClient(opts);
};

/**
 * Gets the default instance of the `ApifyClient` class provided
 * by the apify-client NPM package.
 * The instance is created automatically by the Apify SDK
 * and it is configured using the `APIFY_API_BASE_URL`, `APIFY_USER_ID` and `APIFY_TOKEN`
 * environment variables.
 *
 * The instance is used for all underlying calls to the Apify API
 * in functions such as Apify.getValue()
 * or Apify.call().
 * The settings of the client can be globally altered by calling the
 * Apify.client.setOptions() function.
 * Beware that altering these settings might have unintended effects on the entire Apify SDK package.
 *
 * @memberof module:Apify
 * @name client
 * @instance
 */
export const apifyClient = newClient();

/**
 * Returns a result of `Promise.resolve()`.
 *
 * @returns {*}
 *
 * @ignore
 */
export const newPromise = () => {
    return Promise.resolve();
};

/**
 * Adds charset=utf-8 to given content type if this parameter is missing.
 *
 * @param contentType
 * @returns {string}
 *
 * @ignore
 */
export const addCharsetToContentType = (contentType) => {
    if (!contentType) return contentType;

    const parsed = contentTypeParser.parse(contentType);

    if (parsed.parameters.charset) return contentType;

    parsed.parameters.charset = 'utf-8';

    return contentTypeParser.format(parsed);
};

let isDockerPromiseCache;
const createIsDockerPromise = () => {
    const promise1 = Promise
        .promisify(fs.stat)('/.dockerenv')
        .then(() => true)
        .catch(() => false);

    const promise2 = Promise
        .promisify(fs.readFile)('/proc/self/cgroup', 'utf8')
        .then(content => content.indexOf('docker') !== -1)
        .catch(() => false);

    return Promise
        .all([promise1, promise2])
        .then(([result1, result2]) => result1 || result2);
};

/**
 * Returns promise that resolves to true if the code is running in a Docker container.
 *
 * @return {Promise}
 *
 * @memberof module:Apify
 * @name isDocker
 * @instance
 * @function
 */
export const isDocker = (forceReset) => {
    // Parameter forceReset is just internal for unit tests.
    if (!isDockerPromiseCache || forceReset) isDockerPromiseCache = createIsDockerPromise();

    return isDockerPromiseCache;
};

/**
 * Sums an array of numbers.
 *
 * @param  {Array} arr An array of numbers.
 * @return {Number} Sum of the numbers.
 *
 * @ignore
 */
export const sum = arr => arr.reduce((total, c) => total + c, 0);

/**
 * Computes an average of an array of numbers.
 *
 * @param  {Array} arr An array of numbers.
 * @return {Number} Average value.
 *
 * @ignore
 */
export const avg = arr => sum(arr) / arr.length;

/**
 * Returns memory statistics of the process and the system, which is an object with the following properties:
 *
 * ```javascript
 * {
 *   // Total memory available in the system or container
 *   totalBytes: Number,
 *
 *   // Amount of free memory in the system or container
 *   freeBytes: Number,
 *
 *   // Amount of memory used (= totalBytes - freeBytes)
 *   usedBytes: Number,
 *   // Amount of memory used the current Node.js process
 *   mainProcessBytes: Number,
 *   // Amount of memory used by child processes of the current Node.js process
 *   childProcessesBytes: Number,
 * }
 * ```
 *
 * If the process runs inside of Docker, the `getMemoryInfo` gets container memory limits,
 * otherwise it gets system memory limits.
 *
 * Beware that the function is quite inefficient because it spawns a new process.
 * Therefore you shouldn't call it too often, like more than once per second.
 *
 * @returns {Promise} Returns a promise.
 *
 * @memberof module:Apify
 * @name getMemoryInfo
 * @instance
 * @function
 */
export const getMemoryInfo = async () => {
    const [isDockerVar, processes] = await Promise.all([
        // module.exports must be here so that we can mock it.
        module.exports.isDocker(),
        // Query both root and child processes
        psTreePromised(process.pid, true),
    ]);

    let mainProcessBytes = -1;
    let childProcessesBytes = 0;
    processes.forEach((rec) => {
        // Skip the 'ps' or 'wmic' commands used by ps-tree to query the processes
        if (rec.COMM === 'ps' || rec.COMM === 'WMIC.exe') {
            return;
        }
        const bytes = parseInt(rec.RSS, 10);
        // Obtain main process' memory separately
        if (rec.PID === `${process.pid}`) {
            mainProcessBytes = bytes;
            return;
        }
        childProcessesBytes += bytes;
    });

    let totalBytes;
    let freeBytes;
    let usedBytes;

    if (!isDockerVar) {
        totalBytes = os.totalmem();
        freeBytes = os.freemem();
        usedBytes = totalBytes - freeBytes;
    } else {
        // When running inside Docker container, use container memory limits
        // This must be promisified here so that we can mock it.
        const readPromised = Promise.promisify(fs.readFile);

        const [totalBytesStr, usedBytesStr] = await Promise.all([
            readPromised('/sys/fs/cgroup/memory/memory.limit_in_bytes'),
            readPromised('/sys/fs/cgroup/memory/memory.usage_in_bytes'),
        ]);

        totalBytes = parseInt(totalBytesStr, 10);
        usedBytes = parseInt(usedBytesStr, 10);
        freeBytes = totalBytes - usedBytes;
    }

    return {
        totalBytes,
        freeBytes,
        usedBytes,
        mainProcessBytes,
        childProcessesBytes,
    };
};

/**
 * Helper function that detrermines if given parameter is an instance of Promise.
 *
 * @ignore
 */
export const isPromise = (maybePromise) => {
    return maybePromise && typeof maybePromise.then === 'function' && typeof maybePromise.catch === 'function';
};

/**
 * Returns true if node is in production environment and false otherwise.
 *
 * @ignore
 */
export const isProduction = () => process.env.NODE_ENV === 'production';

/**
 * Helper function used for local implementations. Creates dir.
 *
 * @ignore
 */
export const ensureDirExists = path => ensureDirPromised(path);

/**
 * Helper function that returns the first key from plan object.
 *
 * @ignore
 */
export const getFirstKey = (dict) => {
    for (const key in dict) { // eslint-disable-line guard-for-in, no-restricted-syntax
        return key;
    }
};

/**
 * Gets a typical path to Chrome executable, depending on the current operating system.
 *
 * @return {string}
 * @ignore
 */
export const getTypicalChromeExecutablePath = () => {
    switch (os.platform()) {
    case 'darwin': return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
    case 'win32': return 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe';
    default: return 'google-chrome';
    }
};

/**
 * Creates a promise that after given time gets rejected with given error.
 *
 * @return {Promise<Error>}
 * @ignore
 */
export const createTimeoutPromise = (timeoutMillis, errorMessage) => {
    return delayPromise(timeoutMillis).then(() => {
        throw new Error(errorMessage);
    });
};

/**
 * Returns `true` when code is running on Apify platform and `false` otherwise (for example locally).
 *
 * @returns {Boolean}
 *
 * @memberof module:Apify
 * @name isAtHome
 * @instance
 * @function
 */
export const isAtHome = () => !!process.env[ENV_VARS.IS_AT_HOME];

/**
 * Returns a promise that resolves after a specific period of time. This is useful to implement waiting
 * in your code, e.g. to prevent overloading of target website or to avoid bot detection.
 *
 * Example usage:
 * ```javascript
 * const Apify = require('apify);
 *
 * ...
 *
 * // Sleep 1.5 seconds
 * await Apify.utils.sleep(1500);
 * ```
 * @param millis Period of time to sleep, in milliseconds. If not a positive number, the returned promise resolves immediately.
 * @memberof utils
 * @return {Promise}
 */
const sleep = (millis) => {
    return delayPromise(millis);
};

/**
 * Returns a promise that resolves to an array of urls parsed from the resource available at the provided url.
 * Optionally, custom regular expression and encoding may be provided.
 *
 * @param {String} url
 * @param {String} [encoding='utf8']
 * @param {RegExp} [urlRegExp=URL_NO_COMMAS_REGEX]
 * @returns {Promise}
 * @memberOf utils
 */
const downloadListOfUrls = ({ url, encoding = 'utf8', urlRegExp = URL_NO_COMMAS_REGEX }) => {
    try {
        checkParamOrThrow(url, 'url', 'String');
        checkParamOrThrow(encoding, 'string', 'String');
        checkParamOrThrow(urlRegExp, 'urlRegExp', 'RegExp');
    } catch (err) {
        return Promise.reject(err);
    }
    return requestPromise.get({ url, encoding })
        .then(string => extractUrls({ string, urlRegExp }));
};

/**
 * Collects all URLs in an arbitrary string to an array, optionally using a custom regular expression.
 * @param {String} string
 * @param {RegExp} [urlRegExp=URL_NO_COMMAS_REGEX]
 * @returns {Array}
 * @memberOf utils
 */
const extractUrls = ({ string, urlRegExp = URL_NO_COMMAS_REGEX }) => {
    checkParamOrThrow(string, 'string', 'String');
    checkParamOrThrow(urlRegExp, 'urlRegExp', 'RegExp');
    return string.match(urlRegExp) || [];
};

/**
 * Returns a randomly selected User-Agent header out of a list of the most common headers.
 * @returns {String}
 * @memberOf utils
 */
const getRandomUserAgent = () => {
    const index = getRandomInt(USER_AGENT_LIST.length);
    return USER_AGENT_LIST[index];
};

/**
 * A namespace that contains various utilities.
 *
 * Example usage:
 * ```javascript
 * const Apify = require('apify);
 *
 * ...
 *
 * // Sleep 1.5 seconds
 * await Apify.utils.sleep(1500);
 * ```
 * @namespace utils
 */
export const publicUtils = {
    sleep,
    downloadListOfUrls,
    extractUrls,
    getRandomUserAgent,
    URL_NO_COMMAS_REGEX,
    URL_WITH_COMMAS_REGEX,
};
<commit_msg>Temporary test<commit_after>import Promise from 'bluebird';
import contentTypeParser from 'content-type';
import os from 'os';
import fs from 'fs';
import fsExtra from 'fs-extra';
import ApifyClient from 'apify-client';
import psTree from 'ps-tree';
import requestPromise from 'request-promise';
import XRegExp from 'xregexp';
import { delayPromise, getRandomInt } from 'apify-shared/utilities';
import { checkParamOrThrow } from 'apify-client/build/utils';
import { ENV_VARS, USER_AGENT_LIST } from './constants';

/* globals process */

/**
 * Default regular expression to match URLs in a string that may be plain text, JSON, CSV or other. It supports common URL characters
 * and does not support URLs containing commas or spaces. The URLs also may contain Unicode letters (not symbols).
 * @memberOf utils
 */
const URL_NO_COMMAS_REGEX = XRegExp('https?://(www\\.)?[\\p{L}0-9][-\\p{L}0-9@:%._\\+~#=]{0,254}[\\p{L}0-9]\\.[a-z]{2,63}(:\\d{1,5})?(/[-\\p{L}0-9@:%_\\+.~#?&//=\\(\\)]*)?', 'gi'); // eslint-disable-line
/**
 * Regular expression that, in addition to the default regular expression URL_NO_COMMAS_REGEX, supports matching commas in URL path and query.
 * Note, however, that this may prevent parsing URLs from comma delimited lists, or the URLs may become malformed.
 * @memberOf utils
 */
const URL_WITH_COMMAS_REGEX = XRegExp('https?://(www\\.)?[\\p{L}0-9][-\\p{L}0-9@:%._\\+~#=]{0,254}[\\p{L}0-9]\\.[a-z]{2,63}(:\\d{1,5})?(/[-\\p{L}0-9@:%_\\+,.~#?&//=\\(\\)]*)?', 'gi'); // eslint-disable-line

const ensureDirPromised = Promise.promisify(fsExtra.ensureDir);
const psTreePromised = Promise.promisify(psTree);

/**
 * Creates an instance of ApifyClient using options as defined in the environment variables.
 * This function is exported to enable unit testing.
 *
 * @returns {*}
 * @ignore
 */
export const newClient = () => {
    const opts = {
        userId: process.env[ENV_VARS.USER_ID] || null,
        token: process.env[ENV_VARS.TOKEN] || null,
    };

    // Only set baseUrl if overridden by env var, so that 'https://api.apify.com' is used by default.
    // This simplifies local development, which should run against production unless user wants otherwise.
    const apiBaseUrl = process.env[ENV_VARS.API_BASE_URL];
    if (apiBaseUrl) opts.baseUrl = apiBaseUrl;

    return new ApifyClient(opts);
};

/**
 * Gets the default instance of the `ApifyClient` class provided
 * by the apify-client NPM package.
 * The instance is created automatically by the Apify SDK
 * and it is configured using the `APIFY_API_BASE_URL`, `APIFY_USER_ID` and `APIFY_TOKEN`
 * environment variables.
 *
 * The instance is used for all underlying calls to the Apify API
 * in functions such as Apify.getValue()
 * or Apify.call().
 * The settings of the client can be globally altered by calling the
 * Apify.client.setOptions() function.
 * Beware that altering these settings might have unintended effects on the entire Apify SDK package.
 *
 * @memberof module:Apify
 * @name client
 * @instance
 */
export const apifyClient = newClient();

/**
 * Returns a result of `Promise.resolve()`.
 *
 * @returns {*}
 *
 * @ignore
 */
export const newPromise = () => {
    return Promise.resolve();
};

/**
 * Adds charset=utf-8 to given content type if this parameter is missing.
 *
 * @param contentType
 * @returns {string}
 *
 * @ignore
 */
export const addCharsetToContentType = (contentType) => {
    if (!contentType) return contentType;

    const parsed = contentTypeParser.parse(contentType);

    if (parsed.parameters.charset) return contentType;

    parsed.parameters.charset = 'utf-8';

    return contentTypeParser.format(parsed);
};

let isDockerPromiseCache;
const createIsDockerPromise = () => {
    const promise1 = Promise
        .promisify(fs.stat)('/.dockerenv')
        .then(() => true)
        .catch(() => false);

    const promise2 = Promise
        .promisify(fs.readFile)('/proc/self/cgroup', 'utf8')
        .then(content => content.indexOf('docker') !== -1)
        .catch(() => false);

    return Promise
        .all([promise1, promise2])
        .then(([result1, result2]) => result1 || result2);
};

/**
 * Returns promise that resolves to true if the code is running in a Docker container.
 *
 * @return {Promise}
 *
 * @memberof module:Apify
 * @name isDocker
 * @instance
 * @function
 */
export const isDocker = (forceReset) => {
    // Parameter forceReset is just internal for unit tests.
    if (!isDockerPromiseCache || forceReset) isDockerPromiseCache = createIsDockerPromise();

    return isDockerPromiseCache;
};

/**
 * Sums an array of numbers.
 *
 * @param  {Array} arr An array of numbers.
 * @return {Number} Sum of the numbers.
 *
 * @ignore
 */
export const sum = arr => arr.reduce((total, c) => total + c, 0);

/**
 * Computes an average of an array of numbers.
 *
 * @param  {Array} arr An array of numbers.
 * @return {Number} Average value.
 *
 * @ignore
 */
export const avg = arr => sum(arr) / arr.length;

/**
 * Returns memory statistics of the process and the system, which is an object with the following properties:
 *
 * ```javascript
 * {
 *   // Total memory available in the system or container
 *   totalBytes: Number,
 *
 *   // Amount of free memory in the system or container
 *   freeBytes: Number,
 *
 *   // Amount of memory used (= totalBytes - freeBytes)
 *   usedBytes: Number,
 *   // Amount of memory used the current Node.js process
 *   mainProcessBytes: Number,
 *   // Amount of memory used by child processes of the current Node.js process
 *   childProcessesBytes: Number,
 * }
 * ```
 *
 * If the process runs inside of Docker, the `getMemoryInfo` gets container memory limits,
 * otherwise it gets system memory limits.
 *
 * Beware that the function is quite inefficient because it spawns a new process.
 * Therefore you shouldn't call it too often, like more than once per second.
 *
 * @returns {Promise} Returns a promise.
 *
 * @memberof module:Apify
 * @name getMemoryInfo
 * @instance
 * @function
 */
export const getMemoryInfo = async () => {
    const [isDockerVar, processes] = await Promise.all([
        // module.exports must be here so that we can mock it.
        module.exports.isDocker(),
        // Query both root and child processes
        psTreePromised(process.pid, true),
    ]);

    // Temporary test
    console.dir(processes);

    let mainProcessBytes = -1;
    let childProcessesBytes = 0;
    processes.forEach((rec) => {
        // Skip the 'ps' or 'wmic' commands used by ps-tree to query the processes
        if (rec.COMM === 'ps' || rec.COMM === 'WMIC.exe') {
            return;
        }
        const bytes = parseInt(rec.RSS, 10);
        // Obtain main process' memory separately
        if (rec.PID === `${process.pid}`) {
            mainProcessBytes = bytes;
            return;
        }
        childProcessesBytes += bytes;
    });

    let totalBytes;
    let freeBytes;
    let usedBytes;

    if (!isDockerVar) {
        totalBytes = os.totalmem();
        freeBytes = os.freemem();
        usedBytes = totalBytes - freeBytes;
    } else {
        // When running inside Docker container, use container memory limits
        // This must be promisified here so that we can mock it.
        const readPromised = Promise.promisify(fs.readFile);

        const [totalBytesStr, usedBytesStr] = await Promise.all([
            readPromised('/sys/fs/cgroup/memory/memory.limit_in_bytes'),
            readPromised('/sys/fs/cgroup/memory/memory.usage_in_bytes'),
        ]);

        totalBytes = parseInt(totalBytesStr, 10);
        usedBytes = parseInt(usedBytesStr, 10);
        freeBytes = totalBytes - usedBytes;
    }

    return {
        totalBytes,
        freeBytes,
        usedBytes,
        mainProcessBytes,
        childProcessesBytes,
    };
};

/**
 * Helper function that detrermines if given parameter is an instance of Promise.
 *
 * @ignore
 */
export const isPromise = (maybePromise) => {
    return maybePromise && typeof maybePromise.then === 'function' && typeof maybePromise.catch === 'function';
};

/**
 * Returns true if node is in production environment and false otherwise.
 *
 * @ignore
 */
export const isProduction = () => process.env.NODE_ENV === 'production';

/**
 * Helper function used for local implementations. Creates dir.
 *
 * @ignore
 */
export const ensureDirExists = path => ensureDirPromised(path);

/**
 * Helper function that returns the first key from plan object.
 *
 * @ignore
 */
export const getFirstKey = (dict) => {
    for (const key in dict) { // eslint-disable-line guard-for-in, no-restricted-syntax
        return key;
    }
};

/**
 * Gets a typical path to Chrome executable, depending on the current operating system.
 *
 * @return {string}
 * @ignore
 */
export const getTypicalChromeExecutablePath = () => {
    switch (os.platform()) {
    case 'darwin': return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
    case 'win32': return 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe';
    default: return 'google-chrome';
    }
};

/**
 * Creates a promise that after given time gets rejected with given error.
 *
 * @return {Promise<Error>}
 * @ignore
 */
export const createTimeoutPromise = (timeoutMillis, errorMessage) => {
    return delayPromise(timeoutMillis).then(() => {
        throw new Error(errorMessage);
    });
};

/**
 * Returns `true` when code is running on Apify platform and `false` otherwise (for example locally).
 *
 * @returns {Boolean}
 *
 * @memberof module:Apify
 * @name isAtHome
 * @instance
 * @function
 */
export const isAtHome = () => !!process.env[ENV_VARS.IS_AT_HOME];

/**
 * Returns a promise that resolves after a specific period of time. This is useful to implement waiting
 * in your code, e.g. to prevent overloading of target website or to avoid bot detection.
 *
 * Example usage:
 * ```javascript
 * const Apify = require('apify);
 *
 * ...
 *
 * // Sleep 1.5 seconds
 * await Apify.utils.sleep(1500);
 * ```
 * @param millis Period of time to sleep, in milliseconds. If not a positive number, the returned promise resolves immediately.
 * @memberof utils
 * @return {Promise}
 */
const sleep = (millis) => {
    return delayPromise(millis);
};

/**
 * Returns a promise that resolves to an array of urls parsed from the resource available at the provided url.
 * Optionally, custom regular expression and encoding may be provided.
 *
 * @param {String} url
 * @param {String} [encoding='utf8']
 * @param {RegExp} [urlRegExp=URL_NO_COMMAS_REGEX]
 * @returns {Promise}
 * @memberOf utils
 */
const downloadListOfUrls = ({ url, encoding = 'utf8', urlRegExp = URL_NO_COMMAS_REGEX }) => {
    try {
        checkParamOrThrow(url, 'url', 'String');
        checkParamOrThrow(encoding, 'string', 'String');
        checkParamOrThrow(urlRegExp, 'urlRegExp', 'RegExp');
    } catch (err) {
        return Promise.reject(err);
    }
    return requestPromise.get({ url, encoding })
        .then(string => extractUrls({ string, urlRegExp }));
};

/**
 * Collects all URLs in an arbitrary string to an array, optionally using a custom regular expression.
 * @param {String} string
 * @param {RegExp} [urlRegExp=URL_NO_COMMAS_REGEX]
 * @returns {Array}
 * @memberOf utils
 */
const extractUrls = ({ string, urlRegExp = URL_NO_COMMAS_REGEX }) => {
    checkParamOrThrow(string, 'string', 'String');
    checkParamOrThrow(urlRegExp, 'urlRegExp', 'RegExp');
    return string.match(urlRegExp) || [];
};

/**
 * Returns a randomly selected User-Agent header out of a list of the most common headers.
 * @returns {String}
 * @memberOf utils
 */
const getRandomUserAgent = () => {
    const index = getRandomInt(USER_AGENT_LIST.length);
    return USER_AGENT_LIST[index];
};

/**
 * A namespace that contains various utilities.
 *
 * Example usage:
 * ```javascript
 * const Apify = require('apify);
 *
 * ...
 *
 * // Sleep 1.5 seconds
 * await Apify.utils.sleep(1500);
 * ```
 * @namespace utils
 */
export const publicUtils = {
    sleep,
    downloadListOfUrls,
    extractUrls,
    getRandomUserAgent,
    URL_NO_COMMAS_REGEX,
    URL_WITH_COMMAS_REGEX,
};
<|endoftext|>


<commit_before>/**
* @fileoverview Handles session related actions like configuration,
*   requests, tokens and responses.
* @author Cosmitar
*/
import 'fetch-everywhere';
import { generate_uuid } from './utils/uuid';
/**
* @class Session
*/
class Session {
  /**
  * @param {Object} config Configuration object with optional params:
  *   app_token
  *   client_id
  *   scope
  *   state
  *   client_secret
  *   token <- this is the access token
  * @constructor
  */
  constructor(config = {}) {
    this._app_token = config.app_token || '';
    this._client = config.client_id || '';
    this._scope = config.scope || 'data:read_write,data:delete,project:delete';
    this._state = config.state || generate_uuid();
    this._secret = config.client_secret || '';
    this._token = config.token || ''; // access token
    this._sync_token = '*';
    this._auth_url = 'https://todoist.com/oauth/authorize';
    this._exchange_token_url = 'https://todoist.com/oauth/access_token';
  }

  /**
  * Simplifies deferred config after creating an instance
  *   of a session.
  * @param {Object} config An object that can contain
  *   app_token
  *   client_id
  *   scope
  *   state
  *   client_secret
  */
  config(config = {}) {
    this._app_token = config.app_token || this._app_token;
    this._client = config.client_id || this._client;
    this._scope = config.scope || this._scope;
    this._state = config.state || this._state;
    this._secret = config.client_secret || this._secret;
  }

  /**
  * Sets an access token for current session.
  * @param {string} token
  */
  set accessToken(token) {
    this._token = token;
  }

  /**
  * Sets the authorization code needed later for access token exchange.
  * @param {string} token
  */
  set code(code) {
    this._code = code;
  }

  /**
  * Returns the authorization url based on configurations.
  * @return string The full authorization url.
  */
  requestAuthorizationUrl() {
    const query = this._dataToQueryString({
      client_id: this._client,
      scope: this._scope,
      state: this._state,
    });
    return `${this._auth_url}?${query}`;
  }

  /**
  * Requests an access token to the server.
  * @return {Promise}
  */
  getAccessToken() {
    return this.request(this._exchange_token_url, 'POST', {
      client_id: this._client,
      client_secret: this._secret,
      code: this._code,
    });
  }

  /**
  * Performs a GET request for the given url and parameters.
  * @param {string} url
  * @param {Object} data
  * @return {Promise}
  */
  get(url, data = {}) {
    return this.request(url, 'GET', data);
  }

  /**
  * Performs a POST request for the given url and parameters.
  * @param {string} url
  * @param {Object} data
  * @return {Promise}
  */
  post(url, data = {}) {
    return this.request(url, 'POST', data);
  }

  _dataToQueryString(data) {
    return Object.keys(data)
      .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k]))
      .join('&');
  }

  /**
  * Executes a request, handling headers, tokens and response.
  * @param {string} url The URL to fetch.
  * @param {string} method An http verb, for this API only GET or POST.
  * @param {Object} data
  */
  request(url, method = 'GET', data = {}) {
    let headers = {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': method === 'GET'
        ? 'application/json'
        : 'application/x-www-form-urlencoded',
    };

    if (this._token) {
      data.token = this._token;
    }

    if (method === 'POST') {
      data.sync_token = this._sync_token;
    }

    const query = this._dataToQueryString(data);

    const request_url = `${url}?${query}`;
    return fetch(request_url, {
      method: method,
      headers: headers,
      body: JSON.stringify(data),
      credentials: 'include',
    }).then(response => {
      if (response.sync_token) {
        this._sync_token = response.sync_token;
      }

      // Todoist API always returns a JSON, even on error (except on templates as files)
      if (/attachment/.test(response.headers._headers['content-disposition'])) {
        return response;
      } else {
        return response.json();
      }
    }).then((response) => {
      if (response.error_code) {
        throw new Error(`(cod: ${response.error_code}) ${response.error}`);
      }
      return response;
    });
  }
}

export default Session;
<commit_msg>fix fetch paramters<commit_after>/**
* @fileoverview Handles session related actions like configuration,
*   requests, tokens and responses.
* @author Cosmitar
*/
import 'fetch-everywhere';
import { generate_uuid } from './utils/uuid';
/**
* @class Session
*/
class Session {
  /**
  * @param {Object} config Configuration object with optional params:
  *   app_token
  *   client_id
  *   scope
  *   state
  *   client_secret
  *   token <- this is the access token
  * @constructor
  */
  constructor(config = {}) {
    this._app_token = config.app_token || '';
    this._client = config.client_id || '';
    this._scope = config.scope || 'data:read_write,data:delete,project:delete';
    this._state = config.state || generate_uuid();
    this._secret = config.client_secret || '';
    this._token = config.token || ''; // access token
    this._sync_token = '*';
    this._auth_url = 'https://todoist.com/oauth/authorize';
    this._exchange_token_url = 'https://todoist.com/oauth/access_token';
  }

  /**
  * Simplifies deferred config after creating an instance
  *   of a session.
  * @param {Object} config An object that can contain
  *   app_token
  *   client_id
  *   scope
  *   state
  *   client_secret
  */
  config(config = {}) {
    this._app_token = config.app_token || this._app_token;
    this._client = config.client_id || this._client;
    this._scope = config.scope || this._scope;
    this._state = config.state || this._state;
    this._secret = config.client_secret || this._secret;
  }

  /**
  * Sets an access token for current session.
  * @param {string} token
  */
  set accessToken(token) {
    this._token = token;
  }

  /**
  * Sets the authorization code needed later for access token exchange.
  * @param {string} token
  */
  set code(code) {
    this._code = code;
  }

  /**
  * Returns the authorization url based on configurations.
  * @return string The full authorization url.
  */
  requestAuthorizationUrl() {
    const query = this._dataToQueryString({
      client_id: this._client,
      scope: this._scope,
      state: this._state,
    });
    return `${this._auth_url}?${query}`;
  }

  /**
  * Requests an access token to the server.
  * @return {Promise}
  */
  getAccessToken() {
    return this.request(this._exchange_token_url, 'POST', {
      client_id: this._client,
      client_secret: this._secret,
      code: this._code,
    });
  }

  /**
  * Performs a GET request for the given url and parameters.
  * @param {string} url
  * @param {Object} data
  * @return {Promise}
  */
  get(url, data = {}) {
    return this.request(url, 'GET', data);
  }

  /**
  * Performs a POST request for the given url and parameters.
  * @param {string} url
  * @param {Object} data
  * @return {Promise}
  */
  post(url, data = {}) {
    return this.request(url, 'POST', data);
  }

  _dataToQueryString(data) {
    return Object.keys(data)
      .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k]))
      .join('&');
  }

  /**
  * Executes a request, handling headers, tokens and response.
  * @param {string} url The URL to fetch.
  * @param {string} method An http verb, for this API only GET or POST.
  * @param {Object} data
  */
  request(url, method = 'GET', data = {}) {
    let headers = {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': method === 'GET'
        ? 'application/json'
        : 'application/x-www-form-urlencoded',
    };

    if (this._token) {
      data.token = this._token;
    }

    if (method === 'POST') {
      data.sync_token = this._sync_token;
    }

    const query = this._dataToQueryString(data);

    const request_url = `${url}?${query}`;
    return fetch(request_url, {
      method: method,
      headers: headers,
      body: /GET|HEAD/.test(method) ? null : JSON.stringify(data),
      // credentials: 'include',
    }).then(response => {
      if (response.error_code) {
        throw new Error(`(cod: ${response.error_code}) ${response.error}`);
      }
      return response;
    }).then((response) => {
      if (!response.ok) {
        throw new Error(`(${response.status}) ${response.statusText}`);
      }

      return response;
    }).then(response => {
      if (response.sync_token) {
        this._sync_token = response.sync_token;
      }

      // Todoist API always returns a JSON, even on error (except on templates as files)
      if (/attachment/.test(response.headers.get('content-disposition'))) {
        return response;
      } else {
        return response.json();
      }
    });
  }
}

export default Session;
<|endoftext|>


<commit_before>
describe('client', function () {

  var $scope = null;
  var controller = null;

  var $secondScope = null;
  var secondController = null;

  beforeEach(function () {

    module('app');

    inject(function ($rootScope, $controller) {

      $scope = $rootScope.$new();
      controller = $controller('clientCtrl', {
        $scope: $scope
      });

      $secondScope = $rootScope.$new();
      secondController = $controller('clientCtrl', {
        $scope: $secondScope
      });

    });

  });

  describe('waiting for json', function () {

    it('should not add message', function () {

      $scope.text = 'something';
      $scope.submit();

      expect($scope.messages.length).to.eql(1);

    });

  });

  describe('waiting for connect', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });

    it('should get error message not connected', function () {

      $scope.text = 'something';
      $scope.submit();

      expect($scope.messages.length).to.eql(3);

    });

  });

  describe('tempjson', function () {

    it('should have message json:', function () {

      expect($scope.messages.length).to.eql(1);

    });

    it('should get error message json not correct', function () {

      $scope.tempAddFile('temp.json', '{"username": "first", "id": "1"}');

      expect($scope.tempJson).to.eql(null);
      expect($scope.messages.length).to.eql(2);
      expect($scope.messages[$scope.messages.length - 1]).to.eql('json is not correct');

    });

    it('should set tempjson', function () {

      $scope.tempAddFile('temp.json', '{"name": "first", "id": "1"}');

      expect($scope.tempJson).not.to.eql(null);
      expect($scope.messages.length).to.eql(2);
      expect($scope.messages[$scope.messages.length - 1]).to.eql('did get json');

    });

  });

  describe('connect', function () {

    it('should not connect', function (done) {

      $scope.$on('noJsonError', function () {
        expect($scope.messages.length).to.eql(1);
        done();
      });

      $scope.text = '--connect';
      $scope.submit();

    });

    it('should connect', function (done) {

      somethingSetTempJson($scope, $secondScope);
      somethingConnect($scope, $secondScope, function () {

        expect($scope.messages.length).to.eql(4);

        somethingDisconnect($scope, $secondScope, function () {
          done();
        });

      });

    });

  });

  /*
  describe('disconnect', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });
    beforeEach(function (done) { somethingDisconnect($scope, $secondScope, done) });

    it('should disconnect', function () {

      expect($scope.messages.length).to.eql(5);
      expect($scope.users.length).to.eql(0);

    });

  });
  */

  describe('connected users', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });
    afterEach(function (done) { somethingDisconnect($scope, $secondScope, done) });

    it('should have connected users', function () {

      expect($scope.users.length).to.eql(1);
      expect($secondScope.users.length).to.eql(1);

      expect($scope.users[0].json.name).to.eql('second');
      expect($secondScope.users[0].json.name).to.eql('first');

    });

  });

  describe('message', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });
    afterEach(function (done) { somethingDisconnect($scope, $secondScope, done) });

    it('should send message', function (done) {

      $scope.text = 'something';
      $scope.submit();

      expect($scope.messages.length).to.eql(5);

      $secondScope.$on('message', function () {

        expect($secondScope.messages.length).to.eql(4);

        done();

      });

    });

  });

  describe('filetransfer', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });
    afterEach(function (done) { somethingDisconnect($scope, $secondScope, done) });

    it('should send request', function (done) {

      $scope.tempAddFile('something.json', 'some data');

      expect($scope.messages[$scope.messages.length - 1]).to.eql('did upload file');
      expect($scope.sendFiles.length).to.eql(1);

      $secondScope.$on('hasRequest', function () {

        expect($secondScope.messages[$secondScope.messages.length - 1]).to.eql('first: request to send file: something.json');
        expect($secondScope.acceptFiles.length).to.eql(1);

        done();

      });

    });

    it('should accept request', function (done) {

      $scope.tempAddFile('something.json', 'some data');

      $secondScope.$on('hasRequest', function () {

        $secondScope.text = '--accept something.json';
        $secondScope.submit();

      });

      $scope.$on('acceptRequest', function () {

        expect($scope.messages[$scope.messages.length - 1]).to.eql('did send file');

      });

      $secondScope.$on('fileData', function () {

        expect($secondScope.messages[$secondScope.messages.length - 1]).to.eql('did get file: something.json');

        done();

      });

    });

    it('should remove request', function (done) {

      $scope.tempAddFile('something.json', 'some data');

      $secondScope.$on('hasRequest', function () {

        $scope.text = '--rmsend something.json';
        $scope.submit();

        expect($scope.messages[$scope.messages.length - 1]).to.eql('did remove file');
        expect($scope.sendFiles.length).to.eql(0);

      });

      $secondScope.$on('rmsend', function () {

        expect($secondScope.messages[$secondScope.messages.length - 1]).to.eql('first: rmsend: something.json');
        expect($secondScope.acceptFiles.length).to.eql(0);

        done();

      });

    });

  });

  describe('disconnect', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });

    beforeEach(function (done) {

      $scope.tempAddFile('something.json', 'some data');

      $secondScope.$on('hasRequest', function () {

        $secondScope.tempAddFile('something.json', 'some data');

        $scope.$on('hasRequest', function () {

          done();

        });

      });

    });

    afterEach(function (done) {

      $secondScope.$on('disconnect', function () { done() });

      $secondScope.text = '--disconnect';
      $secondScope.submit();

    });

    it('should disconnect', function (done) {

      var count = 0;
      function did() {
        count++;
        if (count === 2) done();
      }

      $scope.$on('disconnect', function () {

        expect($scope.messages.length).to.eql(7);
        expect($scope.users.length).to.eql(0);
        expect($scope.sendFiles.length).to.eql(0);

        did();

      });

      $secondScope.$on('didDisconnect', function () {

        expect($secondScope.messages.length).to.eql(6);
        expect($secondScope.users.length).to.eql(0);
        expect($secondScope.acceptFiles.length).to.eql(0);

        did();

      });

      $scope.text = '--disconnect';
      $scope.submit();

    });

  });

});

function somethingSetTempJson($scope, $secondScope) {

  $scope.tempAddFile('temp.json', '{"name": "first", "id": "1"}');
  $secondScope.tempAddFile('temp.json', '{"name": "second", "id": "2"}');

}

function somethingConnect($scope, $secondScope, fn) {

  var count = 0;
  function did() {
    count++;
    if (count == 2) {
      fn();
    }
  }

  $scope.$on('connect', function () {

    $scope.$on('something', function () { did() });
    $secondScope.$on('userjson', function () { did() });

    $secondScope.text = '--connect';
    $secondScope.submit();

  });

  $scope.text = '--connect';
  $scope.submit();

}

function somethingDisconnect($scope, $secondScope, fn) {

  var count = 0;
  function did() {
    count++;
    if (count == 2) fn();
  }

  $scope.$on('disconnect', function () { did() });
  $secondScope.$on('disconnect', function () { did() });

  $scope.text = '--disconnect';
  $secondScope.text = '--disconnect';
  $scope.submit();
  $secondScope.submit();

}
<commit_msg>refactor somethingconnect<commit_after>
describe('client', function () {

  var $scope = null;
  var controller = null;

  var $secondScope = null;
  var secondController = null;

  beforeEach(function () {

    module('app');

    inject(function ($rootScope, $controller) {

      $scope = $rootScope.$new();
      controller = $controller('clientCtrl', {
        $scope: $scope
      });

      $secondScope = $rootScope.$new();
      secondController = $controller('clientCtrl', {
        $scope: $secondScope
      });

    });

  });

  describe('waiting for json', function () {

    it('should not add message', function () {

      $scope.text = 'something';
      $scope.submit();

      expect($scope.messages.length).to.eql(1);

    });

  });

  describe('waiting for connect', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });

    it('should get error message not connected', function () {

      $scope.text = 'something';
      $scope.submit();

      expect($scope.messages.length).to.eql(3);

    });

  });

  describe('tempjson', function () {

    it('should have message json:', function () {

      expect($scope.messages.length).to.eql(1);

    });

    it('should get error message json not correct', function () {

      $scope.tempAddFile('temp.json', '{"username": "first", "id": "1"}');

      expect($scope.tempJson).to.eql(null);
      expect($scope.messages.length).to.eql(2);
      expect($scope.messages[$scope.messages.length - 1]).to.eql('json is not correct');

    });

    it('should set tempjson', function () {

      $scope.tempAddFile('temp.json', '{"name": "first", "id": "1"}');

      expect($scope.tempJson).not.to.eql(null);
      expect($scope.messages.length).to.eql(2);
      expect($scope.messages[$scope.messages.length - 1]).to.eql('did get json');

    });

  });

  describe('connect', function () {

    it('should not connect', function (done) {

      $scope.$on('noJsonError', function () {
        expect($scope.messages.length).to.eql(1);
        done();
      });

      $scope.text = '--connect';
      $scope.submit();

    });

    it('should connect', function (done) {

      somethingSetTempJson($scope, $secondScope);
      somethingConnect($scope, $secondScope, function () {

        expect($scope.messages.length).to.eql(4);

        somethingDisconnect($scope, $secondScope, function () {
          done();
        });

      });

    });

  });

  /*
  describe('disconnect', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });
    beforeEach(function (done) { somethingDisconnect($scope, $secondScope, done) });

    it('should disconnect', function () {

      expect($scope.messages.length).to.eql(5);
      expect($scope.users.length).to.eql(0);

    });

  });
  */

  describe('connected users', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });
    afterEach(function (done) { somethingDisconnect($scope, $secondScope, done) });

    it('should have connected users', function () {

      expect($scope.users.length).to.eql(1);
      expect($secondScope.users.length).to.eql(1);

      expect($scope.users[0].json.name).to.eql('second');
      expect($secondScope.users[0].json.name).to.eql('first');

    });

  });

  describe('message', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });
    afterEach(function (done) { somethingDisconnect($scope, $secondScope, done) });

    it('should send message', function (done) {

      $scope.text = 'something';
      $scope.submit();

      expect($scope.messages.length).to.eql(5);

      $secondScope.$on('message', function () {

        expect($secondScope.messages.length).to.eql(4);

        done();

      });

    });

  });

  describe('filetransfer', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });
    afterEach(function (done) { somethingDisconnect($scope, $secondScope, done) });

    it('should send request', function (done) {

      $scope.tempAddFile('something.json', 'some data');

      expect($scope.messages[$scope.messages.length - 1]).to.eql('did upload file');
      expect($scope.sendFiles.length).to.eql(1);

      $secondScope.$on('hasRequest', function () {

        expect($secondScope.messages[$secondScope.messages.length - 1]).to.eql('first: request to send file: something.json');
        expect($secondScope.acceptFiles.length).to.eql(1);

        done();

      });

    });

    it('should accept request', function (done) {

      $scope.tempAddFile('something.json', 'some data');

      $secondScope.$on('hasRequest', function () {

        $secondScope.text = '--accept something.json';
        $secondScope.submit();

      });

      $scope.$on('acceptRequest', function () {

        expect($scope.messages[$scope.messages.length - 1]).to.eql('did send file');

      });

      $secondScope.$on('fileData', function () {

        expect($secondScope.messages[$secondScope.messages.length - 1]).to.eql('did get file: something.json');

        done();

      });

    });

    it('should remove request', function (done) {

      $scope.tempAddFile('something.json', 'some data');

      $secondScope.$on('hasRequest', function () {

        $scope.text = '--rmsend something.json';
        $scope.submit();

        expect($scope.messages[$scope.messages.length - 1]).to.eql('did remove file');
        expect($scope.sendFiles.length).to.eql(0);

      });

      $secondScope.$on('rmsend', function () {

        expect($secondScope.messages[$secondScope.messages.length - 1]).to.eql('first: rmsend: something.json');
        expect($secondScope.acceptFiles.length).to.eql(0);

        done();

      });

    });

  });

  describe('disconnect', function () {

    beforeEach(function () { somethingSetTempJson($scope, $secondScope) });
    beforeEach(function (done) { somethingConnect($scope, $secondScope, done) });

    beforeEach(function (done) {

      $scope.tempAddFile('something.json', 'some data');

      $secondScope.$on('hasRequest', function () {

        $secondScope.tempAddFile('something.json', 'some data');

        $scope.$on('hasRequest', function () {

          done();

        });

      });

    });

    afterEach(function (done) {

      $secondScope.$on('disconnect', function () { done() });

      $secondScope.text = '--disconnect';
      $secondScope.submit();

    });

    it('should disconnect', function (done) {

      var count = 0;
      function did() {
        count++;
        if (count === 2) done();
      }

      $scope.$on('disconnect', function () {

        expect($scope.messages.length).to.eql(7);
        expect($scope.users.length).to.eql(0);
        expect($scope.sendFiles.length).to.eql(0);

        did();

      });

      $secondScope.$on('didDisconnect', function () {

        expect($secondScope.messages.length).to.eql(6);
        expect($secondScope.users.length).to.eql(0);
        expect($secondScope.acceptFiles.length).to.eql(0);

        did();

      });

      $scope.text = '--disconnect';
      $scope.submit();

    });

  });

});

function somethingSetTempJson($scope, $secondScope) {

  $scope.tempAddFile('temp.json', '{"name": "first", "id": "1"}');
  $secondScope.tempAddFile('temp.json', '{"name": "second", "id": "2"}');

}

function somethingConnect($scope, $secondScope, fn) {

  var count = 0;
  function did() {
    count++;
    if (count == 2) {
      fn();
    }
  }

  $scope.$on('something', function () {
    did();
  });
  $scope.$on('userjson', function () {
    did();
  });

  $secondScope.$on('something', function () {
    did();
  });
  $secondScope.$on('userjson', function () {
    did();
  });

  $scope.text = '--connect';
  $secondScope.text = '--connect';

  $scope.submit();
  $secondScope.submit();

}

function somethingDisconnect($scope, $secondScope, fn) {

  var count = 0;
  function did() {
    count++;
    if (count == 2) fn();
  }

  $scope.$on('disconnect', function () { did() });
  $secondScope.$on('disconnect', function () { did() });

  $scope.text = '--disconnect';
  $secondScope.text = '--disconnect';
  $scope.submit();
  $secondScope.submit();

}
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'

import Icon from './icon'
import ButtonContent from './button-content'

class GoogleLogin extends Component {
  constructor(props) {
    super(props)
    this.signIn = this.signIn.bind(this)
    this.enableButton = this.enableButton.bind(this)
    this.state = {
      disabled: true,
      hovered: false,
      active: false
    }
  }
  componentDidMount() {
    const {
      clientId,
      cookiePolicy,
      loginHint,
      hostedDomain,
      autoLoad,
      isSignedIn,
      fetchBasicProfile,
      redirectUri,
      discoveryDocs,
      onFailure,
      uxMode,
      scope,
      accessType,
      responseType,
      jsSrc
    } = this.props
    ;((d, s, id, cb) => {
      const element = d.getElementsByTagName(s)[0]
      const fjs = element
      let js = element
      js = d.createElement(s)
      js.id = id
      js.src = jsSrc
      if (fjs && fjs.parentNode) {
        fjs.parentNode.insertBefore(js, fjs)
      } else {
        d.head.appendChild(js)
      }
      js.onload = cb
    })(document, 'script', 'google-login', () => {
      const params = {
        client_id: clientId,
        cookie_policy: cookiePolicy,
        login_hint: loginHint,
        hosted_domain: hostedDomain,
        fetch_basic_profile: fetchBasicProfile,
        discoveryDocs,
        ux_mode: uxMode,
        redirect_uri: redirectUri,
        scope,
        access_type: accessType
      }

      if (responseType === 'code') {
        params.access_type = 'offline'
      }

      window.gapi.load('auth2', () => {
        this.enableButton()
        if (!window.gapi.auth2.getAuthInstance()) {
          window.gapi.auth2.init(params).then(
            res => {
              if (isSignedIn && res.isSignedIn.get()) {
                this.handleSigninSuccess(res.currentUser.get())
              }
            },
            err => onFailure(err)
          )
        }
        if (autoLoad) {
          this.signIn()
        }
      })
    })
  }
  componentWillUnmount() {
    this.enableButton = () => {}
  }
  enableButton() {
    this.setState({
      disabled: false
    })
  }
  signIn(e) {
    if (e) {
      e.preventDefault() // to prevent submit if used within form
    }
    if (!this.state.disabled) {
      const auth2 = window.gapi.auth2.getAuthInstance()
      const { onSuccess, onRequest, onFailure, prompt, responseType } = this.props
      const options = {
        prompt
      }
      onRequest()
      if (responseType === 'code') {
        auth2.grantOfflineAccess(options).then(res => onSuccess(res), err => onFailure(err))
      } else {
        auth2.signIn(options).then(res => this.handleSigninSuccess(res), err => onFailure(err))
      }
    }
  }
  handleSigninSuccess(res) {
    /*
      offer renamed response keys to names that match use
    */
    const basicProfile = res.getBasicProfile()
    const authResponse = res.getAuthResponse()
    res.googleId = basicProfile.getId()
    res.tokenObj = authResponse
    res.tokenId = authResponse.id_token
    res.accessToken = authResponse.access_token
    res.profileObj = {
      googleId: basicProfile.getId(),
      imageUrl: basicProfile.getImageUrl(),
      email: basicProfile.getEmail(),
      name: basicProfile.getName(),
      givenName: basicProfile.getGivenName(),
      familyName: basicProfile.getFamilyName()
    }
    this.props.onSuccess(res)
  }

  render() {
    const { tag, type, className, disabledStyle, buttonText, children, render, theme, icon } = this.props
    const disabled = this.state.disabled || this.props.disabled

    if (render) {
      return render({ onClick: this.signIn })
    }

    const initialStyle = {
      backgroundColor: theme === 'dark' ? 'rgb(66, 133, 244)' : '#fff',
      display: 'inline-flex',
      alignItems: 'center',
      color: theme === 'dark' ? '#fff' : 'rgba(0, 0, 0, .54)',
      boxShadow: '0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)',
      padding: 0,
      borderRadius: 2,
      border: '1px solid transparent',
      fontSize: 14,
      fontWeight: '500',
      fontFamily: 'Roboto, sans-serif'
    }

    const hoveredStyle = {
      cursor: 'pointer',
      opacity: 0.9
    }

    const activeStyle = {
      cursor: 'pointer',
      backgroundColor: theme === 'dark' ? '#3367D6' : '#eee',
      color: theme === 'dark' ? '#fff' : 'rgba(0, 0, 0, .54)',
      opacity: 1
    }

    const defaultStyle = (() => {
      if (disabled) {
        return Object.assign({}, initialStyle, disabledStyle)
      }

      if (this.state.active) {
        if (theme === 'dark') {
          return Object.assign({}, initialStyle, activeStyle)
        }

        return Object.assign({}, initialStyle, activeStyle)
      }

      if (this.state.hovered) {
        return Object.assign({}, initialStyle, hoveredStyle)
      }

      return initialStyle
    })()
    const googleLoginButton = React.createElement(
      tag,
      {
        onMouseEnter: () => this.setState({ hovered: true }),
        onMouseLeave: () => this.setState({ hovered: false, active: false }),
        onMouseDown: () => this.setState({ active: true }),
        onMouseUp: () => this.setState({ active: false }),
        onClick: this.signIn,
        style: defaultStyle,
        type,
        disabled,
        className
      },
      [
        icon && <Icon key={1} active={this.state.active} />,
        <ButtonContent icon={icon} key={2}>
          {children || buttonText}
        </ButtonContent>
      ]
    )

    return googleLoginButton
  }
}

GoogleLogin.propTypes = {
  onSuccess: PropTypes.func.isRequired,
  onFailure: PropTypes.func.isRequired,
  clientId: PropTypes.string.isRequired,
  jsSrc: PropTypes.string,
  onRequest: PropTypes.func,
  buttonText: PropTypes.string,
  scope: PropTypes.string,
  className: PropTypes.string,
  redirectUri: PropTypes.string,
  cookiePolicy: PropTypes.string,
  loginHint: PropTypes.string,
  hostedDomain: PropTypes.string,
  children: PropTypes.node,
  disabledStyle: PropTypes.object,
  fetchBasicProfile: PropTypes.bool,
  prompt: PropTypes.string,
  tag: PropTypes.string,
  autoLoad: PropTypes.bool,
  disabled: PropTypes.bool,
  discoveryDocs: PropTypes.array,
  uxMode: PropTypes.string,
  isSignedIn: PropTypes.bool,
  responseType: PropTypes.string,
  type: PropTypes.string,
  accessType: PropTypes.string,
  render: PropTypes.func,
  theme: PropTypes.string,
  icon: PropTypes.bool
}

GoogleLogin.defaultProps = {
  type: 'button',
  tag: 'button',
  buttonText: 'Sign in with Google',
  scope: 'profile email',
  accessType: 'online',
  prompt: '',
  cookiePolicy: 'single_host_origin',
  fetchBasicProfile: true,
  isSignedIn: false,
  uxMode: 'popup',
  disabledStyle: {
    opacity: 0.6
  },
  icon: true,
  theme: 'light',
  onRequest: () => {},
  jsSrc: 'https://apis.google.com/js/api.js'
}

export default GoogleLogin
<commit_msg>change buttonText propType to node<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'

import Icon from './icon'
import ButtonContent from './button-content'

class GoogleLogin extends Component {
  constructor(props) {
    super(props)
    this.signIn = this.signIn.bind(this)
    this.enableButton = this.enableButton.bind(this)
    this.state = {
      disabled: true,
      hovered: false,
      active: false
    }
  }
  componentDidMount() {
    const {
      clientId,
      cookiePolicy,
      loginHint,
      hostedDomain,
      autoLoad,
      isSignedIn,
      fetchBasicProfile,
      redirectUri,
      discoveryDocs,
      onFailure,
      uxMode,
      scope,
      accessType,
      responseType,
      jsSrc
    } = this.props
    ;((d, s, id, cb) => {
      const element = d.getElementsByTagName(s)[0]
      const fjs = element
      let js = element
      js = d.createElement(s)
      js.id = id
      js.src = jsSrc
      if (fjs && fjs.parentNode) {
        fjs.parentNode.insertBefore(js, fjs)
      } else {
        d.head.appendChild(js)
      }
      js.onload = cb
    })(document, 'script', 'google-login', () => {
      const params = {
        client_id: clientId,
        cookie_policy: cookiePolicy,
        login_hint: loginHint,
        hosted_domain: hostedDomain,
        fetch_basic_profile: fetchBasicProfile,
        discoveryDocs,
        ux_mode: uxMode,
        redirect_uri: redirectUri,
        scope,
        access_type: accessType
      }

      if (responseType === 'code') {
        params.access_type = 'offline'
      }

      window.gapi.load('auth2', () => {
        this.enableButton()
        if (!window.gapi.auth2.getAuthInstance()) {
          window.gapi.auth2.init(params).then(
            res => {
              if (isSignedIn && res.isSignedIn.get()) {
                this.handleSigninSuccess(res.currentUser.get())
              }
            },
            err => onFailure(err)
          )
        }
        if (autoLoad) {
          this.signIn()
        }
      })
    })
  }
  componentWillUnmount() {
    this.enableButton = () => {}
  }
  enableButton() {
    this.setState({
      disabled: false
    })
  }
  signIn(e) {
    if (e) {
      e.preventDefault() // to prevent submit if used within form
    }
    if (!this.state.disabled) {
      const auth2 = window.gapi.auth2.getAuthInstance()
      const { onSuccess, onRequest, onFailure, prompt, responseType } = this.props
      const options = {
        prompt
      }
      onRequest()
      if (responseType === 'code') {
        auth2.grantOfflineAccess(options).then(res => onSuccess(res), err => onFailure(err))
      } else {
        auth2.signIn(options).then(res => this.handleSigninSuccess(res), err => onFailure(err))
      }
    }
  }
  handleSigninSuccess(res) {
    /*
      offer renamed response keys to names that match use
    */
    const basicProfile = res.getBasicProfile()
    const authResponse = res.getAuthResponse()
    res.googleId = basicProfile.getId()
    res.tokenObj = authResponse
    res.tokenId = authResponse.id_token
    res.accessToken = authResponse.access_token
    res.profileObj = {
      googleId: basicProfile.getId(),
      imageUrl: basicProfile.getImageUrl(),
      email: basicProfile.getEmail(),
      name: basicProfile.getName(),
      givenName: basicProfile.getGivenName(),
      familyName: basicProfile.getFamilyName()
    }
    this.props.onSuccess(res)
  }

  render() {
    const { tag, type, className, disabledStyle, buttonText, children, render, theme, icon } = this.props
    const disabled = this.state.disabled || this.props.disabled

    if (render) {
      return render({ onClick: this.signIn })
    }

    const initialStyle = {
      backgroundColor: theme === 'dark' ? 'rgb(66, 133, 244)' : '#fff',
      display: 'inline-flex',
      alignItems: 'center',
      color: theme === 'dark' ? '#fff' : 'rgba(0, 0, 0, .54)',
      boxShadow: '0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)',
      padding: 0,
      borderRadius: 2,
      border: '1px solid transparent',
      fontSize: 14,
      fontWeight: '500',
      fontFamily: 'Roboto, sans-serif'
    }

    const hoveredStyle = {
      cursor: 'pointer',
      opacity: 0.9
    }

    const activeStyle = {
      cursor: 'pointer',
      backgroundColor: theme === 'dark' ? '#3367D6' : '#eee',
      color: theme === 'dark' ? '#fff' : 'rgba(0, 0, 0, .54)',
      opacity: 1
    }

    const defaultStyle = (() => {
      if (disabled) {
        return Object.assign({}, initialStyle, disabledStyle)
      }

      if (this.state.active) {
        if (theme === 'dark') {
          return Object.assign({}, initialStyle, activeStyle)
        }

        return Object.assign({}, initialStyle, activeStyle)
      }

      if (this.state.hovered) {
        return Object.assign({}, initialStyle, hoveredStyle)
      }

      return initialStyle
    })()
    const googleLoginButton = React.createElement(
      tag,
      {
        onMouseEnter: () => this.setState({ hovered: true }),
        onMouseLeave: () => this.setState({ hovered: false, active: false }),
        onMouseDown: () => this.setState({ active: true }),
        onMouseUp: () => this.setState({ active: false }),
        onClick: this.signIn,
        style: defaultStyle,
        type,
        disabled,
        className
      },
      [
        icon && <Icon key={1} active={this.state.active} />,
        <ButtonContent icon={icon} key={2}>
          {children || buttonText}
        </ButtonContent>
      ]
    )

    return googleLoginButton
  }
}

GoogleLogin.propTypes = {
  onSuccess: PropTypes.func.isRequired,
  onFailure: PropTypes.func.isRequired,
  clientId: PropTypes.string.isRequired,
  jsSrc: PropTypes.string,
  onRequest: PropTypes.func,
  buttonText: PropTypes.node,
  scope: PropTypes.string,
  className: PropTypes.string,
  redirectUri: PropTypes.string,
  cookiePolicy: PropTypes.string,
  loginHint: PropTypes.string,
  hostedDomain: PropTypes.string,
  children: PropTypes.node,
  disabledStyle: PropTypes.object,
  fetchBasicProfile: PropTypes.bool,
  prompt: PropTypes.string,
  tag: PropTypes.string,
  autoLoad: PropTypes.bool,
  disabled: PropTypes.bool,
  discoveryDocs: PropTypes.array,
  uxMode: PropTypes.string,
  isSignedIn: PropTypes.bool,
  responseType: PropTypes.string,
  type: PropTypes.string,
  accessType: PropTypes.string,
  render: PropTypes.func,
  theme: PropTypes.string,
  icon: PropTypes.bool
}

GoogleLogin.defaultProps = {
  type: 'button',
  tag: 'button',
  buttonText: 'Sign in with Google',
  scope: 'profile email',
  accessType: 'online',
  prompt: '',
  cookiePolicy: 'single_host_origin',
  fetchBasicProfile: true,
  isSignedIn: false,
  uxMode: 'popup',
  disabledStyle: {
    opacity: 0.6
  },
  icon: true,
  theme: 'light',
  onRequest: () => {},
  jsSrc: 'https://apis.google.com/js/api.js'
}

export default GoogleLogin
<|endoftext|>


<commit_before>import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal as OverlayModal } from 'react-overlays';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { Transition, TransitionGroup } from 'react-transition-group';

import IconButton from '../IconButton';
import Headline from '../Headline';
import css from './Modal.css';

const propTypes = {
  ariaLabel: PropTypes.string,
  children: PropTypes.node.isRequired,
  /** Modal can be dismissed by clicking the background overlay */
  closeOnBackgroundClick: PropTypes.bool,
  /* Apply custom CSS classes on the content element */
  contentClass: PropTypes.string,
  /** If true, renders a close 'X' in the starting corner of the modal */
  dismissible: PropTypes.bool,
  /**
   * When `true` The modal will prevent focus from leaving the Modal while open.
   *
   * Generally this should never be set to `false` as it makes the Modal less
   * accessible to assistive technologies, like screen readers.
   */
  enforceFocus: PropTypes.bool,

  /* Add a footer to the Modal (Optional) */
  footer: PropTypes.node,

  /** Unique identifier for modal window. */
  id: PropTypes.string,
  /** Descriptive title for top of modal. */
  label: PropTypes.node.isRequired,
  /** Callback that signals intent to close window -
   * boolean passed to 'open' prop can be set using
   * callback does not actually close the modal.
   */
  onClose: PropTypes.func,
  /** Callback triggered when modal opens */
  onOpen: PropTypes.func,
  /** Size defaults to 60% the scope element's width.
   * "Small" can be applied for 40% and "Large" for 90%.
   */
  /** Deciding value for rendering the modal(true) or not(false). */
  open: PropTypes.bool,
  /** When `true` The modal will restore focus to previously focused element once modal is hidden */
  restoreFocus: PropTypes.bool,
  scope: PropTypes.oneOf([
    'root',
    'module',
  ]),
  showHeader: PropTypes.bool,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  /** Parent element for modal.
   * Defaults to 'module' which keeps the main navigation visible.
   * A value of 'root' covers the entire view.
   */

  /* Optionally update the wrapping element if needed - useful if the modal itself is a form */
  wrappingElement: PropTypes.oneOf(['div', 'form']),
};

const defaultProps = {
  closeOnBackgroundClick: false,
  dismissible: false,
  enforceFocus: true,
  onOpen: () => { },
  open: false,
  restoreFocus: true,
  scope: 'module',
  showHeader: true,
  size: 'medium',
  wrappingElement: 'div',
};

const Modal = (props) => {
  const WrappingElement = props.wrappingElement;

  const getModalScope = () => {
    let container;

    if (props.scope === 'module') {
      container = document.getElementById('OverlayContainer');
    }

    return container;
  };

  const getModalClass = () => classNames(
    css.modal,
    css[props.size],
  );

  const getContentClass = () => classNames(
    css.modalContent,
    props.contentClass,
  );

  return (
    <TransitionGroup>
      { props.open &&
        <Transition
          in={props.open}
          unmountOnExit
          timeout={400}
          appear
        >
          {
            (status) => (
              <OverlayModal
                show={props.open}
                backdropClassName={css.backdrop}
                className={classNames(css.modalRoot, css[status])}
                container={getModalScope()}
                onHide={props.onClose}
                onShow={props.onOpen}
                onBackdropClick={props.closeOnBackgroundClick ? props.onClose : () => {}}
                enforceFocus={props.enforceFocus}
                restoreFocus={props.restoreFocus}
              >
                <WrappingElement
                  className={getModalClass()}
                  aria-label={props.ariaLabel}
                  id={props.id}
                >
                  { props.showHeader &&

                      <Headline
                        tag="h1"
                        size="small"
                        margin="none"
                        className={css.modalLabel}
                        id={props.id && `${props.id}-label`}
                      >
                        {props.label}
                      </Headline>

                        {props.dismissible &&
                          <FormattedMessage id="stripes-components.dismissModal">
                            {ariaLabel => (
                              <IconButton
                                className={css.closeModal}
                                id={props.id && `${props.id}-close-button`}
                                onClick={props.onClose}
                                aria-label={ariaLabel}
                                icon="closeX"
                              />
                            )}
                          </FormattedMessage>
                        }

                  }
                  <div className={getContentClass()} id={props.id && `${props.id}-content`}>
                    {props.children}

                  {
                    props.footer && (
                      <div className={css.modalFooter} id={props.id && `${props.id}-footer`}>
                        {props.footer}

                    )
                  }
                </WrappingElement>
              </OverlayModal>
            )
          }
        </Transition>
      }
    </TransitionGroup>
  );
};

Modal.propTypes = propTypes;
Modal.defaultProps = defaultProps;

export default Modal;
<commit_msg>Pass along props to Modal wrapping element<commit_after>import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal as OverlayModal } from 'react-overlays';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { Transition, TransitionGroup } from 'react-transition-group';

import IconButton from '../IconButton';
import Headline from '../Headline';
import css from './Modal.css';

const propTypes = {
  ariaLabel: PropTypes.string,
  children: PropTypes.node.isRequired,
  /** Modal can be dismissed by clicking the background overlay */
  closeOnBackgroundClick: PropTypes.bool,
  /* Apply custom CSS classes on the content element */
  contentClass: PropTypes.string,
  /** If true, renders a close 'X' in the starting corner of the modal */
  dismissible: PropTypes.bool,
  /**
   * When `true` The modal will prevent focus from leaving the Modal while open.
   *
   * Generally this should never be set to `false` as it makes the Modal less
   * accessible to assistive technologies, like screen readers.
   */
  enforceFocus: PropTypes.bool,

  /* Add a footer to the Modal (Optional) */
  footer: PropTypes.node,

  /** Unique identifier for modal window. */
  id: PropTypes.string,
  /** Descriptive title for top of modal. */
  label: PropTypes.node.isRequired,
  /** Callback that signals intent to close window -
   * boolean passed to 'open' prop can be set using
   * callback does not actually close the modal.
   */
  onClose: PropTypes.func,
  /** Callback triggered when modal opens */
  onOpen: PropTypes.func,
  /** Size defaults to 60% the scope element's width.
   * "Small" can be applied for 40% and "Large" for 90%.
   */
  /** Deciding value for rendering the modal(true) or not(false). */
  open: PropTypes.bool,
  /** When `true` The modal will restore focus to previously focused element once modal is hidden */
  restoreFocus: PropTypes.bool,
  scope: PropTypes.oneOf([
    'root',
    'module',
  ]),
  showHeader: PropTypes.bool,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  /** Parent element for modal.
   * Defaults to 'module' which keeps the main navigation visible.
   * A value of 'root' covers the entire view.
   */

  /* Optionally update the wrapping element if needed - useful if the modal itself is a form */
  wrappingElement: PropTypes.oneOf(['div', 'form']),
};

const defaultProps = {
  closeOnBackgroundClick: false,
  dismissible: false,
  enforceFocus: true,
  onOpen: () => { },
  open: false,
  restoreFocus: true,
  scope: 'module',
  showHeader: true,
  size: 'medium',
  wrappingElement: 'div',
};

const Modal = (props) => {
  const {
    children,
    closeOnBackgroundClick,
    contentClass,
    dismissible,
    enforceFocus,
    footer,
    id,
    label,
    onClose,
    onOpen,
    open,
    restoreFocus,
    scope,
    showHeader,
    size,
    wrappingElement,
    ...rest
  } = props;

  const WrappingElement = wrappingElement;

  const getModalScope = () => {
    let container;

    if (scope === 'module') {
      container = document.getElementById('OverlayContainer');
    }

    return container;
  };

  const getModalClass = () => classNames(
    css.modal,
    css[size],
  );

  const getContentClass = () => classNames(
    css.modalContent,
    contentClass,
  );

  return (
    <TransitionGroup>
      { open &&
        <Transition
          in={open}
          unmountOnExit
          timeout={400}
          appear
        >
          {
            (status) => (
              <OverlayModal
                show={open}
                backdropClassName={css.backdrop}
                className={classNames(css.modalRoot, css[status])}
                container={getModalScope()}
                onHide={onClose}
                onShow={onOpen}
                onBackdropClick={closeOnBackgroundClick ? onClose : () => {}}
                enforceFocus={enforceFocus}
                restoreFocus={restoreFocus}
              >
                <WrappingElement
                  className={getModalClass()}
                  aria-label={props.ariaLabel}
                  id={props.id}
                  {...rest}
                >
                  { showHeader &&

                      <Headline
                        tag="h1"
                        size="small"
                        margin="none"
                        className={css.modalLabel}
                        id={id && `${id}-label`}
                      >
                        {props.label}
                      </Headline>

                        {dismissible &&
                          <FormattedMessage id="stripes-components.dismissModal">
                            {ariaLabel => (
                              <IconButton
                                className={css.closeModal}
                                id={id && `${id}-close-button`}
                                onClick={onClose}
                                aria-label={ariaLabel}
                                icon="closeX"
                              />
                            )}
                          </FormattedMessage>
                        }

                  }
                  <div className={getContentClass()} id={id && `${id}-content`}>
                    {children}

                  {
                    footer && (
                      <div className={css.modalFooter} id={id && `${id}-footer`}>
                        {footer}

                    )
                  }
                </WrappingElement>
              </OverlayModal>
            )
          }
        </Transition>
      }
    </TransitionGroup>
  );
};

Modal.propTypes = propTypes;
Modal.defaultProps = defaultProps;

export default Modal;
<|endoftext|>


<commit_before>'use strict';

var listener = require('../service/listener'),
    servers = require('../service/servers'),
    router = require('../service/router'),
    RedisConnector = require('../service/RedisConnector');

var keysExplorerCtrl = function() {
    console.log('loading keys explorer');

    // Init Redis connection
    var client;

    var redisConnector = RedisConnector();

    redisConnector.on('ready', function (_client) {
        client = _client;

        console.log('client connected!')

        client.on("error", function (err) {
            console.log("Error " + err);
        });
    });

    var createServer = function(event) {
        servers.add(event.detail);
        router.to('bslt-server-list');
    };

    var back = function() {
        router.to('bslt-server-list');
    };

    var $serverConfiguration = document.querySelector('bslt-server-configuration');

    listener.add($serverConfiguration, 'add-server-configuration', createServer);
    listener.add($serverConfiguration, 'go-back', back);
};

module.exports = keysExplorerCtrl;
<commit_msg>Add function fetchKeys<commit_after>'use strict';

var listener = require('../service/listener'),
    servers = require('../service/servers'),
    router = require('../service/router'),
    RedisConnector = require('../service/RedisConnector');

var keysExplorerCtrl = function() {
    console.log('loading keys explorer');

    var fetchKeys = function(client, fn) {
        client.keys('*', fn);
    };

    // Init Redis connection
    var client;

    var redisConnector = RedisConnector();

    redisConnector.on('ready', function (_client) {
        client = _client;

        console.log('client connected!')

        fetchKeys(client, function(err, replies) {
            console.log('keys:', replies);
        });

        client.on('error', function (err) {
            console.log('Error ' + err);
        });
    });

    var back = function() {
        router.to('bslt-server-list');
    };

    var $keysExplorer = document.querySelector('bslt-keys-explorer');
};

module.exports = keysExplorerCtrl;
<|endoftext|>