/*
 * Caudium - An extensible World Wide Web server
 * Copyright © 2000-2004 The Caudium Group
 * Copyright © 1994-2001 Roxen Internet Software
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

//
//! module: PHP Script Support
//!  This module allows Caudium users to run PHP scripts,
//!  optionally with RXML post-parsing. Note that this requires
//!  that PHP4 is compiled with Caudium support.
//! inherits: module
//! inherits: caudiumlib
//! type: MODULE_FILE_EXTENSION
//! cvs_version: $Id: php4.pike,v 1.25.2.3 2004/01/15 01:58:11 grendel Exp $
//

#include <module.h>
#include <variables.h>
inherit "module";
inherit "caudiumlib";
//#define PHP_DEBUG
#ifdef PHP_DEBUG
#define DWERROR(X)	report_debug(X)
#else /* !PHP_DEBUG */
#define DWERROR(X)
#endif /* PHP_DEBUG */

#include <roxen.h>

constant cvs_version="$Id: php4.pike,v 1.25.2.3 2004/01/15 01:58:11 grendel Exp $";
constant thread_safe=1;

constant module_type = MODULE_FILE_EXTENSION;
constant module_name = "PHP Script Support";
constant module_doc  = "This module allows Caudium users to run PHP scripts, \
optionally with RXML post-parsing. See <a href=\"http://\
www.php.net/\">www.php.net</a> for further information about \
PHP. <font color=\"red\">\
<p>A name conflict exists between Pike and PHP. This problem is fixed in \
the Pike CVS as of November 6th, 2000, 13:30 PST. Please make sure that your \
Pike is newer than this. </p></font>";
constant module_unique = 0;

#if constant(PHP4.Interpreter)
class PHPScript
{
  PHP4.Interpreter interpreter;
  string command;
  string buffer="";
  // stderr is handled by run().
  mapping (string:string) environment;
  int written, close_when_done;
  object mid;
  void done(int sent)
  {
    DWERROR("PHP:Wrapper::done()\n");
    if(intp(sent)) written += sent;
    if(strlen(buffer))
    {
      close_when_done = 1;
      if(QUERY(rxml))
      {
        if( mid )
          buffer = parse_rxml(buffer, mid);
	/* Loop until all data is written */
	/* If this is not done, not all data is send to the client (only with big pages) */
	/* One solution is: Use not "Parse RXML" */
	/* If you have a better solution contact us */
        close_when_done = 0;
	do {
		write_callback();
	} while(strlen(buffer)>0);
	destruct();
      }
    } else
      destruct();
  }

  void destroy()
  { 
   if( mid )
    {
      mid->file = ([ "len": written, "raw":1 ]);
      mid->pipe = 0;
      mid->do_not_disconnect = 0;
      mid->do_log();
    }
  }

  void write_callback()
  {
    DWERROR("PHP:Wrapper::write_callback()\n");
    if(!strlen(buffer))
      return;
    int nelems;
    array err = catch { nelems = mid->my_fd->write(buffer); };
    DWERROR(sprintf("PHP:Wrapper::write_callback(): write(%O) => %d\n",
		    buffer, nelems));
    if(err) werror(describe_backtrace(err));
    if( err || nelems < 0 )
    // if nelems == 0, network buffer is full. We still want to continue.
    {
      buffer="";
      close_when_done = -1;
    }
    else if(nelems>0) {
      written += nelems;
      buffer = buffer[nelems..];
      DWERROR(sprintf("Done: %d %d...\n", strlen(buffer), close_when_done));
      if(close_when_done && !strlen(buffer)) {
	destruct();
      }
    }
  }

  int write( string what )
  {
    DWERROR(sprintf("PHP:Wrapper::write(%O)\n", what));
    if(close_when_done == -1) // Remote closed
      return -1;
    if(buffer == "" )
    {
      buffer = what;
      if(!QUERY(rxml)) write_callback();
    }
    else
      buffer += what;
    return strlen(what);
  }

