=begin

= NAME

Ruby/ASP - Active Server Pages for Apache with mod_ruby

= SYNOPSIS

# .htaccess
AuthName "Ruby/ASP"

RubySetEnv Global /var/www/tests/ruby-asp/global-asa

RubyRequire apache/aspruby-run
<Files ~ (\.asp)>
  SetHandler ruby-object
  RubyHandler Apache::AspRun.instance
</Files>

= DESCRIPTION

    Ruby-ASP provides an Active Server Pages port to the Apache Web Server with 
    Ruby scripting only, and enables developing of dynamic web applications with 
    session management and embedded Ruby code.

= WEBSITE

    The Ruby/ASP web site is at http://www.webtime-project.net/projects.asp. 
    More informations can be found at http://www.webtime-project.net/ruby-asp/index.en.html.

= INSTALL

Ruby/Asp require libxml, libxslt, mod_ruby and (of course) Ruby ;)

Install Ruby/Asp like this:

    $ tar zxvf ruby-asp_x.y.z.tar.gz
    $ cd ruby-asp
    $ ruby install.rb config 
    $ ruby install.rb setup 
    # ruby install.rb install

Add these lines to httpd.conf or in a .htaccess file: 

    RubySetEnv Global /var/www/global-asa
    RubyRequire apache/aspruby-run
    <Files ~ (\.asp)>
      SetHandler ruby-object
      RubyHandler Apache::AspRun.instance
    </Files>

= CONFIG

Global
    Global is the nerve center of a Ruby/Asp application, in which the global.asa 
    may reside defining the web application's event handlers.

MailHost
    The mail host is the smtp server that the below Mail config directives will 
    use when sending their emails. Default : localhost.

MailPort
    SMTP port. Default : 25.

MailErrorsFrom
    Default NONE, set this to specify the default mail address placed in the 
    From: mail header for server errors.

MailErrorsTo
    No default, if set, ASP server errors, error code 500, that result while 
    compiling or running scripts under Ruby/Asp will automatically be emailed to 
    the email address set for this config.

ErrorFile
    URI for error redirection.

StateDir
    State files for ASP application go to this directory. Default : /tmp

Debug
    Enable or disable debug trace. Default : 1

= Class Reference 
== $Session

    $Session[key]
    
    $Session[key] = value
    
    $Session.timeout( minutes )
        Timeout property, if minutes is being assigned, sets this default timeout for 
        the user session.

    $Session.sessionID
        SessionID property, returns the id for the current session, which is exchanged 
        between the client and the server as a cookie.

    $Session.abandon
        The abandon method times out the session immediately.

== $Request

    $Request.form( data )
        Returns the value of the input of name used in a form with POST method.

    $Request.queryString( data )
        Returns the value of the input of name used in a form with GET method.

    $Request.cookies( name, [key] )
        Returns the value of the Cookie with name. If a key is specified, then 
        a lookup will be done on the cookie as if it were a query string.

    $Request.serverVariables( name )
        Returns the value of the server variable / environment variable with name
        name. If name is not specified, returns a ref to a hash of all the server
        / environment variables data. The following would be a common use of this
        method:                                                                                                                          
                                                                                                                                               
          env = $Request.serverVariables();
          # env here would be equivalent to the ENV.
    
    $Request.method
        API extension. Returns the client HTTP request method, as in GET or POST.
    
    $Request.totalBytes
        The amount of data sent by the client in the body of the request, usually
        the length of the form data. This is the same value as
        $Request.serverVariables('CONTENT_LENGTH')


== $Response

    $Response.write( string )
        Write output to the HTML page.

    $Response.addHeader( key, value )
        Adds a custom header to a web page.

    $Response.appendToLog( message )
        Adds message to the server log.

    $Response.end
        Sends result to client, and immediately exits script.

    $Response.include( filename, *args )
        This API extension calls the routine compiled from asp script in filename 
        with the args *args.

    $Response.trapInclude( filename, *args )
        Calls $Response.include() with same arguments as passed to it, but instead 
        traps the include output buffer and returns it.

    $Response.redirect( url )
        Sends the client a command to go to a different url.

    $Response.cookies( name, [key], value )
        Sets the key or attribute of cookie with name name to the value value.

    $Response.charset = value
        This member when set appends itself to the value of the Content-Type HTTP 
        header.

    $Response.contentType = value
        Sets the MIME type for the current response being sent to the client.

    $Response.expires = time
        Sends a response header to the client indicating the time in SECONDS in 
        which the document should expire.

    $Response.expiresAbsolute. = date
        Sends a response header to the client with date being an absolute time 
        to expire.

