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