diff options
Diffstat (limited to 'lib/prado/framework/Web/Services')
-rw-r--r-- | lib/prado/framework/Web/Services/TFeedService.php | 187 | ||||
-rw-r--r-- | lib/prado/framework/Web/Services/TJsonService.php | 209 | ||||
-rw-r--r-- | lib/prado/framework/Web/Services/TPageService.php | 890 | ||||
-rw-r--r-- | lib/prado/framework/Web/Services/TRpcService.php | 722 | ||||
-rw-r--r-- | lib/prado/framework/Web/Services/TSoapService.php | 622 |
5 files changed, 2630 insertions, 0 deletions
diff --git a/lib/prado/framework/Web/Services/TFeedService.php b/lib/prado/framework/Web/Services/TFeedService.php new file mode 100644 index 0000000..dc9c064 --- /dev/null +++ b/lib/prado/framework/Web/Services/TFeedService.php @@ -0,0 +1,187 @@ +<?php +/** + * TFeedService and TFeed class file + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Knut Urdalen <knut.urdalen@gmail.com> + * @link https://github.com/pradosoft/prado + * @copyright Copyright © 2005-2015 The PRADO Group + * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT + * @version $Id$ + * @package System.Web.Services + */ + +/** + * TFeedService class + * + * TFeedService provides to end-users feed content. + * + * TFeedService manages a set of feeds. The service parameter, referring + * to the ID of the feed, specifies which feed content to be provided to end-users. + * + * To use TFeedService, configure it in application configuration as follows, + * <code> + * <service id="feed" class="System.Web.Services.TFeedService"> + * <feed id="ch1" class="Path.To.FeedClass1" .../> + * <feed id="ch2" class="Path.To.FeedClass2" .../> + * <feed id="ch3" class="Path.To.FeedClass3" .../> + * </service> + * </code> + * where each <feed> element specifies a feed identified by its "id" value (case-sensitive). + * + * PHP configuration style: + * <code> + * array( + * 'feed' => array( + * 'ch1' => array( + * 'class' => 'Path.To.FeedClass1', + * 'properties' => array( + * ... + * ), + * ), + * ) + * </code> + * + * The class attribute indicates which PHP class will provide the actual feed + * content. Note, the class must implement {@link IFeedContentProvider} interface. + * Other initial properties for the feed class may also be specified in the + * corresponding <feed> element. + * + * To retrieve the feed content identified by "ch2", use the URL + * <code>/path/to/index.php?feed=ch2</code> + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Knut Urdalen <knut.urdalen@gmail.com> + * @author Carl G. Mathisen <carlgmathisen@gmail.com> + * @package System.Web.Services + * @since 3.1 + */ +class TFeedService extends TService +{ + private $_feeds=array(); + + /** + * Initializes this module. + * This method is required by the IModule interface. + * @param mixed configuration for this module, can be null + */ + public function init($config) + { + if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) + { + if(is_array($config)) + { + foreach($config as $id => $feed) + $this->_feeds[$id] = $feed; + } + } + else + { + foreach($config->getElementsByTagName('feed') as $feed) + { + if(($id=$feed->getAttributes()->remove('id'))!==null) + $this->_feeds[$id]=$feed; + else + throw new TConfigurationException('feedservice_id_required'); + } + } + } + + /** + * @return string the requested feed path + */ + protected function determineRequestedFeedPath() + { + return $this->getRequest()->getServiceParameter(); + } + + /** + * Runs the service. + * This method is invoked by application automatically. + */ + public function run() + { + $id=$this->getRequest()->getServiceParameter(); + if(isset($this->_feeds[$id])) + { + $feedConfig=$this->_feeds[$id]; + $properties = array(); + $feed = null; + if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) + { + if(isset($feedConfig['class'])) + { + $feed=Prado::createComponent($feedConfig['class']); + if($service instanceof IFeedContentProvider) + $properties=isset($feedConfig['properties'])?$feedConfig['properties']:array(); + else + throw new TConfigurationException('jsonservice_response_type_invalid',$id); + } + else + throw new TConfigurationException('jsonservice_class_required',$id); + } + else + { + $properties=$feedConfig->getAttributes(); + if(($class=$properties->remove('class'))!==null) + { + $feed=Prado::createComponent($class); + if(!($feed instanceof IFeedContentProvider)) + throw new TConfigurationException('feedservice_feedtype_invalid',$id); + } + else + throw new TConfigurationException('feedservice_class_required',$id); + } + + // init feed properties + foreach($properties as $name=>$value) + $feed->setSubproperty($name,$value); + $feed->init($feedConfig); + + $content=$feed->getFeedContent(); + //$this->getResponse()->setContentType('application/rss+xml'); + $this->getResponse()->setContentType($feed->getContentType()); + $this->getResponse()->write($content); + } + else + throw new THttpException(404,'feedservice_feed_unknown',$id); + } +} + +/** + * IFeedContentProvider interface. + * + * IFeedContentProvider interface must be implemented by a feed class who + * provides feed content. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Knut Urdalen <knut.urdalen@gmail.com> + * @package System.Web.Services + * @since 3.1 + */ +interface IFeedContentProvider +{ + /** + * Initializes the feed content provider. + * This method is invoked (before {@link getFeedContent}) + * when the feed provider is requested by a user. + * @param TXmlElement configurations specified within the <feed> element + * corresponding to this feed provider when configuring {@link TFeedService}. + */ + public function init($config); + /** + * @return string feed content in proper XML format + */ + public function getFeedContent(); + /** + * Sets the content type of the feed content to be sent. + * Some examples are: + * RSS 1.0 feed: application/rdf+xml + * RSS 2.0 feed: application/rss+xml or application/xml or text/xml + * ATOM feed: application/atom+xml + * @return string the content type for the feed content. + * @since 3.1.1 + */ + public function getContentType(); +} + diff --git a/lib/prado/framework/Web/Services/TJsonService.php b/lib/prado/framework/Web/Services/TJsonService.php new file mode 100644 index 0000000..d17b330 --- /dev/null +++ b/lib/prado/framework/Web/Services/TJsonService.php @@ -0,0 +1,209 @@ +<?php +/** + * TJsonService and TJsonResponse class file. + * + * @author Wei Zhuo <weizhuo[at]gamil[dot]com> + * @link https://github.com/pradosoft/prado + * @copyright Copyright © 2005-2015 The PRADO Group + * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT + * @package System.Web.Services + */ + +/** + * TJsonService class provides to end-users javascript content response in + * JSON format. + * + * TJsonService manages a set of {@link TJsonResponse}, each + * representing specific response with javascript content. + * The service parameter, referring to the ID of the service, specifies + * which javascript content to be provided to end-users. + * + * To use TJsonService, configure it in application configuration as follows, + * <code> + * <service id="json" class="System.Web.Services.TJsonService"> + * <json id="get_article" class="Path.To.JsonResponseClass1" .../> + * <json id="register_rating" class="Path.To.JsonResponseClass2" .../> + * </service> + * </code> + * where each JSON response is specified via a <json> element. + * Initial property values can be configured in a <json> element. + * + * + * PHP configuration style: + * <code> + * 'services' => array( + * 'get_article' => array( + * 'class' => 'Path.To.JsonResponseClass1', + * 'properties' => array( + * ... + * ) + * ) + * ) + * </code> + * + * To retrieve the JSON content provided by "get_article", use the URL + * <code>index.php?json=get_article</code> + * + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + * @author Carl G. Mathisen <carlgmathisen@gmail.com> + * @package System.Web.Services + * @since 3.1 + */ +class TJsonService extends TService +{ + /** + * @var array registered services + */ + private $_services=array(); + + /** + * Initializes this module. + * This method is required by the IModule interface. + * @param mixed configuration for this module, can be null + */ + public function init($xml) + { + $this->loadJsonServices($xml); + } + + /** + * Load the service definitions. + * @param mixed configuration for this module, can be null + */ + protected function loadJsonServices($config) + { + if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) + { + if(is_array($config)) + { + foreach($config['json'] as $id => $json) + $this->_services[$id] = $json; + } + } + else + { + foreach($config->getElementsByTagName('json') as $json) + { + if(($id=$json->getAttribute('id'))!==null) + $this->_services[$id]=$json; + else + throw new TConfigurationException('jsonservice_id_required'); + } + } + } + + /** + * Runs the service. + * This method is invoked by application automatically. + */ + public function run() + { + $id=$this->getRequest()->getServiceParameter(); + if(isset($this->_services[$id])) + { + $serviceConfig=$this->_services[$id]; + if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) + { + if(isset($serviceConfig['class'])) + { + $service=Prado::createComponent($serviceConfig['class']); + if($service instanceof TJsonResponse) + { + $properties = isset($serviceConfig['properties'])?$serviceConfig['properties']:array(); + $this->createJsonResponse($service,$properties,$serviceConfig); + } + else + throw new TConfigurationException('jsonservice_response_type_invalid',$id); + } + else + throw new TConfigurationException('jsonservice_class_required',$id); + } + else + { + $properties=$serviceConfig->getAttributes(); + if(($class=$properties->remove('class'))!==null) + { + $service=Prado::createComponent($class); + if($service instanceof TJsonResponse) + $this->createJsonResponse($service,$properties,$serviceConfig); + else + throw new TConfigurationException('jsonservice_response_type_invalid',$id); + } + else + throw new TConfigurationException('jsonservice_class_required',$id); + } + } + else + throw new THttpException(404,'jsonservice_provider_unknown',$id); + } + + /** + * Renders content provided by TJsonResponse::getJsonContent() as + * javascript in JSON format. + */ + protected function createJsonResponse($service,$properties,$config) + { + // init service properties + foreach($properties as $name=>$value) + $service->setSubproperty($name,$value); + $service->init($config); + + //send content if not null + if(($content=$service->getJsonContent())!==null) + { + $response = $this->getResponse(); + $response->setContentType('application/json'); + $response->setCharset('UTF-8'); + //send content + $response->write(TJavaScript::jsonEncode($content)); + } + } +} + +/** + * TJsonResponse Class + * + * TJsonResponse is the base class for all JSON response provider classes. + * + * Derived classes must implement {@link getJsonContent()} to return + * an object or literals to be converted to JSON format. The response + * will be empty if the returned content is null. + * + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + * @package System.Web.Services + * @since 3.1 + */ +abstract class TJsonResponse extends TApplicationComponent +{ + private $_id=''; + + /** + * Initializes the feed. + * @param TXmlElement configurations specified in {@link TJsonService}. + */ + public function init($config) + { + } + + /** + * @return string ID of this response + */ + public function getID() + { + return $this->_id; + } + + /** + * @param string ID of this response + */ + public function setID($value) + { + $this->_id=$value; + } + + /** + * @return object json response content, null to suppress output. + */ + abstract public function getJsonContent(); +} + diff --git a/lib/prado/framework/Web/Services/TPageService.php b/lib/prado/framework/Web/Services/TPageService.php new file mode 100644 index 0000000..4a422c4 --- /dev/null +++ b/lib/prado/framework/Web/Services/TPageService.php @@ -0,0 +1,890 @@ +<?php +/** + * TPageService class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link https://github.com/pradosoft/prado + * @copyright Copyright © 2005-2015 The PRADO Group + * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT + * @package System.Web.Services + */ + +/** + * Include classes to be used by page service + */ +Prado::using('System.Web.UI.TPage'); +Prado::using('System.Web.UI.TTemplateManager'); +Prado::using('System.Web.UI.TThemeManager'); + +/** + * TPageService class. + * + * TPageService implements the service for serving user page requests. + * + * Pages that are available to client users are stored under a directory specified by + * {@link setBasePath BasePath}. The directory may contain subdirectories. + * Pages serving for a similar goal are usually placed under the same directory. + * A directory may contain a configuration file <b>config.xml</b> whose content + * is similar to that of application configuration file. + * + * A page is requested via page path, which is a dot-connected directory names + * appended by the page name. Assume '<BasePath>/Users/Admin' is the directory + * containing the page 'Update'. Then the page can be requested via 'Users.Admin.Update'. + * By default, the {@link setBasePath BasePath} of the page service is the "pages" + * directory under the application base path. You may change this default + * by setting {@link setBasePath BasePath} with a different path you prefer. + * + * Page name refers to the file name (without extension) of the page template. + * In order to differentiate from the common control template files, the extension + * name of the page template files must be '.page'. If there is a PHP file with + * the same page name under the same directory as the template file, that file + * will be considered as the page class file and the file name is the page class name. + * If such a file is not found, the page class is assumed as {@link TPage}. + * + * Modules can be configured and loaded in page directory configurations. + * Configuration of a module in a subdirectory will overwrite its parent + * directory's configuration, if both configurations refer to the same module. + * + * By default, TPageService will automatically load two modules: + * - {@link TTemplateManager} : manages page and control templates + * - {@link TThemeManager} : manages themes used in a Prado application + * + * In page directory configurations, static authorization rules can also be specified, + * which governs who and which roles can access particular pages. + * Refer to {@link TAuthorizationRule} for more details about authorization rules. + * Page authorization rules can be configured within an <authorization> tag in + * each page directory configuration as follows, + * <authorization> + * <deny pages="Update" users="?" /> + * <allow pages="Admin" roles="administrator" /> + * <deny pages="Admin" users="*" /> + * </authorization> + * where the 'pages' attribute may be filled with a sequence of comma-separated + * page IDs. If 'pages' attribute does not appear in a rule, the rule will be + * applied to all pages in this directory and all subdirectories (recursively). + * Application of authorization rules are in a bottom-up fashion, starting from + * the directory containing the requested page up to all parent directories. + * The first matching rule will be used. The last rule always allows all users + * accessing to any resources. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Carl G. Mathisen <carlgmathisen@gmail.com> + * @package System.Web.Services + * @since 3.0 + */ +class TPageService extends TService +{ + /** + * Configuration file name + */ + const CONFIG_FILE_XML='config.xml'; + /** + * Configuration file name + */ + const CONFIG_FILE_PHP='config.php'; + /** + * Default base path + */ + const DEFAULT_BASEPATH='Pages'; + /** + * Fallback base path - used to be the default up to Prado < 3.2 + */ + const FALLBACK_BASEPATH='pages'; + /** + * Prefix of ID used for storing parsed configuration in cache + */ + const CONFIG_CACHE_PREFIX='prado:pageservice:'; + /** + * Page template file extension + */ + const PAGE_FILE_EXT='.page'; + /** + * @var string root path of pages + */ + private $_basePath=null; + /** + * @var string base path class in namespace format + */ + private $_basePageClass='TPage'; + /** + * @var string clientscript manager class in namespace format + * @since 3.1.7 + */ + private $_clientScriptManagerClass='System.Web.UI.TClientScriptManager'; + /** + * @var string default page + */ + private $_defaultPage='Home'; + /** + * @var string requested page (path) + */ + private $_pagePath=null; + /** + * @var TPage the requested page + */ + private $_page=null; + /** + * @var array list of initial page property values + */ + private $_properties=array(); + /** + * @var boolean whether service is initialized + */ + private $_initialized=false; + /** + * @var TThemeManager theme manager + */ + private $_themeManager=null; + /** + * @var TTemplateManager template manager + */ + private $_templateManager=null; + + /** + * Initializes the service. + * This method is required by IService interface and is invoked by application. + * @param TXmlElement service configuration + */ + public function init($config) + { + Prado::trace("Initializing TPageService",'System.Web.Services.TPageService'); + + $pageConfig=$this->loadPageConfig($config); + + $this->initPageContext($pageConfig); + + $this->_initialized=true; + } + + /** + * Initializes page context. + * Page context includes path alias settings, namespace usages, + * parameter initialization, module loadings, page initial properties + * and authorization rules. + * @param TPageConfiguration + */ + protected function initPageContext($pageConfig) + { + $application=$this->getApplication(); + foreach($pageConfig->getApplicationConfigurations() as $appConfig) + $application->applyConfiguration($appConfig); + + $this->applyConfiguration($pageConfig); + } + + /** + * Applies a page configuration. + * @param TPageConfiguration the configuration + */ + protected function applyConfiguration($config) + { + // initial page properties (to be set when page runs) + $this->_properties=array_merge($this->_properties, $config->getProperties()); + $this->getApplication()->getAuthorizationRules()->mergeWith($config->getRules()); + $pagePath=$this->getRequestedPagePath(); + // external configurations + foreach($config->getExternalConfigurations() as $filePath=>$params) + { + list($configPagePath,$condition)=$params; + if($condition!==true) + $condition=$this->evaluateExpression($condition); + if($condition) + { + if(($path=Prado::getPathOfNamespace($filePath,Prado::getApplication()->getConfigurationFileExt()))===null || !is_file($path)) + throw new TConfigurationException('pageservice_includefile_invalid',$filePath); + $c=new TPageConfiguration($pagePath); + $c->loadFromFile($path,$configPagePath); + $this->applyConfiguration($c); + } + } + + } + + /** + * Determines the requested page path. + * @return string page path requested + */ + protected function determineRequestedPagePath() + { + $pagePath=$this->getRequest()->getServiceParameter(); + if(empty($pagePath)) + $pagePath=$this->getDefaultPage(); + return $pagePath; + } + + /** + * Collects configuration for a page. + * @param TXmlElement additional configuration specified in the application configuration + * @return TPageConfiguration + */ + protected function loadPageConfig($config) + { + $application=$this->getApplication(); + $pagePath=$this->getRequestedPagePath(); + if(($cache=$application->getCache())===null) + { + $pageConfig=new TPageConfiguration($pagePath); + if($config!==null) + { + if($application->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) + $pageConfig->loadPageConfigurationFromPhp($config,$application->getBasePath(),''); + else + $pageConfig->loadPageConfigurationFromXml($config,$application->getBasePath(),''); + } + $pageConfig->loadFromFiles($this->getBasePath()); + } + else + { + $configCached=true; + $currentTimestamp=array(); + $arr=$cache->get(self::CONFIG_CACHE_PREFIX.$this->getID().$pagePath); + if(is_array($arr)) + { + list($pageConfig,$timestamps)=$arr; + if($application->getMode()!==TApplicationMode::Performance) + { + foreach($timestamps as $fileName=>$timestamp) + { + if($fileName===0) // application config file + { + $appConfigFile=$application->getConfigurationFile(); + $currentTimestamp[0]=$appConfigFile===null?0:@filemtime($appConfigFile); + if($currentTimestamp[0]>$timestamp || ($timestamp>0 && !$currentTimestamp[0])) + $configCached=false; + } + else + { + $currentTimestamp[$fileName]=@filemtime($fileName); + if($currentTimestamp[$fileName]>$timestamp || ($timestamp>0 && !$currentTimestamp[$fileName])) + $configCached=false; + } + } + } + } + else + { + $configCached=false; + $paths=explode('.',$pagePath); + $configPath=$this->getBasePath(); + $fileName = $this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP + ? self::CONFIG_FILE_PHP + : self::CONFIG_FILE_XML; + foreach($paths as $path) + { + $configFile=$configPath.DIRECTORY_SEPARATOR.$fileName; + $currentTimestamp[$configFile]=@filemtime($configFile); + $configPath.=DIRECTORY_SEPARATOR.$path; + } + $appConfigFile=$application->getConfigurationFile(); + $currentTimestamp[0]=$appConfigFile===null?0:@filemtime($appConfigFile); + } + if(!$configCached) + { + $pageConfig=new TPageConfiguration($pagePath); + if($config!==null) + { + if($application->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) + $pageConfig->loadPageConfigurationFromPhp($config,$application->getBasePath(),''); + else + $pageConfig->loadPageConfigurationFromXml($config,$application->getBasePath(),''); + } + $pageConfig->loadFromFiles($this->getBasePath()); + $cache->set(self::CONFIG_CACHE_PREFIX.$this->getID().$pagePath,array($pageConfig,$currentTimestamp)); + } + } + return $pageConfig; + } + + /** + * @return TTemplateManager template manager + */ + public function getTemplateManager() + { + if(!$this->_templateManager) + { + $this->_templateManager=new TTemplateManager; + $this->_templateManager->init(null); + } + return $this->_templateManager; + } + + /** + * @param TTemplateManager template manager + */ + public function setTemplateManager(TTemplateManager $value) + { + $this->_templateManager=$value; + } + + /** + * @return TThemeManager theme manager + */ + public function getThemeManager() + { + if(!$this->_themeManager) + { + $this->_themeManager=new TThemeManager; + $this->_themeManager->init(null); + } + return $this->_themeManager; + } + + /** + * @param TThemeManager theme manager + */ + public function setThemeManager(TThemeManager $value) + { + $this->_themeManager=$value; + } + + /** + * @return string the requested page path + */ + public function getRequestedPagePath() + { + if($this->_pagePath===null) + { + $this->_pagePath=strtr($this->determineRequestedPagePath(),'/\\','..'); + if(empty($this->_pagePath)) + throw new THttpException(404,'pageservice_page_required'); + } + return $this->_pagePath; + } + + /** + * @return TPage the requested page + */ + public function getRequestedPage() + { + return $this->_page; + } + + /** + * @return string default page path to be served if no explicit page is request. Defaults to 'Home'. + */ + public function getDefaultPage() + { + return $this->_defaultPage; + } + + /** + * @param string default page path to be served if no explicit page is request + * @throws TInvalidOperationException if the page service is initialized + */ + public function setDefaultPage($value) + { + if($this->_initialized) + throw new TInvalidOperationException('pageservice_defaultpage_unchangeable'); + else + $this->_defaultPage=$value; + } + + /** + * @return string the URL for the default page + */ + public function getDefaultPageUrl() + { + return $this->constructUrl($this->getDefaultPage()); + } + + /** + * @return string the root directory for storing pages. Defaults to the 'pages' directory under the application base path. + */ + public function getBasePath() + { + if($this->_basePath===null) + { + $basePath=$this->getApplication()->getBasePath().DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH; + if(($this->_basePath=realpath($basePath))===false || !is_dir($this->_basePath)) + { + $basePath=$this->getApplication()->getBasePath().DIRECTORY_SEPARATOR.self::FALLBACK_BASEPATH; + if(($this->_basePath=realpath($basePath))===false || !is_dir($this->_basePath)) + throw new TConfigurationException('pageservice_basepath_invalid',$basePath); + } + } + return $this->_basePath; + } + + /** + * @param string root directory (in namespace form) storing pages + * @throws TInvalidOperationException if the service is initialized already or basepath is invalid + */ + public function setBasePath($value) + { + if($this->_initialized) + throw new TInvalidOperationException('pageservice_basepath_unchangeable'); + else if(($path=Prado::getPathOfNamespace($value))===null || !is_dir($path)) + throw new TConfigurationException('pageservice_basepath_invalid',$value); + $this->_basePath=realpath($path); + } + + /** + * Sets the base page class name (in namespace format). + * If a page only has a template file without page class file, + * this base page class will be instantiated. + * @param string class name + */ + public function setBasePageClass($value) + { + $this->_basePageClass=$value; + } + + /** + * @return string base page class name in namespace format. Defaults to 'TPage'. + */ + public function getBasePageClass() + { + return $this->_basePageClass; + } + + /** + * Sets the clientscript manager class (in namespace format). + * @param string class name + * @since 3.1.7 + */ + public function setClientScriptManagerClass($value) + { + $this->_clientScriptManagerClass=$value; + } + + /** + * @return string clientscript manager class in namespace format. Defaults to 'System.Web.UI.TClientScriptManager'. + * @since 3.1.7 + */ + public function getClientScriptManagerClass() + { + return $this->_clientScriptManagerClass; + } + + /** + * Runs the service. + * This will create the requested page, initializes it with the property values + * specified in the configuration, and executes the page. + */ + public function run() + { + Prado::trace("Running page service",'System.Web.Services.TPageService'); + $this->_page=$this->createPage($this->getRequestedPagePath()); + $this->runPage($this->_page,$this->_properties); + } + + /** + * Creates a page instance based on requested page path. + * @param string requested page path + * @return TPage the requested page instance + * @throws THttpException if requested page path is invalid + * @throws TConfigurationException if the page class cannot be found + */ + protected function createPage($pagePath) + { + $path=$this->getBasePath().DIRECTORY_SEPARATOR.strtr($pagePath,'.',DIRECTORY_SEPARATOR); + $hasTemplateFile=is_file($path.self::PAGE_FILE_EXT); + $hasClassFile=is_file($path.Prado::CLASS_FILE_EXT); + + if(!$hasTemplateFile && !$hasClassFile) + throw new THttpException(404,'pageservice_page_unknown',$pagePath); + + if($hasClassFile) + { + $className=basename($path); + if(!class_exists($className,false)) + include_once($path.Prado::CLASS_FILE_EXT); + } + else + { + $className=$this->getBasePageClass(); + Prado::using($className); + if(($pos=strrpos($className,'.'))!==false) + $className=substr($className,$pos+1); + } + + if(!class_exists($className,false) || ($className!=='TPage' && !is_subclass_of($className,'TPage'))) + throw new THttpException(404,'pageservice_page_unknown',$pagePath); + + $page=Prado::createComponent($className); + $page->setPagePath($pagePath); + + if($hasTemplateFile) + $page->setTemplate($this->getTemplateManager()->getTemplateByFileName($path.self::PAGE_FILE_EXT)); + + return $page; + } + + /** + * Executes a page. + * @param TPage the page instance to be run + * @param array list of initial page properties + */ + protected function runPage($page,$properties) + { + foreach($properties as $name=>$value) + $page->setSubProperty($name,$value); + $page->run($this->getResponse()->createHtmlWriter()); + } + + /** + * Constructs a URL with specified page path and GET parameters. + * @param string page path + * @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($pagePath,$getParams=null,$encodeAmpersand=true,$encodeGetItems=true) + { + return $this->getRequest()->constructUrl($this->getID(),$pagePath,$getParams,$encodeAmpersand,$encodeGetItems); + } +} + + +/** + * TPageConfiguration class + * + * TPageConfiguration represents the configuration for a page. + * The page is specified by a dot-connected path. + * Configurations along this path are merged together to be provided for the page. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @package System.Web.Services + * @since 3.0 + */ +class TPageConfiguration extends TComponent +{ + /** + * @var array list of application configurations + */ + private $_appConfigs=array(); + /** + * @var array list of page initial property values + */ + private $_properties=array(); + /** + * @var TAuthorizationRuleCollection list of authorization rules + */ + private $_rules=array(); + /** + * @var array list of included configurations + */ + private $_includes=array(); + /** + * @var string the currently request page in the format of Path.To.PageName + */ + private $_pagePath=''; + + /** + * Constructor. + * @param string the currently request page in the format of Path.To.PageName + */ + public function __construct($pagePath) + { + $this->_pagePath=$pagePath; + } + + /** + * @return array list of external configuration files. Each element is like $filePath=>$condition + */ + public function getExternalConfigurations() + { + return $this->_includes; + } + + /** + * Returns list of page initial property values. + * Each array element represents a single property with the key + * being the property name and the value the initial property value. + * @return array list of page initial property values + */ + public function getProperties() + { + return $this->_properties; + } + + /** + * Returns list of authorization rules. + * The authorization rules are aggregated (bottom-up) from configuration files + * along the path to the specified page. + * @return TAuthorizationRuleCollection collection of authorization rules + */ + public function getRules() + { + return $this->_rules; + } + + /** + * @return array list of application configurations specified along page path + */ + public function getApplicationConfigurations() + { + return $this->_appConfigs; + } + + /** + * Loads configuration for a page specified in a path format. + * @param string root path for pages + */ + public function loadFromFiles($basePath) + { + $paths=explode('.',$this->_pagePath); + $page=array_pop($paths); + $path=$basePath; + $configPagePath=''; + $fileName = Prado::getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP + ? TPageService::CONFIG_FILE_PHP + : TPageService::CONFIG_FILE_XML; + foreach($paths as $p) + { + $this->loadFromFile($path.DIRECTORY_SEPARATOR.$fileName,$configPagePath); + $path.=DIRECTORY_SEPARATOR.$p; + if($configPagePath==='') + $configPagePath=$p; + else + $configPagePath.='.'.$p; + } + $this->loadFromFile($path.DIRECTORY_SEPARATOR.$fileName,$configPagePath); + $this->_rules=new TAuthorizationRuleCollection($this->_rules); + } + + /** + * Loads a specific config file. + * @param string config file name + * @param string the page path that the config file is associated with. The page path doesn't include the page name. + */ + public function loadFromFile($fname,$configPagePath) + { + Prado::trace("Loading page configuration file $fname",'System.Web.Services.TPageService'); + if(empty($fname) || !is_file($fname)) + return; + + if(Prado::getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) + { + $fcontent = include $fname; + $this->loadFromPhp($fcontent,dirname($fname),$configPagePath); + } + else + { + $dom=new TXmlDocument; + if($dom->loadFromFile($fname)) + $this->loadFromXml($dom,dirname($fname),$configPagePath); + else + throw new TConfigurationException('pageserviceconf_file_invalid',$fname); + } + } + + public function loadFromPhp($config,$configPath,$configPagePath) + { + $this->loadApplicationConfigurationFromPhp($config,$configPath); + $this->loadPageConfigurationFromPhp($config,$configPath,$configPagePath); + } + + /** + * Loads a page configuration. + * The configuration includes information for both application + * and page service. + * @param TXmlElement config xml element + * @param string the directory containing this configuration + * @param string the page path that the config XML is associated with. The page path doesn't include the page name. + */ + public function loadFromXml($dom,$configPath,$configPagePath) + { + $this->loadApplicationConfigurationFromXml($dom,$configPath); + $this->loadPageConfigurationFromXml($dom,$configPath,$configPagePath); + } + + public function loadApplicationConfigurationFromPhp($config,$configPath) + { + $appConfig=new TApplicationConfiguration; + $appConfig->loadFromPhp($config,$configPath); + $this->_appConfigs[]=$appConfig; + } + + /** + * Loads the configuration specific for application part + * @param TXmlElement config xml element + * @param string base path corresponding to this xml element + */ + public function loadApplicationConfigurationFromXml($dom,$configPath) + { + $appConfig=new TApplicationConfiguration; + $appConfig->loadFromXml($dom,$configPath); + $this->_appConfigs[]=$appConfig; + } + + public function loadPageConfigurationFromPhp($config, $configPath, $configPagePath) + { + // authorization + if(isset($config['authorization']) && is_array($config['authorization'])) + { + $rules = array(); + foreach($config['authorization'] as $authorization) + { + $patterns=isset($authorization['pages'])?$authorization['pages']:''; + $ruleApplies=false; + if(empty($patterns) || trim($patterns)==='*') // null or empty string + $ruleApplies=true; + else + { + foreach(explode(',',$patterns) as $pattern) + { + if(($pattern=trim($pattern))!=='') + { + // we know $configPagePath and $this->_pagePath + if($configPagePath!=='') // prepend the pattern with ConfigPagePath + $pattern=$configPagePath.'.'.$pattern; + if(strcasecmp($pattern,$this->_pagePath)===0) + { + $ruleApplies=true; + break; + } + if($pattern[strlen($pattern)-1]==='*') // try wildcard matching + { + if(strncasecmp($this->_pagePath,$pattern,strlen($pattern)-1)===0) + { + $ruleApplies=true; + break; + } + } + } + } + } + if($ruleApplies) + { + $action = isset($authorization['action'])?$authorization['action']:''; + $users = isset($authorization['users'])?$authorization['users']:''; + $roles = isset($authorization['roles'])?$authorization['roles']:''; + $verb = isset($authorization['verb'])?$authorization['verb']:''; + $ips = isset($authorization['ips'])?$authorization['ips']:''; + $rules[]=new TAuthorizationRule($action,$users,$roles,$verb,$ips); + } + } + $this->_rules=array_merge($rules,$this->_rules); + } + // pages + if(isset($config['pages']) && is_array($config['pages'])) + { + if(isset($config['pages']['properties'])) + { + $this->_properties = array_merge($this->_properties, $config['pages']['properties']); + unset($config['pages']['properties']); + } + foreach($config['pages'] as $id => $page) + { + $properties = array(); + if(isset($page['properties'])) + { + $properties=$page['properties']; + unset($page['properties']); + } + $matching=false; + $id=($configPagePath==='')?$id:$configPagePath.'.'.$id; + if(strcasecmp($id,$this->_pagePath)===0) + $matching=true; + else if($id[strlen($id)-1]==='*') // try wildcard matching + $matching=strncasecmp($this->_pagePath,$id,strlen($id)-1)===0; + if($matching) + $this->_properties=array_merge($this->_properties,$properties); + } + } + + // external configurations + if(isset($config['includes']) && is_array($config['includes'])) + { + foreach($config['includes'] as $include) + { + $when = isset($include['when'])?true:false; + if(!isset($include['file'])) + throw new TConfigurationException('pageserviceconf_includefile_required'); + $filePath = $include['file']; + if(isset($this->_includes[$filePath])) + $this->_includes[$filePath]=array($configPagePath,'('.$this->_includes[$filePath][1].') || ('.$when.')'); + else + $this->_includes[$filePath]=array($configPagePath,$when); + } + } + } + + /** + * Loads the configuration specific for page service. + * @param TXmlElement config xml element + * @param string base path corresponding to this xml element + * @param string the page path that the config XML is associated with. The page path doesn't include the page name. + */ + public function loadPageConfigurationFromXml($dom,$configPath,$configPagePath) + { + // authorization + if(($authorizationNode=$dom->getElementByTagName('authorization'))!==null) + { + $rules=array(); + foreach($authorizationNode->getElements() as $node) + { + $patterns=$node->getAttribute('pages'); + $ruleApplies=false; + if(empty($patterns) || trim($patterns)==='*') // null or empty string + $ruleApplies=true; + else + { + foreach(explode(',',$patterns) as $pattern) + { + if(($pattern=trim($pattern))!=='') + { + // we know $configPagePath and $this->_pagePath + if($configPagePath!=='') // prepend the pattern with ConfigPagePath + $pattern=$configPagePath.'.'.$pattern; + if(strcasecmp($pattern,$this->_pagePath)===0) + { + $ruleApplies=true; + break; + } + if($pattern[strlen($pattern)-1]==='*') // try wildcard matching + { + if(strncasecmp($this->_pagePath,$pattern,strlen($pattern)-1)===0) + { + $ruleApplies=true; + break; + } + } + } + } + } + if($ruleApplies) + $rules[]=new TAuthorizationRule($node->getTagName(),$node->getAttribute('users'),$node->getAttribute('roles'),$node->getAttribute('verb'),$node->getAttribute('ips')); + } + $this->_rules=array_merge($rules,$this->_rules); + } + + // pages + if(($pagesNode=$dom->getElementByTagName('pages'))!==null) + { + $this->_properties=array_merge($this->_properties,$pagesNode->getAttributes()->toArray()); + // at the page folder + foreach($pagesNode->getElementsByTagName('page') as $node) + { + $properties=$node->getAttributes(); + $id=$properties->remove('id'); + if(empty($id)) + throw new TConfigurationException('pageserviceconf_page_invalid',$configPath); + $matching=false; + $id=($configPagePath==='')?$id:$configPagePath.'.'.$id; + if(strcasecmp($id,$this->_pagePath)===0) + $matching=true; + else if($id[strlen($id)-1]==='*') // try wildcard matching + $matching=strncasecmp($this->_pagePath,$id,strlen($id)-1)===0; + if($matching) + $this->_properties=array_merge($this->_properties,$properties->toArray()); + } + } + + // external configurations + foreach($dom->getElementsByTagName('include') as $node) + { + if(($when=$node->getAttribute('when'))===null) + $when=true; + if(($filePath=$node->getAttribute('file'))===null) + throw new TConfigurationException('pageserviceconf_includefile_required'); + if(isset($this->_includes[$filePath])) + $this->_includes[$filePath]=array($configPagePath,'('.$this->_includes[$filePath][1].') || ('.$when.')'); + else + $this->_includes[$filePath]=array($configPagePath,$when); + } + } +} + diff --git a/lib/prado/framework/Web/Services/TRpcService.php b/lib/prado/framework/Web/Services/TRpcService.php new file mode 100644 index 0000000..cf57944 --- /dev/null +++ b/lib/prado/framework/Web/Services/TRpcService.php @@ -0,0 +1,722 @@ +<?php + +/** + * @author Robin J. Rogge <rrogge@bigpoint.net> + * @link https://github.com/pradosoft/prado + * @copyright 2010 Bigpoint GmbH + * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT + * @version $Id$ + * @since 3.2 + * @package System.Web.Services + */ + +/** + * TRpcService class + * + * The TRpcService class is a generic class that can be extended and used to implement + * rpc services using different servers and protocols. + * + * A server is a {@link TModule} that must subclass {@link TRpcServer}: its role is + * to be an intermediate, moving data between the service and the provider. The base + * {@link TRpcServer} class should suit the most common needs, but can be sublassed for + * logging and debugging purposes, or to filter and modify the request/response on the fly. + * + * A protocol is a {@link TModule} that must subclass {@link TRpcProtocol}: its role is + * to implement the protocol that exposes the rpc api. Prado already implements two + * protocols: {@link TXmlRpcProtocol} for Xml-Rpc request and {@link TJsonRpcProtocol} for + * JSON-Rpc requests. + * + * A provider is a {@link TModule} that must subclass {@link TRpcApiProvider}: its role is + * to implement the methods that are available through the api. Each defined api must be + * a sublass of the abstract class {@link TRpcApiProvider} and implement its methods. + * + * The flow of requests and reponses is the following: + * Request <-> TRpcService <-> TRpcServer <-> TRpcProtocol <-> TRpcApiProvider <-> Response + * + * To define an rpc service, add the proper application configuration: + * + * <code> + * <service id="rpc" class="System.Web.Services.TRpcService"> + * <rpcapi id="customers" class="Application.Api.CustomersApi" /> + * <modules> + * <!-- register any module needed by the service here --> + * </modules> + * </service> + * </code> + * + * An api can be registered adding a proper <rpcapi ..> definition inside the service + * configuration. Each api definition must contain an id property and a class name + * expressed in namespace format. When the service receives a request for that api, + * the specified class will be instanciated in order to satisfy the request. + * + * @author Robin J. Rogge <rrogge@bigpoint.net> + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + **/ +class TRpcService extends TService +{ + /** + * const string base api provider class which every API must extend + */ + const BASE_API_PROVIDER = 'TRpcApiProvider'; + + /** + * const string base RPC server implementation + */ + const BASE_RPC_SERVER = 'TRpcServer'; + + /** + * @var array containing mimetype to protocol handler mappings + */ + protected $protocolHandlers = array( + 'application/json' => 'TJsonRpcProtocol', + 'text/xml' => 'TXmlRpcProtocol' + ); + + /** + * @var array containing API provider and their configured properties + */ + protected $apiProviders = array(); + + // methods + + /** + * Creates the API provider instance for the current request + * @param TRpcProtocol $protocolHandler instance + * @param string $providerId + */ + public function createApiProvider(TRpcProtocol $protocolHandler, $providerId) + { + $_properties = $this->apiProviders[$providerId]; + + if(($_providerClass = $_properties->remove('class')) === null) + throw new TConfigurationException('rpcservice_apiprovider_required'); + + Prado::using($_providerClass); + + $_providerClassName = ($_pos = strrpos($_providerClass, '.')) !== false ? substr($_providerClass, $_pos + 1) : $_providerClass; + if(!is_subclass_of($_providerClassName, self::BASE_API_PROVIDER)) + throw new TConfigurationException('rpcservice_apiprovider_invalid'); + + if(($_rpcServerClass = $_properties->remove('server')) === null) + $_rpcServerClass = self::BASE_RPC_SERVER; + + Prado::using($_rpcServerClass); + + $_rpcServerClassName = ($_pos = strrpos($_rpcServerClass, '.')) !== false ? substr($_rpcServerClass, $_pos + 1) : $_rpcServerClass; + if($_rpcServerClassName!==self::BASE_RPC_SERVER && !is_subclass_of($_rpcServerClassName, self::BASE_RPC_SERVER)) + throw new TConfigurationException('rpcservice_rpcserver_invalid'); + + $_apiProvider = new $_providerClassName(new $_rpcServerClassName($protocolHandler)); + $_apiProvider->setId($providerId); + + foreach($_properties as $_key => $_value) + $_apiProvider->setSubProperty($_key, $_value); + + return $_apiProvider; + } + + /** + * Initializes the service + * @param TXmlElement $config containing the module configuration + */ + public function init($config) + { + $this->loadConfig($config); + } + + /** + * Loads the service configuration + * @param TXmlElement $xml configuration + */ + public function loadConfig(TXmlElement $xml) + { + foreach($xml->getElementsByTagName('rpcapi') as $_apiProviderXml) + { + $_properties = $_apiProviderXml->getAttributes(); + + if(($_id = $_properties->remove('id')) === null || $_id == "") + throw new TConfigurationException('rpcservice_apiproviderid_required'); + + if(isset($this->apiProviders[$_id])) + throw new TConfigurationException('rpcservice_apiproviderid_duplicated'); + + $this->apiProviders[$_id] = $_properties; + } + } + + /** + * Runs the service + */ + public function run() + { + $_request = $this->getRequest(); + + if(($_providerId = $_request->getServiceParameter()) == "") + throw new THttpException(400, 'RPC API-Provider id required'); + + if(($_method = $_request->getRequestType()) != 'POST') + throw new THttpException(405, 'Invalid request method "'.$_method.'"!'); // TODO Exception muss "Allow POST" Header setzen + + if(($_mimeType = $_request->getContentType()) === null) + throw new THttpException(406, 'Content-Type is missing!'); // TODO Exception muss gültige Content-Type werte zurück geben + + if(!in_array($_mimeType, array_keys($this->protocolHandlers))) + throw new THttpException(406, 'Unsupported Content-Type!'); // TODO see previous + + $_protocolHandlerClass = $this->protocolHandlers[$_mimeType]; + $_protocolHandler = new $_protocolHandlerClass; + + if(($_result = $this->createApiProvider($_protocolHandler, $_providerId)->processRequest()) !== null) + { + $_response = $this->getResponse(); + $_protocolHandler->createResponseHeaders($_response); + $_response->write($_result); + } + } +} + +/** + * TRpcServer class + * + * TRpcServer is a class + * + * TRpcServer is the base class used to creare a server to be used in conjunction with + * {@link TRpcService}. + * The role of TRpcServer is to be an intermediate, moving data between the service and + * the provider. This base class should suit the most common needs, but can be sublassed for + * logging and debugging purposes, or to filter and modify the request/response on the fly. + * + * @author Robin J. Rogge <rrogge@bigpoint.net> + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + **/ +class TRpcServer extends TModule +{ + /** + * @var TRpcProtocol instance + */ + protected $handler; + + /** + * Constructor + * @param TRpcProtocol $protocolHandler instance + */ + public function __construct(TRpcProtocol $protocolHandler) + { + $this->handler = $protocolHandler; + } + + /** + * Registers the method in the protocol handler + * @param string $methodName + * @param array $methodDetails + */ + public function addRpcMethod($methodName, $methodDetails) + { + $this->handler->addMethod($methodName, $methodDetails); + } + + /** + * Retrieves the request payload + * @return string request payload + */ + public function getPayload() + { + return file_get_contents('php://input'); + } + + /** + * Passes the request payload to the protocol handler and returns the result + * @return string rpc response + */ + public function processRequest() + { + try + { + return $this->handler->callMethod($this->getPayload()); + } + catch(TRpcException $e) + { + return $this->handler->createErrorResponse($e); + } + } +} + +/** + * TRpcException class + * + * A TRpcException represents a RPC fault i.e. an error that is caused by the input data + * sent from the client. + * + * @author Robin J. Rogge <rrogge@bigpoint.net> + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + */ +class TRpcException extends TException +{ + public function __construct($message, $errorCode = -1) + { + $this->setErrorCode($errorCode); + + parent::__construct($message); + } +} + +/** + * TRpcApiProvider class + * + * TRpcApiProvider is an abstract class the can be subclasses in order to implement an + * api for a {@link TRpcService}. A subclass of TRpcApiProvider must implement the + * {@link registerMethods} method in order to declare the available methods, their + * names and the associated callback. + * + * <code> + * public function registerMethods() + * { + * return array( + * 'apiMethodName1' => array('method' => array($this, 'objectMethodName1')), + * 'apiMethodName2' => array('method' => array('ClassName', 'staticMethodName')), + * ); + * } + * </code> + * + * In this example, two api method have been defined. The first refers to an object + * method that must be implemented in the same class, the second to a static method + * implemented in a 'ClassName' class. + * In both cases, the method implementation will receive the request parameters as its + * method parameters. Since the number of received parameters depends on + * external-supplied data, it's adviced to use php's func_get_args() funtion to + * validate them. + * + * Providers must be registered in the service configuration in order to be available, + * as explained in {@link TRpcService}'s documentation. + * + * @author Robin J. Rogge <rrogge@bigpoint.net> + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + */ +abstract class TRpcApiProvider extends TModule +{ + /** + * @var TRpcServer instance + */ + protected $rpcServer; + + /** + * Must return an array of the available methods + * @abstract + */ + abstract public function registerMethods(); + + /** + * Constructor: informs the rpc server of the registered methods + */ + public function __construct(TRpcServer $rpcServer) + { + $this->rpcServer = $rpcServer; + + foreach($this->registerMethods() as $_methodName => $_methodDetails) + $this->rpcServer->addRpcMethod($_methodName, $_methodDetails); + } + + /** + * Processes the request using the server + * @return processed request + */ + public function processRequest() + { + return $this->rpcServer->processRequest(); + } + + /** + * @return rpc server instance + */ + public function getRpcServer() + { + return $this->rpcServer; + } +} + +/** + * TRpcProtocol class + * + * TRpcProtocol is the base class used to implement a protocol in a {@link TRpcService}. + * Prado already implements two protocols: {@link TXmlRpcProtocol} for Xml-Rpc request + * and {@link TJsonRpcProtocol} for JSON-Rpc requests. + * + * @author Robin J. Rogge <rrogge@bigpoint.net> + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + **/ +abstract class TRpcProtocol +{ + /** + * @var array containing the mapping from RPC method names to the actual handlers + */ + protected $rpcMethods = array(); + + // abstracts + + /** + * @param string request payload + * Processed the request ans returns the response, if any + * @return processed response + * @abstract + */ + abstract public function callMethod($requestPayload); + /** + * @param TRpcException the exception with error details + * Creates a proper response for an error condition + * @return a response representing the error + * @abstract + */ + abstract public function createErrorResponse(TRpcException $exception); + /** + * @param response + * Sets the needed headers for the response (eg: content-type, charset) + * @abstract + */ + abstract public function createResponseHeaders($response); + /** + * Encodes the response + * @param mixed reponse data + * @return string encoded response + * @abstract + */ + abstract public function encode($data); + /** + * Decodes the request payload + * @param string request payload + * @return mixed decoded request + * @abstract + */ + abstract public function decode($data); + + // methods + + /** + * Registers a new RPC method and handler details + * @param string $methodName + * @param array $handlerDetails containing the callback handler + */ + public function addMethod($methodName, $handlerDetails) + { + $this->rpcMethods[$methodName] = $handlerDetails; + } + + /** + * Calls the callback handler for the given method + * @param string $methodName of the RPC + * @param array $parameters for the callback handler as provided by the client + * @return mixed whatever the callback handler returns + */ + public function callApiMethod($methodName, $parameters) + { + if(!isset($this->rpcMethods[$methodName])) + throw new TRpcException('Method "'.$methodName.'" not found'); + + if($parameters === null) + $parameters = array(); + + if(!is_array($parameters)) + $parameters = array($parameters); + return call_user_func_array($this->rpcMethods[$methodName]['method'], $parameters); + } +} + +/** + * TJsonRpcProtocol class + * + * TJsonRpcProtocol is a class that implements JSON-Rpc protocol in {@link TRpcService}. + * Both version 1.0 and 2.0 of the specification are implemented, and the server will try + * to answer using the same version of the protocol used by the requesting client. + * + * @author Robin J. Rogge <rrogge@bigpoint.net> + * @author Fabio Bas <ctrlaltca@gmail.com> + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + */ +class TJsonRpcProtocol extends TRpcProtocol +{ + protected $_id=null; + protected $_specificationVersion=1.0; + + /** + * Handles the RPC request + * @param string $requestPayload + * @return string JSON RPC response + */ + public function callMethod($requestPayload) + { + try + { + $_request = $this->decode($requestPayload); + + if(isset($_request['jsonrpc'])) + { + $this->_specificationVersion=$_request['jsonrpc']; + if($this->_specificationVersion > 2.0) + throw new TRpcException('Unsupported specification version', '-32600'); + } + + if(isset($_request['id'])) + $this->_id=$_request['id']; + + if(!isset($_request['method'])) + throw new TRpcException('Missing request method', '-32600'); + + if(!isset($_request['params'])) + $parameters = array(); + else + $parameters = $_request['params']; + + if(!is_array($parameters)) + $parameters = array($parameters); + + // a request without an id is a notification that doesn't need a response + if($this->_id !== null) + { + if($this->_specificationVersion==2.0) + { + return $this->encode(array( + 'jsonrpc' => '2.0', + 'id' => $this->_id, + 'result' => $this->callApiMethod($_request['method'], $parameters), + )); + } else { + return $this->encode(array( + 'id' => $this->_id, + 'result' => $this->callApiMethod($_request['method'], $_request['params']), + 'error' => null + )); + } + } + } + catch(TRpcException $e) + { + return $this->createErrorResponse($e); + } + catch(THttpException $e) + { + throw $e; + } + catch(Exception $e) + { + return $this->createErrorResponse(new TRpcException('An internal error occured', '-32603')); + } + } + + /** + * Turns the given exception into an JSON RPC fault + * @param TRpcException $exception + * @return string JSON RPC fault + */ + public function createErrorResponse(TRpcException $exception) + { + if($this->_specificationVersion==2.0) + { + return $this->encode(array( + 'id' => $this->_id, + 'result' => null, + 'error'=> array( + 'code' => $exception->getCode(), + 'message'=> $exception->getMessage(), + 'data' => null, + ) + )); + } else { + return $this->encode(array( + 'id' => $this->_id, + 'error'=> array( + 'code' => $exception->getCode(), + 'message'=> $exception->getMessage(), + 'data' => null, + ) + )); + } + } + + /** + * Sets the correct response headers + * @param THttpResponse $response + */ + public function createResponseHeaders($response) + { + $response->setContentType('application/json'); + $response->setCharset('UTF-8'); + } + + /** + * Decodes JSON encoded data into PHP data + * @param string $data in JSON format + * @return array PHP data + */ + public function decode($data) + { + $s = json_decode($data, true); + self::checkJsonError(); + return $s; + } + + /** + * Encodes PHP data into JSON data + * @param mixed PHP data + * @return string JSON encoded PHP data + */ + public function encode($data) + { + $s = json_encode($data); + self::checkJsonError(); + return $s; + } + + private static function checkJsonError() + { + $errnum = json_last_error(); + if($errnum != JSON_ERROR_NONE) + throw new Exception("JSON error: $msg", $err); + } + + /** + * Calls the callback handler for the given method + * Overrides parent implementation to correctly handle error codes + * @param string $methodName of the RPC + * @param array $parameters for the callback handler as provided by the client + * @return mixed whatever the callback handler returns + */ + public function callApiMethod($methodName, $parameters) + { + if(!isset($this->rpcMethods[$methodName])) + throw new TRpcException('Method "'.$methodName.'" not found', '-32601'); + + return call_user_func_array($this->rpcMethods[$methodName]['method'], $parameters); + } +} + +/** + * TXmlRpcProtocol class + * + * TXmlRpcProtocol is a class that implements XML-Rpc protocol in {@link TRpcService}. + * It's basically a wrapper to the xmlrpc_server_* family of php methods. + * + * @author Robin J. Rogge <rrogge@bigpoint.net> + * @version $Id$ + * @package System.Web.Services + * @since 3.2 + */ +class TXmlRpcProtocol extends TRpcProtocol +{ + /** + * @var XML RPC server resource + */ + private $_xmlrpcServer; + + // magics + + /** + * Constructor + */ + public function __construct() + { + $this->_xmlrpcServer = xmlrpc_server_create(); + } + + /** + * Destructor + */ + public function __destruct() + { + xmlrpc_server_destroy($this->_xmlrpcServer); + } + + // methods + + /** + * Registers a new RPC method and handler details + * @param string $methodName + * @param array $handlerDetails containing the callback handler + */ + public function addMethod($methodName, $methodDetails) + { + parent::addMethod($methodName, $methodDetails); + + xmlrpc_server_register_method($this->_xmlrpcServer, $methodName, array($this, 'callApiMethod')); + } + + // methods + + /** + * Handles the RPC request + * @param string $requestPayload + * @return string XML RPC response + */ + public function callMethod($requestPayload) + { + try + { + return xmlrpc_server_call_method($this->_xmlrpcServer, $requestPayload, null); + } + catch(TRpcException $e) + { + return $this->createErrorResponse($e); + } + catch(THttpException $e) + { + throw $e; + } + catch(Exception $e) + { + return $this->createErrorResponse(new TRpcException('An internal error occured')); + } + } + + /** + * Turns the given exception into an XML RPC fault + * @param TRpcException $exception + * @return string XML RPC fault + */ + public function createErrorResponse(TRpcException $exception) + { + return $this->encode(array( + 'faultCode' => $exception->getCode(), + 'faultString' => $exception->getMessage() + )); + } + + /** + * Sets the correct response headers + * @param THttpResponse $response + */ + public function createResponseHeaders($response) + { + $response->setContentType('text/xml'); + $response->setCharset('UTF-8'); + } + + /** + * Decodes XML encoded data into PHP data + * @param string $data in XML format + * @return array PHP data + */ + public function decode($data) + { + return xmlrpc_decode($data); + } + + /** + * Encodes PHP data into XML data + * @param mixed PHP data + * @return string XML encoded PHP data + */ + public function encode($data) + { + return xmlrpc_encode($data); + } +} diff --git a/lib/prado/framework/Web/Services/TSoapService.php b/lib/prado/framework/Web/Services/TSoapService.php new file mode 100644 index 0000000..9554af2 --- /dev/null +++ b/lib/prado/framework/Web/Services/TSoapService.php @@ -0,0 +1,622 @@ +<?php +/** + * TSoapService and TSoapServer class file + * + * @author Knut Urdalen <knut.urdalen@gmail.com> + * @author Qiang Xue <qiang.xue@gmail.com> + * @link https://github.com/pradosoft/prado + * @copyright Copyright © 2005-2015 The PRADO Group + * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT + * @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()->clear(); + $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> + * @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; + } +} + |