<commit_before>import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { WalletContext } from '@utils/context';
import { Form, message, Row, Col, Alert, Descriptions } from 'antd';
import TokenIconAlert from '@components/Common/Alerts.js';
import PrimaryButton, {
    SecondaryButton,
} from '@components/Common/PrimaryButton';
import {
    DestinationAmount,
    DestinationAddressSingle,
} from '@components/Common/EnhancedInputs';
import useBCH from '@hooks/useBCH';
import BalanceHeader from '@components/Common/BalanceHeader';
import { Redirect } from 'react-router-dom';
import useWindowDimensions from '@hooks/useWindowDimensions';
import { isMobile, isIOS, isSafari } from 'react-device-detect';
import { Img } from 'react-image';
import makeBlockie from 'ethereum-blockies-base64';
import BigNumber from 'bignumber.js';
import {
    currency,
    parseAddress,
    isValidTokenPrefix,
} from '@components/Common/Ticker.js';
import { Event } from '@utils/GoogleAnalytics';
import {
    getWalletState,
    convertEtokenToSimpleledger,
} from '@utils/cashMethods';
import ApiError from '@components/Common/ApiError';
import {
    sendTokenNotification,
    errorNotification,
} from '@components/Common/Notifications';

const SendToken = ({ tokenId, jestBCH, passLoadingStatus }) => {
    const { wallet, apiError } = React.useContext(WalletContext);
    const walletState = getWalletState(wallet);
    const { tokens, slpBalancesAndUtxos } = walletState;
    const token = tokens.find(token => token.tokenId === tokenId);

    const [tokenStats, setTokenStats] = useState(null);
    const [queryStringText, setQueryStringText] = useState(null);
    const [sendTokenAddressError, setSendTokenAddressError] = useState(false);
    const [sendTokenAmountError, setSendTokenAmountError] = useState(false);
    // Get device window width
    // If this is less than 769, the page will open with QR scanner open
    const { width } = useWindowDimensions();
    // Load with QR code open if device is mobile and NOT iOS + anything but safari
    const scannerSupported = width < 769 && isMobile && !(isIOS && !isSafari);

    const [formData, setFormData] = useState({
        dirty: true,
        value: '',
        address: '',
    });

    const { getBCH, getRestUrl, sendToken, getTokenStats } = useBCH();

    // jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
    const BCH = jestBCH ? jestBCH : getBCH();

    // Fetch token stats if you do not have them and API did not return an error
    if (tokenStats === null) {
        getTokenStats(BCH, tokenId).then(
            result => {
                setTokenStats(result);
            },
            err => {
                console.log(`Error getting token stats: ${err}`);
            },
        );
    }

    async function submit() {
        setFormData({
            ...formData,
            dirty: false,
        });

        if (
            !formData.address ||
            !formData.value ||
            Number(formData.value <= 0) ||
            sendTokenAmountError
        ) {
            return;
        }

        // Event("Category", "Action", "Label")
        // Track number of SLPA send transactions and
        // SLPA token IDs
        Event('SendToken.js', 'Send', tokenId);

        passLoadingStatus(true);
        const { address, value } = formData;

        // Clear params from address
        let cleanAddress = address.split('?')[0];

        // Convert to simpleledger prefix if etoken
        cleanAddress = convertEtokenToSimpleledger(cleanAddress);

        try {
            const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, {
                tokenId: tokenId,
                tokenReceiverAddress: cleanAddress,
                amount: value,
            });

            sendTokenNotification(link);
        } catch (e) {
            passLoadingStatus(false);
            let message;

            if (!e.error && !e.message) {
                message = `Transaction failed: no response from ${getRestUrl()}.`;
            } else if (
                /Could not communicate with full node or other external service/.test(
                    e.error,
                )
            ) {
                message = 'Could not communicate with API. Please try again.';
            } else {
                message = e.message || e.error || JSON.stringify(e);
            }
            errorNotification(e, message, 'Sending eToken');
        }
    }

    const handleSlpAmountChange = e => {
        let error = false;
        const { value, name } = e.target;

        // test if exceeds balance using BigNumber
        let isGreaterThanBalance = false;
        if (!isNaN(value)) {
            const bigValue = new BigNumber(value);
            // Returns 1 if greater, -1 if less, 0 if the same, null if n/a
            isGreaterThanBalance = bigValue.comparedTo(token.balance);
        }

        // Validate value for > 0
        if (isNaN(value)) {
            error = 'Amount must be a number';
        } else if (value <= 0) {
            error = 'Amount must be greater than 0';
        } else if (token && token.balance && isGreaterThanBalance === 1) {
            error = `Amount cannot exceed your ${token.info.tokenTicker} balance of ${token.balance}`;
        } else if (!isNaN(value) && value.toString().includes('.')) {
            if (value.toString().split('.')[1].length > token.info.decimals) {
                error = `This token only supports ${token.info.decimals} decimal places`;
            }
        }
        setSendTokenAmountError(error);
        setFormData(p => ({
            ...p,
            [name]: value,
        }));
    };

    const handleTokenAddressChange = e => {
        const { value, name } = e.target;
        // validate for token address
        // validate for parameters
        // show warning that query strings are not supported

        let error = false;
        let addressString = value;

        const addressInfo = parseAddress(BCH, addressString, true);
        /*
        Model

        addressInfo =
        {
            address: '',
            isValid: false,
            queryString: '',
            amount: null,
        };
        */

        const { address, isValid, queryString } = addressInfo;

        // If query string,
        // Show an alert that only amount and currency.ticker are supported
        setQueryStringText(queryString);

        // Is this valid address?
        if (!isValid) {
            error = 'Address is not a valid etoken: address';
            // If valid address but token format
        } else if (!isValidTokenPrefix(address)) {
            error = `Cashtab only supports sending to ${currency.tokenPrefixes[0]} prefixed addresses`;
        }
        setSendTokenAddressError(error);

        setFormData(p => ({
            ...p,
            [name]: value,
        }));
    };

    const onMax = async () => {
        // Clear this error before updating field
        setSendTokenAmountError(false);
        try {
            let value = token.balance;

            setFormData({
                ...formData,
                value,
            });
        } catch (err) {
            console.log(`Error in onMax:`);
            console.log(err);
            message.error(
                'Unable to calculate the max value due to network errors',
            );
        }
    };

    useEffect(() => {
        // If the balance has changed, unlock the UI
        // This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked

        passLoadingStatus(false);
    }, [token]);

    return (
        <>
            {!token && <Redirect to="/" />}

            {token && (
                <>
                    <BalanceHeader
                        balance={token.balance}
                        ticker={token.info.tokenTicker}
                    />
                    <TokenIconAlert />
                    <Row type="flex">
                        <Col span={24}>
                            <Form
                                style={{
                                    width: 'auto',
                                }}
                            >
                                <DestinationAddressSingle
                                    loadWithCameraOpen={scannerSupported}
                                    validateStatus={
                                        sendTokenAddressError ? 'error' : ''
                                    }
                                    help={
                                        sendTokenAddressError
                                            ? sendTokenAddressError
                                            : ''
                                    }
                                    onScan={result =>
                                        handleTokenAddressChange({
                                            target: {
                                                name: 'address',
                                                value: result,
                                            },
                                        })
                                    }
                                    inputProps={{
                                        placeholder: `${currency.tokenTicker} Address`,
                                        name: 'address',
                                        onChange: e =>
                                            handleTokenAddressChange(e),
                                        required: true,
                                        value: formData.address,
                                    }}
                                />
                                <DestinationAmount
                                    validateStatus={
                                        sendTokenAmountError ? 'error' : ''
                                    }
                                    help={
                                        sendTokenAmountError
                                            ? sendTokenAmountError
                                            : ''
                                    }
                                    onMax={onMax}
                                    inputProps={{
                                        name: 'value',
                                        step: 1 / 10 ** token.info.decimals,
                                        placeholder: 'Amount',
                                        prefix:
                                            currency.tokenIconsUrl !== '' ? (
                                                <Img
                                                    src={`${currency.tokenIconsUrl}/32/${tokenId}.png`}
                                                    width={16}
                                                    height={16}
                                                    unloader={
                                                        <img
                                                            alt={`identicon of tokenId ${tokenId} `}
                                                            heigh="16"
                                                            width="16"
                                                            style={{
                                                                borderRadius:
                                                                    '50%',
                                                            }}
                                                            key={`identicon-${tokenId}`}
                                                            src={makeBlockie(
                                                                tokenId,
                                                            )}
                                                        />
                                                    }
                                                />
                                            ) : (
                                                <img
                                                    alt={`identicon of tokenId ${tokenId} `}
                                                    heigh="16"
                                                    width="16"
                                                    style={{
                                                        borderRadius: '50%',
                                                    }}
                                                    key={`identicon-${tokenId}`}
                                                    src={makeBlockie(tokenId)}
                                                />
                                            ),
                                        suffix: token.info.tokenTicker,
                                        onChange: e => handleSlpAmountChange(e),
                                        required: true,
                                        value: formData.value,
                                    }}
                                />
                                <div
                                    style={{
                                        paddingTop: '12px',
                                    }}
                                >
                                    {apiError ||
                                    sendTokenAmountError ||
                                    sendTokenAddressError ? (
                                        <>
                                            <SecondaryButton>
                                                Send {token.info.tokenName}
                                            </SecondaryButton>
                                        </>
                                    ) : (
                                        <PrimaryButton onClick={() => submit()}>
                                            Send {token.info.tokenName}
                                        </PrimaryButton>
                                    )}
                                </div>

                                {queryStringText && (
                                    <Alert
                                        message={`You are sending a transaction to an address including query parameters "${queryStringText}." Token transactions do not support query parameters and they will be ignored.`}
                                        type="warning"
                                    />
                                )}
                                {apiError && <ApiError />}
                            </Form>
                            {tokenStats !== null && (
                                <Descriptions
                                    column={1}
                                    bordered
                                    title={`Token info for "${token.info.tokenName}"`}
                                >
                                    <Descriptions.Item label="Decimals">
                                        {token.info.decimals}
                                    </Descriptions.Item>
                                    <Descriptions.Item label="Token ID">
                                        {token.tokenId}
                                    </Descriptions.Item>
                                    {tokenStats && (
                                        <>
                                            <Descriptions.Item label="Document URI">
                                                {tokenStats.documentUri}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Genesis Date">
                                                {tokenStats.timestampUnix !==
                                                null
                                                    ? new Date(
                                                          tokenStats.timestampUnix *
                                                              1000,
                                                      ).toLocaleDateString()
                                                    : 'Just now (Genesis tx confirming)'}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Fixed Supply?">
                                                {tokenStats.containsBaton
                                                    ? 'No'
                                                    : 'Yes'}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Initial Quantity">
                                                {tokenStats.initialTokenQty.toLocaleString()}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Total Burned">
                                                {tokenStats.totalBurned.toLocaleString()}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Total Minted">
                                                {tokenStats.totalMinted.toLocaleString()}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Circulating Supply">
                                                {tokenStats.circulatingSupply.toLocaleString()}
                                            </Descriptions.Item>
                                        </>
                                    )}
                                </Descriptions>
                            )}
                        </Col>
                    </Row>
                </>
            )}
        </>
    );
};

