#!/usr/bin/env python
#
# 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.api import appinfo
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(os.path.realpath(sys.argv[0])), 'php/sdk'))


if not os.path.exists(SDK_PATH):
  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
    if appinfo.MODULE_SEPARATOR not in config.version_id:
      module_id = appinfo.DEFAULT_MODULE
      version_id = config.version_id
    else:
      module_id, version_id = config.version_id.split(appinfo.MODULE_SEPARATOR)
    self.environ_template = {
        'APPLICATION_ID': str(config.app_id),
        'CURRENT_MODULE_ID': module_id,
        'CURRENT_VERSION_ID': 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_HOST': str(config.api_host),
        '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]

    # Pass the APPLICATION_ROOT so we can use it in the setup script. We will
    # remove it from the environment before we execute the user script.
    user_environ['APPLICATION_ROOT'] = self.config.application_root

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

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

    # On Windows, in order to run a side-by-side assembly the specified env
    # must include a valid SystemRoot.
    if 'SYSTEMROOT' in os.environ:
      user_environ['SYSTEMROOT'] = os.environ['SYSTEMROOT']

    # 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':
      # See https://bugs.php.net/bug.php?id=46034 for quoting requirements.
      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', '')

    request_type = environ.pop(http_runtime_constants.REQUEST_TYPE_HEADER, None)
    if request_type == 'interactive':
      args.extend(['-d', 'html_errors="0"'])
      user_environ[http_runtime_constants.REQUEST_TYPE_HEADER] = request_type

    try:
      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)
    except Exception as e:
      logging.exception('Failure to start PHP with: %s', args)
      start_response('500 Internal Server Error',
                     [(http_runtime_constants.ERROR_CODE_HEADER, '1')])
      return ['Failure to start the PHP subprocess with %r:\n%s' % (args, e)]

    if p.returncode:
      if request_type == 'interactive':
        start_response('200 OK', [('Content-Type', 'text/plain')])
        message = httplib.HTTPMessage(cStringIO.StringIO(stdout))
        return [message.fp.read()]
      else:
        logging.error('php failure (%r) with:\nstdout:\n%sstderr:\n%s',
                      p.returncode, stdout, stderr)
        start_response('500 Internal Server Error',
                       [(http_runtime_constants.ERROR_CODE_HEADER, '1')])
        return ['php failure (%r) with:\nstdout:%s\nstderr:\n%s' %
                (p.returncode, stdout, stderr)]

    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()