== $Server

    $Server.file( )
        Returns the absolute file path to current executing script.

    $Server.html_encode( string )
        Returns an HTML escapes version of string. &, ", >, <, are each escapes 
        with their HTML equivalents.

    $Server.mapPath( url )
        Given the url, absolute, or relative to the current executing script, 
        this method returns the equivalent filename that the server would 
        translate the request to, regardless or whether the request would be 
        valid.

    $Server.mail( hMailOps, hSMTPOps )
        API extension to send email.

    $Server.url_encode( string )
        Returns the URL-escaped version of the string.

    $Server.url( url, hParams )
        Will return a URL with hParams serialized into a query string.

    $Server.xslt( xsl_data, xml_data )
        This method takes string for XSL and XML data and returns the XSLT 
        output as a string.

    $Server.execute( filename, *args )
        Same as $Response.include( filename, *args ).

    $Server.transfer( filename, *args )
        Transfers control to another script. Same as 
            $Response.include( filename, *args ); $Response.end.
    
== $Application
    
    $Application
        Like the $Session object, you may use the $Application object to store 
        data across the entire life of the application.
    
== Asp::Common
    
    Asp::Common::version

    Asp::Common::sessionID

    Asp::Common::url_decode( value )

    Asp::Common::url_encode( s )

    Asp::Common::html_escape( s )

    Asp::Common::time2str( time )

    Asp::Common::str2time( time ) (TODO)

    Asp::Common::require( file )

    Asp::Common::stateDir

    Asp::Common::globalASA

= Events

    The ASP platform allows developers to create Web Applications. In fulfillment
    of real software requirements, ASP allows event-triggered actions to be taken,
    which are defined in a global.asa file. The global.asa file resides in the
    Global directory, defined as a config option, and may define the following
    actions:
    
      Action              Event
      ------              ------
      script_OnStart *    Beginning of Script execution
      script_OnEnd *      End of Script execution
      application_OnStart Beginning of Application
      application_OnEnd   End of Application
      session_OnStart     Beginning of user Session.
      session_OnEnd       End of user Session.
    
      (*) These are API extensions that are not portable, but were
          added because they are incredibly useful
    
    These actions must be defined in the $Global/global.asa file as subroutines,
    for example:
    
      def session_OnStart
          $Application[$Session.sessionID] = started;
      end
    
    Sessions are easy to understand. When visiting a page in a web application,
    each user has one unique $Session. This session expires, after which the user
    will have a new $Session upon revisiting.
    
    A web application starts when the user visits a page in that application, and
    has a new $Session created. Right before the first $Session is created, the
    $Application is created. When the last user $Session expires, that $Application
    expires also. For some web applications that are always busy, the
    application_OnEnd event may never occur.
    
== script_OnStart & script_OnEnd
    
    The script events are used to run any code for all scripts in an application
    defined by a global.asa. Often, you would like to run the same code for every
    script, which you would otherwise have to add by hand, or add with a file include,
    but with these events, just add your code to the global.asa, and it will be run.
    
    There is one caveat. Code in script_OnEnd is not guaranteed to be run when
    $Response.end() is called, since the program execution ends immediately at this
    event.
    
== session_OnStart
    
    Triggered by the beginning of a user's session, session_OnStart gets run when
    the user's acces the $Session object for the first time.
    
    The session_OnStart is particularly useful for caching database data, and avoids
    having the caching handled by clumsy code inserted into each script being
    executed.
    
== session_OnEnd
    
    Triggered by a user session ending, session_OnEnd can be useful for cleaning up
    and analyzing user data accumulated during a session.
    
    Sessions end when the session timeout expires. The timing of the session_OnEnd
    occur immediately after the session times out.
    
    Thus I would not put anything mission-critical in the session_OnEnd, just stuff
    that would be nice to run whenever it gets run.
    
== application_OnStart
    
    TODO
    
    This event marks the beginning of an ASP application, and is run just before the
    session_OnStart of the first Session of an application. This event is useful to
    load up $Application with data that will be used in all user sessions.
    
== application_OnEnd
    
    TODO
    
    The end of the application is marked by this event, which is run after the last
    user session has timed out for a given ASP application.