/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in SendToken.test.js

status => {console.log(status)} is an arbitrary stub function
*/

SendToken.defaultProps = {
    passLoadingStatus: status => {
        console.log(status);
    },
};

SendToken.propTypes = {
    tokenId: PropTypes.string,
    jestBCH: PropTypes.object,
    passLoadingStatus: PropTypes.func,
};

export default SendToken;
<commit_msg>[Cashtab] Added eToken icon to send page<commit_after>import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { WalletContext } from '@utils/context';
import { Form, message, Row, Col, Alert, Descriptions, Popover } from 'antd';
import TokenIconAlert from '@components/Common/Alerts.js';
import PrimaryButton, {
    SecondaryButton,
} from '@components/Common/PrimaryButton';
import {
    DestinationAmount,
    DestinationAddressSingle,
} from '@components/Common/EnhancedInputs';
import useBCH from '@hooks/useBCH';
import BalanceHeader from '@components/Common/BalanceHeader';
import { Redirect } from 'react-router-dom';
import useWindowDimensions from '@hooks/useWindowDimensions';
import { isMobile, isIOS, isSafari } from 'react-device-detect';
import { Img } from 'react-image';
import makeBlockie from 'ethereum-blockies-base64';
import BigNumber from 'bignumber.js';
import {
    currency,
    parseAddress,
    isValidTokenPrefix,
} from '@components/Common/Ticker.js';
import { Event } from '@utils/GoogleAnalytics';
import {
    getWalletState,
    convertEtokenToSimpleledger,
} from '@utils/cashMethods';
import ApiError from '@components/Common/ApiError';
import {
    sendTokenNotification,
    errorNotification,
} from '@components/Common/Notifications';

const SendToken = ({ tokenId, jestBCH, passLoadingStatus }) => {
    const { wallet, apiError } = React.useContext(WalletContext);
    const walletState = getWalletState(wallet);
    const { tokens, slpBalancesAndUtxos } = walletState;
    const token = tokens.find(token => token.tokenId === tokenId);

    const [tokenStats, setTokenStats] = useState(null);
    const [queryStringText, setQueryStringText] = useState(null);
    const [sendTokenAddressError, setSendTokenAddressError] = useState(false);
    const [sendTokenAmountError, setSendTokenAmountError] = useState(false);
    // Get device window width
    // If this is less than 769, the page will open with QR scanner open
    const { width } = useWindowDimensions();
    // Load with QR code open if device is mobile and NOT iOS + anything but safari
    const scannerSupported = width < 769 && isMobile && !(isIOS && !isSafari);

    const [formData, setFormData] = useState({
        dirty: true,
        value: '',
        address: '',
    });

    const { getBCH, getRestUrl, sendToken, getTokenStats } = useBCH();

    // jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
    const BCH = jestBCH ? jestBCH : getBCH();

    // Fetch token stats if you do not have them and API did not return an error
    if (tokenStats === null) {
        getTokenStats(BCH, tokenId).then(
            result => {
                setTokenStats(result);
            },
            err => {
                console.log(`Error getting token stats: ${err}`);
            },
        );
    }

    async function submit() {
        setFormData({
            ...formData,
            dirty: false,
        });

        if (
            !formData.address ||
            !formData.value ||
            Number(formData.value <= 0) ||
            sendTokenAmountError
        ) {
            return;
        }

        // Event("Category", "Action", "Label")
        // Track number of SLPA send transactions and
        // SLPA token IDs
        Event('SendToken.js', 'Send', tokenId);

        passLoadingStatus(true);
        const { address, value } = formData;

        // Clear params from address
        let cleanAddress = address.split('?')[0];

        // Convert to simpleledger prefix if etoken
        cleanAddress = convertEtokenToSimpleledger(cleanAddress);

        try {
            const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, {
                tokenId: tokenId,
                tokenReceiverAddress: cleanAddress,
                amount: value,
            });

            sendTokenNotification(link);
        } catch (e) {
            passLoadingStatus(false);
            let message;

            if (!e.error && !e.message) {
                message = `Transaction failed: no response from ${getRestUrl()}.`;
            } else if (
                /Could not communicate with full node or other external service/.test(
                    e.error,
                )
            ) {
                message = 'Could not communicate with API. Please try again.';
            } else {
                message = e.message || e.error || JSON.stringify(e);
            }
            errorNotification(e, message, 'Sending eToken');
        }
    }

    const handleSlpAmountChange = e => {
        let error = false;
        const { value, name } = e.target;

        // test if exceeds balance using BigNumber
        let isGreaterThanBalance = false;
        if (!isNaN(value)) {
            const bigValue = new BigNumber(value);
            // Returns 1 if greater, -1 if less, 0 if the same, null if n/a
            isGreaterThanBalance = bigValue.comparedTo(token.balance);
        }

        // Validate value for > 0
        if (isNaN(value)) {
            error = 'Amount must be a number';
        } else if (value <= 0) {
            error = 'Amount must be greater than 0';
        } else if (token && token.balance && isGreaterThanBalance === 1) {
            error = `Amount cannot exceed your ${token.info.tokenTicker} balance of ${token.balance}`;
        } else if (!isNaN(value) && value.toString().includes('.')) {
            if (value.toString().split('.')[1].length > token.info.decimals) {
                error = `This token only supports ${token.info.decimals} decimal places`;
            }
        }
        setSendTokenAmountError(error);
        setFormData(p => ({
            ...p,
            [name]: value,
        }));
    };

    const handleTokenAddressChange = e => {
        const { value, name } = e.target;
        // validate for token address
        // validate for parameters
        // show warning that query strings are not supported

        let error = false;
        let addressString = value;

        const addressInfo = parseAddress(BCH, addressString, true);
        /*
        Model

        addressInfo =
        {
            address: '',
            isValid: false,
            queryString: '',
            amount: null,
        };
        */

        const { address, isValid, queryString } = addressInfo;

        // If query string,
        // Show an alert that only amount and currency.ticker are supported
        setQueryStringText(queryString);

        // Is this valid address?
        if (!isValid) {
            error = 'Address is not a valid etoken: address';
            // If valid address but token format
        } else if (!isValidTokenPrefix(address)) {
            error = `Cashtab only supports sending to ${currency.tokenPrefixes[0]} prefixed addresses`;
        }
        setSendTokenAddressError(error);

        setFormData(p => ({
            ...p,
            [name]: value,
        }));
    };

    const onMax = async () => {
        // Clear this error before updating field
        setSendTokenAmountError(false);
        try {
            let value = token.balance;

            setFormData({
                ...formData,
                value,
            });
        } catch (err) {
            console.log(`Error in onMax:`);
            console.log(err);
            message.error(
                'Unable to calculate the max value due to network errors',
            );
        }
    };

    useEffect(() => {
        // If the balance has changed, unlock the UI
        // This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked

        passLoadingStatus(false);
    }, [token]);

    return (
        <>
            {!token && <Redirect to="/" />}

            {token && (
                <>
                    <BalanceHeader
                        balance={token.balance}
                        ticker={token.info.tokenTicker}
                    />
                    <TokenIconAlert />
                    <Row type="flex">
                        <Col span={24}>
                            <Form
                                style={{
                                    width: 'auto',
                                }}
                            >
                                <DestinationAddressSingle
                                    loadWithCameraOpen={scannerSupported}
                                    validateStatus={
                                        sendTokenAddressError ? 'error' : ''
                                    }
                                    help={
                                        sendTokenAddressError
                                            ? sendTokenAddressError
                                            : ''
                                    }
                                    onScan={result =>
                                        handleTokenAddressChange({
                                            target: {
                                                name: 'address',
                                                value: result,
                                            },
                                        })
                                    }
                                    inputProps={{
                                        placeholder: `${currency.tokenTicker} Address`,
                                        name: 'address',
                                        onChange: e =>
                                            handleTokenAddressChange(e),
                                        required: true,
                                        value: formData.address,
                                    }}
                                />
                                <DestinationAmount
                                    validateStatus={
                                        sendTokenAmountError ? 'error' : ''
                                    }
                                    help={
                                        sendTokenAmountError
                                            ? sendTokenAmountError
                                            : ''
                                    }
                                    onMax={onMax}
                                    inputProps={{
                                        name: 'value',
                                        step: 1 / 10 ** token.info.decimals,
                                        placeholder: 'Amount',
                                        prefix:
                                            currency.tokenIconsUrl !== '' ? (
                                                <Img
                                                    src={`${currency.tokenIconsUrl}/32/${tokenId}.png`}
                                                    width={16}
                                                    height={16}
                                                    unloader={
                                                        <img
                                                            alt={`identicon of tokenId ${tokenId} `}
                                                            heigh="16"
                                                            width="16"
                                                            style={{
                                                                borderRadius:
                                                                    '50%',
                                                            }}
                                                            key={`identicon-${tokenId}`}
                                                            src={makeBlockie(
                                                                tokenId,
                                                            )}
                                                        />
                                                    }
                                                />
                                            ) : (
                                                <img
                                                    alt={`identicon of tokenId ${tokenId} `}
                                                    heigh="16"
                                                    width="16"
                                                    style={{
                                                        borderRadius: '50%',
                                                    }}
                                                    key={`identicon-${tokenId}`}
                                                    src={makeBlockie(tokenId)}
                                                />
                                            ),
                                        suffix: token.info.tokenTicker,
                                        onChange: e => handleSlpAmountChange(e),
                                        required: true,
                                        value: formData.value,
                                    }}
                                />
                                <div
                                    style={{
                                        paddingTop: '12px',
                                    }}
                                >
                                    {apiError ||
                                    sendTokenAmountError ||
                                    sendTokenAddressError ? (
                                        <>
                                            <SecondaryButton>
                                                Send {token.info.tokenName}
                                            </SecondaryButton>
                                        </>
                                    ) : (
                                        <PrimaryButton onClick={() => submit()}>
                                            Send {token.info.tokenName}
                                        </PrimaryButton>
                                    )}
                                </div>

                                {queryStringText && (
                                    <Alert
                                        message={`You are sending a transaction to an address including query parameters "${queryStringText}." Token transactions do not support query parameters and they will be ignored.`}
                                        type="warning"
                                    />
                                )}
                                {apiError && <ApiError />}
                            </Form>
                            {tokenStats !== null && (
                                <Descriptions
                                    column={1}
                                    bordered
                                    title={`Token info for "${token.info.tokenName}"`}
                                >
                                    <Descriptions.Item label="Icon">
                                        {currency.tokenIconsUrl !== '' ? (
                                            <Popover
                                                style={{ width: 256 }}
                                                content={
                                                    <Img
                                                        src={`${currency.tokenIconsUrl}/256/${tokenId}.png`}
                                                    />
                                                }
                                                trigger="click"
                                                color="transparent"
                                            >
                                                <Img
                                                    src={`${currency.tokenIconsUrl}/32/${tokenId}.png`}
                                                    width={32}
                                                    height={32}
                                                    unloader={
                                                        <img
                                                            alt={`identicon of tokenId ${tokenId} `}
                                                            height="32"
                                                            width="32"
                                                            style={{
                                                                borderRadius:
                                                                    '50%',
                                                            }}
                                                            key={`identicon-${tokenId}`}
                                                            src={makeBlockie(
                                                                tokenId,
                                                            )}
                                                        />
                                                    }
                                                />
                                            </Popover>
                                        ) : (
                                            <img
                                                alt={`identicon of tokenId ${tokenId} `}
                                                height="32"
                                                width="32"
                                                style={{
                                                    borderRadius: '50%',
                                                }}
                                                key={`identicon-${tokenId}`}
                                                src={makeBlockie(tokenId)}
                                            />
                                        )}
                                    </Descriptions.Item>
                                    <Descriptions.Item label="Decimals">
                                        {token.info.decimals}
                                    </Descriptions.Item>
                                    <Descriptions.Item label="Token ID">
                                        {token.tokenId}
                                    </Descriptions.Item>
                                    {tokenStats && (
                                        <>
                                            <Descriptions.Item label="Document URI">
                                                {tokenStats.documentUri}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Genesis Date">
                                                {tokenStats.timestampUnix !==
                                                null
                                                    ? new Date(
                                                          tokenStats.timestampUnix *
                                                              1000,
                                                      ).toLocaleDateString()
                                                    : 'Just now (Genesis tx confirming)'}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Fixed Supply?">
                                                {tokenStats.containsBaton
                                                    ? 'No'
                                                    : 'Yes'}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Initial Quantity">
                                                {tokenStats.initialTokenQty.toLocaleString()}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Total Burned">
                                                {tokenStats.totalBurned.toLocaleString()}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Total Minted">
                                                {tokenStats.totalMinted.toLocaleString()}
                                            </Descriptions.Item>
                                            <Descriptions.Item label="Circulating Supply">
                                                {tokenStats.circulatingSupply.toLocaleString()}
                                            </Descriptions.Item>
                                        </>
                                    )}
                                </Descriptions>
                            )}
                        </Col>
                    </Row>
                </>
            )}
        </>
    );
};

