%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 1997-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
%% Implements  The WWW Common Gateway Interface Version 1.1

-module(mod_cgi).
-moduledoc false.

-export([env/3]).

%%% Callback API
-export([do/1, store/2]).

-include("http_internal.hrl").
-include("httpd_internal.hrl").
-include("httpd.hrl").

-define(VMODULE,"CGI").

-define(DEFAULT_CGI_TIMEOUT, 15000).

%%%=========================================================================
%%%  API
%%%=========================================================================
%%--------------------------------------------------------------------------
%% do(ModData, _, AfterScript) ->  [{EnvVariable, Value}]
%%                
%%     AfterScript = string()
%%     ModData = #mod{}
%%     EnvVariable = string() 
%%     Value = term()
%% Description: Keep for now as it is documented in the man page
%%-------------------------------------------------------------------------
env(ModData, _Script, AfterScript) ->
    ScriptElements = script_elements(ModData, AfterScript),
    httpd_script_env:create_env(cgi, ModData, ScriptElements).

%%%=========================================================================
%%%  Callback API
%%%=========================================================================

%%--------------------------------------------------------------------------
%% do(ModData) -> {proceed, OldData} | {proceed, NewData} | {break, NewData} 
%%                | done
%%     ModData = #mod{}
%%
%% Description:  See httpd(3) ESWAPI CALLBACK FUNCTIONS
%%-------------------------------------------------------------------------
do(ModData) ->
    case proplists:get_value(status, ModData#mod.data) of
	%% A status code has been generated!
	{_StatusCode, _PhraseArgs, _Reason} ->
	    {proceed, ModData#mod.data};
	%% No status code has been generated!
	undefined ->
	    case proplists:get_value(response, ModData#mod.data) of
		undefined ->
		    generate_response(ModData);
		_Response ->
		    {proceed, ModData#mod.data}
	    end
    end.

%%--------------------------------------------------------------------------
%% store(Directive, DirectiveList) -> {ok, NewDirective} | 
%%                                    {ok, [NewDirective]} |
%%                                    {error, Reason} 
%% Directive = {DirectiveKey , DirectiveValue}
%% DirectiveKey = DirectiveValue = term()
%% Reason = term() 
%%
%% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS
%%-------------------------------------------------------------------------
store({script_nocache, Value} = Conf, _) 
  when Value == true; Value == false ->
    {ok, Conf};
store({script_nocache, Value}, _) ->
    {error, {wrong_type, {script_nocache, Value}}};
store({script_timeout, Value}, _) 
  when is_integer(Value), Value >= 0 ->
    {ok, {script_timeout, Value * 1000}};
store({script_timeout, Value}, _) ->
    {error, {wrong_type, {script_timeout, Value}}}.
	
%%%========================================================================
%%% Internal functions
%%%========================================================================
generate_response(ModData) ->
    RequestURI =
	case proplists:get_value(new_request_uri, ModData#mod.data) of
	    undefined ->
		ModData#mod.request_uri;
	    Value ->
		Value
	end,
    ScriptAliases =
	httpd_util:multi_lookup(ModData#mod.config_db, script_alias),
    case mod_alias:real_script_name(ModData#mod.config_db, RequestURI,
				    ScriptAliases) of
	{Script, AfterScript} ->
	    exec_script(ModData, Script, AfterScript, 
			RequestURI);
	not_a_script ->
	    {proceed, ModData#mod.data}
    end.

is_executable(File) ->
    Dir      = filename:dirname(File),
    FileName = filename:basename(File),
    case os:type() of
	{win32,_} ->
	    %% temporary (hopefully) fix for win32 OTP-3627
	    is_win32_executable(Dir,FileName);
	_ ->
	    is_executable(Dir, FileName) 
    end.

is_executable(Dir, FilName) ->
    case os:find_executable(FilName, Dir) of
	false ->
	    false;
	_ ->
	    true
    end.

%% Start temporary (hopefully) fix for win32 OTP-3627
%% ---------------------------------
is_win32_executable(Dir, FileName) ->
    NewFileName = strip_extention(FileName, [".bat",".exe",".com", ".cmd"]),
    is_executable(Dir, NewFileName).

strip_extention(FileName, []) ->
    FileName;
strip_extention(FileName, [Extention | Extentions]) ->
    case filename:basename(FileName, Extention) of
	FileName ->
	    strip_extention(FileName, Extentions); 
	NewFileName ->
	    NewFileName
    end.

%% End fix
%% ---------------------------------

exec_script(ModData, Script, AfterScript, RequestURI) ->
    exec_script(is_executable(Script), ModData, Script, 
		AfterScript, RequestURI).

exec_script(true, ModData, Script, AfterScript, _RequestURI) ->
    process_flag(trap_exit,true),
    Dir  = filename:dirname(Script),
    ScriptElements = script_elements(ModData, AfterScript),
    Env = (catch httpd_script_env:create_env(cgi, ModData, ScriptElements)),

    %% Run script
    Port = (catch open_port({spawn, Script},[binary, stream,
					     {cd, Dir}, {env, Env}])),
    case Port of
	Port when is_port(Port) ->
	    send_request_body_to_script(ModData, Port),
	    deliver_webpage(ModData, Port); % Take care of script output
	Error ->
	    exit({open_port_failed, Error,
		  [{mod,?MODULE},
		   {uri,ModData#mod.request_uri}, {script,Script},
		   {env,Env},{dir,Dir}]})
    end;

exec_script(false, ModData, _Script, _AfterScript, _RequestURI) ->
    {proceed,
     [{status,
       {404,ModData#mod.request_uri,
	?NICE("You don't have permission to execute " ++
	      ModData#mod.request_uri ++ " on this server")}}|
      ModData#mod.data]}.
    
send_request_body_to_script(ModData, Port) ->
    case ModData#mod.entity_body of
	[] ->
	    ok;
	EntityBody ->
	    port_command(Port, EntityBody)
    end.	
	   
deliver_webpage(#mod{config_db = Db} = ModData, Port) ->
    Timeout = script_timeout(Db),    
    case receive_headers(Port, httpd_cgi, parse_headers, 
			 [<<>>, [], []], Timeout) of
	{Headers, Body} ->
	    case httpd_cgi:handle_headers(Headers) of
		{proceed, AbsPath} ->
		    {proceed, [{real_name, 
				httpd_util:split_path(AbsPath)} | 
			       ModData#mod.data]};
		{ok, HTTPHeaders, Status} when is_binary(Body)->
		    IsDisableChunkedSend = 
			httpd_response:is_disable_chunked_send(Db),
		    case (ModData#mod.http_version =/= "HTTP/1.1") or 
			(IsDisableChunkedSend) of
			true ->
			    send_headers(ModData, Status, 
					 [{"connection", "close"}
					   | HTTPHeaders]);
			false ->
			    send_headers(ModData, Status,
					 [{"transfer-encoding",
					   "chunked"} | HTTPHeaders])
		    end,
		    handle_body(Port, ModData, Body, Timeout, byte_size(Body),
				IsDisableChunkedSend)
	    end;
	{'EXIT', Port, Reason} ->
	    process_flag(trap_exit, false),
	    {proceed, [{status, {400, none, reason(Reason)}} |
		       ModData#mod.data]};
	timeout ->
	    (catch port_close(Port)), % KILL the port !!!!
	    send_headers(ModData, {504, "Timeout"}, []),
	    httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket),
	    process_flag(trap_exit,false),
	    {proceed,[{response, {already_sent, 200, 0}} | ModData#mod.data]}
    end.
	    
receive_headers(Port, Module, Function, Args, Timeout) ->
      receive
	  {Port, {data, Response}} when is_port(Port) ->
	      case Module:Function([Response | Args]) of
		  {NewModule, NewFunction, NewArgs} ->
		      receive_headers(Port, NewModule, 
				      NewFunction, NewArgs, Timeout);
		  {ok, {Headers, Body}} ->
		      {Headers, Body}
	      end;
	  {'EXIT', Port, Reason} when is_port(Port) ->
	      {'EXIT', Port, Reason};
	  {'EXIT', Pid, Reason} when is_pid(Pid) ->
	      exit({linked_process_died, Pid, Reason})
      after Timeout ->
	      timeout
      end.

send_headers(ModData, {StatusCode, _}, HTTPHeaders) ->
    ExtraHeaders = httpd_response:cache_headers(ModData, script_nocache),
    httpd_response:send_header(ModData, StatusCode, 
			       ExtraHeaders ++ HTTPHeaders).

handle_body(Port, #mod{method = "HEAD"} = ModData, _, _, Size, _) ->
    (catch port_close(Port)), % KILL the port !!!!
    process_flag(trap_exit,false),
    {proceed, [{response, {already_sent, 200, Size}} | ModData#mod.data]};

handle_body(Port, ModData, Body, Timeout, Size, IsDisableChunkedSend) ->
    httpd_response:send_chunk(ModData, Body, IsDisableChunkedSend),
    receive 
	{Port, {data, Data}} when is_port(Port), is_binary(Data) ->
	    handle_body(Port, ModData, Data, Timeout, Size + byte_size(Data),
			IsDisableChunkedSend);
	{'EXIT', Port, normal} when is_port(Port) ->
	    httpd_response:send_final_chunk(ModData, IsDisableChunkedSend),
	    process_flag(trap_exit,false),
	    {proceed, [{response, {already_sent, 200, Size}} |
		       ModData#mod.data]};
	{'EXIT', Port, Reason} when is_port(Port) ->
	    process_flag(trap_exit, false),
	    {proceed, [{status, {400, none, reason(Reason)}} | 
		       ModData#mod.data]};
	{'EXIT', Pid, Reason} when is_pid(Pid) ->
	    exit({mod_cgi_linked_process_died, Pid, Reason})
    after Timeout ->
	    (catch port_close(Port)), % KILL the port !!!!
	    process_flag(trap_exit,false),
	    {proceed,[{response, {already_sent, 200, Size}} |
		      ModData#mod.data]}
    end.

script_elements(#mod{method = "GET"}, {[], QueryString}) ->
    [{query_string, QueryString}];
script_elements(#mod{method = "GET"}, {PathInfo, []}) ->
    [{path_info, PathInfo}];
script_elements(#mod{method = "GET"}, {PathInfo, QueryString}) ->
    [{query_string, QueryString}, {path_info, PathInfo}];
script_elements(#mod{method = "POST", entity_body = Body}, _) ->
    [{entity_body, Body}];
script_elements(#mod{method = "PATCH", entity_body = Body}, _) ->
    [{entity_body, Body}];
script_elements(#mod{method = "PUT", entity_body = Body}, _) ->
    [{entity_body, Body}];
script_elements(_, _) ->
    [].

script_timeout(Db) ->
    httpd_util:lookup(Db, script_timeout, ?DEFAULT_CGI_TIMEOUT).

%% Convert error to printable string
%%
reason({error,emfile})     -> ": To many open files";
reason({error,{enfile,_}}) -> ": File/port table overflow";
reason({error,enomem})     -> ": Not enough memory";
reason({error,eagain})     -> ": No more available OS processes";
reason(Reason) -> lists:flatten(io_lib:format("Reason: ~p~n", [Reason])).