= XMLSubsMatch

    Before XML, there was the need to make HTML markup smarter. Ruby/ASP gives 
    you the ability to have a Ruby subroutine handle the execution of any 
    predefined tag, taking the tag descriptors, and the text contained between, 
    as block of the subroutine. This custom tag technology can be used to extend 
    a web developer's abilities to add dynamic pieces without having to visibly 
    use <% %> style code entries.
    
    So, lets say that you have a table that you want to insert for an employee 
    with contact info and the like, you could set up a tag like:
    
    <my:new-employee name="Jane" last="Doe" phone="555-2222">
      Jane Doe has been here since 1998.
    </my:new-employee>
    
    Any colons, ':', in the XML custom tag will turn into '::', a Ruby module 
    separator, so the my:employee tag would translate to the my::employee 
    subroutine, or the employee subroutine in the my module. Any dash "-" will 
    also be translated to an underscore "_", as dash is not valid in the names 
    of Ruby subroutines.
    
    Then you would create the my::employee subroutine in the my ruby module or 
    whereever like so:
    
    module My
      def My::test( h )
        print "<B>In My::Test</B><BR>\n"
        yield if block_given?
        h.each_pair { |k,v|
          print "#{k} =&gt; #{v}<BR>\n"
        }
      end
    end
    
    Though XML purists would not like this custom tag technology to be related 
    to XML, the reality is that a careful site engineer could render full XML 
    documents with this technology, applying all the correct styles that one 
    might otherwise do with XSLT.
    
    Custom tags defined in this way can be used as XML tags are defined with 
    both a body and without as it
    
    <my:new-employee>...</my:new-employee>
    
    and just
    
    <my:new-employee />
    
    These tags are very powerful in that they can also enclose normal ASP logic, 
    like:
    
    <my:new-employee>
      <!-- normal ASP logic -->
      <% birthday = Time::new %>
    
      <!-- ASP inserts -->
      This employee has been online for <%= int(rand()*600)+1 %>
      seconds, and was born near <%= birthday %>.
    </my:new-employee>   
    
    For an example of this custom XML tagging in action, please check out the 
    tests/subsmatch.asp script.

= SEE ALSO

    Ruby, mod_ruby

= LICENCE

    Copyright (C) 2003, 2004 Gregoire Lejeune <gregoire.lejeune@free.fr>
    
    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., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA

=end

require "singleton"
require "tempfile"
require "net/smtp"
require "asp/common"
require "asp/compile"
require "asp/globalasa"
require "asp/response"
require "asp/request"
require "asp/session"
require "asp/application"
require "asp/server"
require "asp/asperror"
#require "asp/goto"

