"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const deep_freeze_1 = __importDefault(require("deep-freeze"));
const lodash_1 = __importDefault(require("lodash"));
const promise_breaker_1 = __importDefault(require("promise-breaker"));
const mime_1 = require("../utils/mime");
const oasUtils_1 = require("./oasUtils");
const Parameter_1 = __importDefault(require("./Parameter"));
const parameterParsers_1 = require("./parameterParsers");
const extensions_1 = require("./extensions");
const Responses_1 = __importDefault(require("./Responses"));
const SecuritySchemes_1 = __importDefault(require("./SecuritySchemes"));
const METHODS_WITH_BODY = ['post', 'put', 'patch'];
function isAuthenticationFailure(result) {
    return !!(result.type === 'invalid' || result.type === 'missing');
}
function getMissing(required, have) {
    if (!have || have.length === 0) {
        return required;
    }
    else {
        return required.filter((r) => !have.includes(r));
    }
}
function validateController(context, controller, operationId) {
    if (!controller && !context.options.allowMissingControllers) {
        throw new Error(`Missing ${extensions_1.EXEGESIS_CONTROLLER} for ${context.jsonPointer}`);
    }
    if (!operationId && !context.options.allowMissingControllers) {
        throw new Error(`Missing operationId or ${extensions_1.EXEGESIS_OPERATION_ID} for ${context.jsonPointer}`);
    }
    if (controller && operationId) {
        if (!context.options.controllers[controller]) {
            throw new Error(`Could not find controller ${controller} defined in ${context.jsonPointer}`);
        }
        else if (!context.options.controllers[controller][operationId]) {
            throw new Error(`Could not find operation ${controller}#${operationId} defined in ${context.jsonPointer}`);
        }
    }
}
/*
 * Validate that all operations/request bodies have a controller and
 * operationId defined.
 */
