<commit_before>package ufront.web.session;

import haxe.io.Path;
import ufront.web.context.HttpContext;
import ufront.web.HttpCookie;
import ufront.web.session.UFHttpSession;
import haxe.ds.StringMap;
import haxe.Serializer;
import haxe.Unserializer;
import tink.CoreApi;
import ufront.core.Uuid;
#if sys
	import sys.FileSystem;
	import sys.io.File;
#elseif nodejs
	import js.node.Fs;
#end
using ufront.core.AsyncTools;
using StringTools;
using haxe.io.Path;

/**
A session implementation using flat files.

Files are saved to the folder `savePath`, with one file per session.
The folder must be writeable by the web server.

Each session has a unique ID, which is randomly generated and used as the file name.

The contents of the file are a serialized `StringMap` representing the current session.
The serialization is done using `haxe.Serializer` and `haxe.Unserializer`.

The session ID is sent to the client as a `HttpCookie`.
When reading the session ID, `HttpRequest.cookies` is checked first, followed by `HttpRequest.params`.

When searching the parameters or cookies for the session ID, the name to search for is defined by the `this.sessionName` property.
**/
class FileSession implements UFHttpSession {
	// Statics

	/**
	The default session name to use if none is provided.

	The default value is `UfrontSessionID`.
	You can change this static variable to set a new default.
	**/
	public static var defaultSessionName:String = "UfrontSessionID";

	/**
	The default savePath.

	This should be relative to the `HttpContext.contentDirectory`, or absolute.
	The default value is `sessions/`.
	You can change this static value to set a new default.
	**/
	public static var defaultSavePath:String = "sessions/";

	/**
	The default expiry value.

	The default value is 0 (expire when window is closed).
	You can change the default by changing this static variable.
	**/
	public static var defaultExpiry:Int = 0;

	// Private variables

	var started:Bool;
	var commitFlag:Bool;
	var closeFlag:Bool;
	var regenerateFlag:Bool;
	var expiryFlag:Bool;
	var sessionID:String;
	var sessionData:StringMap<Dynamic>;

	// Public members

	/**
	The current session ID.
	It will be read from the cookies, or failing that, the request parameters.
	This cannot be set manually, please see `regenerateID` for a way to change the session ID.
	**/
	public var id(get,never):Null<String>;

	/** The current HttpContext, should be supplied by injection. **/
	@inject public var context:HttpContext;

	/**
	Construct a new session object.

	This does not create the session file or commit any data, rather, it sets up the object so that read or writes can then happen.

	Data is read during `this.init()` and written during `this.commit()`.

	A new session object should be created for each request, and it will then associate itself with the correct session file for the given user.

	In general you should create your object using dependency injections, so that everything is initialized correctly.
	**/
	public function new() {
		started = false;
		commitFlag = false;
		closeFlag = false;
		regenerateFlag = false;
		expiryFlag = false;
		sessionData = null;
		sessionID = null;
	}

	/**
	Use the current injector to check for configuration for this session: `this.sessionName`, `this.expiry` and `this.savePath`.
	If no values are available in the injector, the defaults will be used.
	**/
	@inject public function injectConfig( context:HttpContext ) {
		this.sessionName =
			if ( context.injector.hasMapping(String,"sessionName") )
				context.injector.getValue( String, "sessionName" )
			else defaultSessionName;
		this.expiry =
			if ( context.injector.hasMapping(Int,"sessionExpiry") )
				context.injector.getValue( Int, "sessionExpiry" );
			else defaultExpiry;
		this.savePath =
			if ( context.injector.hasMapping(String,"sessionSavePath") )
				context.injector.getValue( String, "sessionSavePath" )
			else defaultSavePath;

		// Sanitize the savePath, make it absolute if it was specified relative to the content directory.
		savePath = Path.addTrailingSlash( savePath );
		if ( !savePath.startsWith("/") )
			savePath = context.contentDirectory + savePath;
	}