module Apache
  class AspRun
    include Singleton

    ##
    # Method: rCod = handler( oRequest )
    # Type  : public
    # Action: handler method
    # In    : Apache::Request object
    # Out   : handler status return code
    #
    def handler( oRequest )
        ## Act: Verify allowed request method
      if oRequest.method_number == M_OPTIONS
        oRequest.allowed |= (1 << M_GET)
        oRequest.allowed |= (1 << M_POST)
        return DECLINED
      end
      
        ## Act: Verify the File::Stat object associated with the translated filename of the request.
      if oRequest.finfo.mode == 0
        return NOT_FOUND
      end

        ## Act: Clear the current environment and add CGI and common variables to the subprocess_env table.
      oRequest.setup_cgi_env
      
        ## Act: Get begin time
      oBeginTime = Time::now
      
        ## Act: Add Asp::Common::globalASA path to $:
      $:.push( Asp::Common::globalASA.untaint ).untaint

        ## Var: Run state
      iRunState = 0

        ## Var: Translated physical pathname of the document as determined during the URI translation phase.
      xFileName = oRequest.filename.dup
      xFileName.untaint
      
        ## Act: Get ASP compiled code from file
      xRubyCode = compile( xFileName )
      if xRubyCode.nil? == true
        debug( oRequest, 
               @compiler.error_code, 
               xFileName, 
               iRunState, 
               sprintf( "Line %d : %s", @compiler.error_line, @compiler.error_message ) )
        return OK
      end
      
        ## Act: Initialise Ruby/ASP objects
      __oASPError = Asp::Error::new( )
      __oASPError.source = @compiler.init_code
      __oASPError.file = xFileName
      
      $Server      = Asp::Server::new( oRequest, __oASPError, self )
      $Application = Asp::Application::new( oRequest, __oASPError, self )
      $Session     = Asp::Session::new( oRequest, __oASPError, self )
      $Request     = Asp::Request::new( oRequest, __oASPError, self )
      $Response    = Asp::Response::new( oRequest, __oASPError, self )

        ## Act: Load global.asa
      oGAsa = Asp::GlobalASA::new( Asp::Common::globalASA.untaint, oRequest, self ) 
      if oGAsa.getAsaCode == false 
        debug( oRequest, xRubyCode, xFileName, iRunState, "Can't load global.asa file" )
        return OK
      end

      begin
          ## Act: Change the server's current working directory to the directory part of the specified filename.
        Apache.chdir_file( oRequest.filename )

          ## Rem: Run state set to 1
        iRunState = 1

          ## Act: Get evaluated ruby code
        xBody = get_body( xRubyCode, xFileName )
        
          ## Rem: Run state set to 2
        iRunState = 2
          ## Act: Prerun
        prerun( oRequest )
        
          ## Rem: Run state set to 3
        iRunState = 3

          ## Act: Send output 
        begin
          require "html/ssi"
          oSSI = HTML::SSI::new( )
          print oSSI.parse( xBody )
        rescue LoadError => e
          print xBody
        end
        
        if $Response.bEnd == 0 # and $Server.bEnd == 0
            ## Rem: Run state set to 4
          iRunState = 4
            ## Act: postrun
          postrun( oRequest )
        end

          ## Act: Get begin time
        oEndTime = Time::now
          ## Act: Calculate serve time...
        oServedTime = oEndTime - oBeginTime
          ## Act: ...and print it as HTML comment
        print "<!-- Ruby/ASP v",Asp::Common::version," served page in ",oServedTime," seconds -->"
      rescue NameError, SyntaxError, ZeroDivisionError, RuntimeError, ArgumentError, SecurityError, LoadError, TypeError => e
        debug( oRequest, xRubyCode, xFileName, iRunState, e.message )
      end

      return OK
    end

    ##
    # Method: load_code( xRubyCode, xFileName = nil )
    # Type  : public
    # Action: Evaluate xRubyCode from xFileName
    # In    : Ruby code and file name
    #
    def load_code( xRubyCode, xFileName = nil )
      xRubyCode.untaint
      
      if xFileName != nil
        xFileName.untaint
        $SAFE=1
        eval( xRubyCode, binding(), xFileName.untaint ).untaint
      else
        $SAFE=1
        eval( xRubyCode, binding() ).untaint
      end
    end
    
    ##
    # Method: xOutCode = compile( xFileName )
    # Type  : private
    # Action: Compile file et return output code
    # In    : Filename to compile
    # Return: Compiled code
    #
    def compile( xFileName )
      open( xFileName.untaint ) do |f|
        return @compiler.compile(f.read)
      end
    end

    ## ------------------------------------------------------------------------
    
    private
    def initialize
        ## Act: Get compiler object
      @compiler = AspCompiler.new
        ## Act: Set compiler put command - DEPRECATED, just for comatibility
      @compiler.put_cmd = 'print'
    end

    ##
    # Method: prerun( oRequest )
    # Type  : private
    # Action: made pre-run work
    # In    : Apache::Request - not used yet
    # 
    def prerun( oRequest )
        ## Act: Call script_OnStart from global.asa if exist
      if self.public_methods.member?( "script_OnStart" ) == true
        script_OnStart
      end
    end

    ##
    # Method: xOutCode = get_body( xRubyCode, xFileName )
    # Type  : private
    # Action: Eval code page and return it
    # In    : Code to eval and file name
    #
    def get_body( xRubyCode, xFileName )
        ## Act: untaint code to eval
      xRubyCode.untaint
        ## Act: untaint xFileName
      xFileName.untaint
        ## Act: return evaluate code
      return Asp::Common::redirect { eval(xRubyCode, binding(), xFileName).untaint }
    end

