Commit | Line | Data |
---|---|---|
06e7fadc | 1 | <?php |
cc93c7da | 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 | ||
a0a07014 | 17 | |
06e7fadc | 18 | /** |
cc93c7da | 19 | * Web services utility functions and classes |
06e7fadc | 20 | * |
a0a07014 JM |
21 | * @package core_webservice |
22 | * @copyright 2009 Jerome Mouneyrac <jerome@moodle.com> | |
23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
06e7fadc | 24 | */ |
25 | ||
cc93c7da | 26 | require_once($CFG->libdir.'/externallib.php'); |
893d7f0f | 27 | |
a0a07014 JM |
28 | /** |
29 | * WEBSERVICE_AUTHMETHOD_USERNAME - username/password authentication (also called simple authentication) | |
30 | */ | |
2d0acbd5 | 31 | define('WEBSERVICE_AUTHMETHOD_USERNAME', 0); |
a0a07014 JM |
32 | |
33 | /** | |
34 | * WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN - most common token authentication (external app, mobile app...) | |
35 | */ | |
2d0acbd5 | 36 | define('WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN', 1); |
a0a07014 JM |
37 | |
38 | /** | |
39 | * WEBSERVICE_AUTHMETHOD_SESSION_TOKEN - token for embedded application (requires Moodle session) | |
40 | */ | |
2d0acbd5 JP |
41 | define('WEBSERVICE_AUTHMETHOD_SESSION_TOKEN', 2); |
42 | ||
229a7099 | 43 | /** |
44 | * General web service library | |
a0a07014 JM |
45 | * |
46 | * @package core_webservice | |
47 | * @copyright 2010 Jerome Mouneyrac <jerome@moodle.com> | |
48 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
229a7099 | 49 | */ |
50 | class webservice { | |
bb044f53 | 51 | /** |
52 | * Only update token last access once per this many seconds. (This constant controls update of | |
53 | * the external tokens last access field. There is a similar define LASTACCESS_UPDATE_SECS | |
54 | * which controls update of the web site last access fields.) | |
55 | * | |
56 | * @var int | |
57 | */ | |
58 | const TOKEN_LASTACCESS_UPDATE_SECS = 60; | |
229a7099 | 59 | |
07cc3d11 JM |
60 | /** |
61 | * Authenticate user (used by download/upload file scripts) | |
a0a07014 | 62 | * |
07cc3d11 JM |
63 | * @param string $token |
64 | * @return array - contains the authenticated user, token and service objects | |
65 | */ | |
66 | public function authenticate_user($token) { | |
67 | global $DB, $CFG; | |
68 | ||
69 | // web service must be enabled to use this script | |
70 | if (!$CFG->enablewebservices) { | |
96d3b93b | 71 | throw new webservice_access_exception('Web services are not enabled in Advanced features.'); |
07cc3d11 JM |
72 | } |
73 | ||
74 | // Obtain token record | |
75 | if (!$token = $DB->get_record('external_tokens', array('token' => $token))) { | |
96d3b93b JM |
76 | //client may want to display login form => moodle_exception |
77 | throw new moodle_exception('invalidtoken', 'webservice'); | |
07cc3d11 JM |
78 | } |
79 | ||
d733a8cc FM |
80 | $loginfaileddefaultparams = array( |
81 | 'other' => array( | |
82 | 'method' => WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN, | |
83 | 'reason' => null, | |
84 | 'tokenid' => $token->id | |
85 | ) | |
86 | ); | |
87 | ||
07cc3d11 JM |
88 | // Validate token date |
89 | if ($token->validuntil and $token->validuntil < time()) { | |
d733a8cc FM |
90 | $params = $loginfaileddefaultparams; |
91 | $params['other']['reason'] = 'token_expired'; | |
92 | $event = \core\event\webservice_login_failed::create($params); | |
93 | $event->add_record_snapshot('external_tokens', $token); | |
94 | $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '', | |
95 | get_string('invalidtimedtoken', 'webservice'), 0)); | |
96 | $event->trigger(); | |
07cc3d11 | 97 | $DB->delete_records('external_tokens', array('token' => $token->token)); |
96d3b93b | 98 | throw new webservice_access_exception('Invalid token - token expired - check validuntil time for the token'); |
07cc3d11 JM |
99 | } |
100 | ||
101 | // Check ip | |
102 | if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { | |
d733a8cc FM |
103 | $params = $loginfaileddefaultparams; |
104 | $params['other']['reason'] = 'ip_restricted'; | |
105 | $event = \core\event\webservice_login_failed::create($params); | |
106 | $event->add_record_snapshot('external_tokens', $token); | |
107 | $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '', | |
108 | get_string('failedtolog', 'webservice') . ": " . getremoteaddr(), 0)); | |
109 | $event->trigger(); | |
96d3b93b JM |
110 | throw new webservice_access_exception('Invalid token - IP:' . getremoteaddr() |
111 | . ' is not supported'); | |
07cc3d11 JM |
112 | } |
113 | ||
114 | //retrieve user link to the token | |
115 | $user = $DB->get_record('user', array('id' => $token->userid, 'deleted' => 0), '*', MUST_EXIST); | |
116 | ||
117 | // let enrol plugins deal with new enrolments if necessary | |
118 | enrol_check_plugins($user); | |
119 | ||
120 | // setup user session to check capability | |
d79d5ac2 | 121 | \core\session\manager::set_user($user); |
1f2e3279 | 122 | set_login_session_preferences(); |
07cc3d11 JM |
123 | |
124 | //assumes that if sid is set then there must be a valid associated session no matter the token type | |
125 | if ($token->sid) { | |
d79d5ac2 | 126 | if (!\core\session\manager::session_exists($token->sid)) { |
07cc3d11 | 127 | $DB->delete_records('external_tokens', array('sid' => $token->sid)); |
96d3b93b | 128 | throw new webservice_access_exception('Invalid session based token - session not found or expired'); |
07cc3d11 JM |
129 | } |
130 | } | |
131 | ||
59c66f92 MD |
132 | // Cannot authenticate unless maintenance access is granted. |
133 | $hasmaintenanceaccess = has_capability('moodle/site:maintenanceaccess', context_system::instance(), $user); | |
134 | if (!empty($CFG->maintenance_enabled) and !$hasmaintenanceaccess) { | |
96d3b93b JM |
135 | //this is usually temporary, client want to implement code logic => moodle_exception |
136 | throw new moodle_exception('sitemaintenance', 'admin'); | |
07cc3d11 JM |
137 | } |
138 | ||
139 | //retrieve web service record | |
140 | $service = $DB->get_record('external_services', array('id' => $token->externalserviceid, 'enabled' => 1)); | |
141 | if (empty($service)) { | |
142 | // will throw exception if no token found | |
96d3b93b | 143 | throw new webservice_access_exception('Web service is not available (it doesn\'t exist or might be disabled)'); |
07cc3d11 JM |
144 | } |
145 | ||
146 | //check if there is any required system capability | |
43731030 | 147 | if ($service->requiredcapability and !has_capability($service->requiredcapability, context_system::instance(), $user)) { |
96d3b93b | 148 | throw new webservice_access_exception('The capability ' . $service->requiredcapability . ' is required.'); |
07cc3d11 JM |
149 | } |
150 | ||
151 | //specific checks related to user restricted service | |
152 | if ($service->restrictedusers) { | |
153 | $authoriseduser = $DB->get_record('external_services_users', array('externalserviceid' => $service->id, 'userid' => $user->id)); | |
154 | ||
155 | if (empty($authoriseduser)) { | |
96d3b93b JM |
156 | throw new webservice_access_exception( |
157 | 'The user is not allowed for this service. First you need to allow this user on the ' | |
158 | . $service->name . '\'s allowed users administration page.'); | |
07cc3d11 JM |
159 | } |
160 | ||
161 | if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) { | |
96d3b93b | 162 | throw new webservice_access_exception('Invalid service - service expired - check validuntil time for this allowed user'); |
07cc3d11 JM |
163 | } |
164 | ||
165 | if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) { | |
96d3b93b JM |
166 | throw new webservice_access_exception('Invalid service - IP:' . getremoteaddr() |
167 | . ' is not supported - check this allowed user'); | |
07cc3d11 JM |
168 | } |
169 | } | |
170 | ||
171 | //only confirmed user should be able to call web service | |
172 | if (empty($user->confirmed)) { | |
d733a8cc FM |
173 | $params = $loginfaileddefaultparams; |
174 | $params['other']['reason'] = 'user_unconfirmed'; | |
175 | $event = \core\event\webservice_login_failed::create($params); | |
176 | $event->add_record_snapshot('external_tokens', $token); | |
177 | $event->set_legacy_logdata(array(SITEID, 'webservice', 'user unconfirmed', '', $user->username)); | |
178 | $event->trigger(); | |
96d3b93b | 179 | throw new moodle_exception('usernotconfirmed', 'moodle', '', $user->username); |
07cc3d11 JM |
180 | } |
181 | ||
182 | //check the user is suspended | |
183 | if (!empty($user->suspended)) { | |
d733a8cc FM |
184 | $params = $loginfaileddefaultparams; |
185 | $params['other']['reason'] = 'user_suspended'; | |
186 | $event = \core\event\webservice_login_failed::create($params); | |
187 | $event->add_record_snapshot('external_tokens', $token); | |
188 | $event->set_legacy_logdata(array(SITEID, 'webservice', 'user suspended', '', $user->username)); | |
189 | $event->trigger(); | |
96d3b93b | 190 | throw new webservice_access_exception('Refused web service access for suspended username: ' . $user->username); |
07cc3d11 JM |
191 | } |
192 | ||
193 | //check if the auth method is nologin (in this case refuse connection) | |
194 | if ($user->auth == 'nologin') { | |
d733a8cc FM |
195 | $params = $loginfaileddefaultparams; |
196 | $params['other']['reason'] = 'nologin'; | |
197 | $event = \core\event\webservice_login_failed::create($params); | |
198 | $event->add_record_snapshot('external_tokens', $token); | |
199 | $event->set_legacy_logdata(array(SITEID, 'webservice', 'nologin auth attempt with web service', '', $user->username)); | |
200 | $event->trigger(); | |
96d3b93b | 201 | throw new webservice_access_exception('Refused web service access for nologin authentication username: ' . $user->username); |
07cc3d11 JM |
202 | } |
203 | ||
204 | //Check if the user password is expired | |
205 | $auth = get_auth_plugin($user->auth); | |
206 | if (!empty($auth->config->expiration) and $auth->config->expiration == 1) { | |
207 | $days2expire = $auth->password_expire($user->username); | |
208 | if (intval($days2expire) < 0) { | |
d733a8cc FM |
209 | $params = $loginfaileddefaultparams; |
210 | $params['other']['reason'] = 'password_expired'; | |
211 | $event = \core\event\webservice_login_failed::create($params); | |
212 | $event->add_record_snapshot('external_tokens', $token); | |
213 | $event->set_legacy_logdata(array(SITEID, 'webservice', 'expired password', '', $user->username)); | |
214 | $event->trigger(); | |
96d3b93b | 215 | throw new moodle_exception('passwordisexpired', 'webservice'); |
07cc3d11 JM |
216 | } |
217 | } | |
218 | ||
219 | // log token access | |
bb044f53 | 220 | self::update_token_lastaccess($token); |
07cc3d11 JM |
221 | |
222 | return array('user' => $user, 'token' => $token, 'service' => $service); | |
223 | } | |
224 | ||
bb044f53 | 225 | /** |
226 | * Updates the last access time for a token. | |
227 | * | |
228 | * @param \stdClass $token Token object (must include id, lastaccess fields) | |
229 | * @param int $time Time of access (0 = use current time) | |
230 | * @throws dml_exception If database error | |
231 | */ | |
232 | public static function update_token_lastaccess($token, int $time = 0) { | |
233 | global $DB; | |
234 | ||
235 | if (!$time) { | |
236 | $time = time(); | |
237 | } | |
238 | ||
239 | // Only update the field if it is a different time from previous request, | |
240 | // so as not to waste database effort. | |
241 | if ($time >= $token->lastaccess + self::TOKEN_LASTACCESS_UPDATE_SECS) { | |
242 | $DB->set_field('external_tokens', 'lastaccess', $time, array('id' => $token->id)); | |
243 | } | |
244 | } | |
245 | ||
86dcc6f0 | 246 | /** |
a0a07014 JM |
247 | * Allow user to call a service |
248 | * | |
249 | * @param stdClass $user a user | |
86dcc6f0 | 250 | */ |
251 | public function add_ws_authorised_user($user) { | |
252 | global $DB; | |
caee6e6c | 253 | $user->timecreated = time(); |
86dcc6f0 | 254 | $DB->insert_record('external_services_users', $user); |
255 | } | |
256 | ||
257 | /** | |
a0a07014 JM |
258 | * Disallow a user to call a service |
259 | * | |
260 | * @param stdClass $user a user | |
86dcc6f0 | 261 | * @param int $serviceid |
262 | */ | |
263 | public function remove_ws_authorised_user($user, $serviceid) { | |
264 | global $DB; | |
265 | $DB->delete_records('external_services_users', | |
266 | array('externalserviceid' => $serviceid, 'userid' => $user->id)); | |
267 | } | |
268 | ||
269 | /** | |
a0a07014 JM |
270 | * Update allowed user settings (ip restriction, valid until...) |
271 | * | |
272 | * @param stdClass $user | |
86dcc6f0 | 273 | */ |
274 | public function update_ws_authorised_user($user) { | |
275 | global $DB; | |
276 | $DB->update_record('external_services_users', $user); | |
277 | } | |
278 | ||
279 | /** | |
280 | * Return list of allowed users with their options (ip/timecreated / validuntil...) | |
281 | * for a given service | |
a0a07014 JM |
282 | * |
283 | * @param int $serviceid the service id to search against | |
86dcc6f0 | 284 | * @return array $users |
285 | */ | |
286 | public function get_ws_authorised_users($serviceid) { | |
c924a469 PS |
287 | global $DB, $CFG; |
288 | $params = array($CFG->siteguest, $serviceid); | |
86dcc6f0 | 289 | $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname, |
c924a469 PS |
290 | u.lastname as lastname, |
291 | esu.iprestriction as iprestriction, esu.validuntil as validuntil, | |
292 | esu.timecreated as timecreated | |
293 | FROM {user} u, {external_services_users} esu | |
294 | WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1 | |
86dcc6f0 | 295 | AND esu.userid = u.id |
296 | AND esu.externalserviceid = ?"; | |
86dcc6f0 | 297 | |
298 | $users = $DB->get_records_sql($sql, $params); | |
299 | return $users; | |
300 | } | |
301 | ||
302 | /** | |
a0a07014 JM |
303 | * Return an authorised user with their options (ip/timecreated / validuntil...) |
304 | * | |
305 | * @param int $serviceid the service id to search against | |
306 | * @param int $userid the user to search against | |
307 | * @return stdClass | |
86dcc6f0 | 308 | */ |
309 | public function get_ws_authorised_user($serviceid, $userid) { | |
c924a469 PS |
310 | global $DB, $CFG; |
311 | $params = array($CFG->siteguest, $serviceid, $userid); | |
86dcc6f0 | 312 | $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname, |
c924a469 PS |
313 | u.lastname as lastname, |
314 | esu.iprestriction as iprestriction, esu.validuntil as validuntil, | |
315 | esu.timecreated as timecreated | |
316 | FROM {user} u, {external_services_users} esu | |
317 | WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1 | |
86dcc6f0 | 318 | AND esu.userid = u.id |
319 | AND esu.externalserviceid = ? | |
320 | AND u.id = ?"; | |
321 | $user = $DB->get_record_sql($sql, $params); | |
322 | return $user; | |
323 | } | |
324 | ||
229a7099 | 325 | /** |
a0a07014 JM |
326 | * Generate all tokens of a specific user |
327 | * | |
328 | * @param int $userid user id | |
229a7099 | 329 | */ |
330 | public function generate_user_ws_tokens($userid) { | |
331 | global $CFG, $DB; | |
c924a469 | 332 | |
a0a07014 | 333 | // generate a token for non admin if web service are enable and the user has the capability to create a token |
43731030 | 334 | if (!is_siteadmin() && has_capability('moodle/webservice:createtoken', context_system::instance(), $userid) && !empty($CFG->enablewebservices)) { |
a0a07014 | 335 | // for every service than the user is authorised on, create a token (if it doesn't already exist) |
229a7099 | 336 | |
a0a07014 | 337 | // get all services which are set to all user (no restricted to specific users) |
229a7099 | 338 | $norestrictedservices = $DB->get_records('external_services', array('restrictedusers' => 0)); |
339 | $serviceidlist = array(); | |
340 | foreach ($norestrictedservices as $service) { | |
341 | $serviceidlist[] = $service->id; | |
342 | } | |
343 | ||
a0a07014 | 344 | // get all services which are set to the current user (the current user is specified in the restricted user list) |
229a7099 | 345 | $servicesusers = $DB->get_records('external_services_users', array('userid' => $userid)); |
346 | foreach ($servicesusers as $serviceuser) { | |
347 | if (!in_array($serviceuser->externalserviceid,$serviceidlist)) { | |
348 | $serviceidlist[] = $serviceuser->externalserviceid; | |
349 | } | |
350 | } | |
351 | ||
a0a07014 | 352 | // get all services which already have a token set for the current user |
229a7099 | 353 | $usertokens = $DB->get_records('external_tokens', array('userid' => $userid, 'tokentype' => EXTERNAL_TOKEN_PERMANENT)); |
354 | $tokenizedservice = array(); | |
355 | foreach ($usertokens as $token) { | |
356 | $tokenizedservice[] = $token->externalserviceid; | |
357 | } | |
358 | ||
a0a07014 | 359 | // create a token for the service which have no token already |
229a7099 | 360 | foreach ($serviceidlist as $serviceid) { |
361 | if (!in_array($serviceid, $tokenizedservice)) { | |
a0a07014 | 362 | // create the token for this service |
7a424cc4 | 363 | $newtoken = new stdClass(); |
229a7099 | 364 | $newtoken->token = md5(uniqid(rand(),1)); |
a0a07014 | 365 | // check that the user has capability on this service |
229a7099 | 366 | $newtoken->tokentype = EXTERNAL_TOKEN_PERMANENT; |
367 | $newtoken->userid = $userid; | |
368 | $newtoken->externalserviceid = $serviceid; | |
a0a07014 | 369 | // TODO MDL-31190 find a way to get the context - UPDATE FOLLOWING LINE |
43731030 | 370 | $newtoken->contextid = context_system::instance()->id; |
229a7099 | 371 | $newtoken->creatorid = $userid; |
372 | $newtoken->timecreated = time(); | |
69cbe359 | 373 | $newtoken->privatetoken = null; |
229a7099 | 374 | |
375 | $DB->insert_record('external_tokens', $newtoken); | |
376 | } | |
377 | } | |
378 | ||
379 | ||
380 | } | |
381 | } | |
382 | ||
383 | /** | |
a0a07014 JM |
384 | * Return all tokens of a specific user |
385 | * + the service state (enabled/disabled) | |
386 | * + the authorised user mode (restricted/not restricted) | |
387 | * | |
388 | * @param int $userid user id | |
389 | * @return array | |
229a7099 | 390 | */ |
391 | public function get_user_ws_tokens($userid) { | |
392 | global $DB; | |
393 | //here retrieve token list (including linked users firstname/lastname and linked services name) | |
394 | $sql = "SELECT | |
e88c193f | 395 | t.id, t.creatorid, t.token, u.firstname, u.lastname, s.id as wsid, s.name, s.enabled, s.restrictedusers, t.validuntil |
229a7099 | 396 | FROM |
397 | {external_tokens} t, {user} u, {external_services} s | |
398 | WHERE | |
399 | t.userid=? AND t.tokentype = ".EXTERNAL_TOKEN_PERMANENT." AND s.id = t.externalserviceid AND t.userid = u.id"; | |
400 | $tokens = $DB->get_records_sql($sql, array( $userid)); | |
401 | return $tokens; | |
402 | } | |
403 | ||
404 | /** | |
a0a07014 JM |
405 | * Return a token that has been created by the user (i.e. to created by an admin) |
406 | * If no tokens exist an exception is thrown | |
407 | * | |
408 | * The returned value is a stdClass: | |
9c954e88 | 409 | * ->id token id |
410 | * ->token | |
411 | * ->firstname user firstname | |
412 | * ->lastname | |
413 | * ->name service name | |
a0a07014 JM |
414 | * |
415 | * @param int $userid user id | |
416 | * @param int $tokenid token id | |
417 | * @return stdClass | |
229a7099 | 418 | */ |
419 | public function get_created_by_user_ws_token($userid, $tokenid) { | |
420 | global $DB; | |
421 | $sql = "SELECT | |
422 | t.id, t.token, u.firstname, u.lastname, s.name | |
423 | FROM | |
424 | {external_tokens} t, {user} u, {external_services} s | |
425 | WHERE | |
9c954e88 | 426 | t.creatorid=? AND t.id=? AND t.tokentype = " |
427 | . EXTERNAL_TOKEN_PERMANENT | |
428 | . " AND s.id = t.externalserviceid AND t.userid = u.id"; | |
429 | //must be the token creator | |
430 | $token = $DB->get_record_sql($sql, array($userid, $tokenid), MUST_EXIST); | |
229a7099 | 431 | return $token; |
229a7099 | 432 | } |
433 | ||
0c62ca25 JD |
434 | /** |
435 | * Return a token of an arbitrary user by tokenid, including details of the associated user and the service name. | |
436 | * If no tokens exist an exception is thrown | |
437 | * | |
438 | * The returned value is a stdClass: | |
439 | * ->id token id | |
440 | * ->token | |
441 | * ->firstname user firstname | |
442 | * ->lastname | |
443 | * ->name service name | |
444 | * | |
445 | * @param int $tokenid token id | |
446 | * @return stdClass | |
447 | */ | |
448 | public function get_token_by_id_with_details($tokenid) { | |
449 | global $DB; | |
ad2ae6e3 JO |
450 | $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.creatorid |
451 | FROM {external_tokens} t, {user} u, {external_services} s | |
452 | WHERE t.id=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id"; | |
453 | $token = $DB->get_record_sql($sql, array($tokenid, EXTERNAL_TOKEN_PERMANENT), MUST_EXIST); | |
0c62ca25 JD |
454 | return $token; |
455 | } | |
456 | ||
9ef728d6 | 457 | /** |
a0a07014 JM |
458 | * Return a database token record for a token id |
459 | * | |
460 | * @param int $tokenid token id | |
9ef728d6 | 461 | * @return object token |
462 | */ | |
463 | public function get_token_by_id($tokenid) { | |
464 | global $DB; | |
465 | return $DB->get_record('external_tokens', array('id' => $tokenid)); | |
466 | } | |
467 | ||
229a7099 | 468 | /** |
a0a07014 JM |
469 | * Delete a token |
470 | * | |
471 | * @param int $tokenid token id | |
229a7099 | 472 | */ |
473 | public function delete_user_ws_token($tokenid) { | |
474 | global $DB; | |
475 | $DB->delete_records('external_tokens', array('id'=>$tokenid)); | |
476 | } | |
bb98a5b2 | 477 | |
753504fb JL |
478 | /** |
479 | * Delete all the tokens belonging to a user. | |
480 | * | |
481 | * @param int $userid the user id whose tokens must be deleted | |
482 | */ | |
483 | public static function delete_user_ws_tokens($userid) { | |
484 | global $DB; | |
485 | $DB->delete_records('external_tokens', array('userid' => $userid)); | |
486 | } | |
487 | ||
c25662b0 | 488 | /** |
a0a07014 JM |
489 | * Delete a service |
490 | * Also delete function references and authorised user references. | |
491 | * | |
492 | * @param int $serviceid service id | |
c25662b0 | 493 | */ |
494 | public function delete_service($serviceid) { | |
495 | global $DB; | |
496 | $DB->delete_records('external_services_users', array('externalserviceid' => $serviceid)); | |
497 | $DB->delete_records('external_services_functions', array('externalserviceid' => $serviceid)); | |
498 | $DB->delete_records('external_tokens', array('externalserviceid' => $serviceid)); | |
fc0fcb27 | 499 | $DB->delete_records('external_services', array('id' => $serviceid)); |
c25662b0 | 500 | } |
501 | ||
bb98a5b2 | 502 | /** |
a0a07014 JM |
503 | * Get a full database token record for a given token value |
504 | * | |
bb98a5b2 | 505 | * @param string $token |
506 | * @throws moodle_exception if there is multiple result | |
507 | */ | |
508 | public function get_user_ws_token($token) { | |
509 | global $DB; | |
510 | return $DB->get_record('external_tokens', array('token'=>$token), '*', MUST_EXIST); | |
511 | } | |
229a7099 | 512 | |
72f68b51 | 513 | /** |
a0a07014 JM |
514 | * Get the functions list of a service list (by id) |
515 | * | |
516 | * @param array $serviceids service ids | |
517 | * @return array of functions | |
72f68b51 | 518 | */ |
519 | public function get_external_functions($serviceids) { | |
520 | global $DB; | |
521 | if (!empty($serviceids)) { | |
522 | list($serviceids, $params) = $DB->get_in_or_equal($serviceids); | |
523 | $sql = "SELECT f.* | |
524 | FROM {external_functions} f | |
525 | WHERE f.name IN (SELECT sf.functionname | |
526 | FROM {external_services_functions} sf | |
89823b1a JL |
527 | WHERE sf.externalserviceid $serviceids) |
528 | ORDER BY f.name ASC"; | |
72f68b51 | 529 | $functions = $DB->get_records_sql($sql, $params); |
530 | } else { | |
531 | $functions = array(); | |
532 | } | |
533 | return $functions; | |
534 | } | |
535 | ||
0bf486a6 | 536 | /** |
a0a07014 JM |
537 | * Get the functions of a service list (by shortname). It can return only enabled functions if required. |
538 | * | |
539 | * @param array $serviceshortnames service shortnames | |
540 | * @param bool $enabledonly if true then only return functions for services that have been enabled | |
0bf486a6 JM |
541 | * @return array functions |
542 | */ | |
543 | public function get_external_functions_by_enabled_services($serviceshortnames, $enabledonly = true) { | |
544 | global $DB; | |
545 | if (!empty($serviceshortnames)) { | |
546 | $enabledonlysql = $enabledonly?' AND s.enabled = 1 ':''; | |
547 | list($serviceshortnames, $params) = $DB->get_in_or_equal($serviceshortnames); | |
548 | $sql = "SELECT f.* | |
549 | FROM {external_functions} f | |
550 | WHERE f.name IN (SELECT sf.functionname | |
551 | FROM {external_services_functions} sf, {external_services} s | |
552 | WHERE s.shortname $serviceshortnames | |
553 | AND sf.externalserviceid = s.id | |
554 | " . $enabledonlysql . ")"; | |
555 | $functions = $DB->get_records_sql($sql, $params); | |
556 | } else { | |
557 | $functions = array(); | |
558 | } | |
559 | return $functions; | |
72f68b51 | 560 | } |
561 | ||
9c954e88 | 562 | /** |
a0a07014 JM |
563 | * Get functions not included in a service |
564 | * | |
565 | * @param int $serviceid service id | |
9c954e88 | 566 | * @return array functions |
567 | */ | |
568 | public function get_not_associated_external_functions($serviceid) { | |
569 | global $DB; | |
570 | $select = "name NOT IN (SELECT s.functionname | |
571 | FROM {external_services_functions} s | |
572 | WHERE s.externalserviceid = :sid | |
573 | )"; | |
574 | ||
575 | $functions = $DB->get_records_select('external_functions', | |
576 | $select, array('sid' => $serviceid), 'name'); | |
577 | ||
578 | return $functions; | |
579 | } | |
580 | ||
72f68b51 | 581 | /** |
582 | * Get list of required capabilities of a service, sorted by functions | |
a0a07014 | 583 | * Example of returned value: |
72f68b51 | 584 | * Array |
585 | * ( | |
f23e9b6b | 586 | * [core_group_create_groups] => Array |
72f68b51 | 587 | * ( |
588 | * [0] => moodle/course:managegroups | |
589 | * ) | |
590 | * | |
f23e9b6b | 591 | * [core_enrol_get_enrolled_users] => Array |
72f68b51 | 592 | * ( |
f23e9b6b CB |
593 | * [0] => moodle/user:viewdetails |
594 | * [1] => moodle/user:viewhiddendetails | |
595 | * [2] => moodle/course:useremail | |
596 | * [3] => moodle/user:update | |
597 | * [4] => moodle/site:accessallgroups | |
72f68b51 | 598 | * ) |
599 | * ) | |
a0a07014 JM |
600 | * @param int $serviceid service id |
601 | * @return array | |
72f68b51 | 602 | */ |
603 | public function get_service_required_capabilities($serviceid) { | |
604 | $functions = $this->get_external_functions(array($serviceid)); | |
605 | $requiredusercaps = array(); | |
606 | foreach ($functions as $function) { | |
8819c47b | 607 | $functioncaps = explode(',', $function->capabilities); |
72f68b51 | 608 | if (!empty($functioncaps) and !empty($functioncaps[0])) { |
609 | foreach ($functioncaps as $functioncap) { | |
610 | $requiredusercaps[$function->name][] = trim($functioncap); | |
611 | } | |
612 | } | |
613 | } | |
614 | return $requiredusercaps; | |
615 | } | |
616 | ||
617 | /** | |
618 | * Get user capabilities (with context) | |
a0a07014 | 619 | * Only useful for documentation purpose |
b449d3b7 JM |
620 | * WARNING: do not use this "broken" function. It was created in the goal to display some capabilities |
621 | * required by users. In theory we should not need to display this kind of information | |
622 | * as the front end does not display it itself. In pratice, | |
623 | * admins would like the info, for more info you can follow: MDL-29962 | |
a0a07014 JM |
624 | * |
625 | * @param int $userid user id | |
72f68b51 | 626 | * @return array |
627 | */ | |
628 | public function get_user_capabilities($userid) { | |
629 | global $DB; | |
630 | //retrieve the user capabilities | |
fbf6cfe6 JM |
631 | $sql = "SELECT DISTINCT rc.id, rc.capability FROM {role_capabilities} rc, {role_assignments} ra |
632 | WHERE rc.roleid=ra.roleid AND ra.userid= ? AND rc.permission = ?"; | |
633 | $dbusercaps = $DB->get_records_sql($sql, array($userid, CAP_ALLOW)); | |
72f68b51 | 634 | $usercaps = array(); |
635 | foreach ($dbusercaps as $usercap) { | |
636 | $usercaps[$usercap->capability] = true; | |
637 | } | |
638 | return $usercaps; | |
639 | } | |
640 | ||
641 | /** | |
a0a07014 | 642 | * Get missing user capabilities for a given service |
b449d3b7 JM |
643 | * WARNING: do not use this "broken" function. It was created in the goal to display some capabilities |
644 | * required by users. In theory we should not need to display this kind of information | |
645 | * as the front end does not display it itself. In pratice, | |
646 | * admins would like the info, for more info you can follow: MDL-29962 | |
a0a07014 JM |
647 | * |
648 | * @param array $users users | |
649 | * @param int $serviceid service id | |
650 | * @return array of missing capabilities, keys being the user ids | |
72f68b51 | 651 | */ |
652 | public function get_missing_capabilities_by_users($users, $serviceid) { | |
653 | global $DB; | |
654 | $usersmissingcaps = array(); | |
655 | ||
656 | //retrieve capabilities required by the service | |
657 | $servicecaps = $this->get_service_required_capabilities($serviceid); | |
658 | ||
659 | //retrieve users missing capabilities | |
660 | foreach ($users as $user) { | |
661 | //cast user array into object to be a bit more flexible | |
662 | if (is_array($user)) { | |
663 | $user = (object) $user; | |
664 | } | |
665 | $usercaps = $this->get_user_capabilities($user->id); | |
666 | ||
667 | //detect the missing capabilities | |
668 | foreach ($servicecaps as $functioname => $functioncaps) { | |
669 | foreach ($functioncaps as $functioncap) { | |
12fc8acf | 670 | if (!array_key_exists($functioncap, $usercaps)) { |
72f68b51 | 671 | if (!isset($usersmissingcaps[$user->id]) |
672 | or array_search($functioncap, $usersmissingcaps[$user->id]) === false) { | |
673 | $usersmissingcaps[$user->id][] = $functioncap; | |
674 | } | |
675 | } | |
676 | } | |
677 | } | |
678 | } | |
679 | ||
680 | return $usersmissingcaps; | |
681 | } | |
682 | ||
c25662b0 | 683 | /** |
a0a07014 JM |
684 | * Get an external service for a given service id |
685 | * | |
686 | * @param int $serviceid service id | |
687 | * @param int $strictness IGNORE_MISSING, MUST_EXIST... | |
688 | * @return stdClass external service | |
c25662b0 | 689 | */ |
690 | public function get_external_service_by_id($serviceid, $strictness=IGNORE_MISSING) { | |
691 | global $DB; | |
692 | $service = $DB->get_record('external_services', | |
693 | array('id' => $serviceid), '*', $strictness); | |
694 | return $service; | |
695 | } | |
696 | ||
c1b65883 | 697 | /** |
a0a07014 JM |
698 | * Get an external service for a given shortname |
699 | * | |
700 | * @param string $shortname service shortname | |
701 | * @param int $strictness IGNORE_MISSING, MUST_EXIST... | |
702 | * @return stdClass external service | |
c1b65883 JM |
703 | */ |
704 | public function get_external_service_by_shortname($shortname, $strictness=IGNORE_MISSING) { | |
705 | global $DB; | |
706 | $service = $DB->get_record('external_services', | |
707 | array('shortname' => $shortname), '*', $strictness); | |
708 | return $service; | |
709 | } | |
710 | ||
72f68b51 | 711 | /** |
a0a07014 JM |
712 | * Get an external function for a given function id |
713 | * | |
714 | * @param int $functionid function id | |
715 | * @param int $strictness IGNORE_MISSING, MUST_EXIST... | |
716 | * @return stdClass external function | |
72f68b51 | 717 | */ |
718 | public function get_external_function_by_id($functionid, $strictness=IGNORE_MISSING) { | |
719 | global $DB; | |
720 | $function = $DB->get_record('external_functions', | |
721 | array('id' => $functionid), '*', $strictness); | |
722 | return $function; | |
723 | } | |
724 | ||
725 | /** | |
726 | * Add a function to a service | |
a0a07014 JM |
727 | * |
728 | * @param string $functionname function name | |
729 | * @param int $serviceid service id | |
72f68b51 | 730 | */ |
731 | public function add_external_function_to_service($functionname, $serviceid) { | |
732 | global $DB; | |
7a424cc4 | 733 | $addedfunction = new stdClass(); |
72f68b51 | 734 | $addedfunction->externalserviceid = $serviceid; |
735 | $addedfunction->functionname = $functionname; | |
736 | $DB->insert_record('external_services_functions', $addedfunction); | |
737 | } | |
738 | ||
c25662b0 | 739 | /** |
740 | * Add a service | |
a0a07014 JM |
741 | * It generates the timecreated field automatically. |
742 | * | |
743 | * @param stdClass $service | |
c25662b0 | 744 | * @return serviceid integer |
745 | */ | |
746 | public function add_external_service($service) { | |
747 | global $DB; | |
caee6e6c | 748 | $service->timecreated = time(); |
c25662b0 | 749 | $serviceid = $DB->insert_record('external_services', $service); |
750 | return $serviceid; | |
751 | } | |
752 | ||
a0a07014 | 753 | /** |
c25662b0 | 754 | * Update a service |
a0a07014 JM |
755 | * It modifies the timemodified automatically. |
756 | * | |
757 | * @param stdClass $service | |
c25662b0 | 758 | */ |
759 | public function update_external_service($service) { | |
760 | global $DB; | |
caee6e6c | 761 | $service->timemodified = time(); |
c25662b0 | 762 | $DB->update_record('external_services', $service); |
763 | } | |
764 | ||
72f68b51 | 765 | /** |
a0a07014 JM |
766 | * Test whether an external function is already linked to a service |
767 | * | |
768 | * @param string $functionname function name | |
769 | * @param int $serviceid service id | |
72f68b51 | 770 | * @return bool true if a matching function exists for the service, else false. |
771 | * @throws dml_exception if error | |
772 | */ | |
773 | public function service_function_exists($functionname, $serviceid) { | |
774 | global $DB; | |
775 | return $DB->record_exists('external_services_functions', | |
776 | array('externalserviceid' => $serviceid, | |
777 | 'functionname' => $functionname)); | |
778 | } | |
779 | ||
a0a07014 JM |
780 | /** |
781 | * Remove a function from a service | |
782 | * | |
783 | * @param string $functionname function name | |
784 | * @param int $serviceid service id | |
785 | */ | |
72f68b51 | 786 | public function remove_external_function_from_service($functionname, $serviceid) { |
787 | global $DB; | |
788 | $DB->delete_records('external_services_functions', | |
789 | array('externalserviceid' => $serviceid, 'functionname' => $functionname)); | |
790 | ||
791 | } | |
792 | ||
9d382a94 JL |
793 | /** |
794 | * Return a list with all the valid user tokens for the given user, it only excludes expired tokens. | |
795 | * | |
796 | * @param string $userid user id to retrieve tokens from | |
797 | * @return array array of token entries | |
798 | * @since Moodle 3.2 | |
799 | */ | |
800 | public static function get_active_tokens($userid) { | |
801 | global $DB; | |
72f68b51 | 802 | |
9d382a94 JL |
803 | $sql = 'SELECT t.*, s.name as servicename FROM {external_tokens} t JOIN |
804 | {external_services} s ON t.externalserviceid = s.id WHERE | |
805 | t.userid = :userid AND (t.validuntil IS NULL OR t.validuntil > :now)'; | |
806 | $params = array('userid' => $userid, 'now' => time()); | |
807 | return $DB->get_records_sql($sql, $params); | |
808 | } | |
72f68b51 | 809 | } |
229a7099 | 810 | |
5593d2dc | 811 | /** |
812 | * Exception indicating access control problem in web service call | |
96d3b93b JM |
813 | * This exception should return general errors about web service setup. |
814 | * Errors related to the user like wrong username/password should not use it, | |
815 | * you should not use this exception if you want to let the client implement | |
816 | * some code logic against an access error. | |
a0a07014 JM |
817 | * |
818 | * @package core_webservice | |
819 | * @copyright 2009 Petr Skodak | |
820 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
5593d2dc | 821 | */ |
822 | class webservice_access_exception extends moodle_exception { | |
a0a07014 | 823 | |
5593d2dc | 824 | /** |
825 | * Constructor | |
a0a07014 JM |
826 | * |
827 | * @param string $debuginfo the debug info | |
5593d2dc | 828 | */ |
829 | function __construct($debuginfo) { | |
e8b21670 | 830 | parent::__construct('accessexception', 'webservice', '', null, $debuginfo); |
5593d2dc | 831 | } |
832 | } | |
833 | ||
f0dafb3c | 834 | /** |
a0a07014 JM |
835 | * Check if a protocol is enabled |
836 | * | |
13ae7db2 | 837 | * @param string $protocol name of WS protocol ('rest', 'soap', 'xmlrpc'...) |
a0a07014 | 838 | * @return bool true if the protocol is enabled |
f0dafb3c | 839 | */ |
cc93c7da | 840 | function webservice_protocol_is_enabled($protocol) { |
841 | global $CFG; | |
893d7f0f | 842 | |
cc93c7da | 843 | if (empty($CFG->enablewebservices)) { |
844 | return false; | |
893d7f0f | 845 | } |
846 | ||
cc93c7da | 847 | $active = explode(',', $CFG->webserviceprotocols); |
893d7f0f | 848 | |
cc93c7da | 849 | return(in_array($protocol, $active)); |
850 | } | |
893d7f0f | 851 | |
f0dafb3c | 852 | /** |
3f599588 | 853 | * Mandatory interface for all test client classes. |
a0a07014 JM |
854 | * |
855 | * @package core_webservice | |
856 | * @copyright 2009 Petr Skodak | |
857 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
f0dafb3c | 858 | */ |
859 | interface webservice_test_client_interface { | |
a0a07014 | 860 | |
f0dafb3c | 861 | /** |
862 | * Execute test client WS request | |
a0a07014 JM |
863 | * |
864 | * @param string $serverurl server url (including the token param) | |
865 | * @param string $function web service function name | |
866 | * @param array $params parameters of the web service function | |
f0dafb3c | 867 | * @return mixed |
868 | */ | |
869 | public function simpletest($serverurl, $function, $params); | |
870 | } | |
871 | ||
06e7fadc | 872 | /** |
3f599588 | 873 | * Mandatory interface for all web service protocol classes |
a0a07014 JM |
874 | * |
875 | * @package core_webservice | |
876 | * @copyright 2009 Petr Skodak | |
877 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
06e7fadc | 878 | */ |
01482a4a | 879 | interface webservice_server_interface { |
a0a07014 | 880 | |
01482a4a PS |
881 | /** |
882 | * Process request from client. | |
01482a4a PS |
883 | */ |
884 | public function run(); | |
885 | } | |
88098133 | 886 | |
01482a4a PS |
887 | /** |
888 | * Abstract web service base class. | |
a0a07014 JM |
889 | * |
890 | * @package core_webservice | |
891 | * @copyright 2009 Petr Skodak | |
892 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
01482a4a PS |
893 | */ |
894 | abstract class webservice_server implements webservice_server_interface { | |
88098133 | 895 | |
a0a07014 | 896 | /** @var string Name of the web server plugin */ |
88098133 | 897 | protected $wsname = null; |
898 | ||
a0a07014 | 899 | /** @var string Name of local user */ |
c187722c PS |
900 | protected $username = null; |
901 | ||
a0a07014 | 902 | /** @var string Password of the local user */ |
c187722c | 903 | protected $password = null; |
b107e647 | 904 | |
a0a07014 | 905 | /** @var int The local user */ |
c3517f05 | 906 | protected $userid = null; |
907 | ||
a0a07014 | 908 | /** @var integer Authentication method one of WEBSERVICE_AUTHMETHOD_* */ |
2d0acbd5 | 909 | protected $authmethod; |
88098133 | 910 | |
a0a07014 | 911 | /** @var string Authentication token*/ |
01482a4a | 912 | protected $token = null; |
88098133 | 913 | |
a0a07014 | 914 | /** @var stdClass Restricted context */ |
88098133 | 915 | protected $restricted_context; |
916 | ||
a0a07014 | 917 | /** @var int Restrict call to one service id*/ |
01482a4a PS |
918 | protected $restricted_serviceid = null; |
919 | ||
2d0acbd5 | 920 | /** |
a0a07014 JM |
921 | * Constructor |
922 | * | |
c924a469 | 923 | * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* |
2d0acbd5 JP |
924 | */ |
925 | public function __construct($authmethod) { | |
926 | $this->authmethod = $authmethod; | |
c924a469 PS |
927 | } |
928 | ||
929 | ||
01482a4a PS |
930 | /** |
931 | * Authenticate user using username+password or token. | |
932 | * This function sets up $USER global. | |
933 | * It is safe to use has_capability() after this. | |
934 | * This method also verifies user is allowed to use this | |
935 | * server. | |
01482a4a PS |
936 | */ |
937 | protected function authenticate_user() { | |
938 | global $CFG, $DB; | |
939 | ||
940 | if (!NO_MOODLE_COOKIES) { | |
941 | throw new coding_exception('Cookies must be disabled in WS servers!'); | |
942 | } | |
943 | ||
d733a8cc | 944 | $loginfaileddefaultparams = array( |
d733a8cc FM |
945 | 'other' => array( |
946 | 'method' => $this->authmethod, | |
ee2df1a8 | 947 | 'reason' => null |
d733a8cc FM |
948 | ) |
949 | ); | |
950 | ||
2d0acbd5 | 951 | if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { |
01482a4a | 952 | |
1839e2f0 | 953 | //we check that authentication plugin is enabled |
954 | //it is only required by simple authentication | |
955 | if (!is_enabled_auth('webservice')) { | |
96d3b93b | 956 | throw new webservice_access_exception('The web service authentication plugin is disabled.'); |
1839e2f0 | 957 | } |
01482a4a | 958 | |
1839e2f0 | 959 | if (!$auth = get_auth_plugin('webservice')) { |
96d3b93b | 960 | throw new webservice_access_exception('The web service authentication plugin is missing.'); |
1839e2f0 | 961 | } |
01482a4a | 962 | |
43731030 | 963 | $this->restricted_context = context_system::instance(); |
01482a4a PS |
964 | |
965 | if (!$this->username) { | |
96d3b93b | 966 | throw new moodle_exception('missingusername', 'webservice'); |
01482a4a PS |
967 | } |
968 | ||
969 | if (!$this->password) { | |
96d3b93b | 970 | throw new moodle_exception('missingpassword', 'webservice'); |
01482a4a PS |
971 | } |
972 | ||
973 | if (!$auth->user_login_webservice($this->username, $this->password)) { | |
d733a8cc FM |
974 | |
975 | // Log failed login attempts. | |
976 | $params = $loginfaileddefaultparams; | |
977 | $params['other']['reason'] = 'password'; | |
978 | $params['other']['username'] = $this->username; | |
979 | $event = \core\event\webservice_login_failed::create($params); | |
980 | $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('simpleauthlog', 'webservice'), '' , | |
981 | get_string('failedtolog', 'webservice').": ".$this->username."/".$this->password." - ".getremoteaddr() , 0)); | |
982 | $event->trigger(); | |
983 | ||
96d3b93b | 984 | throw new moodle_exception('wrongusernamepassword', 'webservice'); |
01482a4a PS |
985 | } |
986 | ||
07a90ec3 | 987 | $user = $DB->get_record('user', array('username'=>$this->username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); |
01482a4a | 988 | |
2d0acbd5 JP |
989 | } else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN){ |
990 | $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT); | |
01482a4a | 991 | } else { |
2d0acbd5 | 992 | $user = $this->authenticate_by_token(EXTERNAL_TOKEN_EMBEDDED); |
01482a4a | 993 | } |
c924a469 | 994 | |
59c66f92 MD |
995 | // Cannot authenticate unless maintenance access is granted. |
996 | $hasmaintenanceaccess = has_capability('moodle/site:maintenanceaccess', context_system::instance(), $user); | |
997 | if (!empty($CFG->maintenance_enabled) and !$hasmaintenanceaccess) { | |
96d3b93b | 998 | throw new moodle_exception('sitemaintenance', 'admin'); |
07a90ec3 JM |
999 | } |
1000 | ||
1001 | //only confirmed user should be able to call web service | |
1002 | if (!empty($user->deleted)) { | |
d733a8cc FM |
1003 | $params = $loginfaileddefaultparams; |
1004 | $params['other']['reason'] = 'user_deleted'; | |
1005 | $params['other']['username'] = $user->username; | |
1006 | $event = \core\event\webservice_login_failed::create($params); | |
1007 | $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessuserdeleted', 'webservice', | |
1008 | $user->username) . " - ".getremoteaddr(), 0, $user->id)); | |
1009 | $event->trigger(); | |
96d3b93b | 1010 | throw new webservice_access_exception('Refused web service access for deleted username: ' . $user->username); |
07a90ec3 JM |
1011 | } |
1012 | ||
1013 | //only confirmed user should be able to call web service | |
1014 | if (empty($user->confirmed)) { | |
d733a8cc FM |
1015 | $params = $loginfaileddefaultparams; |
1016 | $params['other']['reason'] = 'user_unconfirmed'; | |
1017 | $params['other']['username'] = $user->username; | |
1018 | $event = \core\event\webservice_login_failed::create($params); | |
1019 | $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessuserunconfirmed', 'webservice', | |
1020 | $user->username) . " - ".getremoteaddr(), 0, $user->id)); | |
1021 | $event->trigger(); | |
96d3b93b | 1022 | throw new moodle_exception('wsaccessuserunconfirmed', 'webservice', '', $user->username); |
07a90ec3 JM |
1023 | } |
1024 | ||
1025 | //check the user is suspended | |
1026 | if (!empty($user->suspended)) { | |
d733a8cc FM |
1027 | $params = $loginfaileddefaultparams; |
1028 | $params['other']['reason'] = 'user_unconfirmed'; | |
1029 | $params['other']['username'] = $user->username; | |
1030 | $event = \core\event\webservice_login_failed::create($params); | |
1031 | $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessusersuspended', 'webservice', | |
1032 | $user->username) . " - ".getremoteaddr(), 0, $user->id)); | |
1033 | $event->trigger(); | |
96d3b93b | 1034 | throw new webservice_access_exception('Refused web service access for suspended username: ' . $user->username); |
07a90ec3 JM |
1035 | } |
1036 | ||
1037 | //retrieve the authentication plugin if no previously done | |
1038 | if (empty($auth)) { | |
1039 | $auth = get_auth_plugin($user->auth); | |
1040 | } | |
1041 | ||
1042 | // check if credentials have expired | |
1043 | if (!empty($auth->config->expiration) and $auth->config->expiration == 1) { | |
1044 | $days2expire = $auth->password_expire($user->username); | |
1045 | if (intval($days2expire) < 0 ) { | |
d733a8cc FM |
1046 | $params = $loginfaileddefaultparams; |
1047 | $params['other']['reason'] = 'password_expired'; | |
1048 | $params['other']['username'] = $user->username; | |
1049 | $event = \core\event\webservice_login_failed::create($params); | |
1050 | $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessuserexpired', 'webservice', | |
1051 | $user->username) . " - ".getremoteaddr(), 0, $user->id)); | |
1052 | $event->trigger(); | |
96d3b93b | 1053 | throw new webservice_access_exception('Refused web service access for password expired username: ' . $user->username); |
07a90ec3 JM |
1054 | } |
1055 | } | |
1056 | ||
1057 | //check if the auth method is nologin (in this case refuse connection) | |
1058 | if ($user->auth=='nologin') { | |
d733a8cc FM |
1059 | $params = $loginfaileddefaultparams; |
1060 | $params['other']['reason'] = 'login'; | |
1061 | $params['other']['username'] = $user->username; | |
1062 | $event = \core\event\webservice_login_failed::create($params); | |
1063 | $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessusernologin', 'webservice', | |
1064 | $user->username) . " - ".getremoteaddr(), 0, $user->id)); | |
1065 | $event->trigger(); | |
96d3b93b | 1066 | throw new webservice_access_exception('Refused web service access for nologin authentication username: ' . $user->username); |
07a90ec3 JM |
1067 | } |
1068 | ||
01482a4a | 1069 | // now fake user login, the session is completely empty too |
e922fe23 | 1070 | enrol_check_plugins($user); |
d79d5ac2 | 1071 | \core\session\manager::set_user($user); |
1f2e3279 | 1072 | set_login_session_preferences(); |
c3517f05 | 1073 | $this->userid = $user->id; |
01482a4a | 1074 | |
0f1e3914 | 1075 | if ($this->authmethod != WEBSERVICE_AUTHMETHOD_SESSION_TOKEN && !has_capability("webservice/$this->wsname:use", $this->restricted_context)) { |
96d3b93b | 1076 | throw new webservice_access_exception('You are not allowed to use the {$a} protocol (missing capability: webservice/' . $this->wsname . ':use)'); |
01482a4a PS |
1077 | } |
1078 | ||
1079 | external_api::set_context_restriction($this->restricted_context); | |
1080 | } | |
c924a469 | 1081 | |
a0a07014 JM |
1082 | /** |
1083 | * User authentication by token | |
1084 | * | |
1085 | * @param string $tokentype token type (EXTERNAL_TOKEN_EMBEDDED or EXTERNAL_TOKEN_PERMANENT) | |
1086 | * @return stdClass the authenticated user | |
1087 | * @throws webservice_access_exception | |
1088 | */ | |
2d0acbd5 JP |
1089 | protected function authenticate_by_token($tokentype){ |
1090 | global $DB; | |
d733a8cc FM |
1091 | |
1092 | $loginfaileddefaultparams = array( | |
d733a8cc FM |
1093 | 'other' => array( |
1094 | 'method' => $this->authmethod, | |
ee2df1a8 | 1095 | 'reason' => null |
d733a8cc FM |
1096 | ) |
1097 | ); | |
1098 | ||
2d0acbd5 | 1099 | if (!$token = $DB->get_record('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype))) { |
d733a8cc FM |
1100 | // Log failed login attempts. |
1101 | $params = $loginfaileddefaultparams; | |
1102 | $params['other']['reason'] = 'invalid_token'; | |
1103 | $event = \core\event\webservice_login_failed::create($params); | |
1104 | $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '' , | |
1105 | get_string('failedtolog', 'webservice').": ".$this->token. " - ".getremoteaddr() , 0)); | |
1106 | $event->trigger(); | |
96d3b93b | 1107 | throw new moodle_exception('invalidtoken', 'webservice'); |
2d0acbd5 | 1108 | } |
c924a469 | 1109 | |
2d0acbd5 JP |
1110 | if ($token->validuntil and $token->validuntil < time()) { |
1111 | $DB->delete_records('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype)); | |
96d3b93b | 1112 | throw new webservice_access_exception('Invalid token - token expired - check validuntil time for the token'); |
2d0acbd5 | 1113 | } |
c924a469 | 1114 | |
2d0acbd5 | 1115 | if ($token->sid){//assumes that if sid is set then there must be a valid associated session no matter the token type |
d79d5ac2 | 1116 | if (!\core\session\manager::session_exists($token->sid)){ |
2d0acbd5 | 1117 | $DB->delete_records('external_tokens', array('sid'=>$token->sid)); |
96d3b93b | 1118 | throw new webservice_access_exception('Invalid session based token - session not found or expired'); |
2d0acbd5 JP |
1119 | } |
1120 | } | |
1121 | ||
1122 | if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { | |
d733a8cc FM |
1123 | $params = $loginfaileddefaultparams; |
1124 | $params['other']['reason'] = 'ip_restricted'; | |
1125 | $params['other']['tokenid'] = $token->id; | |
1126 | $event = \core\event\webservice_login_failed::create($params); | |
1127 | $event->add_record_snapshot('external_tokens', $token); | |
1128 | $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '' , | |
1129 | get_string('failedtolog', 'webservice').": ".getremoteaddr() , 0)); | |
1130 | $event->trigger(); | |
96d3b93b JM |
1131 | throw new webservice_access_exception('Invalid service - IP:' . getremoteaddr() |
1132 | . ' is not supported - check this allowed user'); | |
2d0acbd5 JP |
1133 | } |
1134 | ||
d197ea43 | 1135 | $this->restricted_context = context::instance_by_id($token->contextid); |
2d0acbd5 JP |
1136 | $this->restricted_serviceid = $token->externalserviceid; |
1137 | ||
07a90ec3 | 1138 | $user = $DB->get_record('user', array('id'=>$token->userid), '*', MUST_EXIST); |
2d0acbd5 JP |
1139 | |
1140 | // log token access | |
bb044f53 | 1141 | webservice::update_token_lastaccess($token); |
c924a469 | 1142 | |
2d0acbd5 | 1143 | return $user; |
c924a469 | 1144 | |
2d0acbd5 | 1145 | } |
93ce0e82 JM |
1146 | |
1147 | /** | |
1148 | * Intercept some moodlewssettingXXX $_GET and $_POST parameter | |
1149 | * that are related to the web service call and are not the function parameters | |
1150 | */ | |
1151 | protected function set_web_service_call_settings() { | |
1152 | global $CFG; | |
1153 | ||
1154 | // Default web service settings. | |
1155 | // Must be the same XXX key name as the external_settings::set_XXX function. | |
1156 | // Must be the same XXX ws parameter name as 'moodlewssettingXXX'. | |
1157 | $externalsettings = array( | |
bb14a488 JL |
1158 | 'raw' => array('default' => false, 'type' => PARAM_BOOL), |
1159 | 'fileurl' => array('default' => true, 'type' => PARAM_BOOL), | |
1160 | 'filter' => array('default' => false, 'type' => PARAM_BOOL), | |
1161 | 'lang' => array('default' => '', 'type' => PARAM_LANG), | |
1162 | ); | |
93ce0e82 JM |
1163 | |
1164 | // Load the external settings with the web service settings. | |
1165 | $settings = external_settings::get_instance(); | |
bb14a488 | 1166 | foreach ($externalsettings as $name => $settingdata) { |
93ce0e82 JM |
1167 | |
1168 | $wsparamname = 'moodlewssetting' . $name; | |
1169 | ||
1170 | // Retrieve and remove the setting parameter from the request. | |
bb14a488 | 1171 | $value = optional_param($wsparamname, $settingdata['default'], $settingdata['type']); |
93ce0e82 JM |
1172 | unset($_GET[$wsparamname]); |
1173 | unset($_POST[$wsparamname]); | |
1174 | ||
1175 | $functioname = 'set_' . $name; | |
1176 | $settings->$functioname($value); | |
1177 | } | |
1178 | ||
1179 | } | |
01482a4a PS |
1180 | } |
1181 | ||
886d7556 | 1182 | /** |
a0a07014 JM |
1183 | * Web Service server base class. |
1184 | * | |
1185 | * This class handles both simple and token authentication. | |
1186 | * | |
1187 | * @package core_webservice | |
1188 | * @copyright 2009 Petr Skodak | |
1189 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
886d7556 | 1190 | */ |
01482a4a | 1191 | abstract class webservice_base_server extends webservice_server { |
88098133 | 1192 | |
a0a07014 | 1193 | /** @var array The function parameters - the real values submitted in the request */ |
cc93c7da | 1194 | protected $parameters = null; |
1195 | ||
a0a07014 | 1196 | /** @var string The name of the function that is executed */ |
cc93c7da | 1197 | protected $functionname = null; |
1198 | ||
a0a07014 | 1199 | /** @var stdClass Full function description */ |
cc93c7da | 1200 | protected $function = null; |
1201 | ||
a0a07014 | 1202 | /** @var mixed Function return value */ |
cc93c7da | 1203 | protected $returns = null; |
06e7fadc | 1204 | |
79c7fffc JP |
1205 | /** @var array List of methods and their information provided by the web service. */ |
1206 | protected $servicemethods; | |
1207 | ||
1208 | /** @var array List of struct classes generated for the web service methods. */ | |
1209 | protected $servicestructs; | |
1210 | ||
24350e06 | 1211 | /** |
cc93c7da | 1212 | * This method parses the request input, it needs to get: |
1213 | * 1/ user authentication - username+password or token | |
1214 | * 2/ function name | |
1215 | * 3/ function parameters | |
24350e06 | 1216 | */ |
cc93c7da | 1217 | abstract protected function parse_request(); |
24350e06 | 1218 | |
cc93c7da | 1219 | /** |
1220 | * Send the result of function call to the WS client. | |
cc93c7da | 1221 | */ |
1222 | abstract protected function send_response(); | |
24350e06 | 1223 | |
fa0797ec | 1224 | /** |
cc93c7da | 1225 | * Send the error information to the WS client. |
a0a07014 | 1226 | * |
cc93c7da | 1227 | * @param exception $ex |
fa0797ec | 1228 | */ |
cc93c7da | 1229 | abstract protected function send_error($ex=null); |
fa0797ec | 1230 | |
cc93c7da | 1231 | /** |
1232 | * Process request from client. | |
a0a07014 JM |
1233 | * |
1234 | * @uses die | |
cc93c7da | 1235 | */ |
2458e30a | 1236 | public function run() { |
bb14a488 JL |
1237 | global $CFG, $SESSION; |
1238 | ||
cc93c7da | 1239 | // we will probably need a lot of memory in some functions |
346c5887 | 1240 | raise_memory_limit(MEMORY_EXTRA); |
fa0797ec | 1241 | |
cc93c7da | 1242 | // set some longer timeout, this script is not sending any output, |
1243 | // this means we need to manually extend the timeout operations | |
1244 | // that need longer time to finish | |
1245 | external_api::set_timeout(); | |
fa0797ec | 1246 | |
cc93c7da | 1247 | // set up exception handler first, we want to sent them back in correct format that |
1248 | // the other system understands | |
1249 | // we do not need to call the original default handler because this ws handler does everything | |
1250 | set_exception_handler(array($this, 'exception_handler')); | |
06e7fadc | 1251 | |
cc93c7da | 1252 | // init all properties from the request data |
1253 | $this->parse_request(); | |
06e7fadc | 1254 | |
cc93c7da | 1255 | // authenticate user, this has to be done after the request parsing |
1256 | // this also sets up $USER and $SESSION | |
1257 | $this->authenticate_user(); | |
06e7fadc | 1258 | |
cc93c7da | 1259 | // find all needed function info and make sure user may actually execute the function |
1260 | $this->load_function_info(); | |
c924a469 | 1261 | |
d733a8cc FM |
1262 | // Log the web service request. |
1263 | $params = array( | |
1264 | 'other' => array( | |
1265 | 'function' => $this->functionname | |
1266 | ) | |
1267 | ); | |
1268 | $event = \core\event\webservice_function_called::create($params); | |
1269 | $event->set_legacy_logdata(array(SITEID, 'webservice', $this->functionname, '' , getremoteaddr() , 0, $this->userid)); | |
1270 | $event->trigger(); | |
f7631e73 | 1271 | |
bb14a488 JL |
1272 | // Do additional setup stuff. |
1273 | $settings = external_settings::get_instance(); | |
1274 | $sessionlang = $settings->get_lang(); | |
1275 | if (!empty($sessionlang)) { | |
1276 | $SESSION->lang = $sessionlang; | |
1277 | } | |
1278 | ||
1279 | setup_lang_from_browser(); | |
1280 | ||
1281 | if (empty($CFG->lang)) { | |
1282 | if (empty($SESSION->lang)) { | |
1283 | $CFG->lang = 'en'; | |
1284 | } else { | |
1285 | $CFG->lang = $SESSION->lang; | |
1286 | } | |
1287 | } | |
1288 | ||
cc93c7da | 1289 | // finally, execute the function - any errors are catched by the default exception handler |
1290 | $this->execute(); | |
06e7fadc | 1291 | |
cc93c7da | 1292 | // send the results back in correct format |
1293 | $this->send_response(); | |
06e7fadc | 1294 | |
cc93c7da | 1295 | // session cleanup |
1296 | $this->session_cleanup(); | |
06e7fadc | 1297 | |
cc93c7da | 1298 | die; |
f7631e73 | 1299 | } |
1300 | ||
cc93c7da | 1301 | /** |
1302 | * Specialised exception handler, we can not use the standard one because | |
1303 | * it can not just print html to output. | |
1304 | * | |
1305 | * @param exception $ex | |
a0a07014 | 1306 | * $uses exit |
cc93c7da | 1307 | */ |
1308 | public function exception_handler($ex) { | |
cc93c7da | 1309 | // detect active db transactions, rollback and log as error |
3086dd59 | 1310 | abort_all_db_transactions(); |
06e7fadc | 1311 | |
cc93c7da | 1312 | // some hacks might need a cleanup hook |
1313 | $this->session_cleanup($ex); | |
06e7fadc | 1314 | |
ca6340bf | 1315 | // now let the plugin send the exception to client |
1316 | $this->send_error($ex); | |
1317 | ||
cc93c7da | 1318 | // not much else we can do now, add some logging later |
1319 | exit(1); | |
f7631e73 | 1320 | } |
1321 | ||
1322 | /** | |
cc93c7da | 1323 | * Future hook needed for emulated sessions. |
a0a07014 | 1324 | * |
cc93c7da | 1325 | * @param exception $exception null means normal termination, $exception received when WS call failed |
f7631e73 | 1326 | */ |
cc93c7da | 1327 | protected function session_cleanup($exception=null) { |
ad8b5ba2 | 1328 | if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { |
cc93c7da | 1329 | // nothing needs to be done, there is no persistent session |
1330 | } else { | |
1331 | // close emulated session if used | |
1332 | } | |
f7631e73 | 1333 | } |
1334 | ||
24350e06 | 1335 | /** |
cc93c7da | 1336 | * Fetches the function description from database, |
1337 | * verifies user is allowed to use this function and | |
1338 | * loads all paremeters and return descriptions. | |
24350e06 | 1339 | */ |
cc93c7da | 1340 | protected function load_function_info() { |
1341 | global $DB, $USER, $CFG; | |
40f024c9 | 1342 | |
cc93c7da | 1343 | if (empty($this->functionname)) { |
1344 | throw new invalid_parameter_exception('Missing function name'); | |
1345 | } | |
24350e06 | 1346 | |
cc93c7da | 1347 | // function must exist |
11c16f5f | 1348 | $function = external_api::external_function_info($this->functionname); |
cc93c7da | 1349 | |
01482a4a PS |
1350 | if ($this->restricted_serviceid) { |
1351 | $params = array('sid1'=>$this->restricted_serviceid, 'sid2'=>$this->restricted_serviceid); | |
1352 | $wscond1 = 'AND s.id = :sid1'; | |
1353 | $wscond2 = 'AND s.id = :sid2'; | |
1354 | } else { | |
1355 | $params = array(); | |
1356 | $wscond1 = ''; | |
1357 | $wscond2 = ''; | |
1358 | } | |
1359 | ||
cc93c7da | 1360 | // now let's verify access control |
b8c5309e | 1361 | |
1362 | // now make sure the function is listed in at least one service user is allowed to use | |
1363 | // allow access only if: | |
1364 | // 1/ entry in the external_services_users table if required | |
1365 | // 2/ validuntil not reached | |
1366 | // 3/ has capability if specified in service desc | |
1367 | // 4/ iprestriction | |
1368 | ||
1369 | $sql = "SELECT s.*, NULL AS iprestriction | |
1370 | FROM {external_services} s | |
1371 | JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1) | |
1372 | WHERE s.enabled = 1 $wscond1 | |
1373 | ||
1374 | UNION | |
1375 | ||
1376 | SELECT s.*, su.iprestriction | |
1377 | FROM {external_services} s | |
1378 | JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2) | |
1379 | JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid) | |
4c7f5363 | 1380 | WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2"; |
b8c5309e | 1381 | $params = array_merge($params, array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time())); |
88098133 | 1382 | |
1383 | $rs = $DB->get_recordset_sql($sql, $params); | |
1384 | // now make sure user may access at least one service | |
1385 | $remoteaddr = getremoteaddr(); | |
1386 | $allowed = false; | |
1387 | foreach ($rs as $service) { | |
1388 | if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { | |
1389 | continue; // cap required, sorry | |
cc93c7da | 1390 | } |
88098133 | 1391 | if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { |
1392 | continue; // wrong request source ip, sorry | |
cc93c7da | 1393 | } |
88098133 | 1394 | $allowed = true; |
1395 | break; // one service is enough, no need to continue | |
1396 | } | |
1397 | $rs->close(); | |
1398 | if (!$allowed) { | |
96d3b93b JM |
1399 | throw new webservice_access_exception( |
1400 | 'Access to the function '.$this->functionname.'() is not allowed. | |
ca11d390 JM |
1401 | There could be multiple reasons for this: |
1402 | 1. The service linked to the user token does not contain the function. | |
1403 | 2. The service is user-restricted and the user is not listed. | |
1404 | 3. The service is IP-restricted and the user IP is not listed. | |
1405 | 4. The service is time-restricted and the time has expired. | |
1406 | 5. The token is time-restricted and the time has expired. | |
1407 | 6. The service requires a specific capability which the user does not have. | |
1408 | 7. The function is called with username/password (no user token is sent) | |
1409 | and none of the services has the function to allow the user. | |
1410 | These settings can be found in Administration > Site administration | |
1411 | > Plugins > Web services > External services and Manage tokens.'); | |
cc93c7da | 1412 | } |
9baf6825 | 1413 | |
cc93c7da | 1414 | // we have all we need now |
1415 | $this->function = $function; | |
1416 | } | |
1417 | ||
1418 | /** | |
1419 | * Execute previously loaded function using parameters parsed from the request data. | |
cc93c7da | 1420 | */ |
1421 | protected function execute() { | |
1422 | // validate params, this also sorts the params properly, we need the correct order in the next part | |
1423 | $params = call_user_func(array($this->function->classname, 'validate_parameters'), $this->function->parameters_desc, $this->parameters); | |
b5311ce4 | 1424 | $params = array_values($params); |
1425 | ||
1426 | // Allow any Moodle plugin a chance to override this call. This is a convenient spot to | |
1427 | // make arbitrary behaviour customisations, for example to affect the mobile app behaviour. | |
1428 | // The overriding plugin could call the 'real' function first and then modify the results, | |
1429 | // or it could do a completely separate thing. | |
1430 | $callbacks = get_plugins_with_function('override_webservice_execution'); | |
1431 | foreach ($callbacks as $plugintype => $plugins) { | |
1432 | foreach ($plugins as $plugin => $callback) { | |
1433 | $result = $callback($this->function, $params); | |
1434 | if ($result !== false) { | |
1435 | // If the callback returns anything other than false, we assume it replaces the | |
1436 | // real function. | |
1437 | $this->returns = $result; | |
1438 | return; | |
1439 | } | |
1440 | } | |
1441 | } | |
9baf6825 | 1442 | |
cc93c7da | 1443 | // execute - yay! |
b5311ce4 | 1444 | $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), $params); |
9baf6825 | 1445 | } |
79c7fffc JP |
1446 | |
1447 | /** | |
1448 | * Load the virtual class needed for the web service. | |
1449 | * | |
1450 | * Initialises the virtual class that contains the web service functions that the user is allowed to use. | |
1451 | * The web service function will be available if the user: | |
1452 | * - is validly registered in the external_services_users table. | |
1453 | * - has the required capability. | |
1454 | * - meets the IP restriction requirement. | |
1455 | * This virtual class can be used by web service protocols such as SOAP, especially when generating WSDL. | |
79c7fffc JP |
1456 | */ |
1457 | protected function init_service_class() { | |
1458 | global $USER, $DB; | |
1459 | ||
1460 | // Initialise service methods and struct classes. | |
1461 | $this->servicemethods = array(); | |
1462 | $this->servicestructs = array(); | |
1463 | ||
1464 | $params = array(); | |
1465 | $wscond1 = ''; | |
1466 | $wscond2 = ''; | |
1467 | if ($this->restricted_serviceid) { | |
1468 | $params = array('sid1' => $this->restricted_serviceid, 'sid2' => $this->restricted_serviceid); | |
1469 | $wscond1 = 'AND s.id = :sid1'; | |
1470 | $wscond2 = 'AND s.id = :sid2'; | |
1471 | } | |
1472 | ||
1473 | $sql = "SELECT s.*, NULL AS iprestriction | |
1474 | FROM {external_services} s | |
1475 | JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0) | |
1476 | WHERE s.enabled = 1 $wscond1 | |
1477 | ||
1478 | UNION | |
1479 | ||
1480 | SELECT s.*, su.iprestriction | |
1481 | FROM {external_services} s | |
1482 | JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1) | |
1483 | JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid) | |
1484 | WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2"; | |
1485 | $params = array_merge($params, array('userid' => $USER->id, 'now' => time())); | |
1486 | ||
1487 | $serviceids = array(); | |
1488 | $remoteaddr = getremoteaddr(); | |
1489 | ||
1490 | // Query list of external services for the user. | |
1491 | $rs = $DB->get_recordset_sql($sql, $params); | |
1492 | ||
1493 | // Check which service ID to include. | |
1494 | foreach ($rs as $service) { | |
1495 | if (isset($serviceids[$service->id])) { | |
1496 | continue; // Service already added. | |
1497 | } | |
1498 | if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { | |
1499 | continue; // Cap required, sorry. | |
1500 | } | |
1501 | if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { | |
1502 | continue; // Wrong request source ip, sorry. | |
1503 | } | |
1504 | $serviceids[$service->id] = $service->id; | |
1505 | } | |
1506 | $rs->close(); | |
1507 | ||
1508 | // Generate the virtual class name. | |
1509 | $classname = 'webservices_virtual_class_000000'; | |
1510 | while (class_exists($classname)) { | |
1511 | $classname++; | |
1512 | } | |
1513 | $this->serviceclass = $classname; | |
1514 | ||
1515 | // Get the list of all available external functions. | |
1516 | $wsmanager = new webservice(); | |
1517 | $functions = $wsmanager->get_external_functions($serviceids); | |
1518 | ||
1519 | // Generate code for the virtual methods for this web service. | |
1520 | $methods = ''; | |
1521 | foreach ($functions as $function) { | |
1522 | $methods .= $this->get_virtual_method_code($function); | |
1523 | } | |
1524 | ||
1525 | $code = <<<EOD | |
1526 | /** | |
1527 | * Virtual class web services for user id $USER->id in context {$this->restricted_context->id}. | |
1528 | */ | |
1529 | class $classname { | |
1530 | $methods | |
1531 | } | |
1532 | EOD; | |
1533 | // Load the virtual class definition into memory. | |
1534 | eval($code); | |
1535 | } | |
1536 | ||
1537 | /** | |
1538 | * Generates a struct class. | |
1539 | * | |
79c7fffc JP |
1540 | * @param external_single_structure $structdesc The basis of the struct class to be generated. |
1541 | * @return string The class name of the generated struct class. | |
1542 | */ | |
1543 | protected function generate_simple_struct_class(external_single_structure $structdesc) { | |
1544 | global $USER; | |
1545 | ||
1546 | $propeties = array(); | |
1547 | $fields = array(); | |
1548 | foreach ($structdesc->keys as $name => $fieldsdesc) { | |
1549 | $type = $this->get_phpdoc_type($fieldsdesc); | |
1550 | $propertytype = array('type' => $type); | |
1551 | if (empty($fieldsdesc->allownull) || $fieldsdesc->allownull == NULL_ALLOWED) { | |
1552 | $propertytype['nillable'] = true; | |
1553 | } | |
1554 | $propeties[$name] = $propertytype; | |
1555 | $fields[] = ' /** @var ' . $type . ' $' . $name . '*/'; | |
1556 | $fields[] = ' public $' . $name .';'; | |
1557 | } | |
1558 | $fieldsstr = implode("\n", $fields); | |
1559 | ||
1560 | // We do this after the call to get_phpdoc_type() to avoid duplicate class creation. | |
1561 | $classname = 'webservices_struct_class_000000'; | |
1562 | while (class_exists($classname)) { | |
1563 | $classname++; | |
1564 | } | |
1565 | $code = <<<EOD | |
1566 | /** | |
1567 | * Virtual struct class for web services for user id $USER->id in context {$this->restricted_context->id}. | |
1568 | */ | |
1569 | class $classname { | |
1570 | $fieldsstr | |
9baf6825 | 1571 | } |
79c7fffc JP |
1572 | EOD; |
1573 | // Load into memory. | |
1574 | eval($code); | |
9baf6825 | 1575 | |
79c7fffc JP |
1576 | // Prepare struct info. |
1577 | $structinfo = new stdClass(); | |
1578 | $structinfo->classname = $classname; | |
1579 | $structinfo->properties = $propeties; | |
1580 | // Add the struct info the the list of service struct classes. | |
1581 | $this->servicestructs[] = $structinfo; | |
1582 | ||
1583 | return $classname; | |
1584 | } | |
9baf6825 | 1585 | |
79c7fffc JP |
1586 | /** |
1587 | * Returns a virtual method code for a web service function. | |
1588 | * | |
79c7fffc JP |
1589 | * @param stdClass $function a record from external_function |
1590 | * @return string The PHP code of the virtual method. | |
1591 | * @throws coding_exception | |
1592 | * @throws moodle_exception | |
1593 | */ | |
1594 | protected function get_virtual_method_code($function) { | |
11c16f5f | 1595 | $function = external_api::external_function_info($function); |
79c7fffc JP |
1596 | |
1597 | // Parameters and their defaults for the method signature. | |
1598 | $paramanddefaults = array(); | |
1599 | // Parameters for external lib call. | |
1600 | $params = array(); | |
1601 | $paramdesc = array(); | |
1602 | // The method's input parameters and their respective types. | |
1603 | $inputparams = array(); | |
1604 | // The method's output parameters and their respective types. | |
1605 | $outputparams = array(); | |
1606 | ||
1607 | foreach ($function->parameters_desc->keys as $name => $keydesc) { | |
1608 | $param = '$' . $name; | |
1609 | $paramanddefault = $param; | |
1610 | if ($keydesc->required == VALUE_OPTIONAL) { | |
1611 | // It does not make sense to declare a parameter VALUE_OPTIONAL. VALUE_OPTIONAL is used only for array/object key. | |
1612 | throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name); | |
1613 | } else if ($keydesc->required == VALUE_DEFAULT) { | |
1614 | // Need to generate the default, if there is any. | |
1615 | if ($keydesc instanceof external_value) { | |
1616 | if ($keydesc->default === null) { | |
1617 | $paramanddefault .= ' = null'; | |
1618 | } else { | |
1619 | switch ($keydesc->type) { | |
1620 | case PARAM_BOOL: | |
1621 | $default = (int)$keydesc->default; | |
1622 | break; | |
1623 | case PARAM_INT: | |
1624 | $default = $keydesc->default; | |
1625 | break; | |
1626 | case PARAM_FLOAT; | |
1627 | $default = $keydesc->default; | |
1628 | break; | |
1629 | default: | |
1630 | $default = "'$keydesc->default'"; | |
1631 | } | |
1632 | $paramanddefault .= " = $default"; | |
1633 | } | |
1634 | } else { | |
1635 | // Accept empty array as default. | |
1636 | if (isset($keydesc->default) && is_array($keydesc->default) && empty($keydesc->default)) { | |
1637 | $paramanddefault .= ' = array()'; | |
1638 | } else { | |
1639 | // For the moment we do not support default for other structure types. | |
1640 | throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name); | |
1641 | } | |
1642 | } | |
1643 | } | |
1644 | ||
1645 | $params[] = $param; | |
1646 | $paramanddefaults[] = $paramanddefault; | |
1647 | $type = $this->get_phpdoc_type($keydesc); | |
1648 | $inputparams[$name]['type'] = $type; | |
1649 | ||
1650 | $paramdesc[] = '* @param ' . $type . ' $' . $name . ' ' . $keydesc->desc; | |
1651 | } | |
1652 | $paramanddefaults = implode(', ', $paramanddefaults); | |
1653 | $paramdescstr = implode("\n ", $paramdesc); | |
1654 | ||
1655 | $serviceclassmethodbody = $this->service_class_method_body($function, $params); | |
1656 | ||
1657 | if (empty($function->returns_desc)) { | |
1658 | $return = '* @return void'; | |
1659 | } else { | |
1660 | $type = $this->get_phpdoc_type($function->returns_desc); | |
1661 | $outputparams['return']['type'] = $type; | |
1662 | $return = '* @return ' . $type . ' ' . $function->returns_desc->desc; | |
1663 | } | |
1664 | ||
1665 | // Now create the virtual method that calls the ext implementation. | |
1666 | $code = <<<EOD | |
1667 | /** | |
1668 | * $function->description. | |
1669 | * | |
1670 | $paramdescstr | |
1671 | $return | |
1672 | */ | |
1673 | public function $function->name($paramanddefaults) { | |
1674 | $serviceclassmethodbody | |
1675 | } | |
1676 | EOD; | |
1677 | ||
1678 | // Prepare the method information. | |
1679 | $methodinfo = new stdClass(); | |
1680 | $methodinfo->name = $function->name; | |
1681 | $methodinfo->inputparams = $inputparams; | |
1682 | $methodinfo->outputparams = $outputparams; | |
1683 | $methodinfo->description = $function->description; | |
1684 | // Add the method information into the list of service methods. | |
1685 | $this->servicemethods[] = $methodinfo; | |
1686 | ||
1687 | return $code; | |
1688 | } | |
1689 | ||
1690 | /** | |
1691 | * Get the phpdoc type for an external_description object. | |
1692 | * external_value => int, double or string | |
1693 | * external_single_structure => object|struct, on-fly generated stdClass name. | |
1694 | * external_multiple_structure => array | |
1695 | * | |
1696 | * @param mixed $keydesc The type description. | |
1697 | * @return string The PHP doc type of the external_description object. | |
1698 | */ | |
1699 | protected function get_phpdoc_type($keydesc) { | |
1700 | $type = null; | |
1701 | if ($keydesc instanceof external_value) { | |
1702 | switch ($keydesc->type) { | |
1703 | case PARAM_BOOL: // 0 or 1 only for now. | |
1704 | case PARAM_INT: | |
1705 | $type = 'int'; | |
1706 | break; | |
1707 | case PARAM_FLOAT; | |
1708 | $type = 'double'; | |
1709 | break; | |
1710 | default: | |
1711 | $type = 'string'; | |
1712 | } | |
1713 | } else if ($keydesc instanceof external_single_structure) { | |
1714 | $type = $this->generate_simple_struct_class($keydesc); | |
1715 | } else if ($keydesc instanceof external_multiple_structure) { | |
1716 | $type = 'array'; | |
1717 | } | |
1718 | ||
1719 | return $type; | |
1720 | } | |
1721 | ||
1722 | /** | |
1723 | * Generates the method body of the virtual external function. | |
1724 | * | |
79c7fffc JP |
1725 | * @param stdClass $function a record from external_function. |
1726 | * @param array $params web service function parameters. | |
1727 | * @return string body of the method for $function ie. everything within the {} of the method declaration. | |
1728 | */ | |
1729 | protected function service_class_method_body($function, $params) { | |
1730 | // Cast the param from object to array (validate_parameters except array only). | |
1731 | $castingcode = ''; | |
1732 | $paramsstr = ''; | |
1733 | if (!empty($params)) { | |
1734 | foreach ($params as $paramtocast) { | |
1735 | // Clean the parameter from any white space. | |
1736 | $paramtocast = trim($paramtocast); | |
1737 | $castingcode .= " $paramtocast = json_decode(json_encode($paramtocast), true);\n"; | |
1738 | } | |
1739 | $paramsstr = implode(', ', $params); | |
1740 | } | |
1741 | ||
1742 | $descriptionmethod = $function->methodname . '_returns()'; | |
1743 | $callforreturnvaluedesc = $function->classname . '::' . $descriptionmethod; | |
1744 | ||
1745 | $methodbody = <<<EOD | |
1746 | $castingcode | |
1747 | if ($callforreturnvaluedesc == null) { | |
1748 | $function->classname::$function->methodname($paramsstr); | |
1749 | return null; | |
1750 | } | |
1751 | return external_api::clean_returnvalue($callforreturnvaluedesc, $function->classname::$function->methodname($paramsstr)); | |
1752 | EOD; | |
1753 | return $methodbody; | |
1754 | } | |
1755 | } | |
4ed6010a PFO |
1756 | |
1757 | /** | |
1758 | * Early WS exception handler. | |
1759 | * It handles exceptions during setup and returns the Exception text in the WS format. | |
1760 | * If a raise function is found nothing is returned. Throws Exception otherwise. | |
1761 | * | |
1762 | * @param Exception $ex Raised exception. | |
1763 | * @throws Exception | |
1764 | */ | |
1765 | function early_ws_exception_handler(Exception $ex): void { | |
1766 | if (function_exists('raise_early_ws_exception')) { | |
1767 | raise_early_ws_exception($ex); | |
1768 | die; | |
1769 | } | |
1770 | ||
1771 | throw $ex; | |
1772 | } |