	/**
	The variable name to reference the session ID.

	This will be the name set in the cookie sent to the client, or the name to search for in the parameters or cookies.

	This is set by injecting a String named `sessionName`, otherwise the default `FileSession.defaultSessionName` value is used.
	**/
	public var sessionName(default,null):String;

	/**
	The lifetime/expiry of the cookie, in seconds.

	- A positive value sets the cookie to expire that many seconds from the current time.
	- A value of 0 represents expiry when the browser window is closed.
	- A negative value expires the cookie immediately.

	This is set by injecting an `Int` named `sessionExpiry`, otherwise the default `FileSession.defaultExpiry` value is used.
	**/
	public var expiry(default,null):Null<Int>;

	/**
	The save path for the session files.

	This should be absolute, or relative to the `HttpContext.contentDirectory`

	Relative paths should not have a leading slash.
	If a trailing slash is not included, it will be added.

	This is set by injecting a `String` named `sessionSavePath`, otherwise the default `FileSession.defaultSavePath` value is used.
	**/
	public var savePath(default,null):String;

	/**
	Set the number of seconds the session should last before expiring.

	Note in this implementation only the cookie expiry is affected - the file is not deleted from the server.
	The user could manually override this or send the session variable in the request parameters, and the session would still work.
	**/
	public function setExpiry( e:Int ) {
		expiry = e;
	}

