]> git.mjollnir.org Git - moodle.git/commitdiff
MDL-12886 first attempt to create the Zend compatibility layer for WS
authorskodak <skodak>
Wed, 14 Oct 2009 16:48:38 +0000 (16:48 +0000)
committerskodak <skodak>
Wed, 14 Oct 2009 16:48:38 +0000 (16:48 +0000)
webservice/lib.php

index b37a48c4816d48b0a688f72321aecd283f0eb895..7443a19f5ee31255b67096c19f4a904df7b00b67 100644 (file)
@@ -56,9 +56,361 @@ interface webservice_server {
  * @author skodak
  */
 abstract class webservice_zend_server implements webservice_server {
-    //TODO: implement base class for all ws servers in zend framework
-    //      the idea is to create one huge class on the fly, this class contains all
-    //      methods user is allowed to access and contains all needed PHPDoc metadata.
+
+    /** @property string name of the zend server class */
+    protected $zend_class;
+
+    /** @property object Zend server instance */
+    protected $zend_server;
+
+    /** @property string $wsname name of the web server plugin */
+    protected $wsname = null;
+
+    /** @property bool $simple true if simple auth used */
+    protected $simple;
+
+    /** @property string $service_class virtual web service class with all functions user name execute, created on the fly */
+    protected $service_class;
+
+    /** @property object restricted context */
+    protected $restricted_context;
+
+    /**
+     * Contructor
+     */
+    public function __construct($zend_class) {
+        $this->zend_class = $zend_class;
+    }
+
+    /**
+     * Process request from client.
+     * @param bool $simple use simple authentication
+     * @return void
+     */
+    public function run($simple) {
+        $this->simple = $simple;
+
+        // we will probably need a lot of memory in some functions
+        @raise_memory_limit('128M');
+
+        // set some longer timeout, this script is not sending any output,
+        // this means we need to manually extend the timeout operations
+        // that need longer time to finish
+        external_api::set_timeout();
+
+        // set up exception handler first, we want to sent them back in correct format that
+        // the other system understands
+        // we do not need to call the original default handler because this ws handler does everything
+        set_exception_handler(array($this, 'exception_handler'));
+
+        // now create the instance of zend server
+        $this->init_zend_server();
+
+        // this sets up $USER and $SESSION and context restrictions
+        $this->authenticate_user();
+
+        // make a list of all functions user is allowed to excecute
+        $this->init_service_class();
+
+        // start the server
+        $this->zend_server->setClass($this->service_class);
+        $response = $this->zend_server->handle();
+
+        // session cleanup
+        $this->session_cleanup();
+
+        //TODO: we need to send some headers too I guess
+        echo $response;
+        die;
+    }
+
+    /**
+     * Load virtual class needed for Zend api
+     * @return void
+     */
+    protected function init_service_class() {
+        global $USER, $DB;
+
+        // first ofall get a complete list of services user is allowed to access
+        if ($this->simple) {
+            // now make sure the function is listed in at least one service user is allowed to use
+            // allow access only if:
+            //  1/ entry in the external_services_users table if required
+            //  2/ validuntil not reached
+            //  3/ has capability if specified in service desc
+            //  4/ iprestriction
+
+            $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
+
+                     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";
+            $params = array('userid'=>$USER->id, 'now'=>time());
+        } else {
+
+            //TODO: token may restrict access to one service only
+            die('not implemented yet');
+        }
+
+        $serviceids = array();
+        $rs = $DB->get_recordset_sql($sql, $params);
+
+        // now make sure user may access at least one service
+        $remoteaddr = getremoteaddr();
+        $allowed = false;
+        foreach ($rs as $service) {
+            if (isset($serviceids[$service->id])) {
+                continue;
+            }
+            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();
+
+        // now get the list of all functions
+        if ($serviceids) {
+            list($serviceids, $params) = $DB->get_in_or_equal($serviceids);
+            $sql = "SELECT f.*
+                      FROM {external_functions} f
+                     WHERE f.name IN (SELECT sf.functionname
+                                        FROM {external_services_functions} sf
+                                       WHERE sf.externalserviceid $serviceids)";
+            $functions = $DB->get_records_sql($sql, $params);
+        } else {
+            $functions = array();
+        }
+
+        // now make the virtual WS class with all the fuctions for this particular user
+        $methods = '';
+        foreach ($functions as $function) {
+            $methods .= $this->get_virtual_method_code($function);
+        }
+
+        $classname = 'webservices_virtual_class_000000';
+        while(class_exists($classname)) {
+            $classname++;
+        }
+
+        $code = '
+/**
+ * Virtual class web services for user id '.$USER->id.' in context '.$this->restricted_context->id.'.
+ */
+class '.$classname.' {
+'.$methods.'
+}
+';
+        // load the virtual class definition into memory
+        eval($code);
+echo "<xmp>".$code."</xmp>";
+        $this->service_class = $classname;
+    }
+
+    /**
+     * returns virtual method code
+     * @param object $function
+     * @return string PHP code
+     */
+    protected function get_virtual_method_code($function) {
+        global $CFG;
+
+        //first find and include the ext implementation class
+        $function->classpath = empty($function->classpath) ? get_component_directory($function->component).'/externallib.php' : $CFG->dirroot.'/'.$function->classpath;
+        if (!file_exists($function->classpath)) {
+            throw new coding_exception('Can not find file with external function implementation');
+        }
+        require_once($function->classpath);
+
+        $function->parameters_method = $function->methodname.'_parameters';
+        $function->returns_method    = $function->methodname.'_returns';
+
+        // make sure the implementaion class is ok
+        if (!method_exists($function->classname, $function->methodname)) {
+            throw new coding_exception('Missing implementation method');
+        }
+        if (!method_exists($function->classname, $function->parameters_method)) {
+            throw new coding_exception('Missing parameters description');
+        }
+        if (!method_exists($function->classname, $function->returns_method)) {
+            throw new coding_exception('Missing returned values description');
+        }
+
+        // fetch the parameters description
+        $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
+        if (!($function->parameters_desc instanceof external_function_parameters)) {
+            throw new coding_exception('Invalid parameters description');
+        }
+
+        // fetch the return values description
+        $function->returns_desc = call_user_func(array($function->classname, $function->returns_method));
+        // null means void result or result is ignored
+        if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) {
+            throw new coding_exception('Invalid return description');
+        }
+
+        $params      = array();
+        $params_desc = array();
+        foreach ($function->parameters_desc->keys as $name=>$unused) {
+            $params[]      = '$'.$name;
+            $params_desc[] = '     * @param mixed '.$name. '';
+        }
+        $params      = implode(', ', $params);
+        $params_desc = implode("\n", $params_desc);
+
+
+        // now crate a virtual method that calls the ext implemenation
+        // TODO: add PHP docs and all missing info here
+
+        $code = '
+    /**
+     * External function: '.$function->name.'
+     * TODO: add proper param and return description
+'.$params_desc.'
+     * @return mixed result
+     */
+    public function '.$function->name.'('.$params.') {
+        return '.$function->classname.'::'.$function->methodname.'('.$params.');
+    }
+';
+        return $code;
+    }
+
+    /**
+     * Set up zend serice class
+     * @return void
+     */
+    protected function init_zend_server() {
+        include "Zend/Loader.php";
+        Zend_Loader::registerAutoload();
+        //TODO: set up some server options and debugging too - maybe a new method
+        //TODO: add some zend exeption handler too
+        $this->zend_server = new $this->zend_class();
+    }
+
+    /**
+     * Send the error information to the WS client.
+     * @param exception $ex
+     * @return void
+     */
+    protected function send_error($ex=null) {
+        var_dump($ex);
+        die('TODO');
+        // TODO: find some way to send the error back through the Zend
+    }
+
+    /**
+     * Authenticate user using username+password or token.
+     * This function sets up $USER global.
+     * It is safe to use has_capability() after this.
+     * This method also verifies user is allowed to use this
+     * server.
+     * @return void
+     */
+    protected function authenticate_user() {
+        global $CFG, $DB;
+
+        if (!NO_MOODLE_COOKIES) {
+            throw new coding_exception('Cookies must be disabled in WS servers!');
+        }
+
+        if ($this->simple) {
+            $this->restricted_context = get_context_instance(CONTEXT_SYSTEM);
+
+            if (!is_enabled_auth('webservice')) {
+                die('WS auth not enabled');
+            }
+
+            if (!$auth = get_auth_plugin('webservice')) {
+                die('WS auth missing');
+            }
+
+            // the username is hardcoded as URL parameter because we can not easily parse the request data :-(
+            if (!$username = optional_param('wsusername', '', PARAM_RAW)) {
+                throw new invalid_parameter_exception('Missing username');
+            }
+
+            // the password is hardcoded as URL parameter because we can not easily parse the request data :-(
+            if (!$password = optional_param('wspassword', '', PARAM_RAW)) {
+                throw new invalid_parameter_exception('Missing password');
+            }
+
+            if (!$auth->user_login_webservice($username, $password)) {
+                throw new invalid_parameter_exception('Wrong username or password');
+            }
+
+            $user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>0), '*', MUST_EXIST);
+
+            // now fake user login, the session is completely empty too
+            session_set_user($user);
+
+        } else {
+
+            //TODO: not implemented yet
+            die('token login not implemented yet');
+            //TODO: $this->restricted_context is derived from the token context
+        }
+
+        if (!has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
+            throw new invalid_parameter_exception('Access to web service not allowed');
+        }
+
+        external_api::set_context_restriction($this->restricted_context);
+    }
+
+    /**
+     * Specialised exception handler, we can not use the standard one because
+     * it can not just print html to output.
+     *
+     * @param exception $ex
+     * @return void does not return
+     */
+    public function exception_handler($ex) {
+        global $CFG, $DB, $SCRIPT;
+
+        // detect active db transactions, rollback and log as error
+        if ($DB->is_transaction_started()) {
+            error_log('Database transaction aborted by exception in ' . $CFG->dirroot . $SCRIPT);
+            try {
+                // note: transaction blocks should never change current $_SESSION
+                $DB->rollback_sql();
+            } catch (Exception $ignored) {
+            }
+        }
+
+        // now let the plugin send the exception to client
+        $this->send_error($ex);
+
+        // some hacks might need a cleanup hook
+        $this->session_cleanup($ex);
+
+        // not much else we can do now, add some logging later
+        exit(1);
+    }
+
+    /**
+     * Future hook needed for emulated sessions.
+     * @param exception $exception null means normal termination, $exception received when WS call failed
+     * @return void
+     */
+    protected function session_cleanup($exception=null) {
+        if ($this->simple) {
+            // nothing needs to be done, there is no persistent session
+        } else {
+            // close emulated session if used
+        }
+    }
+
 }
 
 