/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in SendToken.test.js

status => {console.log(status)} is an arbitrary stub function
*/

SendToken.defaultProps = {
    passLoadingStatus: status => {
        console.log(status);
    },
};

SendToken.propTypes = {
    tokenId: PropTypes.string,
    jestBCH: PropTypes.object,
    passLoadingStatus: PropTypes.func,
};

export default SendToken;
<|endoftext|>


<commit_before>/*
 * client.js: Base client from which all pkgcloud clients inherit from
 *
 * (C) 2013 Nodejitsu Inc.
 *
 */

var fs = require('fs'),
    events = require('eventemitter2'),
    request = require('request'),
    utile = require('utile'),
    qs = require('qs'),
    common = require('../../common'),
    pkgcloud = require('../../../pkgcloud'),
    errs = require('errs');

/**
 * Client
 *
 * @description base Client from which all pkgcloud clients inherit
 *
 * @param {object}    options   options are stored as client.config
 * @type {Function}
 */
var Client = exports.Client = function (options) {
  events.EventEmitter2.call(this, { delimiter: '::', wildcard: true });
  this.config = options || {};
};

utile.inherits(Client, events.EventEmitter2);

/**
 * Client.request
 *
 * @description is the global request handler for a pkgcloud client request.
 *
 * @param {object}          options     options for this client request
 * @param {Function}        callback    the callback for the client request
 */
Client.prototype.request = function (options, callback) {
  return this._doRequest(options, callback);
};

/**
 * Client._doRequest
 *
 * @description Private function that handles the prepared payload and then
 * spawns the request. Some clients can override this function, for example
 * rackspace and openstack providers implement an inline authentication.
 *
 * @param {object}    options   the prepared options from Client.request
 * @param {Function}  callback  the callback for the request
 * @returns {*}
 * @private
 */
Client.prototype._doRequest = function (options, callback) {
  var self = this;

  var requestOptions = {};

  requestOptions.method = options.method || 'GET';
  requestOptions.headers = options.headers || {};
  requestOptions.path = options.path

  if (options.qs) {
    requestOptions.qs = options.qs;
  }

  if (options.body) {
    requestOptions.body = options.body;
  }

  if (options.container) {
    requestOptions.signingUrl = '/' + options.container + '/';

    if (options.path) {
      requestOptions.signingUrl += options.path;
    }

    if (options.qs) {
      requestOptions.signingUrl += '?' + qs.stringify(options.qs);
    }
  }

  function sendRequest(opts) {

    //
    // Setup any specific request options before
    // making the request
    //
    if (self.before) {
      var errors = false;
      for (var i = 0; i < self.before.length; i++) {
        var fn = self.before[i];
        try {
          opts = fn.call(self, opts) || opts;
          // on errors do error handling, break.
        } catch (exc) {
          errs.handle(exc, callback);
          errors = true;
          break;
        }
      }
      if (errors) {
        return;
      }
    }

    opts.uri = options.uri || self.getUrl(options);

    // Clean up our polluted options
    //
    // TODO refactor the options used in Before methods
    // to not require polluting request options
    //
    delete opts.path;
    delete opts.signingUrl;

    // Set our User Agent
    opts.headers['User-Agent'] = utile.format('nodejs-pkgcloud/%s', pkgcloud.version);

    // If we are missing callback
    if (!callback) {
      try {
        self.emit('log::debug', 'Sending (non-callback) client request', opts);
        return request(opts);
      } // if request throws still return an EE
      catch (exc1) {
        self.emit('log::error', 'Unable to create (non-callback) request', opts);
        return errs.handle(exc1);
      }
    } else {
      try {
        self.emit('log::debug', 'Sending client request', opts);
        return request(opts, self.defaultRequestHandler(callback));
      } catch (exc2) {
        self.emit('log::error', 'Unable to create request', opts);
        return errs.handle(exc2, callback);
      }
    }
  }

  return sendRequest(requestOptions);
};

Client.prototype.defaultRequestHandler = function (callback) {

  var self = this;

  return function (err, res, body) {
    if (err) {
      return callback(err);
    }

    var statusCode = res.statusCode.toString(),
        err2;

    if (Object.keys(self.failCodes).indexOf(statusCode) !== -1) {
      //
      // TODO: Support more than JSON errors here
      //
      err2 = {
        provider: self.provider,
        failCode: self.failCodes[statusCode],
        statusCode: res.statusCode,
        message: self.provider + ' Error (' +
          statusCode + '): ' + self.failCodes[statusCode],
        href: res.request.uri.href,
        method: res.request.method
      };

      try {
        err2.result = typeof body === 'string' ? JSON.parse(body) : body;
      } catch (e) {
        err2.result = { err: body };
      }

      self.emit('log::error', 'Error during provider response', err2);
      return callback(errs.create(err2));
    }

    self.emit('log::debug', 'Provider Response', {
      href: res.request.uri.href,
      method: res.request.method,
      statusCode: res.statusCode
    });

    callback(err, body, res);
  }
};
<commit_msg>[minor] Adding headers into debug logs<commit_after>/*
 * client.js: Base client from which all pkgcloud clients inherit from
 *
 * (C) 2013 Nodejitsu Inc.
 *
 */

var fs = require('fs'),
    events = require('eventemitter2'),
    request = require('request'),
    utile = require('utile'),
    qs = require('qs'),
    common = require('../../common'),
    pkgcloud = require('../../../pkgcloud'),
    errs = require('errs');

/**
 * Client
 *
 * @description base Client from which all pkgcloud clients inherit
 *
 * @param {object}    options   options are stored as client.config
 * @type {Function}
 */
var Client = exports.Client = function (options) {
  events.EventEmitter2.call(this, { delimiter: '::', wildcard: true });
  this.config = options || {};
};

