#!/usr/bin/env python2.7
#
# Copyright 2007 Google Inc.
#
# 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.
#
"""A PHP devappserver2 runtime."""


import base64
import cStringIO
import httplib
import logging
import os
import subprocess
import sys
import time
import urllib

import google

from google.appengine.tools.devappserver2 import http_runtime_constants
from google.appengine.tools.devappserver2 import php
from google.appengine.tools.devappserver2 import request_rewriter
from google.appengine.tools.devappserver2 import runtime_config_pb2
from google.appengine.tools.devappserver2 import wsgi_server

SDK_PATH = os.path.abspath(
    os.path.join(os.path.dirname(sys.argv[0]), 'php/sdk'))
SETUP_PHP_PATH = os.path.join(os.path.dirname(php.__file__), 'setup.php')


class PHPRuntime(object):
  """A WSGI application that runs PHP scripts using the PHP CGI binary."""

  def __init__(self, config):
    logging.debug('Initializing runtime with %s', config)
    self.config = config
    self.environ_template = {
        'APPLICATION_ID': str(config.app_id),
        'CURRENT_VERSION_ID': str(config.version_id),
        'DATACENTER': str(config.datacenter),
        'INSTANCE_ID': str(config.instance_id),
        'APPENGINE_RUNTIME': 'php',
        'AUTH_DOMAIN': str(config.auth_domain),
        'HTTPS': 'off',
        # By default php-cgi does not allow .php files to be run directly so
        # REDIRECT_STATUS must be set. See:
        # http://php.net/manual/en/security.cgi-bin.force-redirect.php
        'REDIRECT_STATUS': '1',
        'REMOTE_API_PORT': str(config.api_port),
        'SERVER_SOFTWARE': http_runtime_constants.SERVER_SOFTWARE,
        'TZ': 'UTC',
        }
    self.environ_template.update((env.key, env.value) for env in config.environ)

  def __call__(self, environ, start_response):
    """Handles an HTTP request for the runtime using a PHP executable.

    Args:
      environ: An environ dict for the request as defined in PEP-333.
      start_response: A function with semantics defined in PEP-333.

    Returns:
      An iterable over strings containing the body of the HTTP response.
    """
    user_environ = self.environ_template.copy()

    self.copy_headers(environ, user_environ)
    user_environ['REQUEST_METHOD'] = environ.get('REQUEST_METHOD', 'GET')
    user_environ['PATH_INFO'] = environ['PATH_INFO']
    user_environ['QUERY_STRING'] = environ['QUERY_STRING']

    # Construct the partial URL that PHP expects for REQUEST_URI
    # (http://php.net/manual/en/reserved.variables.server.php) using part of
    # the process described in PEP-333
    # (http://www.python.org/dev/peps/pep-0333/#url-reconstruction).
    user_environ['REQUEST_URI'] = urllib.quote(user_environ['PATH_INFO'])
    if user_environ['QUERY_STRING']:
      user_environ['REQUEST_URI'] += '?' + user_environ['QUERY_STRING']

    # Modify the SCRIPT_FILENAME to specify the setup script that readies the
    # PHP environment. Put the user script in REAL_SCRIPT_FILENAME.
    user_environ['REAL_SCRIPT_FILENAME'] = environ[
        http_runtime_constants.SCRIPT_HEADER]
    user_environ['SCRIPT_FILENAME'] = SETUP_PHP_PATH
    user_environ['REMOTE_REQUEST_ID'] = environ[
        http_runtime_constants.REQUEST_ID_ENVIRON]

    if 'CONTENT_TYPE' in environ:
      user_environ['CONTENT_TYPE'] = environ['CONTENT_TYPE']

    if 'CONTENT_LENGTH' in environ:
      user_environ['CONTENT_LENGTH'] = environ['CONTENT_LENGTH']
      content = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
    else:
      content = None

    # See http://www.php.net/manual/en/ini.core.php#ini.include-path.
    include_paths = [self.config.application_root, SDK_PATH]
    if sys.platform == 'win32':
      include_path = 'include_path=%s' % ';'.join(include_paths)
    else:
      include_path = 'include_path=%s' % ':'.join(include_paths)

    args = [self.config.php_config.php_executable_path, '-d', include_path]

    if self.config.php_config.enable_debugger:
      args.extend(['-d', 'xdebug.remote_enable="1"'])
      user_environ['XDEBUG_CONFIG'] = os.environ.get('XDEBUG_CONFIG', '')

    p = subprocess.Popen(args,
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         env=user_environ,
                         cwd=self.config.application_root)
    stdout, stderr = p.communicate(content)

    if p.returncode:
      logging.error('php failure (%r) with:\n%s', p.returncode, stdout+stderr)
      start_response('500 Internal Server Error',
                     [(http_runtime_constants.ERROR_CODE_HEADER, '1')])
      return []

    message = httplib.HTTPMessage(cStringIO.StringIO(stdout))
    assert 'Content-Type' in message, 'invalid CGI response: %r' % stdout

    if 'Status' in message:
      status = message['Status']
      del message['Status']
    else:
      status = '200 OK'

    # Ensures that we avoid merging repeat headers into a single header,
    # allowing use of multiple Set-Cookie headers.
    headers = []
    for name in message:
      for value in message.getheaders(name):
        headers.append((name, value))

    start_response(status, headers)
    return [message.fp.read()]

  def copy_headers(self, source_environ, dest_environ):
    """Copy headers from source_environ to dest_environ.

    This extracts headers that represent environ values and propagates all
    other headers which are not used for internal implementation details or
    headers that are stripped.

    Args:
      source_environ: The source environ dict.
      dest_environ: The environ dict to populate.
    """
    # TODO: This method is copied from python/runtime.py. If this
    # method isn't obsoleted, consider moving it to some sort of utility module.
    for env in http_runtime_constants.ENVIRONS_TO_PROPAGATE:
      value = source_environ.get(
          http_runtime_constants.INTERNAL_ENVIRON_PREFIX + env, None)
      if value is not None:
        dest_environ[env] = value
    for name, value in source_environ.items():
      if (name.startswith('HTTP_') and
          not name.startswith(http_runtime_constants.INTERNAL_ENVIRON_PREFIX)):
        dest_environ[name] = value


def main():
  config = runtime_config_pb2.Config()
  config.ParseFromString(base64.b64decode(sys.stdin.read()))
  server = wsgi_server.WsgiServer(
      ('localhost', 0),
      request_rewriter.runtime_rewriter_middleware(PHPRuntime(config)))
  server.start()
  print server.port
  sys.stdout.close()
  sys.stdout = sys.stderr
  try:
    while True:
      time.sleep(1)
  except KeyboardInterrupt:
    pass
  finally:
    server.quit()


if __name__ == '__main__':
  main()
