<?php
/**
 * TSoapService and TSoapServer class file
 *
 * @author Knut Urdalen <knut.urdalen@gmail.com>
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2011 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System.Web.Services
 */

/**
 * TSoapService class
 *
 * TSoapService processes SOAP requests for a PRADO application.
 * TSoapService requires PHP SOAP extension to be loaded.
 *
 * TSoapService manages a set of SOAP providers. Each SOAP provider
 * is a class that implements a set of SOAP methods which are exposed
 * to SOAP clients for remote invocation. TSoapService generates WSDL
 * automatically for the SOAP providers by default.
 *
 * To use TSoapService, configure it in the application specification like following:
 * <code>
 *   <services>
 *     <service id="soap" class="System.Web.Services.TSoapService">
 *       <soap id="stockquote" provider="MyStockQuote" />
 *     </service>
 *   </services>
 * </code>
 * PHP configuration style:
 * <code>
 *  'services' => array(
 *    'soap' => array(
 *     'class' => 'System.Web.Services.TSoapService'
 *     'properties' => array(
 *       'provider' => 'MyStockQuote'
 *	   )
 *    )
 *  )
 * </code>
 *
 * The WSDL for the provider class "MyStockQuote" is generated based on special
 * comment tags in the class. In particular, if a class method's comment
 * contains the keyword "@soapmethod", it is considered to be a SOAP method
 * and will be exposed to SOAP clients. For example,
 * <code>
 *   class MyStockQuote {
 *      / **
 *       * @param string $symbol the stock symbol
 *       * @return float the stock price
 *       * @soapmethod
 *       * /
 *      public function getQuote($symbol) {...}
 *   }
 * </code>
 *
 * With the above SOAP provider, a typical SOAP client may call the method "getQuote"
 * remotely like the following:
 * <code>
 *   $client=new SoapClient("http://hostname/path/to/index.php?soap=stockquote.wsdl");
 *   echo $client->getQuote("ibm");
 * </code>
 *
 * Each <soap> element in the application specification actually configures
 * the properties of a SOAP server which defaults to {@link TSoapServer}.
 * Therefore, any writable property of {@link TSoapServer} may appear as an attribute
 * in the <soap> element. For example, the "provider" attribute refers to
 * the {@link TSoapServer::setProvider Provider} property of {@link TSoapServer}.
 * The following configuration specifies that the SOAP server is persistent within
 * the user session (that means a MyStockQuote object will be stored in session)
 * <code>
 *   <services>
 *     <service id="soap" class="System.Web.Services.TSoapService">
 *       <soap id="stockquote" provider="MyStockQuote" SessionPersistent="true" />
 *     </service>
 *   </services>
 * </code>
 *
 * You may also use your own SOAP server class by specifying the "class" attribute of <soap>.
 *
 * @author Knut Urdalen <knut.urdalen@gmail.com>
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Carl G. Mathisen <carlgmathisen@gmail.com>
 * @package System.Web.Services
 * @since 3.1
 */
class TSoapService extends TService
{
	const DEFAULT_SOAP_SERVER='TSoapServer';
	private $_servers=array();
	private $_configFile=null;
	private $_wsdlRequest=false;
	private $_serverID=null;

	/**
	 * Constructor.
	 * Sets default service ID to 'soap'.
	 */
	public function __construct()
	{
		$this->setID('soap');
	}

	/**
	 * Initializes this module.
	 * This method is required by the IModule interface.
	 * @param TXmlElement configuration for this module, can be null
	 * @throws TConfigurationException if {@link getConfigFile ConfigFile} is invalid.
	 */
	public function init($config)
	{
		if($this->_configFile!==null)
		{
 			if(is_file($this->_configFile))
 			{
				$dom=new TXmlDocument;
				$dom->loadFromFile($this->_configFile);
				$this->loadConfig($dom);
			}
			else
				throw new TConfigurationException('soapservice_configfile_invalid',$this->_configFile);
		}
		$this->loadConfig($config);

		$this->resolveRequest();
	}

	/**
	 * Resolves the request parameter.
	 * It identifies the server ID and whether the request is for WSDL.
	 * @throws THttpException if the server ID cannot be found
	 * @see getServerID
	 * @see getIsWsdlRequest
	 */
	protected function resolveRequest()
	{
		$serverID=$this->getRequest()->getServiceParameter();
		if(($pos=strrpos($serverID,'.wsdl'))===strlen($serverID)-5)
		{
			$serverID=substr($serverID,0,$pos);
			$this->_wsdlRequest=true;
		}
		else
			$this->_wsdlRequest=false;
		$this->_serverID=$serverID;
		if(!isset($this->_servers[$serverID]))
			throw new THttpException(400,'soapservice_request_invalid',$serverID);
	}

