diff --git a/webservice/lib.php b/webservice/lib.php index 7e24e9d0e9b..1ddd70db3eb 100644 --- a/webservice/lib.php +++ b/webservice/lib.php @@ -1580,6 +1580,12 @@ abstract class webservice_base_server extends webservice_server { /** @var mixed Function return value */ protected $returns = null; + /** @var array List of methods and their information provided by the web service. */ + protected $servicemethods; + + /** @var array List of struct classes generated for the web service methods. */ + protected $servicestructs; + /** * This method parses the request input, it needs to get: * 1/ user authentication - username+password or token @@ -1778,6 +1784,317 @@ abstract class webservice_base_server extends webservice_server { // execute - yay! $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), array_values($params)); } + + /** + * Load the virtual class needed for the web service. + * + * Initialises the virtual class that contains the web service functions that the user is allowed to use. + * The web service function will be available if the user: + * - is validly registered in the external_services_users table. + * - has the required capability. + * - meets the IP restriction requirement. + * This virtual class can be used by web service protocols such as SOAP, especially when generating WSDL. + * NOTE: The implementation of this method has been mostly copied from webservice_zend_server::init_server_class(). + */ + protected function init_service_class() { + global $USER, $DB; + + // Initialise service methods and struct classes. + $this->servicemethods = array(); + $this->servicestructs = array(); + + $params = array(); + $wscond1 = ''; + $wscond2 = ''; + if ($this->restricted_serviceid) { + $params = array('sid1' => $this->restricted_serviceid, 'sid2' => $this->restricted_serviceid); + $wscond1 = 'AND s.id = :sid1'; + $wscond2 = 'AND s.id = :sid2'; + } + + $sql = "SELECT s.*, NULL AS iprestriction + FROM {external_services} s + JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0) + WHERE s.enabled = 1 $wscond1 + + UNION + + SELECT s.*, su.iprestriction + FROM {external_services} s + JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1) + JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid) + WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2"; + $params = array_merge($params, array('userid' => $USER->id, 'now' => time())); + + $serviceids = array(); + $remoteaddr = getremoteaddr(); + + // Query list of external services for the user. + $rs = $DB->get_recordset_sql($sql, $params); + + // Check which service ID to include. + foreach ($rs as $service) { + if (isset($serviceids[$service->id])) { + continue; // Service already added. + } + if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { + continue; // Cap required, sorry. + } + if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { + continue; // Wrong request source ip, sorry. + } + $serviceids[$service->id] = $service->id; + } + $rs->close(); + + // Generate the virtual class name. + $classname = 'webservices_virtual_class_000000'; + while (class_exists($classname)) { + $classname++; + } + $this->serviceclass = $classname; + + // Get the list of all available external functions. + $wsmanager = new webservice(); + $functions = $wsmanager->get_external_functions($serviceids); + + // Generate code for the virtual methods for this web service. + $methods = ''; + foreach ($functions as $function) { + $methods .= $this->get_virtual_method_code($function); + } + + $code = <<id in context {$this->restricted_context->id}. + */ +class $classname { +$methods } +EOD; + // Load the virtual class definition into memory. + eval($code); + } + /** + * Generates a struct class. + * + * NOTE: The implementation of this method has been mostly copied from webservice_zend_server::generate_simple_struct_class(). + * @param external_single_structure $structdesc The basis of the struct class to be generated. + * @return string The class name of the generated struct class. + */ + protected function generate_simple_struct_class(external_single_structure $structdesc) { + global $USER; + $propeties = array(); + $fields = array(); + foreach ($structdesc->keys as $name => $fieldsdesc) { + $type = $this->get_phpdoc_type($fieldsdesc); + $propertytype = array('type' => $type); + if (empty($fieldsdesc->allownull) || $fieldsdesc->allownull == NULL_ALLOWED) { + $propertytype['nillable'] = true; + } + $propeties[$name] = $propertytype; + $fields[] = ' /** @var ' . $type . ' $' . $name . '*/'; + $fields[] = ' public $' . $name .';'; + } + $fieldsstr = implode("\n", $fields); + + // We do this after the call to get_phpdoc_type() to avoid duplicate class creation. + $classname = 'webservices_struct_class_000000'; + while (class_exists($classname)) { + $classname++; + } + $code = <<id in context {$this->restricted_context->id}. + */ +class $classname { +$fieldsstr +} +EOD; + // Load into memory. + eval($code); + + // Prepare struct info. + $structinfo = new stdClass(); + $structinfo->classname = $classname; + $structinfo->properties = $propeties; + // Add the struct info the the list of service struct classes. + $this->servicestructs[] = $structinfo; + + return $classname; + } + + /** + * Returns a virtual method code for a web service function. + * + * NOTE: The implementation of this method has been mostly copied from webservice_zend_server::get_virtual_method_code(). + * @param stdClass $function a record from external_function + * @return string The PHP code of the virtual method. + * @throws coding_exception + * @throws moodle_exception + */ + protected function get_virtual_method_code($function) { + $function = external_function_info($function); + + // Parameters and their defaults for the method signature. + $paramanddefaults = array(); + // Parameters for external lib call. + $params = array(); + $paramdesc = array(); + // The method's input parameters and their respective types. + $inputparams = array(); + // The method's output parameters and their respective types. + $outputparams = array(); + + foreach ($function->parameters_desc->keys as $name => $keydesc) { + $param = '$' . $name; + $paramanddefault = $param; + if ($keydesc->required == VALUE_OPTIONAL) { + // It does not make sense to declare a parameter VALUE_OPTIONAL. VALUE_OPTIONAL is used only for array/object key. + throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name); + } else if ($keydesc->required == VALUE_DEFAULT) { + // Need to generate the default, if there is any. + if ($keydesc instanceof external_value) { + if ($keydesc->default === null) { + $paramanddefault .= ' = null'; + } else { + switch ($keydesc->type) { + case PARAM_BOOL: + $default = (int)$keydesc->default; + break; + case PARAM_INT: + $default = $keydesc->default; + break; + case PARAM_FLOAT; + $default = $keydesc->default; + break; + default: + $default = "'$keydesc->default'"; + } + $paramanddefault .= " = $default"; + } + } else { + // Accept empty array as default. + if (isset($keydesc->default) && is_array($keydesc->default) && empty($keydesc->default)) { + $paramanddefault .= ' = array()'; + } else { + // For the moment we do not support default for other structure types. + throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name); + } + } + } + + $params[] = $param; + $paramanddefaults[] = $paramanddefault; + $type = $this->get_phpdoc_type($keydesc); + $inputparams[$name]['type'] = $type; + + $paramdesc[] = '* @param ' . $type . ' $' . $name . ' ' . $keydesc->desc; + } + $paramanddefaults = implode(', ', $paramanddefaults); + $paramdescstr = implode("\n ", $paramdesc); + + $serviceclassmethodbody = $this->service_class_method_body($function, $params); + + if (empty($function->returns_desc)) { + $return = '* @return void'; + } else { + $type = $this->get_phpdoc_type($function->returns_desc); + $outputparams['return']['type'] = $type; + $return = '* @return ' . $type . ' ' . $function->returns_desc->desc; + } + + // Now create the virtual method that calls the ext implementation. + $code = <<description. + * + $paramdescstr + $return + */ +public function $function->name($paramanddefaults) { +$serviceclassmethodbody +} +EOD; + + // Prepare the method information. + $methodinfo = new stdClass(); + $methodinfo->name = $function->name; + $methodinfo->inputparams = $inputparams; + $methodinfo->outputparams = $outputparams; + $methodinfo->description = $function->description; + // Add the method information into the list of service methods. + $this->servicemethods[] = $methodinfo; + + return $code; + } + + /** + * Get the phpdoc type for an external_description object. + * external_value => int, double or string + * external_single_structure => object|struct, on-fly generated stdClass name. + * external_multiple_structure => array + * + * @param mixed $keydesc The type description. + * @return string The PHP doc type of the external_description object. + */ + protected function get_phpdoc_type($keydesc) { + $type = null; + if ($keydesc instanceof external_value) { + switch ($keydesc->type) { + case PARAM_BOOL: // 0 or 1 only for now. + case PARAM_INT: + $type = 'int'; + break; + case PARAM_FLOAT; + $type = 'double'; + break; + default: + $type = 'string'; + } + } else if ($keydesc instanceof external_single_structure) { + $type = $this->generate_simple_struct_class($keydesc); + } else if ($keydesc instanceof external_multiple_structure) { + $type = 'array'; + } + + return $type; + } + + /** + * Generates the method body of the virtual external function. + * + * NOTE: The implementation of this method has been mostly copied from webservice_zend_server::service_class_method_body(). + * @param stdClass $function a record from external_function. + * @param array $params web service function parameters. + * @return string body of the method for $function ie. everything within the {} of the method declaration. + */ + protected function service_class_method_body($function, $params) { + // Cast the param from object to array (validate_parameters except array only). + $castingcode = ''; + $paramsstr = ''; + if (!empty($params)) { + foreach ($params as $paramtocast) { + // Clean the parameter from any white space. + $paramtocast = trim($paramtocast); + $castingcode .= " $paramtocast = json_decode(json_encode($paramtocast), true);\n"; + } + $paramsstr = implode(', ', $params); + } + + $descriptionmethod = $function->methodname . '_returns()'; + $callforreturnvaluedesc = $function->classname . '::' . $descriptionmethod; + + $methodbody = <<classname::$function->methodname($paramsstr); + return null; + } + return external_api::clean_returnvalue($callforreturnvaluedesc, $function->classname::$function->methodname($paramsstr)); +EOD; + return $methodbody; + } +} diff --git a/webservice/soap/classes/wsdl.php b/webservice/soap/classes/wsdl.php new file mode 100644 index 00000000000..10082ad0ed9 --- /dev/null +++ b/webservice/soap/classes/wsdl.php @@ -0,0 +1,261 @@ +. + +/** + * WSDL generator for the SOAP web service. + * + * @package webservice_soap + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace webservice_soap; + +/** + * WSDL generator for the SOAP web service. + * + * @package webservice_soap + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class wsdl { + /** Namespace URI for the WSDL framework. */ + const NS_WSDL = 'http://schemas.xmlsoap.org/wsdl/'; + + /** Encoding namespace URI as defined by SOAP 1.1 */ + const NS_SOAP_ENC = 'http://schemas.xmlsoap.org/soap/encoding/'; + + /** Namespace URI for the WSDL SOAP binding. */ + const NS_SOAP = 'http://schemas.xmlsoap.org/wsdl/soap/'; + + /** Schema namespace URI as defined by XSD. */ + const NS_XSD = 'http://www.w3.org/2001/XMLSchema'; + + /** WSDL namespace for the WSDL HTTP GET and POST binding. */ + const NS_SOAP_TRANSPORT = 'http://schemas.xmlsoap.org/soap/http'; + + /** BINDING - string constant attached to the service class name to identify binding nodes. */ + const BINDING = 'Binding'; + + /** IN - string constant attached to the function name to identify input nodes. */ + const IN = 'In'; + + /** OUT - string constant attached to the function name to identify output nodes. */ + const OUT = 'Out'; + + /** PORT - string constant attached to the service class name to identify port nodes. */ + const PORT = 'Port'; + + /** SERVICE string constant attached to the service class name to identify service nodes. */ + const SERVICE = 'Service'; + + /** @var string The name of the service class. */ + private $serviceclass; + + /** @var string The WSDL namespace. */ + private $namespace; + + /** @var array The WSDL's message nodes. */ + private $messagenodes; + + /** @var \SimpleXMLElement The WSDL's binding node. */ + private $nodebinding; + + /** @var \SimpleXMLElement The WSDL's definitions node. */ + private $nodedefinitions; + + /** @var \SimpleXMLElement The WSDL's portType node. */ + private $nodeporttype; + + /** @var \SimpleXMLElement The WSDL's service node. */ + private $nodeservice; + + /** @var \SimpleXMLElement The WSDL's types node. */ + private $nodetypes; + + /** + * webservice_soap_wsdl constructor. + * + * @param string $serviceclass The service class' name. + * @param string $namespace The WSDL namespace. + */ + public function __construct($serviceclass, $namespace) { + $this->serviceclass = $serviceclass; + $this->namespace = $namespace; + + // Initialise definitions node. + $this->nodedefinitions = new \SimpleXMLElement(''); + $this->nodedefinitions->addAttribute('xmlns', self::NS_WSDL); + $this->nodedefinitions->addAttribute('x:xmlns:tns', $namespace); + $this->nodedefinitions->addAttribute('x:xmlns:soap', self::NS_SOAP); + $this->nodedefinitions->addAttribute('x:xmlns:xsd', self::NS_XSD); + $this->nodedefinitions->addAttribute('x:xmlns:soap-enc', self::NS_SOAP_ENC); + $this->nodedefinitions->addAttribute('x:xmlns:wsdl', self::NS_WSDL); + $this->nodedefinitions->addAttribute('name', $serviceclass); + $this->nodedefinitions->addAttribute('targetNamespace', $namespace); + + // Initialise types node. + $this->nodetypes = $this->nodedefinitions->addChild('types'); + $typeschema = $this->nodetypes->addChild('x:xsd:schema'); + $typeschema->addAttribute('targetNamespace', $namespace); + + // Initialise the portType node. + $this->nodeporttype = $this->nodedefinitions->addChild('portType'); + $this->nodeporttype->addAttribute('name', $serviceclass . self::PORT); + + // Initialise the binding node. + $this->nodebinding = $this->nodedefinitions->addChild('binding'); + $this->nodebinding->addAttribute('name', $serviceclass . self::BINDING); + $this->nodebinding->addAttribute('type', 'tns:' . $serviceclass . self::PORT); + $soapbinding = $this->nodebinding->addChild('x:soap:binding'); + $soapbinding->addAttribute('style', 'rpc'); + $soapbinding->addAttribute('transport', self::NS_SOAP_TRANSPORT); + + // Initialise the service node. + $this->nodeservice = $this->nodedefinitions->addChild('service'); + $this->nodeservice->addAttribute('name', $serviceclass . self::SERVICE); + $serviceport = $this->nodeservice->addChild('port'); + $serviceport->addAttribute('name', $serviceclass . self::PORT); + $serviceport->addAttribute('binding', 'tns:' . $serviceclass . self::BINDING); + $soapaddress = $serviceport->addChild('x:soap:address'); + $soapaddress->addAttribute('location', $namespace); + + // Initialise message nodes. + $this->messagenodes = array(); + } + + /** + * Adds a complex type to the WSDL. + * + * @param string $classname The complex type's class name. + * @param array $properties An associative array containing the properties of the complex type class. + */ + public function add_complex_type($classname, $properties) { + $typeschema = $this->nodetypes->children(); + // Append the complex type. + $complextype = $typeschema->addChild('x:xsd:complexType'); + $complextype->addAttribute('name', $classname); + $child = $complextype->addChild('x:xsd:all'); + foreach ($properties as $name => $options) { + $param = $child->addChild('x:xsd:element'); + $param->addAttribute('name', $name); + $param->addAttribute('type', $this->get_soap_type($options['type'])); + if (!empty($options['nillable'])) { + $param->addAttribute('nillable', 'true'); + } + } + } + + /** + * Registers the external service method to the WSDL. + * + * @param string $functionname The name of the web service function to be registered. + * @param array $inputparams Contains the function's input parameters with their associated types. + * @param array $outputparams Contains the function's output parameters with their associated types. + * @param string $documentation The function's description. + */ + public function register($functionname, $inputparams = array(), $outputparams = array(), $documentation = '') { + // Process portType operation nodes. + $porttypeoperation = $this->nodeporttype->addChild('operation'); + $porttypeoperation->addAttribute('name', $functionname); + // Documentation node. + $porttypeoperation->addChild('documentation', $documentation); + + // Process binding operation nodes. + $bindingoperation = $this->nodebinding->addChild('operation'); + $bindingoperation->addAttribute('name', $functionname); + $soapoperation = $bindingoperation->addChild('x:soap:operation'); + $soapoperation->addAttribute('soapAction', $this->namespace . '#' . $functionname); + + // Input nodes. + $this->process_params($functionname, $porttypeoperation, $bindingoperation, $inputparams); + + // Output nodes. + $this->process_params($functionname, $porttypeoperation, $bindingoperation, $outputparams, true); + } + + /** + * Outputs the WSDL in XML format. + * + * @return mixed The string value of the WSDL in XML format. False, otherwise. + */ + public function to_xml() { + // Return WSDL in XML format. + return $this->nodedefinitions->asXML(); + } + + /** + * Utility method that returns the encoded SOAP type based on the given type string. + * + * @param string $type The input type string. + * @return string The encoded type for the WSDL. + */ + private function get_soap_type($type) { + switch($type) { + case 'int': + case 'double': + case 'string': + return 'xsd:' . $type; + case 'array': + return 'soap-enc:Array'; + default: + return 'tns:' . $type; + } + } + + /** + * Utility method that creates input/output nodes from input/output params. + * + * @param string $functionname The name of the function being registered. + * @param \SimpleXMLElement $porttypeoperation The port type operation node. + * @param \SimpleXMLElement $bindingoperation The binding operation node. + * @param array $params The function's input/output parameters. + * @param bool $isoutput Flag to indicate if the nodes to be generated are for input or for output. + */ + private function process_params($functionname, \SimpleXMLElement $porttypeoperation, \SimpleXMLElement $bindingoperation, + array $params = null, $isoutput = false) { + // Do nothing if parameter array is empty. + if (empty($params)) { + return; + } + + $postfix = self::IN; + $childtype = 'input'; + if ($isoutput) { + $postfix = self::OUT; + $childtype = 'output'; + } + + // For portType operation node. + $child = $porttypeoperation->addChild($childtype); + $child->addAttribute('message', 'tns:' . $functionname . $postfix); + + // For binding operation node. + $child = $bindingoperation->addChild($childtype); + $soapbody = $child->addChild('x:soap:body'); + $soapbody->addAttribute('use', 'encoded'); + $soapbody->addAttribute('encodingStyle', self::NS_SOAP_ENC); + $soapbody->addAttribute('namespace', $this->namespace); + + // Process message nodes. + $messagein = $this->nodedefinitions->addChild('message'); + $messagein->addAttribute('name', $functionname . $postfix); + foreach ($params as $name => $options) { + $part = $messagein->addChild('part'); + $part->addAttribute('name', $name); + $part->addAttribute('type', $this->get_soap_type($options['type'])); + } + } +} diff --git a/webservice/soap/lib.php b/webservice/soap/lib.php index e2612a923a8..65d7d51fe59 100644 --- a/webservice/soap/lib.php +++ b/webservice/soap/lib.php @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . - /** * Moodle SOAP library * @@ -23,8 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require_once 'Zend/Soap/Client.php'; - /** * Moodle SOAP client * @@ -34,11 +31,17 @@ require_once 'Zend/Soap/Client.php'; * @copyright 2010 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class webservice_soap_client extends Zend_Soap_Client { +class webservice_soap_client { - /** @var string server url e.g. https://yyyyy.com/server.php */ + /** @var moodle_url The server url. */ private $serverurl; + /** @var string The WS token. */ + private $token; + + /** @var array|null SOAP options. */ + private $options; + /** * Constructor * @@ -46,10 +49,10 @@ class webservice_soap_client extends Zend_Soap_Client { * @param string $token the token used to do the web service call * @param array $options PHP SOAP client options - see php.net */ - public function __construct($serverurl, $token, $options = null) { - $this->serverurl = $serverurl; - $wsdl = $serverurl . "?wstoken=" . $token . '&wsdl=1'; - parent::__construct($wsdl, $options); + public function __construct($serverurl, $token = null, array $options = null) { + $this->serverurl = new moodle_url($serverurl); + $this->token = $token ?: $this->serverurl->get_param('wstoken'); + $this->options = $options ?: array(); } /** @@ -58,8 +61,7 @@ class webservice_soap_client extends Zend_Soap_Client { * @param string $token the token used to do the web service call */ public function set_token($token) { - $wsdl = $this->serverurl . "?wstoken=" . $token . '&wsdl=1'; - $this->setWsdl($wsdl); + $this->token = $token; } /** @@ -70,15 +72,22 @@ class webservice_soap_client extends Zend_Soap_Client { * @return mixed */ public function call($functionname, $params) { - global $DB, $CFG; + if ($this->token) { + $this->serverurl->param('wstoken', $this->token); + } + $this->serverurl->param('wsdl', 1); - //zend expects 0 based array with numeric indexes - $params = array_values($params); + $opts = array( + 'http' => array( + 'user_agent' => 'Moodle SOAP Client' + ) + ); + $context = stream_context_create($opts); + $this->options['stream_context'] = $context; + $this->options['cache_wsdl'] = WSDL_CACHE_NONE; - //traditional Zend soap client call (integrating the token into the URL) - $result = $this->__call($functionname, $params); + $client = new SoapClient($this->serverurl->out(false), $this->options); - return $result; + return $client->__soapCall($functionname, $params); } - -} \ No newline at end of file +} diff --git a/webservice/soap/locallib.php b/webservice/soap/locallib.php index 13862560acf..96d715b71ac 100644 --- a/webservice/soap/locallib.php +++ b/webservice/soap/locallib.php @@ -22,26 +22,323 @@ * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -require_once("$CFG->dirroot/webservice/lib.php"); -require_once 'Zend/Soap/Server.php'; +global $CFG; +require_once($CFG->dirroot . '/webservice/lib.php'); +use webservice_soap\wsdl; /** - * The Zend XMLRPC server but with a fault that returns debuginfo + * SOAP service server implementation. + * + * @package webservice_soap + * @copyright 2009 Petr Skodak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class webservice_soap_server extends webservice_base_server { + + /** @var moodle_url The server URL. */ + protected $serverurl; + + /** @var SoapServer The Soap */ + protected $soapserver; + + /** @var string The response. */ + protected $response; + + /** @var string The class name of the virtual class generated for this web service. */ + protected $serviceclass; + + /** @var bool WSDL mode flag. */ + protected $wsdlmode; + + /** @var \webservice_soap\wsdl The object for WSDL generation. */ + protected $wsdl; + + /** + * Contructor. + * + * @param string $authmethod authentication method of the web service (WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN, ...) + */ + public function __construct($authmethod) { + parent::__construct($authmethod); + // Must not cache wsdl - the list of functions is created on the fly. + ini_set('soap.wsdl_cache_enabled', '0'); + $this->wsname = 'soap'; + $this->wsdlmode = false; + } + + /** + * This method parses the $_POST and $_GET superglobals and looks for the following information: + * - User authentication parameters: + * - Username + password (wsusername and wspassword), or + * - Token (wstoken) + */ + protected function parse_request() { + // Retrieve and clean the POST/GET parameters from the parameters specific to the server. + parent::set_web_service_call_settings(); + + if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { + $this->username = optional_param('wsusername', null, PARAM_RAW); + $this->password = optional_param('wspassword', null, PARAM_RAW); + + if (!$this->username or !$this->password) { + // Workaround for the trouble with & in soap urls. + $authdata = get_file_argument(); + $authdata = explode('/', trim($authdata, '/')); + if (count($authdata) == 2) { + list($this->username, $this->password) = $authdata; + } + } + $this->serverurl = new moodle_url('/webservice/soap/simpleserver.php/' . $this->username . '/' . $this->password); + } else { + $this->token = optional_param('wstoken', null, PARAM_RAW); + + $this->serverurl = new moodle_url('/webservice/soap/server.php'); + $this->serverurl->param('wstoken', $this->token); + } + + if ($wsdl = optional_param('wsdl', 0, PARAM_INT)) { + $this->wsdlmode = true; + } + } + + /** + * Runs the SOAP web service. + * + * @throws coding_exception + * @throws moodle_exception + * @throws webservice_access_exception + */ + public function run() { + // We will probably need a lot of memory in some functions. + raise_memory_limit(MEMORY_EXTRA); + + // Set some longer timeout since operations may need longer time to finish. + external_api::set_timeout(); + + // Set up exception handler. + set_exception_handler(array($this, 'exception_handler')); + + // Init all properties from the request data. + $this->parse_request(); + + // Authenticate user, this has to be done after the request parsing. This also sets up $USER and $SESSION. + $this->authenticate_user(); + + // Make a list of all functions user is allowed to execute. + $this->init_service_class(); + + if ($this->wsdlmode) { + // Generate the WSDL. + $this->generate_wsdl(); + } + + // Log the web service request. + $params = array( + 'other' => array( + 'function' => 'unknown' + ) + ); + $event = \core\event\webservice_function_called::create($params); + $logdataparams = array(SITEID, 'webservice_soap', '', '', $this->serviceclass . ' ' . getremoteaddr(), 0, $this->userid); + $event->set_legacy_logdata($logdataparams); + $event->trigger(); + + // Handle the SOAP request. + $this->handle(); + + // Session cleanup. + $this->session_cleanup(); + die; + } + + /** + * Generates the WSDL. + */ + protected function generate_wsdl() { + // Initialise WSDL. + $this->wsdl = new wsdl($this->serviceclass, $this->serverurl); + // Register service struct classes as complex types. + foreach ($this->servicestructs as $structinfo) { + $this->wsdl->add_complex_type($structinfo->classname, $structinfo->properties); + } + // Register the method for the WSDL generation. + foreach ($this->servicemethods as $methodinfo) { + $this->wsdl->register($methodinfo->name, $methodinfo->inputparams, $methodinfo->outputparams, $methodinfo->description); + } + } + + /** + * Handles the web service function call. + */ + protected function handle() { + if ($this->wsdlmode) { + // Prepare the response. + $this->response = $this->wsdl->to_xml(); + + // Send the results back in correct format. + $this->send_response(); + } else { + $wsdlurl = clone($this->serverurl); + $wsdlurl->param('wsdl', 1); + + $options = array( + 'uri' => $this->serverurl->out(false) + ); + // Initialise the SOAP server. + $this->soapserver = new SoapServer($wsdlurl->out(false), $options); + if (!empty($this->serviceclass)) { + $this->soapserver->setClass($this->serviceclass); + // Get all the methods for the generated service class then register to the SOAP server. + $functions = get_class_methods($this->serviceclass); + $this->soapserver->addFunction($functions); + } + + // Get soap request from raw POST data. + $soaprequest = file_get_contents('php://input'); + // Handle the request. + try { + $this->soapserver->handle($soaprequest); + } catch (Exception $e) { + $this->fault($e); + } + } + } + + /** + * Send the error information to the WS client formatted as an XML document. + * + * @param Exception $ex the exception to send back + */ + protected function send_error($ex = null) { + if ($ex) { + $info = $ex->getMessage(); + if (debugging() and isset($ex->debuginfo)) { + $info .= ' - '.$ex->debuginfo; + } + } else { + $info = 'Unknown error'; + } + + // Initialise new DOM document object. + $dom = new DOMDocument('1.0', 'UTF-8'); + + // Fault node. + $fault = $dom->createElement('SOAP-ENV:Fault'); + // Faultcode node. + $fault->appendChild($dom->createElement('faultcode', 'MOODLE:error')); + // Faultstring node. + $fault->appendChild($dom->createElement('faultstring', $info)); + + // Body node. + $body = $dom->createElement('SOAP-ENV:Body'); + $body->appendChild($fault); + + // Envelope node. + $envelope = $dom->createElement('SOAP-ENV:Envelope'); + $envelope->setAttribute('xmlns:SOAP-ENV', 'http://schemas.xmlsoap.org/soap/envelope/'); + $envelope->appendChild($body); + $dom->appendChild($envelope); + + // Send headers. + $this->send_headers(); + + // Output the XML. + echo $dom->saveXML(); + } + + /** + * Send the result of function call to the WS client. + */ + protected function send_response() { + $this->send_headers(); + echo $this->response; + } + + /** + * Internal implementation - sending of page headers. + */ + protected function send_headers() { + header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); + header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . ' GMT'); + header('Pragma: no-cache'); + header('Accept-Ranges: none'); + header('Content-Length: ' . count($this->response)); + header('Content-Type: application/xml; charset=utf-8'); + header('Content-Disposition: inline; filename="response.xml"'); + } + + /** + * Generate a server fault. + * + * Note that the parameter order is the reverse of SoapFault's constructor parameters. + * + * Moodle note: basically we return the faultactor (errorcode) and faultdetails (debuginfo). + * + * If an exception is passed as the first argument, its message and code + * will be used to create the fault object. + * + * @link http://www.w3.org/TR/soap12-part1/#faultcodes + * @param string|Exception $fault + * @param string $code SOAP Fault Codes + */ + public function fault($fault = null, $code = 'Receiver') { + $allowedfaultmodes = array( + 'VersionMismatch', 'MustUnderstand', 'DataEncodingUnknown', + 'Sender', 'Receiver', 'Server' + ); + if (!in_array($code, $allowedfaultmodes)) { + $code = 'Receiver'; + } + + // Intercept any exceptions and add the errorcode and debuginfo (optional). + $actor = null; + $details = null; + $errorcode = 'unknownerror'; + $message = get_string($errorcode); + if ($fault instanceof Exception) { + // Add the debuginfo to the exception message if debuginfo must be returned. + $actor = isset($fault->errorcode) ? $fault->errorcode : null; + $errorcode = $actor; + if (debugging()) { + $message = $fault->getMessage(); + $details = isset($fault->debuginfo) ? $fault->debuginfo : null; + } + } else if (is_string($fault)) { + $message = $fault; + } + + $this->soapserver->fault($code, $message . ' | ERRORCODE: ' . $errorcode, $actor, $details); + } +} + +/** + * The Zend SOAP server but with a fault that returns debuginfo. * * @package webservice_soap * @copyright 2011 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.2 + * @deprecated since 3.1, see {@link webservice_soap_server()}. */ -class moodle_zend_soap_server extends Zend_Soap_Server { +class moodle_zend_soap_server extends webservice_soap_server { /** - * Generate a server fault + * moodle_zend_soap_server constructor. + * + * @param string $authmethod + */ + public function __construct($authmethod) { + debugging('moodle_zend_soap_server is deprecated, please use webservice_soap_server instead.', DEBUG_DEVELOPER); + parent::__construct($authmethod); + } + + /** + * Generate a server fault. * * Note that the arguments are the reverse of those used by SoapFault. * - * Moodle note: basically we return the faultactor (errorcode) and faultdetails (debuginfo) + * Moodle note: basically we return the faultactor (errorcode) and faultdetails (debuginfo). * * If an exception is passed as the first argument, its message and code * will be used to create the fault object if it has been registered via @@ -51,31 +348,16 @@ class moodle_zend_soap_server extends Zend_Soap_Server { * @param string|Exception $fault * @param string $code SOAP Fault Codes * @return SoapFault + * @deprecated since 3.1, see {@link webservice_soap_server::fault()}. */ - public function fault($fault = null, $code = "Receiver") - { - - // Run the zend code that clean/create a soapfault. - $soapfault = parent::fault($fault, $code); - - // Intercept any exceptions and add the errorcode and debuginfo (optional). - $actor = null; - $details = null; - if ($fault instanceof Exception) { - // Add the debuginfo to the exception message if debuginfo must be returned. - $actor = $fault->errorcode; - if (debugging() and isset($fault->debuginfo)) { - $details = $fault->debuginfo; - } - } - - return new SoapFault($soapfault->faultcode, - $soapfault->getMessage() . ' | ERRORCODE: ' . $fault->errorcode, - $actor, $details); + public function fault($fault = null, $code = "Receiver") { + debugging('moodle_zend_soap_server::fault() is deprecated, please use webservice_soap_server::fault() instead.', + DEBUG_DEVELOPER); + parent::fault($fault, $code); } /** - * Handle a request + * Handle a request. * * NOTE: this is basically a copy of the Zend handle() * but with $soap->fault returning faultactor + faultdetail @@ -97,206 +379,12 @@ class moodle_zend_soap_server extends Zend_Soap_Server { * * @param DOMDocument|DOMNode|SimpleXMLElement|stdClass|string $request Optional request * @return void|string + * @deprecated since 3.1, see {@link webservice_soap_server::handle()}. */ - public function handle($request = null) - { - if (null === $request) { - $request = file_get_contents('php://input'); - } - - // Set Zend_Soap_Server error handler - $displayErrorsOriginalState = $this->_initializeSoapErrorContext(); - - $setRequestException = null; - /** - * @see Zend_Soap_Server_Exception - */ - require_once 'Zend/Soap/Server/Exception.php'; - try { - $this->_setRequest($request); - } catch (Zend_Soap_Server_Exception $e) { - $setRequestException = $e; - } - - $soap = $this->_getSoap(); - - ob_start(); - if($setRequestException instanceof Exception) { - // Send SOAP fault message if we've catched exception - $soap->fault("Sender", $setRequestException->getMessage()); - } else { - try { - $soap->handle($request); - } catch (Exception $e) { - $fault = $this->fault($e); - $faultactor = isset($fault->faultactor) ? $fault->faultactor : null; - $detail = isset($fault->detail) ? $fault->detail : null; - $soap->fault($fault->faultcode, $fault->faultstring, $faultactor, $detail); - } - } - $this->_response = ob_get_clean(); - - // Restore original error handler - restore_error_handler(); - ini_set('display_errors', $displayErrorsOriginalState); - - if (!$this->_returnResponse) { - echo $this->_response; - return; - } - - return $this->_response; - } -} - -/** - * SOAP service server implementation. - * - * @package webservice_soap - * @copyright 2009 Petr Skodak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class webservice_soap_server extends webservice_zend_server { - - /** - * Contructor - * - * @param string $authmethod authentication method of the web service (WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN, ...) - */ - public function __construct($authmethod) { - // must not cache wsdl - the list of functions is created on the fly - ini_set('soap.wsdl_cache_enabled', '0'); - require_once 'Zend/Soap/Server.php'; - require_once 'Zend/Soap/AutoDiscover.php'; - - if (optional_param('wsdl', 0, PARAM_BOOL)) { - parent::__construct($authmethod, 'Zend_Soap_AutoDiscover'); - } else { - parent::__construct($authmethod, 'moodle_zend_soap_server'); - } - $this->wsname = 'soap'; - } - - /** - * Set up zend service class - */ - protected function init_zend_server() { - global $CFG; - - parent::init_zend_server(); - - if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { - $username = optional_param('wsusername', '', PARAM_RAW); - $password = optional_param('wspassword', '', PARAM_RAW); - // aparently some clients and zend soap server does not work well with "&" in urls :-( - //TODO MDL-31151 the zend error has been fixed in the last Zend SOAP version, check that is fixed and remove obsolete code - $url = $CFG->wwwroot.'/webservice/soap/simpleserver.php/'.urlencode($username).'/'.urlencode($password); - // the Zend server is using this uri directly in xml - weird :-( - $this->zend_server->setUri(htmlentities($url)); - } else { - $wstoken = optional_param('wstoken', '', PARAM_RAW); - $url = $CFG->wwwroot.'/webservice/soap/server.php?wstoken='.urlencode($wstoken); - // the Zend server is using this uri directly in xml - weird :-( - $this->zend_server->setUri(htmlentities($url)); - } - - if (!optional_param('wsdl', 0, PARAM_BOOL)) { - $this->zend_server->setReturnResponse(true); - $this->zend_server->registerFaultException('moodle_exception'); - $this->zend_server->registerFaultException('webservice_parameter_exception'); //deprecated since Moodle 2.2 - kept for backward compatibility - $this->zend_server->registerFaultException('invalid_parameter_exception'); - $this->zend_server->registerFaultException('invalid_response_exception'); - //when DEBUG >= NORMAL then the thrown exceptions are "casted" into a PHP SoapFault expception - //in order to diplay the $debuginfo (see moodle_zend_soap_server class - MDL-29435) - if (debugging()) { - $this->zend_server->registerFaultException('SoapFault'); - } - } - } - - /** - * This method parses the $_POST and $_GET superglobals and looks for - * the following information: - * user authentication - username+password or token (wsusername, wspassword and wstoken parameters) - */ - protected function parse_request() { - parent::parse_request(); - - if (!$this->username or !$this->password) { - //note: this is the workaround for the trouble with & in soap urls - $authdata = get_file_argument(); - $authdata = explode('/', trim($authdata, '/')); - if (count($authdata) == 2) { - list($this->username, $this->password) = $authdata; - } - } - } - - /** - * Send the error information to the WS client - * formatted as an XML document. - * - * @param exception $ex the exception to send back - */ - protected function send_error($ex=null) { - - if ($ex) { - $info = $ex->getMessage(); - if (debugging() and isset($ex->debuginfo)) { - $info .= ' - '.$ex->debuginfo; - } - } else { - $info = 'Unknown error'; - } - - $xml = ' - - -MOODLE:error -'.$info.' -'; - - $this->send_headers(); - header('Content-Type: application/xml; charset=utf-8'); - header('Content-Disposition: inline; filename="response.xml"'); - - echo $xml; - } - - /** - * Generate 'struct' type name - * This type name is the name of a class generated on the fly. - * - * @param external_single_structure $structdesc - * @return string - */ - protected function generate_simple_struct_class(external_single_structure $structdesc) { - global $USER; - - $fields = array(); - foreach ($structdesc->keys as $name => $fieldsdesc) { - $type = $this->get_phpdoc_type($fieldsdesc); - $fields[] = ' /** @var '.$type." */\n" . - ' public $'.$name.';'; - } - - // We do this after the call to get_phpdoc_type() to avoid duplicate class creation. - $classname = 'webservices_struct_class_000000'; - while (class_exists($classname)) { - $classname++; - } - - $code = ' -/** - * Virtual struct class for web services for user id '.$USER->id.' in context '.$this->restricted_context->id.'. - */ -class '.$classname.' { -'.implode("\n", $fields).' -} -'; - eval($code); - return $classname; + public function handle($request = null) { + debugging('moodle_zend_soap_server::handle() is deprecated, please use webservice_soap_server::handle() instead.', + DEBUG_DEVELOPER); + parent::handle(); } } @@ -319,10 +407,10 @@ class webservice_soap_test_client implements webservice_test_client_interface { * @return mixed */ public function simpletest($serverurl, $function, $params) { - //zend expects 0 based array with numeric indexes - $params = array_values($params); - require_once 'Zend/Soap/Client.php'; - $client = new Zend_Soap_Client($serverurl.'&wsdl=1'); - return $client->__call($function, $params); + global $CFG; + + require_once($CFG->dirroot . '/webservice/soap/lib.php'); + $client = new webservice_soap_client($serverurl); + return $client->call($function, $params); } } diff --git a/webservice/soap/tests/wsdl_test.php b/webservice/soap/tests/wsdl_test.php new file mode 100644 index 00000000000..703c0601e6d --- /dev/null +++ b/webservice/soap/tests/wsdl_test.php @@ -0,0 +1,372 @@ +. + +/** + * Unit tests for the WSDL class. + * + * @package webservice_soap + * @category test + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace webservice_soap; +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/soap/classes/wsdl.php'); + +/** + * Unit tests for the WSDL class. + * + * @package webservice_soap + * @category test + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class wsdl_test extends \advanced_testcase { + + /** + * Test generated WSDL with no added complex types nor functions. + */ + public function test_minimum_wsdl() { + $this->resetAfterTest(); + + $serviceclass = 'testserviceclass'; + $namespace = 'testnamespace'; + $wsdl = new wsdl($serviceclass, $namespace); + + // Test definitions node. + $definitions = new \SimpleXMLElement($wsdl->to_xml()); + $defattrs = $definitions->attributes(); + $this->assertEquals($serviceclass, $defattrs->name); + $this->assertEquals($namespace, $defattrs->targetNamespace); + + // Test types node and attributes. + $this->assertNotNull($definitions->types); + $this->assertEquals($namespace, $definitions->types->children('xsd', true)->schema->attributes()->targetNamespace); + + // Test portType node and attributes. + $this->assertNotNull($definitions->portType); + $this->assertEquals($serviceclass . wsdl::PORT, $definitions->portType->attributes()->name); + + // Test binding node and attributes. + $this->assertNotNull($definitions->binding); + $this->assertEquals($serviceclass . wsdl::BINDING, $definitions->binding->attributes()->name); + $this->assertEquals('tns:' . $serviceclass . wsdl::PORT, $definitions->binding->attributes()->type); + + $bindingattrs = $definitions->binding->children('soap', true)->binding->attributes(); + $this->assertNotEmpty('rpc', $bindingattrs); + $this->assertEquals('rpc', $bindingattrs->style); + $this->assertEquals(wsdl::NS_SOAP_TRANSPORT, $bindingattrs->transport); + + // Test service node. + $this->assertNotNull($definitions->service); + $this->assertEquals($serviceclass . wsdl::SERVICE, $definitions->service->attributes()->name); + + $serviceport = $definitions->service->children()->port; + $this->assertNotEmpty($serviceport); + $this->assertEquals($serviceclass . wsdl::PORT, $serviceport->attributes()->name); + $this->assertEquals('tns:' . $serviceclass . wsdl::BINDING, $serviceport->attributes()->binding); + + $serviceportaddress = $serviceport->children('soap', true)->address; + $this->assertNotEmpty($serviceportaddress); + $this->assertEquals($namespace, $serviceportaddress->attributes()->location); + } + + /** + * Test output WSDL with complex type added. + */ + public function test_add_complex_type() { + $this->resetAfterTest(); + + $classname = 'testcomplextype'; + $classattrs = array( + 'doubleparam' => array( + 'type' => 'double', + 'nillable' => true + ), + 'stringparam' => array( + 'type' => 'string', + 'nillable' => true + ), + 'intparam' => array( + 'type' => 'int', + 'nillable' => true + ), + 'boolparam' => array( + 'type' => 'int', + 'nillable' => true + ), + 'classparam' => array( + 'type' => 'teststruct' + ), + 'arrayparam' => array( + 'type' => 'array', + 'nillable' => true + ), + ); + + $serviceclass = 'testserviceclass'; + $namespace = 'testnamespace'; + $wsdl = new wsdl($serviceclass, $namespace); + $wsdl->add_complex_type($classname, $classattrs); + + $definitions = new \SimpleXMLElement($wsdl->to_xml()); + + // Test types node and attributes. + $this->assertNotNull($definitions->types); + $this->assertEquals($namespace, $definitions->types->children('xsd', true)->schema->attributes()->targetNamespace); + $complextype = $definitions->types->children('xsd', true)->schema->children('xsd', true); + $this->assertNotEmpty($complextype); + + // Test the complex type's attributes. + foreach ($complextype->children('xsd', true)->all->children('xsd', true) as $element) { + foreach ($classattrs as $name => $options) { + if (strcmp($name, $element->attributes()->name) != 0) { + continue; + } + switch ($options['type']) { + case 'double': + case 'int': + case 'string': + $this->assertEquals('xsd:' . $options['type'], $element->attributes()->type); + break; + case 'array': + $this->assertEquals('soap-enc:' . ucfirst($options['type']), $element->attributes()->type); + break; + default: + $this->assertEquals('tns:' . $options['type'], $element->attributes()->type); + break; + } + if (!empty($options['nillable'])) { + $this->assertEquals('true', $element->attributes()->nillable); + } + break; + } + } + } + + /** + * Test output WSDL when registering a web service function. + */ + public function test_register() { + $this->resetAfterTest(); + + $serviceclass = 'testserviceclass'; + $namespace = 'testnamespace'; + $wsdl = new wsdl($serviceclass, $namespace); + + $functionname = 'testfunction'; + $documentation = 'This is a test function'; + $in = array( + 'doubleparam' => array( + 'type' => 'double' + ), + 'stringparam' => array( + 'type' => 'string' + ), + 'intparam' => array( + 'type' => 'int' + ), + 'boolparam' => array( + 'type' => 'int' + ), + 'classparam' => array( + 'type' => 'teststruct' + ), + 'arrayparam' => array( + 'type' => 'array' + ) + ); + $out = array( + 'doubleparam' => array( + 'type' => 'double' + ), + 'stringparam' => array( + 'type' => 'string' + ), + 'intparam' => array( + 'type' => 'int' + ), + 'boolparam' => array( + 'type' => 'int' + ), + 'classparam' => array( + 'type' => 'teststruct' + ), + 'arrayparam' => array( + 'type' => 'array' + ), + 'return' => array( + 'type' => 'teststruct2' + ) + ); + $wsdl->register($functionname, $in, $out, $documentation); + + $definitions = new \SimpleXMLElement($wsdl->to_xml()); + + // Test portType operation node. + $porttypeoperation = $definitions->portType->operation; + $this->assertEquals($documentation, $porttypeoperation->documentation); + $this->assertEquals('tns:' . $functionname . wsdl::IN, $porttypeoperation->input->attributes()->message); + $this->assertEquals('tns:' . $functionname . wsdl::OUT, $porttypeoperation->output->attributes()->message); + + // Test binding operation nodes. + $bindingoperation = $definitions->binding->operation; + $soapoperation = $bindingoperation->children('soap', true)->operation; + $this->assertEquals($namespace . '#' . $functionname, $soapoperation->attributes()->soapAction); + $inputbody = $bindingoperation->input->children('soap', true); + $this->assertEquals('encoded', $inputbody->attributes()->use); + $this->assertEquals(wsdl::NS_SOAP_ENC, $inputbody->attributes()->encodingStyle); + $this->assertEquals($namespace, $inputbody->attributes()->namespace); + $outputbody = $bindingoperation->output->children('soap', true); + $this->assertEquals('encoded', $outputbody->attributes()->use); + $this->assertEquals(wsdl::NS_SOAP_ENC, $outputbody->attributes()->encodingStyle); + $this->assertEquals($namespace, $outputbody->attributes()->namespace); + + // Test messages. + $messagein = $definitions->message[0]; + $this->assertEquals($functionname . wsdl::IN, $messagein->attributes()->name); + foreach ($messagein->children() as $part) { + foreach ($in as $name => $options) { + if (strcmp($name, $part->attributes()->name) != 0) { + continue; + } + switch ($options['type']) { + case 'double': + case 'int': + case 'string': + $this->assertEquals('xsd:' . $options['type'], $part->attributes()->type); + break; + case 'array': + $this->assertEquals('soap-enc:' . ucfirst($options['type']), $part->attributes()->type); + break; + default: + $this->assertEquals('tns:' . $options['type'], $part->attributes()->type); + break; + } + break; + } + } + $messageout = $definitions->message[1]; + $this->assertEquals($functionname . wsdl::OUT, $messageout->attributes()->name); + foreach ($messageout->children() as $part) { + foreach ($out as $name => $options) { + if (strcmp($name, $part->attributes()->name) != 0) { + continue; + } + switch ($options['type']) { + case 'double': + case 'int': + case 'string': + $this->assertEquals('xsd:' . $options['type'], $part->attributes()->type); + break; + case 'array': + $this->assertEquals('soap-enc:' . ucfirst($options['type']), $part->attributes()->type); + break; + default: + $this->assertEquals('tns:' . $options['type'], $part->attributes()->type); + break; + } + break; + } + } + } + + /** + * Test output WSDL when registering a web service function with no input parameters. + */ + public function test_register_without_input() { + $this->resetAfterTest(); + + $serviceclass = 'testserviceclass'; + $namespace = 'testnamespace'; + $wsdl = new wsdl($serviceclass, $namespace); + + $functionname = 'testfunction'; + $documentation = 'This is a test function'; + + $out = array( + 'return' => array( + 'type' => 'teststruct2' + ) + ); + $wsdl->register($functionname, null, $out, $documentation); + + $definitions = new \SimpleXMLElement($wsdl->to_xml()); + + // Test portType operation node. + $porttypeoperation = $definitions->portType->operation; + $this->assertEquals($documentation, $porttypeoperation->documentation); + $this->assertFalse(isset($porttypeoperation->input)); + $this->assertTrue(isset($porttypeoperation->output)); + + // Test binding operation nodes. + $bindingoperation = $definitions->binding->operation; + // Confirm that there is no input node. + $this->assertFalse(isset($bindingoperation->input)); + $this->assertTrue(isset($bindingoperation->output)); + + // Test messages. + // Assert there's only the output message node. + $this->assertEquals(1, count($definitions->message)); + $messageout = $definitions->message[0]; + $this->assertEquals($functionname . wsdl::OUT, $messageout->attributes()->name); + + } + + /** + * Test output WSDL when registering a web service function with no output parameters. + */ + public function test_register_without_output() { + $this->resetAfterTest(); + + $serviceclass = 'testserviceclass'; + $namespace = 'testnamespace'; + $wsdl = new wsdl($serviceclass, $namespace); + + $functionname = 'testfunction'; + $documentation = 'This is a test function'; + + $in = array( + 'return' => array( + 'type' => 'teststruct2' + ) + ); + $wsdl->register($functionname, $in, null, $documentation); + + $definitions = new \SimpleXMLElement($wsdl->to_xml()); + + // Test portType operation node. + $porttypeoperation = $definitions->portType->operation; + $this->assertEquals($documentation, $porttypeoperation->documentation); + $this->assertTrue(isset($porttypeoperation->input)); + $this->assertFalse(isset($porttypeoperation->output)); + + // Test binding operation nodes. + $bindingoperation = $definitions->binding->operation; + // Confirm that there is no input node. + $this->assertTrue(isset($bindingoperation->input)); + $this->assertFalse(isset($bindingoperation->output)); + + // Test messages. + // Assert there's only the output message node. + $this->assertEquals(1, count($definitions->message)); + $messagein = $definitions->message[0]; + $this->assertEquals($functionname . wsdl::IN, $messagein->attributes()->name); + + } +} diff --git a/webservice/soap/version.php b/webservice/soap/version.php index c899be70756..7853393f018 100644 --- a/webservice/soap/version.php +++ b/webservice/soap/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111600; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2016020900.00; // The current plugin version (Date: YYYYMMDDXX) $plugin->requires = 2015111000; // Requires this Moodle version $plugin->component = 'webservice_soap'; // Full name of the plugin (used for diagnostics) diff --git a/webservice/tests/lib_test.php b/webservice/tests/lib_test.php new file mode 100644 index 00000000000..6629899cc72 --- /dev/null +++ b/webservice/tests/lib_test.php @@ -0,0 +1,285 @@ +. + +/** + * Unit tests for the webservice component. + * + * @package core_webservice + * @category test + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/lib.php'); + +/** + * Unit tests for the webservice component. + * + * @package core_webservice + * @category test + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class webservice_test extends advanced_testcase { + + /** + * Setup. + */ + public function setUp() { + // Calling parent is good, always. + parent::setUp(); + + // We always need enabled WS for this testcase. + set_config('enablewebservices', '1'); + } + + /** + * Test init_service_class(). + */ + public function test_init_service_class() { + global $DB, $USER; + + $this->resetAfterTest(true); + + // Set current user. + $this->setAdminUser(); + + // Add a web service. + $webservice = new stdClass(); + $webservice->name = 'Test web service'; + $webservice->enabled = true; + $webservice->restrictedusers = false; + $webservice->component = 'moodle'; + $webservice->timecreated = time(); + $webservice->downloadfiles = true; + $webservice->uploadfiles = true; + $externalserviceid = $DB->insert_record('external_services', $webservice); + + // Add token. + $externaltoken = new stdClass(); + $externaltoken->token = 'testtoken'; + $externaltoken->tokentype = 0; + $externaltoken->userid = $USER->id; + $externaltoken->externalserviceid = $externalserviceid; + $externaltoken->contextid = 1; + $externaltoken->creatorid = $USER->id; + $externaltoken->timecreated = time(); + $DB->insert_record('external_tokens', $externaltoken); + + // Add a function to the service. + $wsmethod = new stdClass(); + $wsmethod->externalserviceid = $externalserviceid; + $wsmethod->functionname = 'core_course_get_contents'; + $DB->insert_record('external_services_functions', $wsmethod); + + // Initialise the dummy web service. + $dummy = new webservice_dummy(WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN); + // Set the token. + $dummy->set_token($externaltoken->token); + // Run the web service. + $dummy->run(); + // Get service methods and structs. + $servicemethods = $dummy->get_service_methods(); + $servicestructs = $dummy->get_service_structs(); + $this->assertNotEmpty($servicemethods); + // The function core_course_get_contents should be only the only web service function in the moment. + $this->assertEquals(1, count($servicemethods)); + // The function core_course_get_contents doesn't have a struct class, so the list of service structs should be empty. + $this->assertEmpty($servicestructs); + + // Add other functions to the service. + // The function core_comment_get_comments has one struct class in its output. + $wsmethod->functionname = 'core_comment_get_comments'; + $DB->insert_record('external_services_functions', $wsmethod); + // The function core_grades_update_grades has one struct class in its input. + $wsmethod->functionname = 'core_grades_update_grades'; + $DB->insert_record('external_services_functions', $wsmethod); + + // Run the web service again. + $dummy->run(); + // Get service methods and structs. + $servicemethods = $dummy->get_service_methods(); + $servicestructs = $dummy->get_service_structs(); + $this->assertEquals(3, count($servicemethods)); + $this->assertEquals(2, count($servicestructs)); + + // Check the contents of service methods. + foreach ($servicemethods as $method) { + // Get the external function info. + $function = external_function_info($method->name); + + // Check input params. + foreach ($function->parameters_desc->keys as $name => $keydesc) { + $this->check_params($method->inputparams[$name]['type'], $keydesc, $servicestructs); + } + + // Check output params. + $this->check_params($method->outputparams['return']['type'], $function->returns_desc, $servicestructs); + + // Check description. + $this->assertEquals($function->description, $method->description); + } + } + + /** + * Utility method that tests the parameter type of a method info's input/output parameter. + * + * @param string $type The parameter type that is being evaluated. + * @param mixed $methoddesc The method description of the WS function. + * @param array $servicestructs The list of generated service struct classes. + */ + private function check_params($type, $methoddesc, $servicestructs) { + if ($methoddesc instanceof external_value) { + // Test for simple types. + if (in_array($methoddesc->type, [PARAM_INT, PARAM_FLOAT, PARAM_BOOL])) { + $this->assertEquals($methoddesc->type, $type); + } else { + $this->assertEquals('string', $type); + } + } else if ($methoddesc instanceof external_single_structure) { + // Test that the class name of the struct class is in the array of service structs. + $structinfo = $this->get_struct_info($servicestructs, $type); + $this->assertNotNull($structinfo); + // Test that the properties of the struct info exist in the method description. + foreach ($structinfo->properties as $propname => $proptype) { + $this->assertTrue($this->in_keydesc($methoddesc, $propname)); + } + } else if ($methoddesc instanceof external_multiple_structure) { + // Test for array types. + $this->assertEquals('array', $type); + } + } + + /** + * Gets the struct information from the list of struct classes based on the given struct class name. + * + * @param array $structarray The list of generated struct classes. + * @param string $structclass The name of the struct class. + * @return object|null The struct class info, or null if it's not found. + */ + private function get_struct_info($structarray, $structclass) { + foreach ($structarray as $struct) { + if ($struct->classname === $structclass) { + return $struct; + } + } + return null; + } + + /** + * Searches the keys of the given external_single_structure object if it contains a certain property name. + * + * @param external_single_structure $keydesc + * @param string $propertyname The property name to be searched for. + * @return bool True if the property name is found in $keydesc. False, otherwise. + */ + private function in_keydesc(external_single_structure $keydesc, $propertyname) { + foreach ($keydesc->keys as $key => $desc) { + if ($key === $propertyname) { + return true; + } + } + return false; + } +} + +/** + * Class webservice_dummy. + * + * Dummy webservice class for testing the webservice_base_server class and enable us to expose variables we want to test. + * + * @package core_webservice + * @category test + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class webservice_dummy extends webservice_base_server { + + /** + * webservice_dummy constructor. + * + * @param int $authmethod The authentication method. + */ + public function __construct($authmethod) { + parent::__construct($authmethod); + + // Arbitrarily naming this as REST in order not to have to register another WS protocol and set capabilities. + $this->wsname = 'rest'; + } + + /** + * Token setter method. + * + * @param string $token The web service token. + */ + public function set_token($token) { + $this->token = $token; + } + + /** + * This method parses the request input, it needs to get: + * 1/ user authentication - username+password or token + * 2/ function name + * 3/ function parameters + */ + protected function parse_request() { + // Just a method stub. No need to implement at the moment since it's not really being used for this test case for now. + } + + /** + * Send the result of function call to the WS client. + */ + protected function send_response() { + // Just a method stub. No need to implement at the moment since it's not really being used for this test case for now. + } + + /** + * Send the error information to the WS client. + * + * @param exception $ex + */ + protected function send_error($ex = null) { + // Just a method stub. No need to implement at the moment since it's not really being used for this test case for now. + } + + /** + * run() method implementation. + */ + public function run() { + $this->authenticate_user(); + $this->init_service_class(); + } + + /** + * Getter method of servicemethods array. + * + * @return array + */ + public function get_service_methods() { + return $this->servicemethods; + } + + /** + * Getter method of servicestructs array. + * + * @return array + */ + public function get_service_structs() { + return $this->servicestructs; + } +} diff --git a/webservice/upgrade.txt b/webservice/upgrade.txt index 98234a9abe7..a0e6cc319b1 100644 --- a/webservice/upgrade.txt +++ b/webservice/upgrade.txt @@ -10,8 +10,11 @@ This information is intended for authors of webservices, not people writing webs "[methodname]" does not exist') are no longer used which may display a different error message depending on the string returned by the getMessage() method of the thrown exception. * The xmlrpc server is no longer enabled when the Mobile service is activated. - * Support for the AMF protocol has been dropped completely. +* Zend_SOAP has been dropped. The native PHP SoapClient and SoapServer classes are now being used instead. WSDL is now + generated by the new class webservice_soap_wsdl. For fault strings, a different error message might be shown depending + on the string returned by the getMessage() method of the thrown exception. +* With Zend_SOAP dropped, moodle_zend_soap_server is now also deprecated. === 3.0 ===