<commit_before>﻿using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using Bloom.Collection;
using Newtonsoft.Json;

namespace Bloom.Api
{
	public delegate void EndpointHandler(ApiRequest request);

	public class EndpointRegistration
	{
		public bool HandleOnUIThread = true;
		public EndpointHandler Handler;
	}


	/// <summary>
	/// When the Bloom UI makes an API call, a method that has been registered to handle that
	/// endpoint is called and given one of these. That method uses this class to get information
	/// on the request, and also to reply to the caller.
	/// </summary>
	/// <remarks>The goal here is to reduce code while increasing clarity and error catching.</remarks>
	public class ApiRequest
	{
		private readonly IRequestInfo _requestInfo;
		public readonly CollectionSettings CurrentCollectionSettings;
		public readonly Book.Book CurrentBook;
		public NameValueCollection Parameters;

		public ApiRequest(IRequestInfo requestinfo, CollectionSettings currentCollectionSettings, Book.Book currentBook)
		{
			_requestInfo = requestinfo;
			CurrentCollectionSettings = currentCollectionSettings;
			CurrentBook = currentBook;
			Parameters = requestinfo.GetQueryParameters();
		}

		/// <summary>
		/// Get the actual local path that the server would retrieve given a Bloom URL
		/// that ends up at a local file. For now it is mainly useful for things in the book folder; it doesn't have
		/// all the smarts to locate files shipped with the application, it is just concerned with reversing
		/// the various tricks we use to encode paths as URLs.
		/// </summary>
		public string LocalPath()
		{
			return ServerBase.GetLocalPathWithoutQuery(this._requestInfo);
		}

	
		public HttpMethods HttpMethod
		{
			get
			{
				return _requestInfo.HttpMethod;
			}
		}

		public void Succeeded()
		{
			_requestInfo.ContentType = "text/plain";
			_requestInfo.WriteCompleteOutput("OK");
		}

		//Used when an anchor has given us info, but we don't actually want the browser to navigate
		//For example, anchors that lead to help lead to an api handler that opens help but then
		//calls this so that the browser just stays where it was.
		public void SucceededDoNotNavigate()
		{
			_requestInfo.SucceededDoNotNavigate();
		}

		public void ReplyWithText(string text)
		{
			//Debug.WriteLine(this.Requestinfo.LocalPathWithoutQuery + ": " + text);
			_requestInfo.ContentType = "text/plain";
			_requestInfo.WriteCompleteOutput(text);
		}
		public void ReplyWithJson(string json)
		{
			//Debug.WriteLine(this.Requestinfo.LocalPathWithoutQuery + ": " + json);
			_requestInfo.ContentType = "application/json";
			_requestInfo.WriteCompleteOutput(json);
		}
		public void ReplyWithJson(object objectToMakeJson)
		{
			//Debug.WriteLine(this.Requestinfo.LocalPathWithoutQuery + ": " + json);
			_requestInfo.ContentType = "application/json";
			_requestInfo.WriteCompleteOutput(JsonConvert.SerializeObject(objectToMakeJson));
		}
		public void ReplyWithImage(string imagePath)
		{
			_requestInfo.ReplyWithImage(imagePath);
		}


		public void Failed(string text)
		{
			//Debug.WriteLine(this.Requestinfo.LocalPathWithoutQuery+": "+text);
			_requestInfo.ContentType = "text/plain";
			_requestInfo.WriteError(503,text);
		}

		public static bool Handle(EndpointRegistration endpointRegistration, IRequestInfo info, CollectionSettings collectionSettings, Book.Book currentBook)
		{
			var request = new ApiRequest(info, collectionSettings, currentBook);
			try
			{
				if(Program.RunningUnitTests) 
				{
					endpointRegistration.Handler(request);
				}
				else
				{
					var formForSynchronizing = Application.OpenForms.Cast<Form>().Last();
					if (endpointRegistration.HandleOnUIThread && formForSynchronizing.InvokeRequired)
					{
						formForSynchronizing.Invoke(endpointRegistration.Handler, request);
					}
					else
					{
						endpointRegistration.Handler(request);
					}
				}
				if(!info.HaveOutput)
				{
					throw new ApplicationException(string.Format("The EndpointHandler for {0} never called a Succeeded(), Failed(), or ReplyWith() Function.", info.RawUrl.ToString()));
				}
			}
			catch (Exception e)
			{
				SIL.Reporting.ErrorReport.ReportNonFatalExceptionWithMessage(e, info.RawUrl);
				return false;
			}
			return true;
		}