utile.inherits(Client, events.EventEmitter2);

/**
 * Client.request
 *
 * @description is the global request handler for a pkgcloud client request.
 *
 * @param {object}          options     options for this client request
 * @param {Function}        callback    the callback for the client request
 */
Client.prototype.request = function (options, callback) {
  return this._doRequest(options, callback);
};

/**
 * Client._doRequest
 *
 * @description Private function that handles the prepared payload and then
 * spawns the request. Some clients can override this function, for example
 * rackspace and openstack providers implement an inline authentication.
 *
 * @param {object}    options   the prepared options from Client.request
 * @param {Function}  callback  the callback for the request
 * @returns {*}
 * @private
 */
Client.prototype._doRequest = function (options, callback) {
  var self = this;

  var requestOptions = {};

  requestOptions.method = options.method || 'GET';
  requestOptions.headers = options.headers || {};
  requestOptions.path = options.path

  if (options.qs) {
    requestOptions.qs = options.qs;
  }

  if (options.body) {
    requestOptions.body = options.body;
  }

  if (options.container) {
    requestOptions.signingUrl = '/' + options.container + '/';

    if (options.path) {
      requestOptions.signingUrl += options.path;
    }

    if (options.qs) {
      requestOptions.signingUrl += '?' + qs.stringify(options.qs);
    }
  }

  function sendRequest(opts) {

    //
    // Setup any specific request options before
    // making the request
    //
    if (self.before) {
      var errors = false;
      for (var i = 0; i < self.before.length; i++) {
        var fn = self.before[i];
        try {
          opts = fn.call(self, opts) || opts;
          // on errors do error handling, break.
        } catch (exc) {
          errs.handle(exc, callback);
          errors = true;
          break;
        }
      }
      if (errors) {
        return;
      }
    }

    opts.uri = options.uri || self.getUrl(options);

    // Clean up our polluted options
    //
    // TODO refactor the options used in Before methods
    // to not require polluting request options
    //
    delete opts.path;
    delete opts.signingUrl;

    // Set our User Agent
    opts.headers['User-Agent'] = utile.format('nodejs-pkgcloud/%s', pkgcloud.version);

    // If we are missing callback
    if (!callback) {
      try {
        self.emit('log::debug', 'Sending (non-callback) client request', opts);
        return request(opts);
      } // if request throws still return an EE
      catch (exc1) {
        self.emit('log::error', 'Unable to create (non-callback) request', opts);
        return errs.handle(exc1);
      }
    } else {
      try {
        self.emit('log::debug', 'Sending client request', opts);
        return request(opts, self.defaultRequestHandler(callback));
      } catch (exc2) {
        self.emit('log::error', 'Unable to create request', opts);
        return errs.handle(exc2, callback);
      }
    }
  }

  return sendRequest(requestOptions);
};

Client.prototype.defaultRequestHandler = function (callback) {

  var self = this;

  return function (err, res, body) {
    if (err) {
      return callback(err);
    }

    var statusCode = res.statusCode.toString(),
        err2;

    if (Object.keys(self.failCodes).indexOf(statusCode) !== -1) {
      //
      // TODO: Support more than JSON errors here
      //
      err2 = {
        provider: self.provider,
        failCode: self.failCodes[statusCode],
        statusCode: res.statusCode,
        message: self.provider + ' Error (' +
          statusCode + '): ' + self.failCodes[statusCode],
        href: res.request.uri.href,
        method: res.request.method,
        headers: res.headers
      };

      try {
        err2.result = typeof body === 'string' ? JSON.parse(body) : body;
      } catch (e) {
        err2.result = { err: body };
      }

      self.emit('log::error', 'Error during provider response', err2);
      return callback(errs.create(err2));
    }

    self.emit('log::debug', 'Provider Response', {
      href: res.request.uri.href,
      method: res.request.method,
      headers: res.headers,
      statusCode: res.statusCode
    });

    callback(err, body, res);
  }
};
<|endoftext|>


<commit_before>'use strict';
var shellEscape = require('shell-escape');
var crypto = require('crypto');

var constants = require('./constants');

var LAMPApp = require('./LAMPApp');

class Drupal extends LAMPApp {

  /**
   * Options (used by this task in addition to the LAMPApp options):
   *   @param {object} container - An instantiated and configured Container object.
   *   @param {object} options - A hash of configuration options specific to this task.
   *   @param {boolean} options.clearCaches - Whether to clear all caches after the build is finished. Defaults to true.
   *   @param {string} options.siteFolder - The site folder to use for this build (the folder within the drupal `sites` folder).  Defaults to `default`.
   *   @param {string} options.database - The name of the database to import if specified. Note that this database *must be added to the assets array separately*.
   *   @param {boolean} options.databaseGzipped - Whether the database was sent gzipped and whether it should therefore be gunzipped before importing.
   *   @param {boolean} options.databaseBzipped - Whether the database was sent bzipped and whether it should therefore be bunzipped before importing.
   *   @param {boolean} options.databasePrefix - A prefix for the database.
   *   @param {boolean} options.databaseUpdates - Determines whether to run `drush updb`.
   *   @param {boolean} options.revertFeatures - Whether to revert features using `drush fra`.
   *   @param {integer} options.drupalVersion - The version of Drupal being used. If set, alters the behavior of certain options, such as clearCaches.
   *   @param {string} options.makeFile - The name of the make file to run to generate the install directory.
   *   @param {string} options.makeArgs - A set of params to concat onto the drush `make` command (defaults to '').
   *   @param {boolean} options.runInstall - If set, run `drush site-install` to perform a fresh install of the site using the profileName as the profile to install and allowing instlallArgs to configure the install.
   *   @param {string} options.profileName - The profileName, used in symlinking this directory if makeFile is specified and used to select the profile to install if `runInstall` is selected.
   *   @param {string} options.installArgs - A set of params to concat onto the drush `site-install` command (defaults to '').
   *   @param {string} options.subDirectory - The directory of the actual web root (defaults to 'docroot').
   *   @param {string} options.configSyncDirectory - The config sync directory used in Drupal 8.
   *   @param {string} [options.settingsAppend] - A snippet to append to the end of the settings.php file.
   *   @param {string} [options.settingsRequireFile] - A file to require at the end of settings.php (in order to get around not
   *   @param {string} [options.fileProxy] - If a file doesn't exist in the repository, the file can be retrieved from the production instance of the site. Enter the protocol and fully qualified domain here, as in "https://example.com".
   *      checking settings.php into your repo).
   *   @augments LAMPApp
   */
  constructor(container, options) {
    super(container, options);

    this.databaseName = constants.DRUPAL_DATABASE_NAME;
    this.options.databasePrefix = this.options.databasePrefix || '';
    this.options.siteFolder = options.siteFolder || 'default';
    this.options.profileName = options.profileName || 'standard';
    // clearCaches must be set to explicitly false
    this.options.clearCaches = (options.clearCaches || typeof options.clearCaches === 'undefined');
    this.options.drupalVersion = options.drupalVersion || constants.DEFAULT_DRUPAL_VERSION;

    // TODO: Add some kind of validation.
    // Filter out secret strings
    options.secrets = [
    ];

    // Allow for subdirectory to be explicitly set to "" without being overwritten for being falsy.
    this.subDirectory = options.subDirectory || 'docroot';
    this.script = [];
    if (this.drupalVersionSupported()) {
      this.populateScriptArray();
    }
    else {
      // I'd like to throw an error instead of adding this message but then the
      // build hands with no feedback.
      this.addScriptUnsupportedDrupalVersion();
    }
    this.setScript(this.script);
  }

  description() {
    return `${this.plugin} 'Provisioning Drupal!'`;
  }

  /**
   *
   */
  populateScriptArray() {
    this.addScriptSetup();
    if (this.options.makeFile) {
      this.addScriptRunMakeFile();
    }
    else {
      this.addScriptSymlinks();
    }
    this.addScriptCreateDatbase();
    this.addScriptAppendSettingsPHPSettings();
    this.addScriptPublicFilesDirectory();
    if (this.options.database) {
      this.addScriptImportDatabase();
    }
    if (this.options.drupalVersion >= 8 && this.options.database) {
      this.addScriptClearCaches();
    }
    if (this.options.runInstall) {
      this.addScriptRunInstall();
    }
    if (this.options.databaseUpdates) {
      this.addScriptDatabaseUpdates();
    }
    if (this.options.revertFeatures) {
      this.addScriptRevertFeatures();
    }
    if (this.options.fileProxy) {
      this.addScriptFileProxy();
    }
    if (this.options.clearCaches) {
      this.addScriptClearCaches();
    }

    this.addScriptApachePhp();
  }

  addScriptAppendSettingsPHPSettings() {
    if (this.options.drupalVersion === 7) {
      this.addD7PHPSettings();
    }
    else if (this.options.drupalVersion === 8) {
      this.addD8PHPSettings();
    }
    this.appendCustomSettings();
  }

  addD7PHPSettings() {
    this.script = this.script.concat([
      `PHP_SNIPPET=$(cat <<END_HEREDOC`,
      `\\$databases = array(`,
      `  'default' => array(`,
      `    'default' => array(`,
      `      'database' => '${constants.DRUPAL_DATABASE_NAME}',`,
      `      'username' => '${constants.DATABASE_USER}',`,
      `      'password' => '${constants.DATABASE_PASSWORD}',`,
      `      'prefix' => '${this.options.databasePrefix}',`,
      `      'host' => 'localhost',`,
      `      'driver' => 'mysql',`,
      `    ),`,
      `  ),`,
      `);`,
      `END_HEREDOC`,
      `)`,
      `if [ ! -e "/var/www/html/sites/${this.options.siteFolder}/settings.php" ] ; then`,
      `  echo '<?php\n' > /var/www/html/sites/${this.options.siteFolder}/settings.php`,
      `fi`,
      `echo "$PHP_SNIPPET" >> /var/www/html/sites/${this.options.siteFolder}/settings.php`,
    ]);
  }