@@ -84,6 +436,9 @@ abstract class webservice_base_server implements webservice_server {
     /** @property string $token authentication token*/
     protected $token = null;
 
+    /** @property object restricted context */
+    protected $restricted_context;
+
     /** @property array $parameters the function parameters - the real values submitted in the request */
     protected $parameters = null;
 
@@ -228,6 +583,8 @@ abstract class webservice_base_server implements webservice_server {
         }
 
         if ($this->simple) {
+            $this->restricted_context = get_context_instance(CONTEXT_SYSTEM);
+
             if (!is_enabled_auth('webservice')) {
                 die('WS auth not enabled');
             }
@@ -252,20 +609,18 @@ abstract class webservice_base_server implements webservice_server {
 
             // now fake user login, the session is completely empty too
             session_set_user($user);
-
-            if (!has_capability("webservice/$this->wsname:use", get_context_instance(CONTEXT_SYSTEM))) {
-                throw new invalid_parameter_exception('Access to web service not allowed');
-            }
-
         } else {
+
             //TODO: not implemented yet
             die('token login not implemented yet');
+            //TODO: $this->restricted_context is derived from the token context
+        }
 
-            // note we had to wait until here because we did not know the security context earlier
-            if (!has_capability("webservice/$this->wsname:use", $context)) {
-                throw new invalid_parameter_exception('Access to web service not allowed');
-            }
+        if (!has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
+            throw new invalid_parameter_exception('Access to web service not allowed');
         }
+
+        external_api::set_context_restriction($this->restricted_context);
     }
 
     /**
@@ -306,36 +661,35 @@ abstract class webservice_base_server implements webservice_server {
                       JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2)
                       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";
+            $params = array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time());
+        } else {
+
+            //TODO: token may restrict access to one service only
+            die('not implemented yet');
+        }
 
-            $rs = $DB->get_recordset_sql($sql, array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time()));
-            // now make sure user may access at least one service
-            $syscontext = get_context_instance(CONTEXT_SYSTEM);
-            $remoteaddr = getremoteaddr();
-            $allowed = false;
-            foreach ($rs as $service) {
-                if ($service->requiredcapability and !has_capability($service->requiredcapability, $syscontext)) {
-                    continue; // cap required, sorry
-                }
-                if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
-                    continue; // wrong request source ip, sorry
-                }
-                $allowed = true;
-                break; // one service is enough, no need to continue
+        $rs = $DB->get_recordset_sql($sql, $params);
+        // now make sure user may access at least one service
+        $remoteaddr = getremoteaddr();
+        $allowed = false;
+        foreach ($rs as $service) {
+            if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) {
+                continue; // cap required, sorry
             }
-            $rs->close();
-            if (!$allowed) {
-                throw new invalid_parameter_exception('Access to external function not allowed');
+            if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
+                continue; // wrong request source ip, sorry
             }
-            // now we finally know the user may execute this function,
-            // the last step is to set context restriction - in this simple case
-            // we use system context because each external system has different user account
-            // and we can manage everything through normal permissions.
-            external_api::set_context_restriction($syscontext);
-
-        } else {
-            //TODO: implement token security checks
-            die('not implemented yet');
+            $allowed = true;
+            break; // one service is enough, no need to continue
+        }
+        $rs->close();
+        if (!$allowed) {
+            throw new invalid_parameter_exception('Access to external function not allowed');
         }
+        // now we finally know the user may execute this function,
+        // the last step is to set context restriction - in this simple case
+        // we use system context because each external system has different user account
+        // and we can manage everything through normal permissions.
 
         // get the params and return descriptions of the function
         unset($function->id); // we want to prevent any accidental db updates ;-)