Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  
  18  /**
  19   * SOAP web service implementation classes and methods.
  20   *
  21   * @package    webservice_soap
  22   * @copyright  2009 Petr Skodak
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  global $CFG;
  26  require_once($CFG->dirroot . '/webservice/lib.php');
  27  use webservice_soap\wsdl;
  28  
  29  /**
  30   * SOAP service server implementation.
  31   *
  32   * @package    webservice_soap
  33   * @copyright  2009 Petr Skodak
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   * @since Moodle 2.0
  36   */
  37  class webservice_soap_server extends webservice_base_server {
  38  
  39      /** @var moodle_url The server URL. */
  40      protected $serverurl;
  41  
  42      /** @var  SoapServer The Soap */
  43      protected $soapserver;
  44  
  45      /** @var  string The response. */
  46      protected $response;
  47  
  48      /** @var  string The class name of the virtual class generated for this web service. */
  49      protected $serviceclass;
  50  
  51      /** @var bool WSDL mode flag. */
  52      protected $wsdlmode;
  53  
  54      /** @var \webservice_soap\wsdl The object for WSDL generation. */
  55      protected $wsdl;
  56  
  57      /**
  58       * Contructor.
  59       *
  60       * @param string $authmethod authentication method of the web service (WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN, ...)
  61       */
  62      public function __construct($authmethod) {
  63          parent::__construct($authmethod);
  64           // Must not cache wsdl - the list of functions is created on the fly.
  65          ini_set('soap.wsdl_cache_enabled', '0');
  66          $this->wsname = 'soap';
  67          $this->wsdlmode = false;
  68      }
  69  
  70      /**
  71       * This method parses the $_POST and $_GET superglobals and looks for the following information:
  72       * - User authentication parameters:
  73       *   - Username + password (wsusername and wspassword), or
  74       *   - Token (wstoken)
  75       */
  76      protected function parse_request() {
  77          // Retrieve and clean the POST/GET parameters from the parameters specific to the server.
  78          parent::set_web_service_call_settings();
  79  
  80          if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
  81              $this->username = optional_param('wsusername', null, PARAM_RAW);
  82              $this->password = optional_param('wspassword', null, PARAM_RAW);
  83  
  84              if (!$this->username or !$this->password) {
  85                  // Workaround for the trouble with & in soap urls.
  86                  $authdata = get_file_argument();
  87                  $authdata = explode('/', trim($authdata, '/'));
  88                  if (count($authdata) == 2) {
  89                      list($this->username, $this->password) = $authdata;
  90                  }
  91              }
  92              $this->serverurl = new moodle_url('/webservice/soap/simpleserver.php/' . $this->username . '/' . $this->password);
  93          } else {
  94              $this->token = optional_param('wstoken', null, PARAM_RAW);
  95  
  96              $this->serverurl = new moodle_url('/webservice/soap/server.php');
  97              $this->serverurl->param('wstoken', $this->token);
  98          }
  99  
 100          if ($wsdl = optional_param('wsdl', 0, PARAM_INT)) {
 101              $this->wsdlmode = true;
 102          }
 103      }
 104  
 105      /**
 106       * Runs the SOAP web service.
 107       *
 108       * @throws coding_exception
 109       * @throws moodle_exception
 110       * @throws webservice_access_exception
 111       */
 112      public function run() {
 113          // We will probably need a lot of memory in some functions.
 114          raise_memory_limit(MEMORY_EXTRA);
 115  
 116          // Set some longer timeout since operations may need longer time to finish.
 117          external_api::set_timeout();
 118  
 119          // Set up exception handler.
 120          set_exception_handler(array($this, 'exception_handler'));
 121  
 122          // Init all properties from the request data.
 123          $this->parse_request();
 124  
 125          // Authenticate user, this has to be done after the request parsing. This also sets up $USER and $SESSION.
 126          $this->authenticate_user();
 127  
 128          // Make a list of all functions user is allowed to execute.
 129          $this->init_service_class();
 130  
 131          if ($this->wsdlmode) {
 132              // Generate the WSDL.
 133              $this->generate_wsdl();
 134          }
 135  
 136          // Log the web service request.
 137          $params = array(
 138              'other' => array(
 139                  'function' => 'unknown'
 140              )
 141          );
 142          $event = \core\event\webservice_function_called::create($params);
 143          $logdataparams = array(SITEID, 'webservice_soap', '', '', $this->serviceclass . ' ' . getremoteaddr(), 0, $this->userid);
 144          $event->set_legacy_logdata($logdataparams);
 145          $event->trigger();
 146  
 147          // Handle the SOAP request.
 148          $this->handle();
 149  
 150          // Session cleanup.
 151          $this->session_cleanup();
 152          die;
 153      }
 154  
 155      /**
 156       * Generates the WSDL.
 157       */
 158      protected function generate_wsdl() {
 159          // Initialise WSDL.
 160          $this->wsdl = new wsdl($this->serviceclass, $this->serverurl);
 161          // Register service struct classes as complex types.
 162          foreach ($this->servicestructs as $structinfo) {
 163              $this->wsdl->add_complex_type($structinfo->classname, $structinfo->properties);
 164          }
 165          // Register the method for the WSDL generation.
 166          foreach ($this->servicemethods as $methodinfo) {
 167              $this->wsdl->register($methodinfo->name, $methodinfo->inputparams, $methodinfo->outputparams, $methodinfo->description);
 168          }
 169      }
 170  
 171      /**
 172       * Handles the web service function call.
 173       */
 174      protected function handle() {
 175          if ($this->wsdlmode) {
 176              // Prepare the response.
 177              $this->response = $this->wsdl->to_xml();
 178  
 179              // Send the results back in correct format.
 180              $this->send_response();
 181          } else {
 182              $wsdlurl = clone($this->serverurl);
 183              $wsdlurl->param('wsdl', 1);
 184  
 185              $options = array(
 186                  'uri' => $this->serverurl->out(false)
 187              );
 188              // Initialise the SOAP server.
 189              $this->soapserver = new SoapServer($wsdlurl->out(false), $options);
 190              if (!empty($this->serviceclass)) {
 191                  $this->soapserver->setClass($this->serviceclass);
 192                  // Get all the methods for the generated service class then register to the SOAP server.
 193                  $functions = get_class_methods($this->serviceclass);
 194                  $this->soapserver->addFunction($functions);
 195              }
 196  
 197              // Get soap request from raw POST data.
 198              $soaprequest = file_get_contents('php://input');
 199              // Handle the request.
 200              try {
 201                  $this->soapserver->handle($soaprequest);
 202              } catch (Exception $e) {
 203                  $this->fault($e);
 204              }
 205          }
 206      }
 207  
 208      /**
 209       * Send the error information to the WS client formatted as an XML document.
 210       *
 211       * @param Exception $ex the exception to send back
 212       */
 213      protected function send_error($ex = null) {
 214          if ($ex) {
 215              $info = $ex->getMessage();
 216              if (debugging() and isset($ex->debuginfo)) {
 217                  $info .= ' - '.$ex->debuginfo;
 218              }
 219          } else {
 220              $info = 'Unknown error';
 221          }
 222  
 223          // Initialise new DOM document object.
 224          $dom = new DOMDocument('1.0', 'UTF-8');
 225  
 226          // Fault node.
 227          $fault = $dom->createElement('SOAP-ENV:Fault');
 228          // Faultcode node.
 229          $fault->appendChild($dom->createElement('faultcode', 'MOODLE:error'));
 230          // Faultstring node.
 231          $fault->appendChild($dom->createElement('faultstring', $info));
 232  
 233          // Body node.
 234          $body = $dom->createElement('SOAP-ENV:Body');
 235          $body->appendChild($fault);
 236  
 237          // Envelope node.
 238          $envelope = $dom->createElement('SOAP-ENV:Envelope');
 239          $envelope->setAttribute('xmlns:SOAP-ENV', 'http://schemas.xmlsoap.org/soap/envelope/');
 240          $envelope->appendChild($body);
 241          $dom->appendChild($envelope);
 242  
 243          $this->response = $dom->saveXML();
 244          $this->send_response();
 245      }
 246  
 247      /**
 248       * Send the result of function call to the WS client.
 249       */
 250      protected function send_response() {
 251          $this->send_headers();
 252          echo $this->response;
 253      }
 254  
 255      /**
 256       * Internal implementation - sending of page headers.
 257       */
 258      protected function send_headers() {
 259          header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
 260          header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . ' GMT');
 261          header('Pragma: no-cache');
 262          header('Accept-Ranges: none');
 263          header('Content-Length: ' . strlen($this->response));
 264          header('Content-Type: application/xml; charset=utf-8');
 265          header('Content-Disposition: inline; filename="response.xml"');
 266      }
 267  
 268      /**
 269       * Generate a server fault.
 270       *
 271       * Note that the parameter order is the reverse of SoapFault's constructor parameters.
 272       *
 273       * Moodle note: basically we return the faultactor (errorcode) and faultdetails (debuginfo).
 274       *
 275       * If an exception is passed as the first argument, its message and code
 276       * will be used to create the fault object.
 277       *
 278       * @link   http://www.w3.org/TR/soap12-part1/#faultcodes
 279       * @param  string|Exception $fault
 280       * @param  string $code SOAP Fault Codes
 281       */
 282      public function fault($fault = null, $code = 'Receiver') {
 283          $allowedfaultmodes = array(
 284              'VersionMismatch', 'MustUnderstand', 'DataEncodingUnknown',
 285              'Sender', 'Receiver', 'Server'
 286          );
 287          if (!in_array($code, $allowedfaultmodes)) {
 288              $code = 'Receiver';
 289          }
 290  
 291          // Intercept any exceptions and add the errorcode and debuginfo (optional).
 292          $actor = null;
 293          $details = null;
 294          $errorcode = 'unknownerror';
 295          $message = get_string($errorcode);
 296          if ($fault instanceof Exception) {
 297              // Add the debuginfo to the exception message if debuginfo must be returned.
 298              $actor = isset($fault->errorcode) ? $fault->errorcode : null;
 299              $errorcode = $actor;
 300              if (debugging()) {
 301                  $message = $fault->getMessage();
 302                  $details = isset($fault->debuginfo) ? $fault->debuginfo : null;
 303              }
 304          } else if (is_string($fault)) {
 305              $message = $fault;
 306          }
 307  
 308          $this->soapserver->fault($code, $message . ' | ERRORCODE: ' . $errorcode, $actor, $details);
 309      }
 310  }
 311  
 312  /**
 313   * SOAP test client class
 314   *
 315   * @package    webservice_soap
 316   * @copyright  2009 Petr Skodak
 317   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 318   * @since Moodle 2.0
 319   */
 320  class webservice_soap_test_client implements webservice_test_client_interface {
 321  
 322      /**
 323       * Execute test client WS request
 324       *
 325       * @param string $serverurl server url (including token parameter or username/password parameters)
 326       * @param string $function function name
 327       * @param array $params parameters of the called function
 328       * @return mixed
 329       */
 330      public function simpletest($serverurl, $function, $params) {
 331          global $CFG;
 332  
 333          require_once($CFG->dirroot . '/webservice/soap/lib.php');
 334          $client = new webservice_soap_client($serverurl);
 335          return $client->call($function, $params);
 336      }
 337  }