  addD8PHPSettings() {
    const hash = crypto.createHash('sha256');
    hash.update(crypto.randomBytes(40));
    const random = hash.digest('base64').toString().replace(/[^a-zA-Z0-9]/ig, '');
    const configSyncDirectory = this.options.configSyncDirectory || `sites/default/files/config_${random}/sync`;
    this.script = this.script.concat([
      `PHP_SNIPPET=$(cat <<END_HEREDOC`,
      `\\$databases = array(`,
      `  'default' => array(`,
      `    'default' => array(`,
      `      'database' => '${constants.DRUPAL_DATABASE_NAME}',`,
      `      'username' => '${constants.DATABASE_USER}',`,
      `      'password' => '${constants.DATABASE_PASSWORD}',`,
      `      'prefix' => '${this.options.databasePrefix}',`,
      `      'host' => 'localhost',`,
      `      'driver' => 'mysql',`,
      `      'port' => '3306',`,
      // This is pretty gross. We should find a better way to handle this.
      `      'namespace' => 'Drupal\\\\\\\\Core\\\\\\\\Database\\\\\\\\Driver\\\\\\\\mysql',`,
      `      'driver' => 'mysql',`,
      `    ),`,
      `  ),`,
      `);`,
      `\\$settings['hash_salt'] = '${random}';`,
      `\\$config_directories['sync'] = '${configSyncDirectory}';`,
      `END_HEREDOC`,
      `)`,
      `if [ ! -e "/var/www/html/sites/${this.options.siteFolder}/settings.php" ] ; then`,
      `  echo '<?php\n' > /var/www/html/sites/${this.options.siteFolder}/settings.php`,
      `fi`,
      `echo "$PHP_SNIPPET" >> /var/www/html/sites/${this.options.siteFolder}/settings.php`,
    ]);
  }

  appendCustomSettings() {
    if (this.options.settingsRequireFile) {
      let command = 'echo "require_once(\'' + this.options.settingsRequireFile + '\');" >> /var/www/html/sites/' + this.options.siteFolder + '/settings.php';
      this.script.push(command);
    }
    if (this.options.settingsAppend) {
      let command = 'echo ' + shellEscape([this.options.settingsAppend]) + ' >> /var/www/html/sites/' + this.options.siteFolder + '/settings.php';
      this.script.push(command);
    }
  }

  addScriptPublicFilesDirectory() {
    this.script = this.script.concat([
      'mkdir -p /var/www/html/sites/' + this.options.siteFolder + '/files',
      'chown www-data:www-data -R /var/www/html/sites/' + this.options.siteFolder + '/files',
    ]);
  }

  addScriptRunInstall() {
    var installArgs = this.options.installArgs || '';
    this.script.push(`drush site-install --root=/var/www/html ${this.options.profileName} ${installArgs}`);
  }

  addScriptDatabaseUpdates() {
    this.script.push('drush --root=/var/www/html updb');
  }

  addScriptRevertFeatures() {
    this.script.push('drush --root=/var/www/html fra');
  }

  addScriptRunMakeFile() {
    var makeArgs = this.options.makeArgs || '';
    this.script.push('cd $SRC_DIR ; drush make ' + this.options.makeFile + ' /var/www/html ' + makeArgs);
    this.script.push('rsync -a $SRC_DIR/ /var/www/html/profiles/' + this.options.profileName);
  }

  addScriptClearCaches() {
    if (this.options.drupalVersion >= 8) {
      this.script.push('drush --root=/var/www/html cache-rebuild');
    }
    else {
      this.script.push('drush --root=/var/www/html cache-clear all');
    }
  }

  addScriptFileProxy() {
    this.script = this.script.concat(
      `drush --root=/var/www/html en stage_file_proxy -y`,
      `drush --root=/var/www/html vset stage_file_proxy_hotlink 1`,
      `drush --root=/var/www/html vset stage_file_proxy_origin '${this.options.fileProxy}'`
    );
  }

  drupalVersionSupported() {
    const v = this.options.drupalVersion;
    return (v <= constants.MAX_DRUPAL_VERSION && v >= constants.MIN_DRUPAL_VERSION);
  }

  addScriptUnsupportedDrupalVersion() {
    this.script.push('echo "Build stopped: Unsupported Drupal version."');
    this.script.push('exit 1');
  }

}

module.exports = Drupal;
<commit_msg>Making changes requested by dzink.<commit_after>'use strict';
var shellEscape = require('shell-escape');
var crypto = require('crypto');

var constants = require('./constants');

var LAMPApp = require('./LAMPApp');

class Drupal extends LAMPApp {

  /**
   * Options (used by this task in addition to the LAMPApp options):
   *   @param {object} container - An instantiated and configured Container object.
   *   @param {object} options - A hash of configuration options specific to this task.
   *   @param {boolean} options.clearCaches - Whether to clear all caches after the build is finished. Defaults to true.
   *   @param {string} options.siteFolder - The site folder to use for this build (the folder within the drupal `sites` folder).  Defaults to `default`.
   *   @param {string} options.database - The name of the database to import if specified. Note that this database *must be added to the assets array separately*.
   *   @param {boolean} options.databaseGzipped - Whether the database was sent gzipped and whether it should therefore be gunzipped before importing.
   *   @param {boolean} options.databaseBzipped - Whether the database was sent bzipped and whether it should therefore be bunzipped before importing.
   *   @param {boolean} options.databasePrefix - A prefix for the database.
   *   @param {boolean} options.databaseUpdates - Determines whether to run `drush updb`.
   *   @param {boolean} options.revertFeatures - Whether to revert features using `drush fra`.
   *   @param {integer} options.drupalVersion - The version of Drupal being used. If set, alters the behavior of certain options, such as clearCaches.
   *   @param {string} options.makeFile - The name of the make file to run to generate the install directory.
   *   @param {boolean} options.makeForceComplete - Whether to use the `--force-complete` option for drush make.  Defaults to true.
   *   @param {array} [options.makeArgs] - An array of additional params to concat onto the drush `make` command.
   *   @param {boolean} options.runInstall - If set, run `drush site-install` to perform a fresh install of the site using the profileName as the profile to install and allowing instlallArgs to configure the install.
   *   @param {string} options.profileName - The profileName, used in symlinking this directory if makeFile is specified and used to select the profile to install if `runInstall` is selected.
   *   @param {string} options.installArgs - A set of params to concat onto the drush `site-install` command (defaults to '').
   *   @param {string} options.subDirectory - The directory of the actual web root (defaults to 'docroot').
   *   @param {string} options.configSyncDirectory - The config sync directory used in Drupal 8.
   *   @param {string} [options.settingsAppend] - A snippet to append to the end of the settings.php file.
   *   @param {string} [options.settingsRequireFile] - A file to require at the end of settings.php (in order to get around not
   *   @param {string} [options.fileProxy] - If a file doesn't exist in the repository, the file can be retrieved from the production instance of the site. Enter the protocol and fully qualified domain here, as in "https://example.com".
   *      checking settings.php into your repo).
   *   @augments LAMPApp
   */
  constructor(container, options) {
    super(container, options);

    this.databaseName = constants.DRUPAL_DATABASE_NAME;
    this.options.databasePrefix = this.options.databasePrefix || '';
    this.options.siteFolder = options.siteFolder || 'default';
    this.options.profileName = options.profileName || 'standard';
    // clearCaches must be set to explicitly false
    this.options.clearCaches = (options.clearCaches || typeof options.clearCaches === 'undefined');
    this.options.drupalVersion = options.drupalVersion || constants.DEFAULT_DRUPAL_VERSION;
    this.options.makeForceComplete = String(options.makeForceComplete) !== 'undefined' ? String(options.makeForceComplete).toLowerCase() === 'true' : true;

    // TODO: Add some kind of validation.
    // Filter out secret strings
    options.secrets = [
    ];

    // Allow for subdirectory to be explicitly set to "" without being overwritten for being falsy.
    this.subDirectory = options.subDirectory || 'docroot';
    this.script = [];
    if (this.drupalVersionSupported()) {
      this.populateScriptArray();
    }
    else {
      // I'd like to throw an error instead of adding this message but then the
      // build hands with no feedback.
      this.addScriptUnsupportedDrupalVersion();
    }
    this.setScript(this.script);
  }

  description() {
    return `${this.plugin} 'Provisioning Drupal!'`;
  }

  /**
   *
   */
  populateScriptArray() {
    this.addScriptSetup();
    if (this.options.makeFile) {
      this.addScriptRunMakeFile();
    }
    else {
      this.addScriptSymlinks();
    }
    this.addScriptCreateDatbase();
    this.addScriptAppendSettingsPHPSettings();
    this.addScriptPublicFilesDirectory();
    if (this.options.database) {
      this.addScriptImportDatabase();
    }
    if (this.options.drupalVersion >= 8 && this.options.database) {
      this.addScriptClearCaches();
    }
    if (this.options.runInstall) {
      this.addScriptRunInstall();
    }
    if (this.options.databaseUpdates) {
      this.addScriptDatabaseUpdates();
    }
    if (this.options.revertFeatures) {
      this.addScriptRevertFeatures();
    }
    if (this.options.fileProxy) {
      this.addScriptFileProxy();
    }
    if (this.options.clearCaches) {
      this.addScriptClearCaches();
    }

    this.addScriptApachePhp();
  }

  addScriptAppendSettingsPHPSettings() {
    if (this.options.drupalVersion === 7) {
      this.addD7PHPSettings();
    }
    else if (this.options.drupalVersion === 8) {
      this.addD8PHPSettings();
    }
    this.appendCustomSettings();
  }

