TryGhost/Ghost

View on GitHub
ghost/api-framework/lib/headers.js

Summary

Maintainability
B
6 hrs
Test Coverage
const url = require('url');
const debug = require('@tryghost/debug')('headers');
const INVALIDATE_ALL = '/*';

const cacheInvalidate = (result, options = {}) => {
    let value = options.value;

    return {
        'X-Cache-Invalidate': value || INVALIDATE_ALL
    };
};

const disposition = {
    /**
     * @description Generate CSV header.
     *
     * @param {Object} result - API response
     * @param {Object} options
     * @return {Object}
     */
    csv(result, options = {}) {
        let value = options.value;

        if (typeof options.value === 'function') {
            value = options.value();
        }

        return {
            'Content-Disposition': `Attachment; filename="${value}"`,
            'Content-Type': 'text/csv'
        };
    },

    /**
     * @description Generate JSON header.
     *
     * @param {Object} result - API response
     * @param {Object} options
     * @return {Object}
     */
    json(result, options = {}) {
        return {
            'Content-Disposition': `Attachment; filename="${options.value}"`,
            'Content-Type': 'application/json',
            'Content-Length': Buffer.byteLength(JSON.stringify(result))
        };
    },

    /**
     * @description Generate YAML header.
     *
     * @param {Object} result - API response
     * @param {Object} options
     * @return {Object}
     */
    yaml(result, options = {}) {
        return {
            'Content-Disposition': `Attachment; filename="${options.value}"`,
            'Content-Type': 'application/yaml',
            'Content-Length': Buffer.byteLength(JSON.stringify(result))
        };
    },

    /**
     * @description Content Disposition Header
     *
     * Create a header that invokes the 'Save As' dialog in the browser when exporting the database to file. The 'filename'
     * parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3).
     *
     * For encoding whitespace and non-ISO-8859-1 characters, you MUST use the "filename*=" attribute, NOT "filename=".
     * Ideally, both. Examples: http://tools.ietf.org/html/rfc6266#section-5
     *
     * We'll use ISO-8859-1 characters here to keep it simple.
     *
     * @see http://tools.ietf.org/html/rfc598
     */
    file(result, options = {}) {
        return Promise.resolve()
            .then(() => {
                let value = options.value;

                if (typeof options.value === 'function') {
                    value = options.value();
                }

                return value;
            })
            .then((filename) => {
                return {
                    'Content-Disposition': `Attachment; filename="${filename}"`
                };
            });
    }
};

module.exports = {
    /**
     * @description Get header based on ctrl configuration.
     *
     * @param {Object} result - API response
     * @param {Object} apiConfigHeaders
     * @param {import('@tryghost/api-framework').Frame} frame
     * @return {Promise<object>}
     */
    async get(result, apiConfigHeaders = {}, frame) {
        let headers = {};

        if (apiConfigHeaders.disposition) {
            const dispositionHeader = await disposition[apiConfigHeaders.disposition.type](result, apiConfigHeaders.disposition);

            if (dispositionHeader) {
                Object.assign(headers, dispositionHeader);
            }
        }

        if (apiConfigHeaders.cacheInvalidate) {
            const cacheInvalidationHeader = cacheInvalidate(result, apiConfigHeaders.cacheInvalidate);

            if (cacheInvalidationHeader) {
                Object.assign(headers, cacheInvalidationHeader);
            }
        }

        const locationHeaderDisabled = apiConfigHeaders?.location === false;
        const hasLocationResolver = apiConfigHeaders?.location?.resolve;
        const hasFrameData = (frame?.method === 'add' || hasLocationResolver) && result[frame.docName]?.[0]?.id;

        if (!locationHeaderDisabled && hasFrameData) {
            const protocol = (frame.original.url.secure === false) ? 'http://' : 'https://';
            const resourceId = result[frame.docName][0].id;

            let locationURL = url.resolve(`${protocol}${frame.original.url.host}`,frame.original.url.pathname);
            if (!locationURL.endsWith('/')) {
                locationURL += '/';
            }

            locationURL += `${resourceId}/`;

            if (hasLocationResolver) {
                locationURL = apiConfigHeaders.location.resolve(locationURL);
            }

            const locationHeader = {
                Location: locationURL
            };

            Object.assign(headers, locationHeader);
        }

        const headersFromFrame = frame.getHeaders();

        Object.assign(headers, headersFromFrame);

        debug(headers);
        return headers;
    }
};