  void send_headers(int code, mapping headers)
  {
    DWERROR(sprintf("PHP:PHPWrapper::send_headers(%d,%O)\n", code, headers));
    string result = "", post="";
    string retcode = errors[code||200];
    int ct_received = 0, sv_received = 0;
    if(headers) {
      foreach(indices(headers), string header)
      {
	string value = headers[header];
	if(!header || !value)
	{
	  // Heavy DWIM. For persons who forget about headers altogether.
	  continue;
	}
	header = String.trim_whites(header);
	foreach(value / "\0", string realvalue) {
	  realvalue = String.trim_whites(realvalue);
	  switch(lower_case( header ))
	  {
	  case "status":
	    retcode = realvalue;
	    break;

	  case "content-type":
	    ct_received=1;
	    result += header+": "+realvalue+"\r\n";
	    break;
	    
	  case "server":
	    sv_received=1;
	    result += header+": "+realvalue+"\r\n";
	    break;

	  case "location":
	    retcode = "302 Redirection";
	    result += header+": "+realvalue+"\r\n";
	    break;

	  default:
	    result += header+": "+realvalue+"\r\n";
	    break;
	  }
	}
      }
    }
    if(!sv_received)
      result += "Server: "+caudium.version()+"\r\n";
    if(!ct_received)
      result += "Content-Type: text/html\r\n";
    write("HTTP/1.0 "+retcode+"\r\n"+result+"\r\n");
  }

  PHPScript run()
  {
    DWERROR("PHP:PHPScript::run()\n");
    mapping options = ([
      "env":environment,
    ]);

    if(!QUERY(rxml))
    {
      mid->my_fd->set_blocking();
      if(_typeof(mid->my_fd) == _typeof(Stdio.File()))
	options->my_fd = mid->my_fd;
    }

    mid->my_fd->set_close_callback(done);
    //    werror("%O\n", options);
    interpreter->run(command, options, this_object(), done);
    return this_object();
  }
  int post_sent;
  string read_post(int length)
  {
    if(!mid->data) return 0;
    string data = mid->data[post_sent..post_sent+length-1];
    post_sent += strlen(data);
    //    werror("%s\n", data);
    return data;
  }
  
  void create( object id )
  {
    DWERROR("PHP:PHPScript()\n");
    interpreter = PHP4.Interpreter();
    mid = id;
    if(!id->realfile)
    {
      id->realfile = id->conf->real_file( id->not_query, id );
      if(!id->realfile)
        error("No real file associated with "+id->not_query+
              ", thus it's not possible to run it as a PHP script.\n");
    }
    command = id->realfile;

    environment =([]);
    environment |= global_env;
    environment |= build_env_vars( id->realfile, id, id->misc->path_info );
    environment |= build_caudium_env_vars(id);
    if(id->misc->ssi_env)     	environment |= id->misc->ssi_env;
    if(id->misc->is_redirected) environment["REDIRECT_STATUS"] = "1";
    if(id->rawauth) { 
      environment["HTTP_AUTHORIZATION"] = (string)id->rawauth;
      environment["AUTH_TYPE"] = (id->rawauth/" ")[0];
    }
    if(id->realauth) {
      array utmp = id->realauth / ":";
      if(sizeof(utmp) >= 2)
      {
	environment["REMOTE_USER"] = utmp[0];
	/* password can have colon in it. */
	environment["REMOTE_PASSWORD"] = utmp[1..] * ":"; 
      }
    }
    // Lets populate more!
    environment["REQUEST_URI"] =  id->raw_url;
    environment["PHP_SELF"]    =  environment["DOCUMENT_URI"];

    // Not part of the "standard" PHP environment apparently...
    m_delete(environment, "DOCUMENT_URI");
    
    if(id->misc->user_document_root)
      environment["DOCUMENT_ROOT"] = id->misc->user_document_root;
  }
}

mapping(string:string) global_env = ([]);
object thfarm; 
void stop() {
}

void start(int n, object conf)
{
  DWERROR("PHP:start()\n");
  thfarm = caudium->query_var("php_thread_farm");
  if(!thfarm) {
    thfarm = ThreadFarm.Farm(QUERY(threads));
    caudium->set_var("php_thread_farm", thfarm);
  } else {
    thfarm->resize(QUERY(threads));
  }

#if 0
#ifndef THREADS
  // Ugly? Yes, definitely. Required? Yes. If there is only one thread
  // the interpreter lock will never be released (naturally) and thus the
  // th_farm threads can never lock it which is required.
  thread_create(lambda() { catch { while(this_object()) sleep(5); }; });
#endif
#endif
  module_dependencies(conf, ({ "pathinfo" }));
  if(conf)
  {
    string tmp=conf->query("MyWorldLocation");
    sscanf(tmp, "%*s//%s", tmp);
    sscanf(tmp, "%s:", tmp);
    sscanf(tmp, "%s/", tmp);
    global_env["SERVER_NAME"]=tmp;
    global_env["SERVER_SOFTWARE"]=roxen.version();
    global_env["GATEWAY_INTERFACE"]="PHP/1.1";
    global_env["SERVER_PROTOCOL"]="HTTP/1.0";
    global_env["SERVER_URL"]=conf->query("MyWorldLocation");

    array us = ({0,0});
    foreach(query("extra_env")/"\n", tmp)
      if(sscanf(tmp, "%s=%s", us[0], us[1])==2)
        global_env[us[0]] = us[1];
  }
}