  addD7PHPSettings() {
    this.script = this.script.concat([
      `PHP_SNIPPET=$(cat <<END_HEREDOC`,
      `\\$databases = array(`,
      `  'default' => array(`,
      `    'default' => array(`,
      `      'database' => '${constants.DRUPAL_DATABASE_NAME}',`,
      `      'username' => '${constants.DATABASE_USER}',`,
      `      'password' => '${constants.DATABASE_PASSWORD}',`,
      `      'prefix' => '${this.options.databasePrefix}',`,
      `      'host' => 'localhost',`,
      `      'driver' => 'mysql',`,
      `    ),`,
      `  ),`,
      `);`,
      `END_HEREDOC`,
      `)`,
      `if [ ! -e "/var/www/html/sites/${this.options.siteFolder}/settings.php" ] ; then`,
      `  echo '<?php\n' > /var/www/html/sites/${this.options.siteFolder}/settings.php`,
      `fi`,
      `echo "$PHP_SNIPPET" >> /var/www/html/sites/${this.options.siteFolder}/settings.php`,
    ]);
  }

  addD8PHPSettings() {
    const hash = crypto.createHash('sha256');
    hash.update(crypto.randomBytes(40));
    const random = hash.digest('base64').toString().replace(/[^a-zA-Z0-9]/ig, '');
    const configSyncDirectory = this.options.configSyncDirectory || `sites/default/files/config_${random}/sync`;
    this.script = this.script.concat([
      `PHP_SNIPPET=$(cat <<END_HEREDOC`,
      `\\$databases = array(`,
      `  'default' => array(`,
      `    'default' => array(`,
      `      'database' => '${constants.DRUPAL_DATABASE_NAME}',`,
      `      'username' => '${constants.DATABASE_USER}',`,
      `      'password' => '${constants.DATABASE_PASSWORD}',`,
      `      'prefix' => '${this.options.databasePrefix}',`,
      `      'host' => 'localhost',`,
      `      'driver' => 'mysql',`,
      `      'port' => '3306',`,
      // This is pretty gross. We should find a better way to handle this.
      `      'namespace' => 'Drupal\\\\\\\\Core\\\\\\\\Database\\\\\\\\Driver\\\\\\\\mysql',`,
      `      'driver' => 'mysql',`,
      `    ),`,
      `  ),`,
      `);`,
      `\\$settings['hash_salt'] = '${random}';`,
      `\\$config_directories['sync'] = '${configSyncDirectory}';`,
      `END_HEREDOC`,
      `)`,
      `if [ ! -e "/var/www/html/sites/${this.options.siteFolder}/settings.php" ] ; then`,
      `  echo '<?php\n' > /var/www/html/sites/${this.options.siteFolder}/settings.php`,
      `fi`,
      `echo "$PHP_SNIPPET" >> /var/www/html/sites/${this.options.siteFolder}/settings.php`,
    ]);
  }

  appendCustomSettings() {
    if (this.options.settingsRequireFile) {
      let command = 'echo "require_once(\'' + this.options.settingsRequireFile + '\');" >> /var/www/html/sites/' + this.options.siteFolder + '/settings.php';
      this.script.push(command);
    }
    if (this.options.settingsAppend) {
      let command = 'echo ' + shellEscape([this.options.settingsAppend]) + ' >> /var/www/html/sites/' + this.options.siteFolder + '/settings.php';
      this.script.push(command);
    }
  }

  addScriptPublicFilesDirectory() {
    this.script = this.script.concat([
      'mkdir -p /var/www/html/sites/' + this.options.siteFolder + '/files',
      'chown www-data:www-data -R /var/www/html/sites/' + this.options.siteFolder + '/files',
    ]);
  }

  addScriptRunInstall() {
    var installArgs = this.options.installArgs || '';
    this.script.push(`drush site-install --root=/var/www/html ${this.options.profileName} ${installArgs}`);
  }

  addScriptDatabaseUpdates() {
    this.script.push('drush --root=/var/www/html updb');
  }

  addScriptRevertFeatures() {
    this.script.push('drush --root=/var/www/html fra');
  }

  addScriptRunMakeFile() {
    var makeArgs = '';
    var makeArgsList = this.options.makeArgs || [];
    if (this.options.makeForceComplete) {
      makeArgsList.push('--force-complete');
    }

    if (!Array.isArray(makeArgsList) || !makeArgsList.length) {
      makeArgs = makeArgsList.join(' ');
    }

    this.script.push('cd $SRC_DIR ; drush make ' + this.options.makeFile + ' /var/www/html ' + makeArgs);
    this.script.push('rsync -a $SRC_DIR/ /var/www/html/profiles/' + this.options.profileName);
  }

  addScriptClearCaches() {
    if (this.options.drupalVersion >= 8) {
      this.script.push('drush --root=/var/www/html cache-rebuild');
    }
    else {
      this.script.push('drush --root=/var/www/html cache-clear all');
    }
  }

  addScriptFileProxy() {
    this.script = this.script.concat(
      `drush --root=/var/www/html en stage_file_proxy -y`,
      `drush --root=/var/www/html vset stage_file_proxy_hotlink 1`,
      `drush --root=/var/www/html vset stage_file_proxy_origin '${this.options.fileProxy}'`
    );
  }

  drupalVersionSupported() {
    const v = this.options.drupalVersion;
    return (v <= constants.MAX_DRUPAL_VERSION && v >= constants.MIN_DRUPAL_VERSION);
  }

  addScriptUnsupportedDrupalVersion() {
    this.script.push('echo "Build stopped: Unsupported Drupal version."');
    this.script.push('exit 1');
  }

}

module.exports = Drupal;
<|endoftext|>