	/**
	 * Loads configuration from an XML element
	 * @param mixed configuration node
	 * @throws TConfigurationException if soap server id is not specified or duplicated
	 */
	private function loadConfig($config)
	{	
		if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP)
		{
			if(is_array($config))
			{
				foreach($config['soap'] as $id => $server)
				{
					$properties = isset($server['properties'])?$server['properties']:array();
					if(isset($this->_servers[$id]))
						throw new TConfigurationException('soapservice_serverid_duplicated',$id);
					$this->_servers[$id]=$properties;
				}
			}
		}
		else
		{
			foreach($config->getElementsByTagName('soap') as $serverXML)
			{
				$properties=$serverXML->getAttributes();
				if(($id=$properties->remove('id'))===null)
					throw new TConfigurationException('soapservice_serverid_required');
				if(isset($this->_servers[$id]))
					throw new TConfigurationException('soapservice_serverid_duplicated',$id);
				$this->_servers[$id]=$properties;
			}
		}
	}

	/**
	 * @return string external configuration file. Defaults to null.
	 */
	public function getConfigFile()
	{
		return $this->_configFile;
	}

	/**
	 * @param string external configuration file in namespace format. The file
	 * must be suffixed with '.xml'.
	 * @throws TInvalidDataValueException if the file is invalid.
	 */
	public function setConfigFile($value)
	{
		if(($this->_configFile=Prado::getPathOfNamespace($value,Prado::getApplication()->getConfigurationFileExt()))===null)
			throw new TConfigurationException('soapservice_configfile_invalid',$value);
	}

	/**
	 * Constructs a URL with specified page path and GET parameters.
	 * @param string soap server ID
	 * @param array list of GET parameters, null if no GET parameters required
	 * @param boolean whether to encode the ampersand in URL, defaults to true.
	 * @param boolean whether to encode the GET parameters (their names and values), defaults to true.
	 * @return string URL for the page and GET parameters
	 */
	public function constructUrl($serverID,$getParams=null,$encodeAmpersand=true,$encodeGetItems=true)
	{
		return $this->getRequest()->constructUrl($this->getID(),$serverID,$getParams,$encodeAmpersand,$encodeGetItems);
	}

	/**
	 * @return boolean whether this is a request for WSDL
	 */
	public function getIsWsdlRequest()
	{
		return $this->_wsdlRequest;
	}

	/**
	 * @return string the SOAP server ID
	 */
	public function getServerID()
	{
		return $this->_serverID;
	}

	/**
	 * Creates the requested SOAP server.
	 * The SOAP server is initialized with the property values specified
	 * in the configuration.
	 * @return TSoapServer the SOAP server instance
	 */
	protected function createServer()
	{
		$properties=$this->_servers[$this->_serverID];
		$serverClass=null;
		if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP && isset($config['class']))
			$serverClass=$config['class'];
		else if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_XML)
			$serverClass=$properties->remove('class');
		if($serverClass===null)
			$serverClass=self::DEFAULT_SOAP_SERVER;
		Prado::using($serverClass);
		$className=($pos=strrpos($serverClass,'.'))!==false?substr($serverClass,$pos+1):$serverClass;
		if($className!==self::DEFAULT_SOAP_SERVER && !is_subclass_of($className,self::DEFAULT_SOAP_SERVER))
			throw new TConfigurationException('soapservice_server_invalid',$serverClass);
		$server=new $className;
		$server->setID($this->_serverID);
		foreach($properties as $name=>$value)
			$server->setSubproperty($name,$value);
		return $server;
	}

	/**
	 * Runs the service.
	 * If the service parameter ends with '.wsdl', it will serve a WSDL file for
	 * the specified soap server.
	 * Otherwise, it will handle the soap request using the specified server.
	 */
	public function run()
	{
		Prado::trace("Running SOAP service",'System.Web.Services.TSoapService');
		$server=$this->createServer();
		$this->getResponse()->setContentType('text/xml');
		$this->getResponse()->setCharset($server->getEncoding());
		if($this->getIsWsdlRequest())
		{
			// server WSDL file
			Prado::trace("Generating WSDL",'System.Web.Services.TSoapService');
			$this->getResponse()->write($server->getWsdl());
		}
		else
		{
			// provide SOAP service
			Prado::trace("Handling SOAP request",'System.Web.Services.TSoapService');
			$server->run();
		}
	}
}