int|mapping handle_file_extension(object o, string e, object id)
{
  DWERROR("PHP:handle_file_extension()\n");
  id->do_not_disconnect = 1;
  // call_out required or the script might actually finish too early
  // causing ugly, but harmless, backtraces.
  call_out(thfarm->enqueue, 0, PHPScript(id)->run);
  //  PHPScript(id)->run();
  DWERROR("PHP:handle_file_extension done\n");
  return http_pipe_in_progress();
}
#else

array register_module() {
  return ({  module_type, 
	     module_name,
	     module_doc + status(),
	     0, 0
  });
}

// Do not dump to a .o file if no PHP4 is available, since it will then
// not be possible to get it later on without removal of the .o file.
constant dont_dump_program = 1; 

string status()
{
  return
    "<p><font color=\"red\">The PHP4 interpreter isn't available."
    "To get PHP4 installed:"
    "<ol>"
    "<li> Check php4 out from CVS or download the release from "
    "<a href=\"http://www.php.net/downloads.php\">http://us.php.net/downloads.php</a>. Please note that you need version 4.0.4-dev (as of 2000-11-02) or newer. "
    "See <a target=\"new\" href=\"http://www.php.net/anoncvs.php\">the PHP4 CVS instructions</a></li>"
    "<li> Configure php4 with --with-caudium="+getcwd()+"</li>"
    "<li> Make and install php4</li>"
    "<li> Restart Caudium</li>"
    "</ol></font></p>";
}

int|mapping handle_file_extension(object o, string e, object id)
{
  return http_string_answer( status(), "text/html" );
}
#endif

array (string) query_file_extensions()
{
  return query("ext");
}

void create(object conf)
{
  defvar("rxml", 0, "Parse RXML in PHP-scripts", TYPE_FLAG,
	 "If this is set, the output from PHP-scripts handled by this "
         "module will be RXMl parsed. NOTE: No data will be returned to the "
         "client until the PHP-script is fully parsed.");
  defvar("threads", 5, "PHP Handler Threads", TYPE_INT,
	 "The number of handler threads to use for running PHP scripts. "
	 "This variable is somewhat special. The created thread pool is "
	 "shared among different instances of the PHP4 module (across virtual "
	 "servers even). The thread pool size will be that of the "
	 "largest value in all PHP4 module instances.");

  defvar("extra_env", "", "Extra environment variables", TYPE_TEXT_FIELD,
	 "Extra variables to be sent to the script, format:<pre>"
	 "NAME=value<br>"
	 "NAME=value"
	 "</pre>"
	 "You can also change variables which are in php.ini just prepend PHP_:<BR>"
	 "You need a patch for php which is available at http://caudium.net/download/source/php-4-caudium-phpini.patch"
         " (to be used with php 4.2.2 and more recent)"
	 "<pre>"
	 "PHP_auto_prepend_file=..../header.php<BR>"
	 "PHP_allow_url_fopen=0<BR>"
	 "</pre>Please note that the standard variables will have higher "
	 "priority.");

  defvar("ext",
	 ({"php", "php3", "php4"}),
         "PHP-script extensions", TYPE_STRING_LIST,
         "All files ending with these extensions, will be parsed as "+
	 "PHP-scripts.");
}

/* START AUTOGENERATED DEFVAR DOCS */

//! defvar: rxml
//! If this is set, the output from PHP-scripts handled by this module will be RXMl parsed. NOTE: No data will be returned to the client until the PHP-script is fully parsed.
//!  type: TYPE_FLAG
//!  name: Parse RXML in PHP-scripts
//
//! defvar: threads
//! The number of handler threads to use for running PHP scripts. This variable is somewhat special. The created thread pool is shared among different instances of the PHP4 module (across virtual servers even). The thread pool size will be that of the largest value in all PHP4 module instances.
//!  type: TYPE_INT
//!  name: PHP Handler Threads
//
//! defvar: extra_env
//! Extra variables to be sent to the script, format:<pre>NAME=value<br />NAME=value</pre>Please note that the standard variables will have higher priority.
//!  type: TYPE_TEXT_FIELD
//!  name: Extra environment variables
//
//! defvar: ext
//! All files ending with these extensions, will be parsed as 
//!  type: TYPE_STRING_LIST
//!  name: PHP-script extensions
//