function validateControllers(context, requestBody, opController, operationId) {
    if (requestBody) {
        for (const mediaType of Object.keys(requestBody.content)) {
            const mediaContext = context.childContext(['requestBody', 'content', mediaType]);
            const mediaTypeObject = requestBody.content[mediaType];
            const mediaController = mediaTypeObject[extensions_1.EXEGESIS_CONTROLLER] || opController;
            const mediaOperationId = mediaTypeObject[extensions_1.EXEGESIS_OPERATION_ID] || operationId;
            validateController(mediaContext, mediaController, mediaOperationId);
        }
    }
    else {
        validateController(context, opController, operationId);
    }
}
class Operation {
    constructor(context, oaOperation, oaPath, method, exegesisController, parentParameters) {
        this.context = context;
        this.oaOperation = oaOperation;
        this.oaPath = oaPath;
        this.exegesisController = oaOperation[extensions_1.EXEGESIS_CONTROLLER] || exegesisController;
        this.operationId = oaOperation[extensions_1.EXEGESIS_OPERATION_ID] || oaOperation.operationId;
        this.securityRequirements = oaOperation.security || context.openApiDoc.security || [];
        this._securitySchemes = new SecuritySchemes_1.default(context.openApiDoc);
        this._responses = new Responses_1.default(context.childContext('responses'), oaOperation.responses);
        for (const securityRequirement of this.securityRequirements) {
            for (const schemeName of Object.keys(securityRequirement)) {
                if (!context.options.authenticators[schemeName]) {
                    throw new Error(`Operation ${context.jsonPointer} references security scheme "${schemeName}" ` +
                        `but no authenticator was provided.`);
                }
            }
        }
        const requestBody = oaOperation.requestBody && METHODS_WITH_BODY.includes(method)
            ? context.resolveRef(oaOperation.requestBody)
            : undefined;
        validateControllers(context, requestBody, this.exegesisController, this.operationId);
        if (requestBody) {
            this.validRequestContentTypes = Object.keys(requestBody.content);
            this.bodyRequired = requestBody.required || false;
            const contentContext = context.childContext(['requestBody', 'content']);
            this._requestBodyContentTypes = (0, oasUtils_1.contentToRequestMediaTypeRegistry)(contentContext, { in: 'request', name: 'body', docPath: contentContext.jsonPointer }, requestBody.required || false, requestBody.content);
        }
        else {
            this._requestBodyContentTypes = new mime_1.MimeTypeRegistry();
            this.bodyRequired = false;
        }
        const localParameters = (this.oaOperation.parameters || []).map((parameter, index) => new Parameter_1.default(context.childContext(['parameters', '' + index]), parameter));
        const allParameters = parentParameters.concat(localParameters);
        this._parameters = allParameters.reduce((result, parameter) => {
            result[parameter.oaParameter.in].push(parameter);
            return result;
        }, { query: [], header: [], path: [], server: [], cookie: [] });
        this.parameterLocations = (0, deep_freeze_1.default)(allParameters.reduce((result, parameter) => {
            result[parameter.oaParameter.in] = parameter.location;
            return result;
        }, { query: {}, header: {}, path: {}, cookie: {} }));
    }
    /**
     * Given a 'content-type' from a request, return a `MediaType` object that
     * matches, or `undefined` if no objects match.
     *
     * @param contentType - The content type from the 'content-type' header on
     *   a request.
     * @returns - The MediaType object to handle this request, or undefined if
     *   no MediaType is set for the given contentType.
     */
    getRequestMediaType(contentType) {
        return this._requestBodyContentTypes.get(contentType);
    }
    /**
     * Parse parameters for this operation.
     * @param params - Raw headers, raw path params and server params from
     *   `PathResolver`, and the raw queryString.
     * @returns parsed parameters.
     */
    parseParameters(params) {
        const { headers, rawPathParams, queryString } = params;
        return {
            query: (0, parameterParsers_1.parseQueryParameters)(this._parameters.query, queryString),
            header: (0, parameterParsers_1.parseParameterGroup)(this._parameters.header, headers || {}),
            server: params.serverParams || {},
            path: rawPathParams ? (0, parameterParsers_1.parseParameterGroup)(this._parameters.path, rawPathParams) : {},
            cookie: {},
        };
    }
    validateParameters(parameterValues) {
        // TODO: We could probably make this a lot more efficient by building the schema
        // for the parameter tree.
        let errors = null;
        for (const parameterLocation of Object.keys(parameterValues)) {
            const parameters = this._parameters[parameterLocation];
            const values = parameterValues[parameterLocation];
            for (const parameter of parameters) {
                const innerResult = parameter.validate(values[parameter.oaParameter.name]);
                if (innerResult && innerResult.errors && innerResult.errors.length > 0) {
                    errors = errors || [];
                    errors = errors.concat(innerResult.errors);
                }
                else {
                    values[parameter.oaParameter.name] = innerResult.value;
                }
            }
        }
        return errors;
    }
    /**
     * Validate a response.
     *
     * @param response - The response generated by a controller.
     * @param validateDefaultResponses - true to validate all responses, false
     *   to only validate non-default responses.
     */
    validateResponse(response, validateDefaultResponses) {
        return this._responses.validateResponse(response.statusCode, response.headers, response.body, validateDefaultResponses);
    }
    _runAuthenticator(schemeName, triedSchemes, exegesisContext, requiredScopes) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!(schemeName in triedSchemes)) {
                const authenticator = this.context.options.authenticators[schemeName];
                const info = this._securitySchemes.getInfo(schemeName);
                const result = (yield promise_breaker_1.default.call(authenticator, null, exegesisContext, info)) || { type: 'missing', status: 401 };
                if (result.type !== 'success' &&
                    result.type !== 'invalid' &&
                    result.type !== 'missing') {
                    throw new Error(`Invalid result ${result.type} from authenticator for ${schemeName}`);
                }
                if (isAuthenticationFailure(result)) {
                    result.status = result.status || 401;
                    if (result.status === 401 && !result.challenge) {
                        result.challenge = this._securitySchemes.getChallenge(schemeName);
                    }
                }
                triedSchemes[schemeName] = result;
            }
            let result = triedSchemes[schemeName];
            if (!isAuthenticationFailure(result)) {
                // For OAuth3, need to verify we have the oauth scopes defined in the API doc.
                const missingScopes = getMissing(requiredScopes, result.scopes);
                if (missingScopes.length > 0) {
                    result = {
                        type: 'invalid',
                        status: 403,
                        message: `Authenticated using '${schemeName}' but missing ` +
                            `required scopes: ${missingScopes.join(', ')}.`,
                    };
                }
            }
            return result;
        });
    }
    /**
     * Checks a single security requirement from an OAS3 `security` field.
     *
     * @param triedSchemes - A cache where keys are names of security schemes
     *   we've already tried, and values are the results returned by the
     *   authenticator.
     * @param errors - An array of strings - we can push any errors we encounter
     *   to this list.
     * @param securityRequirement - The security requirement to check.
     * @param exegesisContext - The context for the request to check.
     * @returns - If the security requirement matches, this returns a
     *   `{type: 'authenticated', result}` object, where result is an object
     *   where keys are security schemes and the values are the results from
     *   the authenticator.  If the requirements are not met, returns a
     *   `{type: 'missing', failure}` object or a `{type: 'invalid', failure}`,
     *   object where `failure` is the the failure that caused this security
     *   requirement to not pass.
     */
    _checkSecurityRequirement(triedSchemes, securityRequirement, exegesisContext) {
        return __awaiter(this, void 0, void 0, function* () {
            const requiredSchemes = Object.keys(securityRequirement);
            const result = Object.create(null);
            let failure;
            let failedSchemeName;
            for (const scheme of requiredSchemes) {
                if (exegesisContext.isResponseFinished()) {
                    // Some authenticator has written a response.  We're done.  :(
                    break;
                }
                const requiredScopes = securityRequirement[scheme];
                const authResult = yield this._runAuthenticator(scheme, triedSchemes, exegesisContext, requiredScopes);
                if (isAuthenticationFailure(authResult)) {
                    // Couldn't authenticate.  Try the next one.
                    failure = authResult;
                    failedSchemeName = scheme;
                    break;
                }
                result[scheme] = authResult;
            }
            if (failure) {
                return { type: failure.type, failure, failedSchemeName };
            }
            else if (result) {
                return { type: 'authenticated', result };
            }
            else {
                return undefined;
            }
        });
    }
    authenticate(exegesisContext) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.securityRequirements.length === 0) {
                // No auth required
                return {};
            }
            let firstFailureResult;
            const challenges = {};
            let firstAuthenticatedResult;
            const triedSchemes = Object.create(null);
            for (const securityRequirement of this.securityRequirements) {
                const securityRequirementResult = yield this._checkSecurityRequirement(triedSchemes, securityRequirement, exegesisContext);
                if (!securityRequirementResult) {
                    break;
                }
                else if (securityRequirementResult.type === 'authenticated') {
                    firstAuthenticatedResult =
                        firstAuthenticatedResult || securityRequirementResult.result;
                }
                else if (securityRequirementResult.type === 'missing' ||
                    securityRequirementResult.type === 'invalid') {
                    const failure = securityRequirementResult.failure;
                    if (!failure) {
                        throw new Error('Missing failure.');
                    }
                    if (!securityRequirementResult.failedSchemeName) {
                        throw new Error('Missing failed scheme name.');
                    }
                    // No luck with this security requirement.
                    if (failure.status === 401 && failure.challenge) {
                        challenges[securityRequirementResult.failedSchemeName] = failure.challenge;
                    }
                    if (securityRequirementResult.type === 'invalid') {
                        firstFailureResult = firstFailureResult || failure;
                        break;
                    }
                }
                else {
                    /* istanbul ignore this */
                    throw new Error('Invalid result from `_checkSecurityRequirement()`');
                }
                if (exegesisContext.isResponseFinished()) {
                    // We're done!
                    break;
                }
            }
            if (firstAuthenticatedResult && !firstFailureResult) {
                // Successs!
                return firstAuthenticatedResult;
            }
            else if (exegesisContext.isResponseFinished()) {
                // Someone already wrote a response.
                return undefined;
            }
            else {
                const authSchemes = this.securityRequirements.map((requirement) => {
                    const schemes = Object.keys(requirement);
                    return schemes.length === 1 ? schemes[0] : `(${schemes.join(' + ')})`;
                });
                const authChallenges = (0, lodash_1.default)(this.securityRequirements)
                    .map((requirement) => Object.keys(requirement))
                    .flatten()
                    .map((schemeName) => challenges[schemeName] || this._securitySchemes.getChallenge(schemeName))
                    .filter((challenge) => challenge !== undefined)
                    .value();
                const message = (firstFailureResult && firstFailureResult.message) ||
                    `Must authenticate using one of the following schemes: ${authSchemes.join(', ')}.`;
                exegesisContext.res
                    .setStatus((firstFailureResult && firstFailureResult.status) || 401)
                    .set('WWW-Authenticate', authChallenges)
                    .setBody({ message });
                return undefined;
            }
        });
    }
}
exports.default = Operation;
//# sourceMappingURL=Operation.js.map