/**
 * TSoapServer class.
 *
 * TSoapServer is a wrapper of the PHP SoapServer class.
 * It associates a SOAP provider class to the SoapServer object.
 * It also manages the URI for the SOAP service and WSDL.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System.Web.Services
 * @since 3.1
 */
class TSoapServer extends TApplicationComponent
{
	const WSDL_CACHE_PREFIX='wsdl.';

	private $_id;
	private $_provider;

	private $_version='';
	private $_actor='';
	private $_encoding='';
	private $_uri='';
	private $_classMap;
	private $_persistent=false;
	private $_wsdlUri='';

	private $_requestedMethod;

	private $_server;

	/**
	 * @return string the ID of the SOAP server
	 */
	public function getID()
	{
		return $this->_id;
	}

	/**
	 * @param string the ID of the SOAP server
	 * @throws TInvalidDataValueException if the ID ends with '.wsdl'.
	 */
	public function setID($id)
	{
		if(strrpos($this->_id,'.wsdl')===strlen($this->_id)-5)
			throw new TInvalidDataValueException('soapserver_id_invalid',$id);
		$this->_id=$id;
	}

	/**
	 * Handles the SOAP request.
	 */
	public function run()
	{
		if(($provider=$this->getProvider())!==null)
		{
			Prado::using($provider);
			$providerClass=($pos=strrpos($provider,'.'))!==false?substr($provider,$pos+1):$provider;
			$this->guessMethodCallRequested($providerClass);
			$server=$this->createServer();
			$server->setClass($providerClass, $this);
			if($this->_persistent)
				$server->setPersistence(SOAP_PERSISTENCE_SESSION);
		}
		else
			$server=$this->createServer();
		try
		{
			$server->handle();
		}
		catch (Exception $e)
		{
			if($this->getApplication()->getMode()===TApplicationMode::Debug)
				$this->fault($e->getMessage(), $e->__toString());
			else
				$this->fault($e->getMessage());
		}
	}

	/**
	 * Generate a SOAP fault message.
	 * @param string message title
	 * @param mixed message details
	 * @param string message code, defalt is 'SERVER'.
	 * @param string actors
	 * @param string message name
	 */
	public function fault($title, $details='', $code='SERVER', $actor='', $name='')
	{
		Prado::trace('SOAP-Fault '.$code. ' '.$title.' : '.$details, 'System.Web.Services.TSoapService');
		$this->_server->fault($code, $title, $actor, $details, $name);
	}

	/**
	 * Guess the SOAP method request from the actual SOAP message
	 *
	 * @param string $class current handler class.
	 */
	protected function guessMethodCallRequested($class)
	{
		$namespace = $class.'wsdl';
		$message = file_get_contents("php://input");
		$matches= array();
		if(preg_match('/xmlns:([^=]+)="urn:'.$namespace.'"/', $message, $matches))
		{
			if(preg_match('/<'.$matches[1].':([a-zA-Z_]+[a-zA-Z0-9_]+)/', $message, $method))
			{
				$this->_requestedMethod = $method[1];
			}
		}
	}

	/**
	 * Soap method guessed from the SOAP message received.
	 * @return string soap method request, null if not found.
	 */
	public function getRequestedMethod()
	{
		return $this->_requestedMethod;
	}

	/**
	 * Creates the SoapServer instance.
	 * @return SoapServer
	 */
	protected function createServer()
	{
		if($this->_server===null)
		{
			if($this->getApplication()->getMode()===TApplicationMode::Debug)
				ini_set("soap.wsdl_cache_enabled",0);
			$this->_server = new SoapServer($this->getWsdlUri(),$this->getOptions());
		}
		return $this->_server;
	}

	/**
	 * @return array options for creating SoapServer instance
	 */
	protected function getOptions()
	{
		$options=array();
		if($this->_version==='1.1')
			$options['soap_version']=SOAP_1_1;
		else if($this->_version==='1.2')
			$options['soap_version']=SOAP_1_2;
		if(!empty($this->_actor))
			$options['actor']=$this->_actor;
		if(!empty($this->_encoding))
			$options['encoding']=$this->_encoding;
		if(!empty($this->_uri))
			$options['uri']=$this->_uri;
		if(is_string($this->_classMap))
		{
			foreach(preg_split('/\s*,\s*/', $this->_classMap) as $className)
				$options['classmap'][$className]=$className; //complex type uses the class name in the wsdl
		}
		return $options;
	}

