"use strict";
/*
 * @adonisjs/session
 *
 * (c) Harminder Virk <virk@adonisjs.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Session = void 0;
/// <reference path="../../adonis-typings/session.ts" />
const utils_1 = require("@poppinss/utils");
const helpers_1 = require("@poppinss/utils/build/helpers");
const Store_1 = require("../Store");
/**
 * Session class exposes the API to read/write values to the session for
 * a given request.
 */
class Session {
    constructor(ctx, config, driver) {
        this.ctx = ctx;
        this.config = config;
        this.driver = driver;
        /**
         * Set to true inside the `initiate` method
         */
        this.initiated = false;
        /**
         * A boolean to know if it's a fresh session or not. Fresh
         * sessions are those, whose session id is not present
         * in cookie
         */
        this.fresh = false;
        /**
         * A boolean to know if store is initiated in readonly mode
         * or not. This is done during Websocket requests
         */
        this.readonly = false;
        /**
         * Session id for the given request. A new session id is only
         * generated when the cookie for the session id is missing
         */
        this.sessionId = this.getSessionId();
        /**
         * A copy of previously set flash messages
         */
        this.flashMessages = new Store_1.Store({});
        /**
         * Session id for the current request. It will be different
         * from the "this.sessionId" when regenerate is called.
         */
        this.currentSessionId = this.sessionId;
        /**
         * Whether or not to re-generate the session id before comitting
         * session values.
         */
        this.regeneratedSessionId = false;
        /**
         * A copy of flash messages. The `input` messages
         * are overwritten when any of the input related
         * methods are used.
         *
         * The `others` object is expanded with each call.
         */
        this.responseFlashMessages = new Store_1.Store({});
        /**
         * Session key for setting flash messages
         */
        this.flashMessagesKey = '__flash__';
    }
    /**
     * Returns a merged copy of flash messages or null
     * when nothing is set
     */
    setFlashMessages() {
        if (this.responseFlashMessages.isEmpty) {
            return;
        }
        const { input, ...others } = this.responseFlashMessages.all();
        this.put(this.flashMessagesKey, { ...input, ...others });
    }
    /**
     * Returns the existing session id or creates one.
     */
    getSessionId() {
        const sessionId = this.ctx.request.cookie(this.config.cookieName);
        if (sessionId) {
            this.ctx.logger.trace('existing session found');
            return sessionId;
        }
        this.fresh = true;
        this.ctx.logger.trace('generating new session id');
        return (0, helpers_1.cuid)();
    }
    /**
     * Ensures the session store is initialized
     */
    ensureIsReady() {
        if (!this.initiated) {
            throw new utils_1.Exception('Session store is not initiated yet. Make sure you are using the session hook', 500, 'E_RUNTIME_EXCEPTION');
        }
    }
    /**
     * Raises exception when session store is in readonly mode
     */
    ensureIsMutable() {
        if (this.readonly) {
            throw new utils_1.Exception('Session store is in readonly mode and cannot be mutated', 500, 'E_RUNTIME_EXCEPTION');
        }
    }
    /**
     * Touches the session cookie
     */
    touchSessionCookie() {
        this.ctx.logger.trace('touching session cookie');
        this.ctx.response.cookie(this.config.cookieName, this.sessionId, this.config.cookie);
    }
    /**
     * Commits the session value to the store
     */
    async commitValuesToStore() {
        this.ctx.logger.trace('persist session store with driver');
        await this.driver.write(this.sessionId, this.store.toJSON());
    }
    /**
     * Touches the driver to make sure the session values doesn't expire
     */
    async touchDriver() {
        this.ctx.logger.trace('touch driver for liveliness');
        await this.driver.touch(this.sessionId);
    }
    /**
     * Reading flash messages from the last HTTP request and
     * updating the flash messages bag
     */
    readLastRequestFlashMessage() {
        if (this.readonly) {
            return;
        }
        this.flashMessages.update(this.pull(this.flashMessagesKey, null));
    }
    /**
     * Share flash messages & read only session's functions with views
     * (only when view property exists)
     */
    shareLocalsWithView() {
        if (!this.ctx['view'] || typeof this.ctx['view'].share !== 'function') {
            return;
        }
        this.ctx['view'].share({
            flashMessages: this.flashMessages,
            session: {
                get: this.get.bind(this),
                has: this.has.bind(this),
                all: this.all.bind(this),
            },
        });
    }
    /**
     * Initiating the session by reading it's value from the
     * driver and feeding it to a store.
     *
     * Multiple calls to `initiate` results in a noop.
     */
    async initiate(readonly) {
        if (this.initiated) {
            return;
        }
        this.readonly = readonly;
        /**
         * Profiling the driver read method
         */
        await this.ctx.profiler.profileAsync('session:initiate', { driver: this.config.driver }, async () => {
            const contents = await this.driver.read(this.sessionId);
            this.store = new Store_1.Store(contents);
        });
        this.initiated = true;
        this.readLastRequestFlashMessage();
        this.shareLocalsWithView();
    }
    /**
     * Re-generates the session id. This can is used to avoid
     * session fixation attacks.
     */
    regenerate() {
        this.ctx.logger.trace('explicitly re-generating session id');
        this.sessionId = (0, helpers_1.cuid)();
        this.regeneratedSessionId = true;
    }
    /**
     * Set/update session value
     */
    put(key, value) {
        this.ensureIsReady();
        this.ensureIsMutable();
        this.store.set(key, value);
    }
    /**
     * Find if the value exists in the session
     */
    has(key) {
        this.ensureIsReady();
        return this.store.has(key);
    }
    /**
     * Get value from the session. The default value is returned
     * when actual value is `undefined`
     */
    get(key, defaultValue) {
        this.ensureIsReady();
        return this.store.get(key, defaultValue);
    }
    /**
     * Returns everything from the session
     */
    all() {
        this.ensureIsReady();
        return this.store.all();
    }
    /**
     * Remove value for a given key from the session
     */
    forget(key) {
        this.ensureIsReady();
        this.ensureIsMutable();
        this.store.unset(key);
    }
    /**
     * The method is equivalent to calling `session.get` followed
     * by `session.forget`
     */
    pull(key, defaultValue) {
        this.ensureIsReady();
        this.ensureIsMutable();
        return this.store.pull(key, defaultValue);
    }
    /**
     * Increment value for a number inside the session store. The
     * method raises an error when underlying value is not
     * a number
     */
    increment(key, steps = 1) {
        this.ensureIsReady();
        this.ensureIsMutable();
        this.store.increment(key, steps);
    }
    /**
     * Decrement value for a number inside the session store. The
     * method raises an error when underlying value is not
     * a number
     */
    decrement(key, steps = 1) {
        this.ensureIsReady();
        this.ensureIsMutable();
        this.store.decrement(key, steps);
    }
    /**
     * Remove everything from the session
     */
    clear() {
        this.ensureIsReady();
        this.ensureIsMutable();
        this.store.clear();
    }
    /**
     * Add a new flash message
     */
    flash(key, value) {
        this.ensureIsReady();
        this.ensureIsMutable();
        /**
         * Update value
         */
        if (typeof key === 'string') {
            if (value) {
                this.responseFlashMessages.set(key, value);
            }
        }
        else {
            this.responseFlashMessages.merge(key);
        }
    }
    /**
     * Flash all form values
     */
    flashAll() {
        this.ensureIsReady();
        this.ensureIsMutable();
        this.responseFlashMessages.set('input', this.ctx.request.original());
    }
    /**
     * Flash all form values except mentioned keys
     */
    flashExcept(keys) {
        this.ensureIsReady();
        this.ensureIsMutable();
        this.responseFlashMessages.set('input', utils_1.lodash.omit(this.ctx.request.original(), keys));
    }
    /**
     * Flash only defined keys from the form values
     */
    flashOnly(keys) {
        this.ensureIsReady();
        this.ensureIsMutable();
        this.responseFlashMessages.set('input', utils_1.lodash.pick(this.ctx.request.original(), keys));
    }
    /**
     * Reflash existing flash messages
     */
    reflash() {
        this.flash(this.flashMessages.all());
    }
    /**
     * Reflash selected keys from the existing flash messages
     */
    reflashOnly(keys) {
        this.flash(utils_1.lodash.pick(this.flashMessages.all(), keys));
    }
    /**
     * Omit selected keys from the existing flash messages
     * and flash the rest of values
     */
    reflashExcept(keys) {
        this.flash(utils_1.lodash.omit(this.flashMessages.all(), keys));
    }
    /**
     * Writes value to the underlying session driver.
     */
    async commit() {
        await this.ctx.profiler.profileAsync('session:commit', { driver: this.config.driver }, async () => {
            if (!this.initiated) {
                this.touchSessionCookie();
                await this.touchDriver();
                return;
            }
            /**
             * Cleanup old session and re-generate new session
             */
            if (this.regeneratedSessionId) {
                await this.driver.destroy(this.currentSessionId);
            }
            /**
             * Touch the session cookie to keep it alive.
             */
            this.touchSessionCookie();
            this.setFlashMessages();
            await this.commitValuesToStore();
        });
    }
}
exports.Session = Session;
