|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00003 // This file is part of Moodle - http://moodle.org/ 00004 // 00005 // Moodle is free software: you can redistribute it and/or modify 00006 // it under the terms of the GNU General Public License as published by 00007 // the Free Software Foundation, either version 3 of the License, or 00008 // (at your option) any later version. 00009 // 00010 // Moodle is distributed in the hope that it will be useful, 00011 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // GNU General Public License for more details. 00014 // 00015 // You should have received a copy of the GNU General Public License 00016 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00017 00026 require_once($CFG->libdir.'/externallib.php'); 00027 00028 define('WEBSERVICE_AUTHMETHOD_USERNAME', 0); 00029 define('WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN', 1); 00030 define('WEBSERVICE_AUTHMETHOD_SESSION_TOKEN', 2); 00031 00035 class webservice { 00036 00042 public function authenticate_user($token) { 00043 global $DB, $CFG; 00044 00045 // web service must be enabled to use this script 00046 if (!$CFG->enablewebservices) { 00047 throw new webservice_access_exception(get_string('enablewsdescription', 'webservice')); 00048 } 00049 00050 // Obtain token record 00051 if (!$token = $DB->get_record('external_tokens', array('token' => $token))) { 00052 throw new webservice_access_exception(get_string('invalidtoken', 'webservice')); 00053 } 00054 00055 // Validate token date 00056 if ($token->validuntil and $token->validuntil < time()) { 00057 add_to_log(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '', get_string('invalidtimedtoken', 'webservice'), 0); 00058 $DB->delete_records('external_tokens', array('token' => $token->token)); 00059 throw new webservice_access_exception(get_string('invalidtimedtoken', 'webservice')); 00060 } 00061 00062 // Check ip 00063 if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { 00064 add_to_log(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '', get_string('failedtolog', 'webservice') . ": " . getremoteaddr(), 0); 00065 throw new webservice_access_exception(get_string('invalidiptoken', 'webservice')); 00066 } 00067 00068 //retrieve user link to the token 00069 $user = $DB->get_record('user', array('id' => $token->userid, 'deleted' => 0), '*', MUST_EXIST); 00070 00071 // let enrol plugins deal with new enrolments if necessary 00072 enrol_check_plugins($user); 00073 00074 // setup user session to check capability 00075 session_set_user($user); 00076 00077 //assumes that if sid is set then there must be a valid associated session no matter the token type 00078 if ($token->sid) { 00079 $session = session_get_instance(); 00080 if (!$session->session_exists($token->sid)) { 00081 $DB->delete_records('external_tokens', array('sid' => $token->sid)); 00082 throw new webservice_access_exception(get_string('invalidtokensession', 'webservice')); 00083 } 00084 } 00085 00086 //Non admin can not authenticate if maintenance mode 00087 $hassiteconfig = has_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM), $user); 00088 if (!empty($CFG->maintenance_enabled) and !$hassiteconfig) { 00089 throw new webservice_access_exception(get_string('sitemaintenance', 'admin')); 00090 } 00091 00092 //retrieve web service record 00093 $service = $DB->get_record('external_services', array('id' => $token->externalserviceid, 'enabled' => 1)); 00094 if (empty($service)) { 00095 // will throw exception if no token found 00096 throw new webservice_access_exception(get_string('servicenotavailable', 'webservice')); 00097 } 00098 00099 //check if there is any required system capability 00100 if ($service->requiredcapability and !has_capability($service->requiredcapability, get_context_instance(CONTEXT_SYSTEM), $user)) { 00101 throw new webservice_access_exception(get_string('missingrequiredcapability', 'webservice', $service->requiredcapability)); 00102 } 00103 00104 //specific checks related to user restricted service 00105 if ($service->restrictedusers) { 00106 $authoriseduser = $DB->get_record('external_services_users', array('externalserviceid' => $service->id, 'userid' => $user->id)); 00107 00108 if (empty($authoriseduser)) { 00109 throw new webservice_access_exception(get_string('usernotallowed', 'webservice', $service->name)); 00110 } 00111 00112 if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) { 00113 throw new webservice_access_exception(get_string('invalidtimedtoken', 'webservice')); 00114 } 00115 00116 if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) { 00117 throw new webservice_access_exception(get_string('invalidiptoken', 'webservice')); 00118 } 00119 } 00120 00121 //only confirmed user should be able to call web service 00122 if (empty($user->confirmed)) { 00123 add_to_log(SITEID, 'webservice', 'user unconfirmed', '', $user->username); 00124 throw new webservice_access_exception(get_string('usernotconfirmed', 'moodle', $user->username)); 00125 } 00126 00127 //check the user is suspended 00128 if (!empty($user->suspended)) { 00129 add_to_log(SITEID, 'webservice', 'user suspended', '', $user->username); 00130 throw new webservice_access_exception(get_string('usersuspended', 'webservice')); 00131 } 00132 00133 //check if the auth method is nologin (in this case refuse connection) 00134 if ($user->auth == 'nologin') { 00135 add_to_log(SITEID, 'webservice', 'nologin auth attempt with web service', '', $user->username); 00136 throw new webservice_access_exception(get_string('nologinauth', 'webservice')); 00137 } 00138 00139 //Check if the user password is expired 00140 $auth = get_auth_plugin($user->auth); 00141 if (!empty($auth->config->expiration) and $auth->config->expiration == 1) { 00142 $days2expire = $auth->password_expire($user->username); 00143 if (intval($days2expire) < 0) { 00144 add_to_log(SITEID, 'webservice', 'expired password', '', $user->username); 00145 throw new webservice_access_exception(get_string('passwordisexpired', 'webservice')); 00146 } 00147 } 00148 00149 // log token access 00150 $DB->set_field('external_tokens', 'lastaccess', time(), array('id' => $token->id)); 00151 00152 return array('user' => $user, 'token' => $token, 'service' => $service); 00153 } 00154 00159 public function add_ws_authorised_user($user) { 00160 global $DB; 00161 $user->timecreated = mktime(); 00162 $DB->insert_record('external_services_users', $user); 00163 } 00164 00170 public function remove_ws_authorised_user($user, $serviceid) { 00171 global $DB; 00172 $DB->delete_records('external_services_users', 00173 array('externalserviceid' => $serviceid, 'userid' => $user->id)); 00174 } 00175 00180 public function update_ws_authorised_user($user) { 00181 global $DB; 00182 $DB->update_record('external_services_users', $user); 00183 } 00184 00191 public function get_ws_authorised_users($serviceid) { 00192 global $DB, $CFG; 00193 $params = array($CFG->siteguest, $serviceid); 00194 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname, 00195 u.lastname as lastname, 00196 esu.iprestriction as iprestriction, esu.validuntil as validuntil, 00197 esu.timecreated as timecreated 00198 FROM {user} u, {external_services_users} esu 00199 WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1 00200 AND esu.userid = u.id 00201 AND esu.externalserviceid = ?"; 00202 00203 $users = $DB->get_records_sql($sql, $params); 00204 return $users; 00205 } 00206 00213 public function get_ws_authorised_user($serviceid, $userid) { 00214 global $DB, $CFG; 00215 $params = array($CFG->siteguest, $serviceid, $userid); 00216 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname, 00217 u.lastname as lastname, 00218 esu.iprestriction as iprestriction, esu.validuntil as validuntil, 00219 esu.timecreated as timecreated 00220 FROM {user} u, {external_services_users} esu 00221 WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1 00222 AND esu.userid = u.id 00223 AND esu.externalserviceid = ? 00224 AND u.id = ?"; 00225 $user = $DB->get_record_sql($sql, $params); 00226 return $user; 00227 } 00228 00233 public function generate_user_ws_tokens($userid) { 00234 global $CFG, $DB; 00235 00237 if (!is_siteadmin() && has_capability('moodle/webservice:createtoken', get_context_instance(CONTEXT_SYSTEM), $userid) && !empty($CFG->enablewebservices)) { 00239 00241 $norestrictedservices = $DB->get_records('external_services', array('restrictedusers' => 0)); 00242 $serviceidlist = array(); 00243 foreach ($norestrictedservices as $service) { 00244 $serviceidlist[] = $service->id; 00245 } 00246 00247 //get all services which are set to the current user (the current user is specified in the restricted user list) 00248 $servicesusers = $DB->get_records('external_services_users', array('userid' => $userid)); 00249 foreach ($servicesusers as $serviceuser) { 00250 if (!in_array($serviceuser->externalserviceid,$serviceidlist)) { 00251 $serviceidlist[] = $serviceuser->externalserviceid; 00252 } 00253 } 00254 00255 //get all services which already have a token set for the current user 00256 $usertokens = $DB->get_records('external_tokens', array('userid' => $userid, 'tokentype' => EXTERNAL_TOKEN_PERMANENT)); 00257 $tokenizedservice = array(); 00258 foreach ($usertokens as $token) { 00259 $tokenizedservice[] = $token->externalserviceid; 00260 } 00261 00262 //create a token for the service which have no token already 00263 foreach ($serviceidlist as $serviceid) { 00264 if (!in_array($serviceid, $tokenizedservice)) { 00265 //create the token for this service 00266 $newtoken = new stdClass(); 00267 $newtoken->token = md5(uniqid(rand(),1)); 00268 //check that the user has capability on this service 00269 $newtoken->tokentype = EXTERNAL_TOKEN_PERMANENT; 00270 $newtoken->userid = $userid; 00271 $newtoken->externalserviceid = $serviceid; 00272 //TODO: find a way to get the context - UPDATE FOLLOWING LINE 00273 $newtoken->contextid = get_context_instance(CONTEXT_SYSTEM)->id; 00274 $newtoken->creatorid = $userid; 00275 $newtoken->timecreated = time(); 00276 00277 $DB->insert_record('external_tokens', $newtoken); 00278 } 00279 } 00280 00281 00282 } 00283 } 00284 00290 public function get_user_ws_tokens($userid) { 00291 global $DB; 00292 //here retrieve token list (including linked users firstname/lastname and linked services name) 00293 $sql = "SELECT 00294 t.id, t.creatorid, t.token, u.firstname, u.lastname, s.id as wsid, s.name, s.enabled, s.restrictedusers, t.validuntil 00295 FROM 00296 {external_tokens} t, {user} u, {external_services} s 00297 WHERE 00298 t.userid=? AND t.tokentype = ".EXTERNAL_TOKEN_PERMANENT." AND s.id = t.externalserviceid AND t.userid = u.id"; 00299 $tokens = $DB->get_records_sql($sql, array( $userid)); 00300 return $tokens; 00301 } 00302 00315 public function get_created_by_user_ws_token($userid, $tokenid) { 00316 global $DB; 00317 $sql = "SELECT 00318 t.id, t.token, u.firstname, u.lastname, s.name 00319 FROM 00320 {external_tokens} t, {user} u, {external_services} s 00321 WHERE 00322 t.creatorid=? AND t.id=? AND t.tokentype = " 00323 . EXTERNAL_TOKEN_PERMANENT 00324 . " AND s.id = t.externalserviceid AND t.userid = u.id"; 00325 //must be the token creator 00326 $token = $DB->get_record_sql($sql, array($userid, $tokenid), MUST_EXIST); 00327 return $token; 00328 } 00329 00335 public function get_token_by_id($tokenid) { 00336 global $DB; 00337 return $DB->get_record('external_tokens', array('id' => $tokenid)); 00338 } 00339 00344 public function delete_user_ws_token($tokenid) { 00345 global $DB; 00346 $DB->delete_records('external_tokens', array('id'=>$tokenid)); 00347 } 00348 00353 public function delete_service($serviceid) { 00354 global $DB; 00355 $DB->delete_records('external_services_users', array('externalserviceid' => $serviceid)); 00356 $DB->delete_records('external_services_functions', array('externalserviceid' => $serviceid)); 00357 $DB->delete_records('external_tokens', array('externalserviceid' => $serviceid)); 00358 $DB->delete_records('external_services', array('id' => $serviceid)); 00359 } 00360 00366 public function get_user_ws_token($token) { 00367 global $DB; 00368 return $DB->get_record('external_tokens', array('token'=>$token), '*', MUST_EXIST); 00369 } 00370 00376 public function get_external_functions($serviceids) { 00377 global $DB; 00378 if (!empty($serviceids)) { 00379 list($serviceids, $params) = $DB->get_in_or_equal($serviceids); 00380 $sql = "SELECT f.* 00381 FROM {external_functions} f 00382 WHERE f.name IN (SELECT sf.functionname 00383 FROM {external_services_functions} sf 00384 WHERE sf.externalserviceid $serviceids)"; 00385 $functions = $DB->get_records_sql($sql, $params); 00386 } else { 00387 $functions = array(); 00388 } 00389 return $functions; 00390 } 00391 00398 public function get_external_functions_by_enabled_services($serviceshortnames, $enabledonly = true) { 00399 global $DB; 00400 if (!empty($serviceshortnames)) { 00401 $enabledonlysql = $enabledonly?' AND s.enabled = 1 ':''; 00402 list($serviceshortnames, $params) = $DB->get_in_or_equal($serviceshortnames); 00403 $sql = "SELECT f.* 00404 FROM {external_functions} f 00405 WHERE f.name IN (SELECT sf.functionname 00406 FROM {external_services_functions} sf, {external_services} s 00407 WHERE s.shortname $serviceshortnames 00408 AND sf.externalserviceid = s.id 00409 " . $enabledonlysql . ")"; 00410 $functions = $DB->get_records_sql($sql, $params); 00411 } else { 00412 $functions = array(); 00413 } 00414 return $functions; 00415 } 00416 00422 public function get_not_associated_external_functions($serviceid) { 00423 global $DB; 00424 $select = "name NOT IN (SELECT s.functionname 00425 FROM {external_services_functions} s 00426 WHERE s.externalserviceid = :sid 00427 )"; 00428 00429 $functions = $DB->get_records_select('external_functions', 00430 $select, array('sid' => $serviceid), 'name'); 00431 00432 return $functions; 00433 } 00434 00457 public function get_service_required_capabilities($serviceid) { 00458 $functions = $this->get_external_functions(array($serviceid)); 00459 $requiredusercaps = array(); 00460 foreach ($functions as $function) { 00461 $functioncaps = explode(',', $function->capabilities); 00462 if (!empty($functioncaps) and !empty($functioncaps[0])) { 00463 foreach ($functioncaps as $functioncap) { 00464 $requiredusercaps[$function->name][] = trim($functioncap); 00465 } 00466 } 00467 } 00468 return $requiredusercaps; 00469 } 00470 00477 public function get_user_capabilities($userid) { 00478 global $DB; 00479 //retrieve the user capabilities 00480 $sql = "SELECT rc.id, rc.capability FROM {role_capabilities} rc, {role_assignments} ra 00481 WHERE rc.roleid=ra.roleid AND ra.userid= ?"; 00482 $dbusercaps = $DB->get_records_sql($sql, array($userid)); 00483 $usercaps = array(); 00484 foreach ($dbusercaps as $usercap) { 00485 $usercaps[$usercap->capability] = true; 00486 } 00487 return $usercaps; 00488 } 00489 00496 public function get_missing_capabilities_by_users($users, $serviceid) { 00497 global $DB; 00498 $usersmissingcaps = array(); 00499 00500 //retrieve capabilities required by the service 00501 $servicecaps = $this->get_service_required_capabilities($serviceid); 00502 00503 //retrieve users missing capabilities 00504 foreach ($users as $user) { 00505 //cast user array into object to be a bit more flexible 00506 if (is_array($user)) { 00507 $user = (object) $user; 00508 } 00509 $usercaps = $this->get_user_capabilities($user->id); 00510 00511 //detect the missing capabilities 00512 foreach ($servicecaps as $functioname => $functioncaps) { 00513 foreach ($functioncaps as $functioncap) { 00514 if (!key_exists($functioncap, $usercaps)) { 00515 if (!isset($usersmissingcaps[$user->id]) 00516 or array_search($functioncap, $usersmissingcaps[$user->id]) === false) { 00517 $usersmissingcaps[$user->id][] = $functioncap; 00518 } 00519 } 00520 } 00521 } 00522 } 00523 00524 return $usersmissingcaps; 00525 } 00526 00533 public function get_external_service_by_id($serviceid, $strictness=IGNORE_MISSING) { 00534 global $DB; 00535 $service = $DB->get_record('external_services', 00536 array('id' => $serviceid), '*', $strictness); 00537 return $service; 00538 } 00539 00546 public function get_external_service_by_shortname($shortname, $strictness=IGNORE_MISSING) { 00547 global $DB; 00548 $service = $DB->get_record('external_services', 00549 array('shortname' => $shortname), '*', $strictness); 00550 return $service; 00551 } 00552 00559 public function get_external_function_by_id($functionid, $strictness=IGNORE_MISSING) { 00560 global $DB; 00561 $function = $DB->get_record('external_functions', 00562 array('id' => $functionid), '*', $strictness); 00563 return $function; 00564 } 00565 00571 public function add_external_function_to_service($functionname, $serviceid) { 00572 global $DB; 00573 $addedfunction = new stdClass(); 00574 $addedfunction->externalserviceid = $serviceid; 00575 $addedfunction->functionname = $functionname; 00576 $DB->insert_record('external_services_functions', $addedfunction); 00577 } 00578 00584 public function add_external_service($service) { 00585 global $DB; 00586 $service->timecreated = mktime(); 00587 $serviceid = $DB->insert_record('external_services', $service); 00588 return $serviceid; 00589 } 00590 00595 public function update_external_service($service) { 00596 global $DB; 00597 $service->timemodified = mktime(); 00598 $DB->update_record('external_services', $service); 00599 } 00600 00608 public function service_function_exists($functionname, $serviceid) { 00609 global $DB; 00610 return $DB->record_exists('external_services_functions', 00611 array('externalserviceid' => $serviceid, 00612 'functionname' => $functionname)); 00613 } 00614 00615 public function remove_external_function_from_service($functionname, $serviceid) { 00616 global $DB; 00617 $DB->delete_records('external_services_functions', 00618 array('externalserviceid' => $serviceid, 'functionname' => $functionname)); 00619 00620 } 00621 00622 00623 } 00624 00629 class webservice_access_exception extends moodle_exception { 00633 function __construct($debuginfo) { 00634 parent::__construct('accessexception', 'webservice', '', null, $debuginfo); 00635 } 00636 } 00637 00643 function webservice_protocol_is_enabled($protocol) { 00644 global $CFG; 00645 00646 if (empty($CFG->enablewebservices)) { 00647 return false; 00648 } 00649 00650 $active = explode(',', $CFG->webserviceprotocols); 00651 00652 return(in_array($protocol, $active)); 00653 } 00654 00655 //=== WS classes === 00656 00661 interface webservice_test_client_interface { 00669 public function simpletest($serverurl, $function, $params); 00670 } 00671 00676 interface webservice_server_interface { 00681 public function run(); 00682 } 00683 00688 abstract class webservice_server implements webservice_server_interface { 00689 00691 protected $wsname = null; 00692 00694 protected $username = null; 00695 00697 protected $password = null; 00698 00700 protected $userid = null; 00701 00703 protected $authmethod; 00704 00706 protected $token = null; 00707 00709 protected $restricted_context; 00710 00712 protected $restricted_serviceid = null; 00713 00718 public function __construct($authmethod) { 00719 $this->authmethod = $authmethod; 00720 } 00721 00722 00731 protected function authenticate_user() { 00732 global $CFG, $DB; 00733 00734 if (!NO_MOODLE_COOKIES) { 00735 throw new coding_exception('Cookies must be disabled in WS servers!'); 00736 } 00737 00738 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { 00739 00740 //we check that authentication plugin is enabled 00741 //it is only required by simple authentication 00742 if (!is_enabled_auth('webservice')) { 00743 throw new webservice_access_exception(get_string('wsauthnotenabled', 'webservice')); 00744 } 00745 00746 if (!$auth = get_auth_plugin('webservice')) { 00747 throw new webservice_access_exception(get_string('wsauthmissing', 'webservice')); 00748 } 00749 00750 $this->restricted_context = get_context_instance(CONTEXT_SYSTEM); 00751 00752 if (!$this->username) { 00753 throw new webservice_access_exception(get_string('missingusername', 'webservice')); 00754 } 00755 00756 if (!$this->password) { 00757 throw new webservice_access_exception(get_string('missingpassword', 'webservice')); 00758 } 00759 00760 if (!$auth->user_login_webservice($this->username, $this->password)) { 00761 // log failed login attempts 00762 add_to_log(SITEID, 'webservice', get_string('simpleauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".$this->username."/".$this->password." - ".getremoteaddr() , 0); 00763 throw new webservice_access_exception(get_string('wrongusernamepassword', 'webservice')); 00764 } 00765 00766 $user = $DB->get_record('user', array('username'=>$this->username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); 00767 00768 } else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN){ 00769 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT); 00770 } else { 00771 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_EMBEDDED); 00772 } 00773 00774 //Non admin can not authenticate if maintenance mode 00775 $hassiteconfig = has_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM), $user); 00776 if (!empty($CFG->maintenance_enabled) and !$hassiteconfig) { 00777 throw new webservice_access_exception(get_string('sitemaintenance', 'admin')); 00778 } 00779 00780 //only confirmed user should be able to call web service 00781 if (!empty($user->deleted)) { 00782 add_to_log(SITEID, '', '', '', get_string('wsaccessuserdeleted', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id); 00783 throw new webservice_access_exception(get_string('wsaccessuserdeleted', 'webservice', $user->username)); 00784 } 00785 00786 //only confirmed user should be able to call web service 00787 if (empty($user->confirmed)) { 00788 add_to_log(SITEID, '', '', '', get_string('wsaccessuserunconfirmed', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id); 00789 throw new webservice_access_exception(get_string('wsaccessuserunconfirmed', 'webservice', $user->username)); 00790 } 00791 00792 //check the user is suspended 00793 if (!empty($user->suspended)) { 00794 add_to_log(SITEID, '', '', '', get_string('wsaccessusersuspended', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id); 00795 throw new webservice_access_exception(get_string('wsaccessusersuspended', 'webservice', $user->username)); 00796 } 00797 00798 //retrieve the authentication plugin if no previously done 00799 if (empty($auth)) { 00800 $auth = get_auth_plugin($user->auth); 00801 } 00802 00803 // check if credentials have expired 00804 if (!empty($auth->config->expiration) and $auth->config->expiration == 1) { 00805 $days2expire = $auth->password_expire($user->username); 00806 if (intval($days2expire) < 0 ) { 00807 add_to_log(SITEID, '', '', '', get_string('wsaccessuserexpired', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id); 00808 throw new webservice_access_exception(get_string('wsaccessuserexpired', 'webservice', $user->username)); 00809 } 00810 } 00811 00812 //check if the auth method is nologin (in this case refuse connection) 00813 if ($user->auth=='nologin') { 00814 add_to_log(SITEID, '', '', '', get_string('wsaccessusernologin', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id); 00815 throw new webservice_access_exception(get_string('wsaccessusernologin', 'webservice', $user->username)); 00816 } 00817 00818 // now fake user login, the session is completely empty too 00819 enrol_check_plugins($user); 00820 session_set_user($user); 00821 $this->userid = $user->id; 00822 00823 if ($this->authmethod != WEBSERVICE_AUTHMETHOD_SESSION_TOKEN && !has_capability("webservice/$this->wsname:use", $this->restricted_context)) { 00824 throw new webservice_access_exception(get_string('protocolnotallowed', 'webservice', $this->wsname)); 00825 } 00826 00827 external_api::set_context_restriction($this->restricted_context); 00828 } 00829 00830 protected function authenticate_by_token($tokentype){ 00831 global $DB; 00832 if (!$token = $DB->get_record('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype))) { 00833 // log failed login attempts 00834 add_to_log(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".$this->token. " - ".getremoteaddr() , 0); 00835 throw new webservice_access_exception(get_string('invalidtoken', 'webservice')); 00836 } 00837 00838 if ($token->validuntil and $token->validuntil < time()) { 00839 $DB->delete_records('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype)); 00840 throw new webservice_access_exception(get_string('invalidtimedtoken', 'webservice')); 00841 } 00842 00843 if ($token->sid){//assumes that if sid is set then there must be a valid associated session no matter the token type 00844 $session = session_get_instance(); 00845 if (!$session->session_exists($token->sid)){ 00846 $DB->delete_records('external_tokens', array('sid'=>$token->sid)); 00847 throw new webservice_access_exception(get_string('invalidtokensession', 'webservice')); 00848 } 00849 } 00850 00851 if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { 00852 add_to_log(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".getremoteaddr() , 0); 00853 throw new webservice_access_exception(get_string('invalidiptoken', 'webservice')); 00854 } 00855 00856 $this->restricted_context = get_context_instance_by_id($token->contextid); 00857 $this->restricted_serviceid = $token->externalserviceid; 00858 00859 $user = $DB->get_record('user', array('id'=>$token->userid), '*', MUST_EXIST); 00860 00861 // log token access 00862 $DB->set_field('external_tokens', 'lastaccess', time(), array('id'=>$token->id)); 00863 00864 return $user; 00865 00866 } 00867 } 00868 00874 abstract class webservice_zend_server extends webservice_server { 00875 00877 protected $zend_class; 00878 00880 protected $zend_server; 00881 00883 protected $service_class; 00884 00889 public function __construct($authmethod, $zend_class) { 00890 parent::__construct($authmethod); 00891 $this->zend_class = $zend_class; 00892 } 00893 00899 public function run() { 00900 // we will probably need a lot of memory in some functions 00901 raise_memory_limit(MEMORY_EXTRA); 00902 00903 // set some longer timeout, this script is not sending any output, 00904 // this means we need to manually extend the timeout operations 00905 // that need longer time to finish 00906 external_api::set_timeout(); 00907 00908 // now create the instance of zend server 00909 $this->init_zend_server(); 00910 00911 // set up exception handler first, we want to sent them back in correct format that 00912 // the other system understands 00913 // we do not need to call the original default handler because this ws handler does everything 00914 set_exception_handler(array($this, 'exception_handler')); 00915 00916 // init all properties from the request data 00917 $this->parse_request(); 00918 00919 // this sets up $USER and $SESSION and context restrictions 00920 $this->authenticate_user(); 00921 00922 // make a list of all functions user is allowed to excecute 00923 $this->init_service_class(); 00924 00925 // tell server what functions are available 00926 $this->zend_server->setClass($this->service_class); 00927 00928 //log the web service request 00929 add_to_log(SITEID, 'webservice', '', '' , $this->zend_class." ".getremoteaddr() , 0, $this->userid); 00930 00931 //send headers 00932 $this->send_headers(); 00933 00934 // execute and return response, this sends some headers too 00935 $response = $this->zend_server->handle(); 00936 00937 // session cleanup 00938 $this->session_cleanup(); 00939 00940 //finally send the result 00941 echo $response; 00942 die; 00943 } 00944 00949 protected function init_service_class() { 00950 global $USER, $DB; 00951 00952 // first ofall get a complete list of services user is allowed to access 00953 00954 if ($this->restricted_serviceid) { 00955 $params = array('sid1'=>$this->restricted_serviceid, 'sid2'=>$this->restricted_serviceid); 00956 $wscond1 = 'AND s.id = :sid1'; 00957 $wscond2 = 'AND s.id = :sid2'; 00958 } else { 00959 $params = array(); 00960 $wscond1 = ''; 00961 $wscond2 = ''; 00962 } 00963 00964 // now make sure the function is listed in at least one service user is allowed to use 00965 // allow access only if: 00966 // 1/ entry in the external_services_users table if required 00967 // 2/ validuntil not reached 00968 // 3/ has capability if specified in service desc 00969 // 4/ iprestriction 00970 00971 $sql = "SELECT s.*, NULL AS iprestriction 00972 FROM {external_services} s 00973 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0) 00974 WHERE s.enabled = 1 $wscond1 00975 00976 UNION 00977 00978 SELECT s.*, su.iprestriction 00979 FROM {external_services} s 00980 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1) 00981 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid) 00982 WHERE s.enabled = 1 AND su.validuntil IS NULL OR su.validuntil < :now $wscond2"; 00983 00984 $params = array_merge($params, array('userid'=>$USER->id, 'now'=>time())); 00985 00986 $serviceids = array(); 00987 $rs = $DB->get_recordset_sql($sql, $params); 00988 00989 // now make sure user may access at least one service 00990 $remoteaddr = getremoteaddr(); 00991 $allowed = false; 00992 foreach ($rs as $service) { 00993 if (isset($serviceids[$service->id])) { 00994 continue; 00995 } 00996 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { 00997 continue; // cap required, sorry 00998 } 00999 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { 01000 continue; // wrong request source ip, sorry 01001 } 01002 $serviceids[$service->id] = $service->id; 01003 } 01004 $rs->close(); 01005 01006 // now get the list of all functions 01007 $wsmanager = new webservice(); 01008 $functions = $wsmanager->get_external_functions($serviceids); 01009 01010 // now make the virtual WS class with all the fuctions for this particular user 01011 $methods = ''; 01012 foreach ($functions as $function) { 01013 $methods .= $this->get_virtual_method_code($function); 01014 } 01015 01016 // let's use unique class name, there might be problem in unit tests 01017 $classname = 'webservices_virtual_class_000000'; 01018 while(class_exists($classname)) { 01019 $classname++; 01020 } 01021 01022 $code = ' 01026 class '.$classname.' { 01027 '.$methods.' 01028 } 01029 '; 01030 01031 // load the virtual class definition into memory 01032 eval($code); 01033 $this->service_class = $classname; 01034 } 01035 01041 protected function get_virtual_method_code($function) { 01042 global $CFG; 01043 01044 $function = external_function_info($function); 01045 01046 //arguments in function declaration line with defaults. 01047 $paramanddefaults = array(); 01048 //arguments used as parameters for external lib call. 01049 $params = array(); 01050 $params_desc = array(); 01051 foreach ($function->parameters_desc->keys as $name=>$keydesc) { 01052 $param = '$'.$name; 01053 $paramanddefault = $param; 01054 //need to generate the default if there is any 01055 if ($keydesc instanceof external_value) { 01056 if ($keydesc->required == VALUE_DEFAULT) { 01057 if ($keydesc->default===null) { 01058 $paramanddefault .= '=null'; 01059 } else { 01060 switch($keydesc->type) { 01061 case PARAM_BOOL: 01062 $paramanddefault .= '='.$keydesc->default; break; 01063 case PARAM_INT: 01064 $paramanddefault .= '='.$keydesc->default; break; 01065 case PARAM_FLOAT; 01066 $paramanddefault .= '='.$keydesc->default; break; 01067 default: 01068 $paramanddefault .= '=\''.$keydesc->default.'\''; 01069 } 01070 } 01071 } else if ($keydesc->required == VALUE_OPTIONAL) { 01072 //it does make sens to declare a parameter VALUE_OPTIONAL 01073 //VALUE_OPTIONAL is used only for array/object key 01074 throw new moodle_exception('parametercannotbevalueoptional'); 01075 } 01076 } else { //for the moment we do not support default for other structure types 01077 if ($keydesc->required == VALUE_DEFAULT) { 01078 //accept empty array as default 01079 if (isset($keydesc->default) and is_array($keydesc->default) 01080 and empty($keydesc->default)) { 01081 $paramanddefault .= '=array()'; 01082 } else { 01083 throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name); 01084 } 01085 } 01086 if ($keydesc->required == VALUE_OPTIONAL) { 01087 throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name); 01088 } 01089 } 01090 $params[] = $param; 01091 $paramanddefaults[] = $paramanddefault; 01092 $type = $this->get_phpdoc_type($keydesc); 01093 $params_desc[] = ' * @param '.$type.' $'.$name.' '.$keydesc->desc; 01094 } 01095 $params = implode(', ', $params); 01096 $paramanddefaults = implode(', ', $paramanddefaults); 01097 $params_desc = implode("\n", $params_desc); 01098 01099 $serviceclassmethodbody = $this->service_class_method_body($function, $params); 01100 01101 if (is_null($function->returns_desc)) { 01102 $return = ' * @return void'; 01103 } else { 01104 $type = $this->get_phpdoc_type($function->returns_desc); 01105 $return = ' * @return '.$type.' '.$function->returns_desc->desc; 01106 } 01107 01108 // now crate the virtual method that calls the ext implementation 01109 01110 $code = ' 01117 public function '.$function->name.'('.$paramanddefaults.') { 01118 '.$serviceclassmethodbody.' 01119 } 01120 '; 01121 return $code; 01122 } 01123 01124 protected function get_phpdoc_type($keydesc) { 01125 if ($keydesc instanceof external_value) { 01126 switch($keydesc->type) { 01127 case PARAM_BOOL: // 0 or 1 only for now 01128 case PARAM_INT: 01129 $type = 'int'; break; 01130 case PARAM_FLOAT; 01131 $type = 'double'; break; 01132 default: 01133 $type = 'string'; 01134 } 01135 01136 } else if ($keydesc instanceof external_single_structure) { 01137 $classname = $this->generate_simple_struct_class($keydesc); 01138 $type = $classname; 01139 01140 } else if ($keydesc instanceof external_multiple_structure) { 01141 $type = 'array'; 01142 } 01143 01144 return $type; 01145 } 01146 01147 protected function generate_simple_struct_class(external_single_structure $structdesc) { 01148 return 'object|struct'; //only 'object' is supported by SOAP, 'struct' by XML-RPC MDL-23083 01149 } 01150 01159 protected function service_class_method_body($function, $params){ 01160 //cast the param from object to array (validate_parameters except array only) 01161 $castingcode = ''; 01162 if ($params){ 01163 $paramstocast = explode(',', $params); 01164 foreach ($paramstocast as $paramtocast) { 01165 //clean the parameter from any white space 01166 $paramtocast = trim($paramtocast); 01167 $castingcode .= $paramtocast . 01168 '=webservice_zend_server::cast_objects_to_array('.$paramtocast.');'; 01169 } 01170 01171 } 01172 01173 $descriptionmethod = $function->methodname.'_returns()'; 01174 $callforreturnvaluedesc = $function->classname.'::'.$descriptionmethod; 01175 return $castingcode . ' if ('.$callforreturnvaluedesc.' == null) {'. 01176 $function->classname.'::'.$function->methodname.'('.$params.'); 01177 return null; 01178 } 01179 return external_api::clean_returnvalue('.$callforreturnvaluedesc.', '.$function->classname.'::'.$function->methodname.'('.$params.'));'; 01180 } 01181 01188 public static function cast_objects_to_array($param){ 01189 if (is_object($param)){ 01190 $param = (array)$param; 01191 } 01192 if (is_array($param)){ 01193 $toreturn = array(); 01194 foreach ($param as $key=> $param){ 01195 $toreturn[$key] = self::cast_objects_to_array($param); 01196 } 01197 return $toreturn; 01198 } else { 01199 return $param; 01200 } 01201 } 01202 01207 protected function init_zend_server() { 01208 $this->zend_server = new $this->zend_class(); 01209 } 01210 01218 protected function parse_request() { 01219 01220 //Get GET and POST paramters 01221 $methodvariables = array_merge($_GET,$_POST); 01222 01223 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { 01224 //note: some clients have problems with entity encoding :-( 01225 if (isset($methodvariables['wsusername'])) { 01226 $this->username = $methodvariables['wsusername']; 01227 } 01228 if (isset($methodvariables['wspassword'])) { 01229 $this->password = $methodvariables['wspassword']; 01230 } 01231 } else { 01232 if (isset($methodvariables['wstoken'])) { 01233 $this->token = $methodvariables['wstoken']; 01234 } 01235 } 01236 } 01237 01242 protected function send_headers() { 01243 header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); 01244 header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT'); 01245 header('Pragma: no-cache'); 01246 header('Accept-Ranges: none'); 01247 } 01248 01256 public function exception_handler($ex) { 01257 // detect active db transactions, rollback and log as error 01258 abort_all_db_transactions(); 01259 01260 // some hacks might need a cleanup hook 01261 $this->session_cleanup($ex); 01262 01263 // now let the plugin send the exception to client 01264 $this->send_error($ex); 01265 01266 // not much else we can do now, add some logging later 01267 exit(1); 01268 } 01269 01276 protected function send_error($ex=null) { 01277 $this->send_headers(); 01278 echo $this->zend_server->fault($ex); 01279 } 01280 01286 protected function session_cleanup($exception=null) { 01287 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { 01288 // nothing needs to be done, there is no persistent session 01289 } else { 01290 // close emulated session if used 01291 } 01292 } 01293 01294 } 01295 01301 abstract class webservice_base_server extends webservice_server { 01302 01304 protected $parameters = null; 01305 01307 protected $functionname = null; 01308 01310 protected $function = null; 01311 01313 protected $returns = null; 01314 01323 abstract protected function parse_request(); 01324 01329 abstract protected function send_response(); 01330 01336 abstract protected function send_error($ex=null); 01337 01342 public function run() { 01343 // we will probably need a lot of memory in some functions 01344 raise_memory_limit(MEMORY_EXTRA); 01345 01346 // set some longer timeout, this script is not sending any output, 01347 // this means we need to manually extend the timeout operations 01348 // that need longer time to finish 01349 external_api::set_timeout(); 01350 01351 // set up exception handler first, we want to sent them back in correct format that 01352 // the other system understands 01353 // we do not need to call the original default handler because this ws handler does everything 01354 set_exception_handler(array($this, 'exception_handler')); 01355 01356 // init all properties from the request data 01357 $this->parse_request(); 01358 01359 // authenticate user, this has to be done after the request parsing 01360 // this also sets up $USER and $SESSION 01361 $this->authenticate_user(); 01362 01363 // find all needed function info and make sure user may actually execute the function 01364 $this->load_function_info(); 01365 01366 //log the web service request 01367 add_to_log(SITEID, 'webservice', $this->functionname, '' , getremoteaddr() , 0, $this->userid); 01368 01369 // finally, execute the function - any errors are catched by the default exception handler 01370 $this->execute(); 01371 01372 // send the results back in correct format 01373 $this->send_response(); 01374 01375 // session cleanup 01376 $this->session_cleanup(); 01377 01378 die; 01379 } 01380 01388 public function exception_handler($ex) { 01389 // detect active db transactions, rollback and log as error 01390 abort_all_db_transactions(); 01391 01392 // some hacks might need a cleanup hook 01393 $this->session_cleanup($ex); 01394 01395 // now let the plugin send the exception to client 01396 $this->send_error($ex); 01397 01398 // not much else we can do now, add some logging later 01399 exit(1); 01400 } 01401 01407 protected function session_cleanup($exception=null) { 01408 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { 01409 // nothing needs to be done, there is no persistent session 01410 } else { 01411 // close emulated session if used 01412 } 01413 } 01414 01421 protected function load_function_info() { 01422 global $DB, $USER, $CFG; 01423 01424 if (empty($this->functionname)) { 01425 throw new invalid_parameter_exception('Missing function name'); 01426 } 01427 01428 // function must exist 01429 $function = external_function_info($this->functionname); 01430 01431 if ($this->restricted_serviceid) { 01432 $params = array('sid1'=>$this->restricted_serviceid, 'sid2'=>$this->restricted_serviceid); 01433 $wscond1 = 'AND s.id = :sid1'; 01434 $wscond2 = 'AND s.id = :sid2'; 01435 } else { 01436 $params = array(); 01437 $wscond1 = ''; 01438 $wscond2 = ''; 01439 } 01440 01441 // now let's verify access control 01442 01443 // now make sure the function is listed in at least one service user is allowed to use 01444 // allow access only if: 01445 // 1/ entry in the external_services_users table if required 01446 // 2/ validuntil not reached 01447 // 3/ has capability if specified in service desc 01448 // 4/ iprestriction 01449 01450 $sql = "SELECT s.*, NULL AS iprestriction 01451 FROM {external_services} s 01452 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1) 01453 WHERE s.enabled = 1 $wscond1 01454 01455 UNION 01456 01457 SELECT s.*, su.iprestriction 01458 FROM {external_services} s 01459 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2) 01460 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid) 01461 WHERE s.enabled = 1 AND su.validuntil IS NULL OR su.validuntil < :now $wscond2"; 01462 $params = array_merge($params, array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time())); 01463 01464 $rs = $DB->get_recordset_sql($sql, $params); 01465 // now make sure user may access at least one service 01466 $remoteaddr = getremoteaddr(); 01467 $allowed = false; 01468 foreach ($rs as $service) { 01469 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { 01470 continue; // cap required, sorry 01471 } 01472 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { 01473 continue; // wrong request source ip, sorry 01474 } 01475 $allowed = true; 01476 break; // one service is enough, no need to continue 01477 } 01478 $rs->close(); 01479 if (!$allowed) { 01480 throw new webservice_access_exception(get_string('accesstofunctionnotallowed', 'webservice', $this->functionname)); 01481 } 01482 01483 // we have all we need now 01484 $this->function = $function; 01485 } 01486 01491 protected function execute() { 01492 // validate params, this also sorts the params properly, we need the correct order in the next part 01493 $params = call_user_func(array($this->function->classname, 'validate_parameters'), $this->function->parameters_desc, $this->parameters); 01494 01495 // execute - yay! 01496 $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), array_values($params)); 01497 } 01498 } 01499 01500