<commit_before>(function(Browser) {

	Browser.Items = Browser.Items || {};
	Browser.Items.Views = Browser.Items || {};

	Browser.Items.Views.List = Backbone.View.extend({

		tagName : 'tr',
		className : 'list-media list-fancymedia',

		initialize: function () {

			this.el.id = this.model.id;

			//list item has a thumbnail item with same model as a subview
			this.thumbnailView = new Browser.Items.Views.Thumb({
				model:this.model,
				thumbnail_width:100,
				thumbnail_height:80,
				show_caption:false,
				fancybox:false,
				draggable:false
			});

		},

		render: function ( done )
		{
			var _this = this;

			var template;
			switch( this.model.get('media_type') )
			{
				case 'Image':
					template = this.getImageTemplate();
					break;
				case 'Document':
					template = this.getDefaultTemplate();
					break;
				case 'Website':
					template = this.getWebsiteTemplate();
					break;
				case 'Tweet':
					template = this.getTweetTemplate();
					break;
				case 'Text':
					template = this.getTestimonialTemplate();
					break;
				case 'Video':
					template = this.getDefaultTemplate();
					break;
				case 'Audio':
					template = this.getDefaultTemplate();
					break;
				case 'PDF':
					template = this.getDefaultTemplate();
					break;
				case 'Collection':
					$(this.el).removeClass('list-fancymedia');
					template = this.getCollectionTemplate();
					break;

				default:
					template = this.getDefaultTemplate();
			}

			var blanks = this.model.attributes;

			if (!_.isUndefined(this.model.get("media_date_created"))&&!_.isNull(this.model.get("media_date_created"))){
				blanks["media_date"] = new Date(this.model.get("media_date_created").replace(" ", "T"));
				blanks["media_date"]=blanks["media_date"].format("mmmm dS, yyyy
h:MM TT");
			} else {
				blanks["media_date"] = "n/a";
			}
			if (this.model.get("text") !== null){
				var excerpt = this.model.get("text").replace(/\r\n/gi, '
');
				blanks["text"] = this.linkifyTweet(excerpt);

			}
			if (this.model.get("description") === null){
				blanks["description"] = " ";
			}
			if (this.model.get("description") !== null && this.model.get("description").length > 255){
				blanks["description"] = this.model.get("description").substring(0,255) + "...";
			}
			if (this.model.get("title") === null || this.model.get("title") == "none" || this.model.get("title") === ""){
				blanks["title"] = "";
			}

			if (this.model.get("media_creator_username") === "rijs"){
				blanks["media_creator_username"] = this.model.get("media_creator_realname");
			}

			if (this.model.get("media_creator_realname") === null || this.model.get("media_creator_realname") === "" || this.model.get("media_creator_realname") == "Unknown" || this.model.get("media_creator_realname") == "unknown"){
				blanks["author"] = this.model.get("media_creator_username");
			} else {
				blanks["author"] = this.model.get("media_creator_realname");
			}
			if (this.model.get('attributes')) {
			    if (this.model.get('attributes').privacy) {
			        if (this.model.get('attributes').privacy == 'Hide') {
			            blanks["author"] = "Anonymous";
			        }
			    }
			}

			if (this.model.get("media_type") == "Text" && this.model.get('description').length < this.model.get('text').length){
				blanks["description"] = this.model.get('description') + '...';
			}

			if (this.model.get("media_type") == "Website"){
				var parts = this.model.get('attribution_uri').split('http');
				blanks["original_url"] = parts[2] !== undefined ? "http" + parts[2] : this.model.get('attribution_uri');
			}

			$(this.el).html( _.template( template, blanks ) );

			$(this.el).find('.zeega-item-thumbnail').append(this.thumbnailView.render().el);

			if (blanks["author"] === ""){
				$(this.el).find('.jda-item-author').hide();
			}

			if (this.model.get("media_type") == "Collection"){
				this.draggable=false;
			}
			else {
				this.draggable=true;
			}

			if(this.draggable){
				$(this.el).draggable({
					cursor : 'crosshair',
					cursorAt : {
						top : 50,
						left : 50
					},
					appendTo : 'body',
					opacity : 0.8,

					helper : function(){
						var drag = $(this).find('.thumbnail')
						.clone()
						.css({
							'z-index':'101'
						});
						return drag;
					},

					//init the dragged item variable
					start : function(){
						$(this).draggable('option','revert',true);
						jda.app.draggedItem = _this.model;
					}
			});
			}
			$(this.el).find(".jdicon-small-drag").tooltip({'title':'Drag to add to your collection','placement':'bottom', delay: { show: 600, hide: 100 }});

			if( blanks["display_name"] === ""){
				$(this.el).find(".jda-user-link").hide();
			} else {
				$(this.el).find(".jda-user-link").click(function(){jda.app.goToUser(_this.model.get('user_id')); return false;});
			}

			return this;
		},

		/* formats tweet text, doesn't linkify bc tweet is already linked to fancybox */
		linkifyTweet : function(tweet){

			// urls
			var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
			tweet = tweet.replace(exp,"$1");

			// users
			tweet = tweet.replace(/(^|)@(\w+)/gi, function (s) {
				return '' + s + '';
			});

			// tags
			tweet = tweet.replace(/(^|)#(\w+)/gi, function (s) {
				return '' + s + '';
			});

			return tweet;
		},

		getImageTemplate : function()
		{
			html =

			''+
				'

'+
			'	'+
			''+
				'
<%= title %>

by: <%= author %>'+
				'
<%= description %>
'+
			'	'+
			'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getDefaultTemplate : function()
		{
			html =

			''+
				'

'+
			'	'+
			''+
				'
<%= title %>

by: <%= author %>
'+
				'
<%= description %>
'+
			'	'+
			'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
			'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getDocumentTemplate : function()
		{
			html =

			''+
				''+
				'
<%= author %>
'+
			'	'+
			''+
				'
<%= title %>
'+
				'
<%= description %>
'+
			'	'+
			''+
				'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getWebsiteTemplate : function()
		{
			html =

			''+
				'

'+
			'	'+
			''+
				'
<%= title %>
'+
				'
<%= original_url %>
'+
				'
<%= description %>
'+
			'	'+
			''+
				'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getTweetTemplate : function()
		{
			html =

			''+
				'

'+
			'	'+
			''+
				'
<%= text %>
'+
			'	'+
			'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getTestimonialTemplate : function()
		{
			html =
			''+
				'

'+
			'	'+
			''+
				'
<%= title %>

Testimonial by: <%= author %>
'+
				'
<%= description %>
'+
			'	'+
			'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';
			return html;
		},

		getCollectionTemplate : function()
		{
			html =

				''+
				'

'+
				'	'+
				''+
					'
<%= title %>

by <%= display_name %>
'+
					'
<%= description %>
'+
				'	'+
				'<%= media_date %>'+
					'<input class="jda-item-checkbox" type="checkbox">'+
				'	';

			return html;
		}

	});

})(jda.module("browser"));
<commit_msg>Pretty much done with mediacreatorchange. Have to check for possible bugs<commit_after>(function(Browser) {

	Browser.Items = Browser.Items || {};
	Browser.Items.Views = Browser.Items || {};

	Browser.Items.Views.List = Backbone.View.extend({

		tagName : 'tr',
		className : 'list-media list-fancymedia',

		initialize: function () {

			this.el.id = this.model.id;

			//list item has a thumbnail item with same model as a subview
			this.thumbnailView = new Browser.Items.Views.Thumb({
				model:this.model,
				thumbnail_width:100,
				thumbnail_height:80,
				show_caption:false,
				fancybox:false,
				draggable:false
			});

		},

		render: function ( done )
		{
			var _this = this;

			var template;
			switch( this.model.get('media_type') )
			{
				case 'Image':
					template = this.getImageTemplate();
					break;
				case 'Document':
					template = this.getDefaultTemplate();
					break;
				case 'Website':
					template = this.getWebsiteTemplate();
					break;
				case 'Tweet':
					template = this.getTweetTemplate();
					break;
				case 'Text':
					template = this.getTestimonialTemplate();
					break;
				case 'Video':
					template = this.getDefaultTemplate();
					break;
				case 'Audio':
					template = this.getDefaultTemplate();
					break;
				case 'PDF':
					template = this.getDefaultTemplate();
					break;
				case 'Collection':
					$(this.el).removeClass('list-fancymedia');
					template = this.getCollectionTemplate();
					break;

				default:
					template = this.getDefaultTemplate();
			}

			var blanks = this.model.attributes;

			if (!_.isUndefined(this.model.get("media_date_created"))&&!_.isNull(this.model.get("media_date_created"))){
				blanks["media_date"] = new Date(this.model.get("media_date_created").replace(" ", "T"));
				blanks["media_date"]=blanks["media_date"].format("mmmm dS, yyyy
h:MM TT");
			} else {
				blanks["media_date"] = "n/a";
			}
			if (this.model.get("text") !== null){
				var excerpt = this.model.get("text").replace(/\r\n/gi, '
');
				blanks["text"] = this.linkifyTweet(excerpt);

			}
			if (this.model.get("description") === null){
				blanks["description"] = " ";
			}
			if (this.model.get("description") !== null && this.model.get("description").length > 255){
				blanks["description"] = this.model.get("description").substring(0,255) + "...";
			}
			if (this.model.get("title") === null || this.model.get("title") == "none" || this.model.get("title") === ""){
				blanks["title"] = "";
			}
			// Checks if the media username is rijs and switches it to media creator username
			if (this.model.get("media_creator_username") === "rijs"){
				blanks["media_creator_username"] = this.model.get("media_creator_realname");
			}

			if (this.model.get("media_creator_realname") === null || this.model.get("media_creator_realname") === "" || this.model.get("media_creator_realname") == "Unknown" || this.model.get("media_creator_realname") == "unknown"){
				blanks["author"] = this.model.get("media_creator_username");
			} else {
				blanks["author"] = this.model.get("media_creator_realname");
			}
			if (this.model.get('attributes')) {
			    if (this.model.get('attributes').privacy) {
			        if (this.model.get('attributes').privacy == 'Hide') {
			            blanks["author"] = "Anonymous";
			        }
			    }
			}

			if (this.model.get("media_type") == "Text" && this.model.get('description').length < this.model.get('text').length){
				blanks["description"] = this.model.get('description') + '...';
			}

			if (this.model.get("media_type") == "Website"){
				var parts = this.model.get('attribution_uri').split('http');
				blanks["original_url"] = parts[2] !== undefined ? "http" + parts[2] : this.model.get('attribution_uri');
			}

			$(this.el).html( _.template( template, blanks ) );

			$(this.el).find('.zeega-item-thumbnail').append(this.thumbnailView.render().el);

			if (blanks["author"] === ""){
				$(this.el).find('.jda-item-author').hide();
			}

			if (this.model.get("media_type") == "Collection"){
				this.draggable=false;
			}
			else {
				this.draggable=true;
			}

			if(this.draggable){
				$(this.el).draggable({
					cursor : 'crosshair',
					cursorAt : {
						top : 50,
						left : 50
					},
					appendTo : 'body',
					opacity : 0.8,

					helper : function(){
						var drag = $(this).find('.thumbnail')
						.clone()
						.css({
							'z-index':'101'
						});
						return drag;
					},

					//init the dragged item variable
					start : function(){
						$(this).draggable('option','revert',true);
						jda.app.draggedItem = _this.model;
					}
			});
			}
			$(this.el).find(".jdicon-small-drag").tooltip({'title':'Drag to add to your collection','placement':'bottom', delay: { show: 600, hide: 100 }});

			if( blanks["display_name"] === ""){
				$(this.el).find(".jda-user-link").hide();
			} else {
				$(this.el).find(".jda-user-link").click(function(){jda.app.goToUser(_this.model.get('user_id')); return false;});
			}

			return this;
		},

		/* formats tweet text, doesn't linkify bc tweet is already linked to fancybox */
		linkifyTweet : function(tweet){

			// urls
			var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
			tweet = tweet.replace(exp,"$1");

			// users
			tweet = tweet.replace(/(^|)@(\w+)/gi, function (s) {
				return '' + s + '';
			});

			// tags
			tweet = tweet.replace(/(^|)#(\w+)/gi, function (s) {
				return '' + s + '';
			});

			return tweet;
		},

		getImageTemplate : function()
		{
			html =

			''+
				'

'+
			'	'+
			''+
				'
<%= title %>

by: <%= author %>'+
				'
<%= description %>
'+
			'	'+
			'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getDefaultTemplate : function()
		{
			html =

			''+
				'

'+
			'	'+
			''+
				'
<%= title %>

by: <%= author %>
'+
				'
<%= description %>
'+
			'	'+
			'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
			'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getDocumentTemplate : function()
		{
			html =

			''+
				''+
				'
<%= author %>
'+
			'	'+
			''+
				'
<%= title %>
'+
				'
<%= description %>
'+
			'	'+
			''+
				'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getWebsiteTemplate : function()
		{
			html =

			''+
				'

'+
			'	'+
			''+
				'
<%= title %>
'+
				'
<%= original_url %>
'+
				'
<%= description %>
'+
			'	'+
			''+
				'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getTweetTemplate : function()
		{
			html =

			''+
				'

'+
			'	'+
			''+
				'
<%= text %>
'+
			'	'+
			'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';

			return html;
		},

		getTestimonialTemplate : function()
		{
			html =
			''+
				'

'+
			'	'+
			''+
				'
<%= title %>

Testimonial by: <%= author %>
'+
				'
<%= description %>
'+
			'	'+
			'<%= media_date %><input class="jda-item-checkbox" type="checkbox">'+
				'

via <%= display_name %>

'+
			'	';
			return html;
		},

		getCollectionTemplate : function()
		{
			html =

				''+
				'

'+
				'	'+
				''+
					'
<%= title %>

by <%= display_name %>
'+
					'
<%= description %>
'+
				'	'+
				'<%= media_date %>'+
					'<input class="jda-item-checkbox" type="checkbox">'+
				'	';

			return html;
		}

	});

})(jda.module("browser"));
<|endoftext|>


<commit_before>'use strict';

const browsertime = require('browsertime'),
  Promise = require('bluebird'),
  path = require('path'),
  urlParser = require('url'),
  messageMaker = require('../../support/messageMaker'),
  filterRegistry = require('../../support/filterRegistry'),
  aggregator = require('./aggregator'),
  api = require('webcoach'),
  log = require('intel'),
  BrowserError = browsertime.errors.BrowserError,
  UrlLoadError = browsertime.errors.UrlLoadError,
  analyzer = require('./analyzer'),
  isNotEmpty = require('../../support/util').isNotEmpty;

function removeHash(url) {
  const parsedUrl = urlParser.parse(url);
  parsedUrl.hash = null;
  return urlParser.format(parsedUrl);
}

function parseDomain(url) {
  return urlParser.parse(url).hostname;
}

const make = messageMaker('browsertime').make;

const visitedUrls = new Set();

const maxDepth = 1;

const DEFAULT_METRICS_PAGE_SUMMARY = [
  'statistics.timings.timings',
  'statistics.timings.rumSpeedIndex',
  'statistics.timings.fullyLoaded',
  'statistics.timings.firstPaint',
  'statistics.timings.userTimings',
  'statistics.timings.visualMetrics.SpeedIndex',
  'statistics.timings.visualMetrics.FirstVisualChange',
  'statistics.timings.visualMetrics.LastVisualChange'
];

const DEFAULT_METRICS_SUMMARY = [
  'firstPaint',
  'rumSpeedIndex',
  'fullyLoaded',
  'timings',
  'userTimings.marks',
  'userTimings.measures',
  'visualMetrics.SpeedIndex',
  'visualMetrics.FirstVisualChange',
  'visualMetrics.LastVisualChange'
];

module.exports = {
  concurrency: 1,
  name() {
    return path.basename(__dirname);
  },
  open(context, options) {
    this.options = options;

    if (!this.options.browsertime) {
      this.options.browsertime = {};
    }

    browsertime.logging.configure(options);

    // manually set the resultBaseDir
    // it's used in BT when we record a video
    this.options.browsertime.resultBaseDir = context.storageManager.getBaseDir();

    filterRegistry.registerFilterForType(DEFAULT_METRICS_PAGE_SUMMARY, 'browsertime.pageSummary');
    filterRegistry.registerFilterForType(DEFAULT_METRICS_SUMMARY, 'browsertime.summary');
  },
  processMessage(message, queue) {
    function processCrawlOutput(url, results) {
      const originalDomain = parseDomain(url);

      let depth = message.data.depth || 0;

      if (depth < maxDepth) {
        depth += 1;
        const links = results.browserScripts[0].crawler.links;

        for (let link of links) {
          link = removeHash(link);
          const domain = parseDomain(link);

          if (!visitedUrls.has(link) && domain === originalDomain) {
            visitedUrls.add(link);
            queue.postMessage(make('url', {
              depth,
              referrer: url
            }, {
              url: link
            }));
          }
        }
      }
    }

    function processCoachOutput(url, results) {
      return Promise.resolve(results.browserScripts)
        .each((run, runIndex) => {
          const coachAdvice = run.coach.coachAdvice;

          // check if the coach has error(s)
          if (isNotEmpty(coachAdvice.errors)) {
            log.error('%s generated the following errors in the coach %:2j', url, coachAdvice.errors);
            queue.postMessage(make('error', 'The coach got the following errors: ' + JSON.stringify(coachAdvice.errors), {
              url,
              runIndex
            }));
          }

          // make sure to get the right run in the HAR
          const myHar = api.pickAPage(results.har, runIndex);

          return api.runHarAdvice(myHar)
            .then((harResult) => api.merge(coachAdvice, harResult))
            .then((total) => queue.postMessage(make('coach.run', total, {
              url,
              runIndex
            })));
        });
    }

    switch (message.type) {
      case 'url':
        {
          const url = message.url;

          visitedUrls.add(url);
          return analyzer.analyzeUrl(url, this.options)
            .tap((results) => {
              log.trace('Result from Browsertime for %s with %:2j', url, results);
              if (this.options.browsertime.crawl) {
                processCrawlOutput(url, results);
              }
            })
            .tap((results) => {
              if (this.options.browsertime.coach) {
                return processCoachOutput(url, results);
              }
            })
            .tap((results) => {
              results.browserScripts.forEach((run, runIndex) => {
                queue.postMessage(make('browsertime.run', run, {
                  url,
                  runIndex
                }));
                aggregator.addToAggregate(run);
              });
              queue.postMessage(make('browsertime.pageSummary', results, {
                url
              }));
            })
            .tap((results) => {
              if (results.har) {
                queue.postMessage(make('browsertime.har', results.har, {
                  url
                }));
              }
            })
            .catch(BrowserError, (err) => {
              log.error('%s generated the following error in Browsertime %s', url, err);
              queue.postMessage(make('error', err.message, err.extra));
            })
            .catch(UrlLoadError, (err) => {
              log.error('%s generated the following error in Browsertime %s', url, err);
              queue.postMessage(make('error', err.message, err.extra));
            });
        }

        case 'summarize':
        {
          let summary = aggregator.summarize();
          if (summary && Object.keys(summary).length > 0) {
            queue.postMessage(make('browsertime.summary', summary));
          }

          break;
        }
    }
  }
};
<commit_msg>right path for getting VisualMetrics<commit_after>'use strict';

const browsertime = require('browsertime'),
  Promise = require('bluebird'),
  path = require('path'),
  urlParser = require('url'),
  messageMaker = require('../../support/messageMaker'),
  filterRegistry = require('../../support/filterRegistry'),
  aggregator = require('./aggregator'),
  api = require('webcoach'),
  log = require('intel'),
  BrowserError = browsertime.errors.BrowserError,
  UrlLoadError = browsertime.errors.UrlLoadError,
  analyzer = require('./analyzer'),
  isNotEmpty = require('../../support/util').isNotEmpty;

function removeHash(url) {
  const parsedUrl = urlParser.parse(url);
  parsedUrl.hash = null;
  return urlParser.format(parsedUrl);
}

function parseDomain(url) {
  return urlParser.parse(url).hostname;
}

const make = messageMaker('browsertime').make;

const visitedUrls = new Set();

const maxDepth = 1;

const DEFAULT_METRICS_PAGE_SUMMARY = [
  'statistics.timings.timings',
  'statistics.timings.rumSpeedIndex',
  'statistics.timings.fullyLoaded',
  'statistics.timings.firstPaint',
  'statistics.timings.userTimings',
  'statistics.visualMetrics.SpeedIndex',
  'statistics.visualMetrics.FirstVisualChange',
  'statistics.visualMetrics.LastVisualChange'
];

const DEFAULT_METRICS_SUMMARY = [
  'firstPaint',
  'rumSpeedIndex',
  'fullyLoaded',
  'timings',
  'userTimings.marks',
  'userTimings.measures',
  'visualMetrics.SpeedIndex',
  'visualMetrics.FirstVisualChange',
  'visualMetrics.LastVisualChange'
];

module.exports = {
  concurrency: 1,
  name() {
    return path.basename(__dirname);
  },
  open(context, options) {
    this.options = options;

    if (!this.options.browsertime) {
      this.options.browsertime = {};
    }

    browsertime.logging.configure(options);

    // manually set the resultBaseDir
    // it's used in BT when we record a video
    this.options.browsertime.resultBaseDir = context.storageManager.getBaseDir();

    filterRegistry.registerFilterForType(DEFAULT_METRICS_PAGE_SUMMARY, 'browsertime.pageSummary');
    filterRegistry.registerFilterForType(DEFAULT_METRICS_SUMMARY, 'browsertime.summary');
  },
  processMessage(message, queue) {
    function processCrawlOutput(url, results) {
      const originalDomain = parseDomain(url);

      let depth = message.data.depth || 0;

      if (depth < maxDepth) {
        depth += 1;
        const links = results.browserScripts[0].crawler.links;

        for (let link of links) {
          link = removeHash(link);
          const domain = parseDomain(link);

          if (!visitedUrls.has(link) && domain === originalDomain) {
            visitedUrls.add(link);
            queue.postMessage(make('url', {
              depth,
              referrer: url
            }, {
              url: link
            }));
          }
        }
      }
    }

    function processCoachOutput(url, results) {
      return Promise.resolve(results.browserScripts)
        .each((run, runIndex) => {
          const coachAdvice = run.coach.coachAdvice;

          // check if the coach has error(s)
          if (isNotEmpty(coachAdvice.errors)) {
            log.error('%s generated the following errors in the coach %:2j', url, coachAdvice.errors);
            queue.postMessage(make('error', 'The coach got the following errors: ' + JSON.stringify(coachAdvice.errors), {
              url,
              runIndex
            }));
          }

          // make sure to get the right run in the HAR
          const myHar = api.pickAPage(results.har, runIndex);

          return api.runHarAdvice(myHar)
            .then((harResult) => api.merge(coachAdvice, harResult))
            .then((total) => queue.postMessage(make('coach.run', total, {
              url,
              runIndex
            })));
        });
    }

    switch (message.type) {
      case 'url':
        {
          const url = message.url;

          visitedUrls.add(url);
          return analyzer.analyzeUrl(url, this.options)
            .tap((results) => {
              log.trace('Result from Browsertime for %s with %:2j', url, results);
              if (this.options.browsertime.crawl) {
                processCrawlOutput(url, results);
              }
            })
            .tap((results) => {
              if (this.options.browsertime.coach) {
                return processCoachOutput(url, results);
              }
            })
            .tap((results) => {
              results.browserScripts.forEach((run, runIndex) => {
                queue.postMessage(make('browsertime.run', run, {
                  url,
                  runIndex
                }));
                aggregator.addToAggregate(run);
              });
              queue.postMessage(make('browsertime.pageSummary', results, {
                url
              }));
            })
            .tap((results) => {
              if (results.har) {
                queue.postMessage(make('browsertime.har', results.har, {
                  url
                }));
              }
            })
            .catch(BrowserError, (err) => {
              log.error('%s generated the following error in Browsertime %s', url, err);
              queue.postMessage(make('error', err.message, err.extra));
            })
            .catch(UrlLoadError, (err) => {
              log.error('%s generated the following error in Browsertime %s', url, err);
              queue.postMessage(make('error', err.message, err.extra));
            });
        }

        case 'summarize':
        {
          let summary = aggregator.summarize();
          if (summary && Object.keys(summary).length > 0) {
            queue.postMessage(make('browsertime.summary', summary));
          }

          break;
        }
    }
  }
};
<|endoftext|>