	/**
	Initiate the session.

	This will check for an existing session ID.
	If one exists, it will read and unserialize the session data from that session's file.

	If a session does not exist, one will be created, including generating and reserving a new session ID.

	This must be called before any other operations which require access to the current session.
	**/
	public function init():Surprise<Noise,Error> {
		if ( !started ) {
			get_id();
			this.sessionData = new StringMap();

			return
				doCreateSessionDirectory() >>
				doReadSessionFile >>
				doUnserializeSessionData;<commit_msg>Make sure to mark the session as started in FileSession<commit_after>package ufront.web.session;

import haxe.io.Path;
import ufront.web.context.HttpContext;
import ufront.web.HttpCookie;
import ufront.web.session.UFHttpSession;
import haxe.ds.StringMap;
import haxe.Serializer;
import haxe.Unserializer;
import tink.CoreApi;
import ufront.core.Uuid;
#if sys
	import sys.FileSystem;
	import sys.io.File;
#elseif nodejs
	import js.node.Fs;
#end
using ufront.core.AsyncTools;
using StringTools;
using haxe.io.Path;

/**
A session implementation using flat files.

Files are saved to the folder `savePath`, with one file per session.
The folder must be writeable by the web server.

Each session has a unique ID, which is randomly generated and used as the file name.

The contents of the file are a serialized `StringMap` representing the current session.
The serialization is done using `haxe.Serializer` and `haxe.Unserializer`.

The session ID is sent to the client as a `HttpCookie`.
When reading the session ID, `HttpRequest.cookies` is checked first, followed by `HttpRequest.params`.

When searching the parameters or cookies for the session ID, the name to search for is defined by the `this.sessionName` property.
**/
class FileSession implements UFHttpSession {
	// Statics

	/**
	The default session name to use if none is provided.

	The default value is `UfrontSessionID`.
	You can change this static variable to set a new default.
	**/
	public static var defaultSessionName:String = "UfrontSessionID";

	/**
	The default savePath.

	This should be relative to the `HttpContext.contentDirectory`, or absolute.
	The default value is `sessions/`.
	You can change this static value to set a new default.
	**/
	public static var defaultSavePath:String = "sessions/";

	/**
	The default expiry value.

	The default value is 0 (expire when window is closed).
	You can change the default by changing this static variable.
	**/
	public static var defaultExpiry:Int = 0;

	// Private variables

	var started:Bool;
	var commitFlag:Bool;
	var closeFlag:Bool;
	var regenerateFlag:Bool;
	var expiryFlag:Bool;
	var sessionID:String;
	var sessionData:StringMap<Dynamic>;

	// Public members

	/**
	The current session ID.
	It will be read from the cookies, or failing that, the request parameters.
	This cannot be set manually, please see `regenerateID` for a way to change the session ID.
	**/
	public var id(get,never):Null<String>;

	/** The current HttpContext, should be supplied by injection. **/
	@inject public var context:HttpContext;

	/**
	Construct a new session object.

	This does not create the session file or commit any data, rather, it sets up the object so that read or writes can then happen.

	Data is read during `this.init()` and written during `this.commit()`.

	A new session object should be created for each request, and it will then associate itself with the correct session file for the given user.

	In general you should create your object using dependency injections, so that everything is initialized correctly.
	**/
	public function new() {
		started = false;
		commitFlag = false;
		closeFlag = false;
		regenerateFlag = false;
		expiryFlag = false;
		sessionData = null;
		sessionID = null;
	}

	/**
	Use the current injector to check for configuration for this session: `this.sessionName`, `this.expiry` and `this.savePath`.
	If no values are available in the injector, the defaults will be used.
	**/
	@inject public function injectConfig( context:HttpContext ) {
		this.sessionName =
			if ( context.injector.hasMapping(String,"sessionName") )
				context.injector.getValue( String, "sessionName" )
			else defaultSessionName;
		this.expiry =
			if ( context.injector.hasMapping(Int,"sessionExpiry") )
				context.injector.getValue( Int, "sessionExpiry" );
			else defaultExpiry;
		this.savePath =
			if ( context.injector.hasMapping(String,"sessionSavePath") )
				context.injector.getValue( String, "sessionSavePath" )
			else defaultSavePath;

		// Sanitize the savePath, make it absolute if it was specified relative to the content directory.
		savePath = Path.addTrailingSlash( savePath );
		if ( !savePath.startsWith("/") )
			savePath = context.contentDirectory + savePath;
	}

	/**
	The variable name to reference the session ID.

	This will be the name set in the cookie sent to the client, or the name to search for in the parameters or cookies.

	This is set by injecting a String named `sessionName`, otherwise the default `FileSession.defaultSessionName` value is used.
	**/
	public var sessionName(default,null):String;

	/**
	The lifetime/expiry of the cookie, in seconds.

	- A positive value sets the cookie to expire that many seconds from the current time.
	- A value of 0 represents expiry when the browser window is closed.
	- A negative value expires the cookie immediately.

	This is set by injecting an `Int` named `sessionExpiry`, otherwise the default `FileSession.defaultExpiry` value is used.
	**/
	public var expiry(default,null):Null<Int>;

	/**
	The save path for the session files.

	This should be absolute, or relative to the `HttpContext.contentDirectory`

	Relative paths should not have a leading slash.
	If a trailing slash is not included, it will be added.

	This is set by injecting a `String` named `sessionSavePath`, otherwise the default `FileSession.defaultSavePath` value is used.
	**/
	public var savePath(default,null):String;

	/**
	Set the number of seconds the session should last before expiring.

	Note in this implementation only the cookie expiry is affected - the file is not deleted from the server.
	The user could manually override this or send the session variable in the request parameters, and the session would still work.
	**/
	public function setExpiry( e:Int ) {
		expiry = e;
	}

	/**
	Initiate the session.

	This will check for an existing session ID.
	If one exists, it will read and unserialize the session data from that session's file.

	If a session does not exist, one will be created, including generating and reserving a new session ID.

	This must be called before any other operations which require access to the current session.
	**/
	public function init():Surprise<Noise,Error> {
		if ( !started ) {
			get_id();
			this.sessionData = new StringMap();

			return
				doCreateSessionDirectory() >>
				doReadSessionFile >>
				doUnserializeSessionData >>
				function(n:Noise) { this.started = true; return Noise; };
{'fraction_non_alphanumeric': 0.07720645869258505, 'fraction_numerical': 0.0004702931493964571, 'mean_word_length': 4.136473429951691, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4807885', 'n_tokens_mistral': 3748, 'n_tokens_neox': 3530, 'n_words': 1573}