#    ## 
#    # Method: run( xRubyCode, xFileName )
#    # Type  : private
#    # DEPRECATED
#    #
#    def run( xRubyCode, xFileName )
#      xRubyCode.untaint
#      xFileName.untaint
#
#      frame_start
#      label(:a){
#        eval( xRubyCode, binding(), xFileName ).untaint
#      }
#      label(:e) {}
#      frame_end
#    end

    ##
    # Method: postrun( oRequest )
    # Type  : private
    # In    : Apache::Request
    #
    def postrun( oRequest )
        ## Act: Call script_OnEnd if exist
      if self.public_methods.member?( "script_OnEnd" ) == true
        script_OnEnd
      end

        ## Act: Set content_type
      if $Response.charset.nil? == false and $Response.charset.length 
        oRequest.content_type = format( "%s; charset=%s", $Response.contentType, $Response.charset )
      else
        oRequest.content_type = format( "%s", $Response.contentType )
      end

        ## Act: Populate response header table
      $Response.getHeader.each_pair{ |key,value|
        oRequest.headers_out[key] = value
      }

        ## Act: Set cookie
      $Response.addCookiesHeader.each { |cookie|
        oRequest.headers_out.add( "Set-Cookie", cookie )
      }

        ## Act: Set cookie session
      vSessionCookieValue = $Session.addSessionCookieHeader
      if vSessionCookieValue.nil? == false
        oRequest.headers_out.add( "Set-Cookie", vSessionCookieValue )
      end

        ## Act: Send HTTP header
      oRequest.send_http_header
    end

    ##
    # Method: debug( oRequest, xRubyCode, xFileName, iRunState, xDebugMessage )
    # Type  : private
    # 
    def debug( oRequest, xRubyCode, xFileName, iRunState, xDebugMessage )

      xErrorPage  = Asp::Common::getEnv( "ErrorFile" )
      xSMTPServer = Asp::Common::getEnv( "MailHost" )
      xSMTPPort   = Asp::Common::getEnv( "MailPort" ) || 25
      xSMTPFrom   = Asp::Common::getEnv( "MailErrorsFrom" )
      xSMTPTo     = Asp::Common::getEnv( "MailErrorsTo" )
      bMailSend   = false

        ## Act: Add error to server log file if debug level is 1
      if Asp::Common::debug == 1 
        result = oRequest.escape_html(xDebugMessage)
        oRequest.server.log_error( "[Ruby/ASP] [%d] : (%s) : %s", $$, __FILE__, result )
      end
      
        ## Act: Send error message by mail
      if xSMTPServer.nil? == false and xSMTPFrom.nil? == false and xSMTPTo.nil? == false
        oSmtp = Net::SMTP.new( xSMTPServer, xSMTPPort )
        oSmtp.start()
        oSmtp.ready( xSMTPFrom, xSMTPTo ) do |m|
          m.write "From: \"WWW Error Report\" <#{xSMTPFrom}>\r\n"
          m.write "To: \"WWW Error Reporting\" <#{xSMTPTo}>\r\n"
          m.write "Subject: WWW Error Report: #{oRequest.unparsed_uri}\r\n"
          m.write "\r\n"
          m.puts "Ruby/Asp v#{Asp::Common::version}"
          m.puts "ERROR State ##{iRunState} : " + xDebugMessage
          m.puts "----------------------------------------------------------"
          m.puts "File " + xFileName
          m.puts "----------------------------------------------------------"
          i = 0
          xRubyCode.each_line { |l|
            i += 1
            m.puts( format( "%04d : %s", i, l.chomp ) )
          }
          m.puts "----------------------------------------------------------"
        end # oSmtp.ready()

        bMailSend = true
      end

        ## Act: Print error message to stdout if no error page defined else redirect to error page
      if xErrorPage.nil? == true
        print "<DIV STYLE='background-color: #ffffff; font-family: sans-serif; border: 1px solid #646464; margin-left: 1em; margin-right: 2em; padding: 0.5em;'>\n"
        print "<B>Ruby/Asp v",Asp::Common::version,"</B><BR>\n"
        print "ERROR State #",iRunState," : \n"
        print "<B>",oRequest.escape_html(xDebugMessage),"</B><BR>\n"
        print "<PRE STYLE='background-color: #e6e6e6; border: 1px solid #646464; margin-left: 1em; margin-right: 2em; padding: 0.5em;'>\n"
        print "<B>File ",oRequest.uri,"</B>\n"
        i = 0
        oRequest.escape_html(xRubyCode).each_line { |l|
          i += 1
          printf( "%04d : %s\n", i, l.chomp )
        }
        print "</PRE>\n"
        print "Bad things happened. An email has been sent to #{xSMTPTo} with diagnostic information and details of your request.\n" if bMailSend == true
        print "</DIV>\n"
      else
        oRequest.status = 302
        oRequest.headers_out["location"] = xErrorPage
        oRequest.send_http_header
      end

    end

  end
end