	/**
	 * Returns the WSDL content of the SOAP server.
	 * If {@link getWsdlUri WsdlUri} is set, its content will be returned.
	 * If not, the {@link setProvider Provider} class will be investigated
	 * and the WSDL will be automatically genearted.
	 * @return string the WSDL content of the SOAP server
	 */
	public function getWsdl()
	{
		if($this->_wsdlUri==='')
		{
			$provider=$this->getProvider();
			$providerClass=($pos=strrpos($provider,'.'))!==false?substr($provider,$pos+1):$provider;
			Prado::using($provider);
			if($this->getApplication()->getMode()===TApplicationMode::Performance && ($cache=$this->getApplication()->getCache())!==null)
			{
				$wsdl=$cache->get(self::WSDL_CACHE_PREFIX.$providerClass);
				if(is_string($wsdl))
					return $wsdl;
				Prado::using('System.3rdParty.WsdlGen.WsdlGenerator');
				$wsdl=WsdlGenerator::generate($providerClass, $this->getUri(), $this->getEncoding());
				$cache->set(self::WSDL_CACHE_PREFIX.$providerClass,$wsdl);
				return $wsdl;
			}
			else
			{
				Prado::using('System.3rdParty.WsdlGen.WsdlGenerator');
				return WsdlGenerator::generate($providerClass, $this->getUri(), $this->getEncoding());
			}
		}
		else
			return file_get_contents($this->_wsdlUri);
	}

	/**
	 * @return string the URI for WSDL
	 */
	public function getWsdlUri()
	{
		if($this->_wsdlUri==='')
			return $this->getRequest()->getBaseUrl().$this->getService()->constructUrl($this->getID().'.wsdl',false);
		else
			return $this->_wsdlUri;
	}

	/**
	 * @param string the URI for WSDL
	 */
	public function setWsdlUri($value)
	{
		$this->_wsdlUri=$value;
	}

	/**
	 * @return string the URI for the SOAP service
	 */
	public function getUri()
	{
		if($this->_uri==='')
			return $this->getRequest()->getBaseUrl().$this->getService()->constructUrl($this->getID(),false);
		else
			return $this->_uri;
	}

	/**
	 * @param string the URI for the SOAP service
	 */
	public function setUri($uri)
	{
		$this->_uri=$uri;
	}

	/**
	 * @return string the SOAP provider class (in namespace format)
	 */
	public function getProvider()
	{
		return $this->_provider;
	}

	/**
	 * @param string the SOAP provider class (in namespace format)
	 */
	public function setProvider($provider)
	{
		$this->_provider=$provider;
	}

	/**
	 * @return string SOAP version, defaults to empty (meaning not set).
	 */
	public function getVersion()
	{
		return $this->_version;
	}

	/**
	 * @param string SOAP version, either '1.1' or '1.2'
	 * @throws TInvalidDataValueException if neither '1.1' nor '1.2'
	 */
	public function setVersion($value)
	{
		if($value==='1.1' || $value==='1.2' || $value==='')
			$this->_version=$value;
		else
			throw new TInvalidDataValueException('soapserver_version_invalid',$value);
	}

	/**
	 * @return string actor of the SOAP service
	 */
	public function getActor()
	{
		return $this->_actor;
	}

	/**
	 * @param string actor of the SOAP service
	 */
	public function setActor($value)
	{
		$this->_actor=$value;
	}

	/**
	 * @return string encoding of the SOAP service
	 */
	public function getEncoding()
	{
		return $this->_encoding;
	}

	/**
	 * @param string encoding of the SOAP service
	 */
	public function setEncoding($value)
	{
		$this->_encoding=$value;
	}

	/**
	 * @return boolean whether the SOAP service is persistent within session. Defaults to false.
	 */
	public function getSessionPersistent()
	{
		return $this->_persistent;
	}

	/**
	 * @param boolean whether the SOAP service is persistent within session.
	 */
	public function setSessionPersistent($value)
	{
		$this->_persistent=TPropertyValue::ensureBoolean($value);
	}

	/**
	 * @return string comma delimit list of complex type classes.
	 */
	public function getClassMaps()
	{
		return $this->_classMap;
	}

	/**
	 * @return string comma delimit list of class names
	 */
	public function setClassMaps($classes)
	{
		$this->_classMap = $classes;
	}
}