From: skodak Date: Wed, 14 Oct 2009 16:48:38 +0000 (+0000) Subject: MDL-12886 first attempt to create the Zend compatibility layer for WS X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=8809813352bd59e0edb49a55f0342ec465ed2257;p=moodle.git MDL-12886 first attempt to create the Zend compatibility layer for WS --- diff --git a/webservice/lib.php b/webservice/lib.php index b37a48c481..7443a19f5e 100644 --- a/webservice/lib.php +++ b/webservice/lib.php @@ -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 "".$code.""; + $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 ;-)