		public UrlPathString RequiredFileNameOrPath(string name)
		{
			if (Parameters.AllKeys.Contains(name))
				return UrlPathString.CreateFromUnencodedString(Parameters[name]);
			throw new ApplicationException("The query " + _requestInfo.RawUrl + " should have parameter " + name);
		}

		public string RequiredParam(string name)
		{
			if (Parameters.AllKeys.Contains(name))
				return Parameters[name];
			throw new ApplicationException("The query " + _requestInfo.RawUrl + " should have parameter " + name);
		}
		public string RequiredPostJson()
		{
			Debug.Assert(_requestInfo.HttpMethod==HttpMethods.Post);
			var json = _requestInfo.GetPostJson();
			if (!string.IsNullOrWhiteSpace(json))
			{
				return json;
			}
			throw new ApplicationException("The query " + _requestInfo.RawUrl + " should have post json");<commit_msg>Don't lose stacktrace in api-related exceptions (BL-4192)<commit_after>﻿using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Windows.Forms;
using Bloom.Collection;
using Newtonsoft.Json;

namespace Bloom.Api
{
	public delegate void EndpointHandler(ApiRequest request);

	public class EndpointRegistration
	{
		public bool HandleOnUIThread = true;
		public EndpointHandler Handler;
	}


	/// <summary>
	/// When the Bloom UI makes an API call, a method that has been registered to handle that
	/// endpoint is called and given one of these. That method uses this class to get information
	/// on the request, and also to reply to the caller.
	/// </summary>
	/// <remarks>The goal here is to reduce code while increasing clarity and error catching.</remarks>
	public class ApiRequest
	{
		private readonly IRequestInfo _requestInfo;
		public readonly CollectionSettings CurrentCollectionSettings;
		public readonly Book.Book CurrentBook;
		public NameValueCollection Parameters;

		public ApiRequest(IRequestInfo requestinfo, CollectionSettings currentCollectionSettings, Book.Book currentBook)
		{
			_requestInfo = requestinfo;
			CurrentCollectionSettings = currentCollectionSettings;
			CurrentBook = currentBook;
			Parameters = requestinfo.GetQueryParameters();
		}

		/// <summary>
		/// Get the actual local path that the server would retrieve given a Bloom URL
		/// that ends up at a local file. For now it is mainly useful for things in the book folder; it doesn't have
		/// all the smarts to locate files shipped with the application, it is just concerned with reversing
		/// the various tricks we use to encode paths as URLs.
		/// </summary>
		public string LocalPath()
		{
			return ServerBase.GetLocalPathWithoutQuery(this._requestInfo);
		}

	
		public HttpMethods HttpMethod
		{
			get
			{
				return _requestInfo.HttpMethod;
			}
		}

		public void Succeeded()
		{
			_requestInfo.ContentType = "text/plain";
			_requestInfo.WriteCompleteOutput("OK");
		}

		//Used when an anchor has given us info, but we don't actually want the browser to navigate
		//For example, anchors that lead to help lead to an api handler that opens help but then
		//calls this so that the browser just stays where it was.
		public void SucceededDoNotNavigate()
		{
			_requestInfo.SucceededDoNotNavigate();
		}

		public void ReplyWithText(string text)
		{
			//Debug.WriteLine(this.Requestinfo.LocalPathWithoutQuery + ": " + text);
			_requestInfo.ContentType = "text/plain";
			_requestInfo.WriteCompleteOutput(text);
		}
		public void ReplyWithJson(string json)
		{
			//Debug.WriteLine(this.Requestinfo.LocalPathWithoutQuery + ": " + json);
			_requestInfo.ContentType = "application/json";
			_requestInfo.WriteCompleteOutput(json);
		}
		public void ReplyWithJson(object objectToMakeJson)
		{
			//Debug.WriteLine(this.Requestinfo.LocalPathWithoutQuery + ": " + json);
			_requestInfo.ContentType = "application/json";
			_requestInfo.WriteCompleteOutput(JsonConvert.SerializeObject(objectToMakeJson));
		}
		public void ReplyWithImage(string imagePath)
		{
			_requestInfo.ReplyWithImage(imagePath);
		}


		public void Failed(string text)
		{
			//Debug.WriteLine(this.Requestinfo.LocalPathWithoutQuery+": "+text);
			_requestInfo.ContentType = "text/plain";
			_requestInfo.WriteError(503,text);
		}

		public static bool Handle(EndpointRegistration endpointRegistration, IRequestInfo info, CollectionSettings collectionSettings, Book.Book currentBook)
		{
			var request = new ApiRequest(info, collectionSettings, currentBook);
			try
			{
				if(Program.RunningUnitTests) 
				{
					endpointRegistration.Handler(request);
				}
				else
				{
					var formForSynchronizing = Application.OpenForms.Cast<Form>().Last();
					if (endpointRegistration.HandleOnUIThread && formForSynchronizing.InvokeRequired)
					{
						InvokeWithErrorHandling(endpointRegistration, formForSynchronizing, request, info.RawUrl);
					}
					else
					{
						endpointRegistration.Handler(request);
					}
				}
				if(!info.HaveOutput)
				{
					throw new ApplicationException(string.Format("The EndpointHandler for {0} never called a Succeeded(), Failed(), or ReplyWith() Function.", info.RawUrl.ToString()));
				}
			}
			catch (Exception e)
			{
				SIL.Reporting.ErrorReport.ReportNonFatalExceptionWithMessage(e, info.RawUrl);
				return false;
			}
			return true;
		}

		// If you just Invoke(), the stack trace of any generated exception gets lost.
		// The stacktrace instead just ends with the invoke(), which isn't useful. So here we wrap
		// the call to the handler in a delegate that catches the exception and saves it
		// in our local scope, where we can then use it for error reporting.
		private static bool InvokeWithErrorHandling(EndpointRegistration endpointRegistration,
			Form formForSynchronizing, ApiRequest request, string extraErrorInfo)
		{
			Exception handlerException = null;
			formForSynchronizing.Invoke(new Action<ApiRequest>((req) =>
			{
				try
				{
					endpointRegistration.Handler(req);
				}
				catch (Exception error)
				{
					handlerException = error;
				}
			}), request);
			if (handlerException != null)
			{
				ExceptionDispatchInfo.Capture(handlerException).Throw();
			}
			return true;
		}

		public UrlPathString RequiredFileNameOrPath(string name)
		{
			if (Parameters.AllKeys.Contains(name))
				return UrlPathString.CreateFromUnencodedString(Parameters[name]);
			throw new ApplicationException("The query " + _requestInfo.RawUrl + " should have parameter " + name);
		}

		public string RequiredParam(string name)
		{
			if (Parameters.AllKeys.Contains(name))
				return Parameters[name];
			throw new ApplicationException("The query " + _requestInfo.RawUrl + " should have parameter " + name);
		}
		public string RequiredPostJson()
		{
			Debug.Assert(_requestInfo.HttpMethod==HttpMethods.Post);
			var json = _requestInfo.GetPostJson();
			if (!string.IsNullOrWhiteSpace(json))
			{
				return json;
			}
			throw new ApplicationException("The query " + _requestInfo.RawUrl + " should have post json");
{'fraction_non_alphanumeric': 0.08990342405618965, 'fraction_numerical': 0.0010535557506584723, 'mean_word_length': 4.011438627364716, 'pattern_counts': {'":': 8, '<': 18, '<?xml version=': 0, '>': 19, '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': '793570', 'n_tokens_mistral': 3689, 'n_tokens_neox': 3110, 'n_words': 905}