.., 'username' => ..) * @param string $institution * @return boolean true on yes */ function mahara_external_in_institution($user, $institution) { $institutions = array_keys(load_user_institutions($user->id)); $auth_instance = get_record('auth_instance', 'id', $user->authinstance); $institutions[]= $auth_instance->institution; if (!in_array($institution, $institutions)) { return false; } return true; } /** * parameter definition for output of any Atom generator * * Returns description of method result value * @return external_description */ function mahara_external_atom_returns() { return new external_single_structure( array( 'id' => new external_value(PARAM_RAW, 'Atom document Id'), 'title' => new external_value(PARAM_RAW, 'Atom document Title'), 'link' => new external_value(PARAM_RAW, 'Atom document Link'), 'email' => new external_value(PARAM_RAW, 'Atom document Author Email'), 'name' => new external_value(PARAM_RAW, 'Atom document Author Name'), 'updated' => new external_value(PARAM_RAW, 'Atom document Updated date'), 'uri' => new external_value(PARAM_RAW, 'Atom document URI'), 'entries' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_RAW, 'Atom entry Id'), 'link' => new external_value(PARAM_RAW, 'Atom entry Link'), 'email' => new external_value(PARAM_RAW, 'Atom entry Author Link'), 'name' => new external_value(PARAM_RAW, 'Atom entry Author Name'), 'updated' => new external_value(PARAM_RAW, 'Atom entry updated date'), 'published' => new external_value(PARAM_RAW, 'Atom entry published date'), 'title' => new external_value(PARAM_RAW, 'Atom entry Title'), 'summary' => new external_value(PARAM_RAW, 'Atom entry Summary', VALUE_OPTIONAL), 'content' => new external_value(PARAM_RAW, 'Atom entry Content', VALUE_OPTIONAL), ), 'Atom entry', VALUE_OPTIONAL) , 'Entries', VALUE_OPTIONAL), ) ); } /** * validate the user for webservices access * the account must use the webservice auth plugin * the account must have membership for the selected auth_instance * * @param object $dbuser * @return object $auth_instance or null if $dbuser is empty */ function webservice_validate_user($dbuser) { global $SESSION; if (!empty($dbuser)) { if ($auth_instance = get_record_sql("SELECT * FROM {auth_instance} WHERE authname = 'webservice' AND active = 1 AND institution = ( SELECT institution FROM {auth_instance} WHERE id = ? AND active = 1 )", array($dbuser->authinstance))) { // User belongs to an institution that contains the 'webservice' auth method $memberships = count_records('usr_institution', 'usr', $dbuser->id); if ($memberships == 0) { // auth instance should be a mahara one if ($auth_instance->institution == 'mahara') { return $auth_instance; } } else { $membership = get_record('usr_institution', 'usr', $dbuser->id, 'institution', $auth_instance->institution); if (!empty($membership)) { return $auth_instance; } } } } return NULL; } /** * List all potential webservice locations * (i.e. plugins, local, and the "pseudo-module" /webservice). * * @return array of web service plugin directories */ function get_ws_subsystems() { static $plugindirs = false; if (!$plugindirs) { $plugindirs = [ 'webservice', 'local' ]; $activeplugins = plugin_all_installed(); foreach ($activeplugins as $plugindata) { $plugindirs[] = "{$plugindata->plugintype}/{$plugindata->name}"; } } return $plugindirs; } /** * Generate a web services token * @param string $tokentype * @param integer $serviceorid * @param integer $userid * @param string $institution * @param integer $validuntil * @param string $iprestriction * @param string $clientname (Optional) Human-readable name of client program using this token * @param string $clientenv (Optional) Human-readable description of device/environment for client * @param string $clientguid (Optional) Unique identifier for the client program * @throws WebserviceException * @return string token */ function webservice_generate_token($tokentype, $serviceorid, $userid, $institution = 'mahara', $validuntil = 0, $iprestriction = null, $clientname = null, $clientenv = null, $clientguid = null) { global $USER; // make sure the token doesn't exist (even if it should be almost impossible with the random generation) $numtries = 0; do { $numtries ++; $generatedtoken = md5(uniqid(rand(),1)); if ($numtries > 5) { throw new WebserviceException('tokengenerationfailed'); } } while (record_exists('external_tokens', 'token', $generatedtoken)); $newtoken = new stdClass(); $newtoken->token = $generatedtoken; if (!is_object($serviceorid)) { $service = get_record('external_services', 'id', $serviceorid); } else { $service = $serviceorid; } $newtoken->externalserviceid = $service->id; $newtoken->tokentype = $tokentype; $newtoken->userid = $userid; if ($tokentype == EXTERNAL_TOKEN_EMBEDDED) { $newtoken->sid = session_id(); } $newtoken->institution = $institution; $newtoken->creatorid = $USER->get('id'); $newtoken->ctime = db_format_timestamp(time()); $newtoken->timecreated = time(); $newtoken->publickeyexpires = time(); $newtoken->wssigenc = 0; $newtoken->publickey = ''; $newtoken->validuntil = $validuntil; $newtoken->clientname = $clientname; $newtoken->clientenv = $clientenv; $newtoken->clientguid = $clientguid; $newtoken->iprestriction = $iprestriction; insert_record('external_tokens', $newtoken); return $newtoken->token; } /** * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate * with the Moodle server through web services. The token is linked to the current session for the current page request. * It is expected this will be called in the script generating the html page that is embedding the client app and that the * returned token will be somehow passed into the client app being embedded in the page. * @param string $servicename name of the web service. Service name as defined in db/services.php * @param integer $userid * @param string $institution * @param integer $validuntil * @param string $iprestriction * @return int returns token id. */ function webservice_create_service_token($servicename, $userid, $institution = 'mahara', $validuntil=0, $iprestriction='') { $service = get_record('external_services', 'name', $servicename, '*'); return webservice_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $userid, $institution, $validuntil, $iprestriction); } /** * Calculate where the webservices directory should be, for a given value of * "component". * * There are three general types of "component" value we expect to see. * 1. "webservice": Indicates a the "core" webservices, which are in htdocs/webservice * 2. "local": Indicates custom webservices under htdocs/local/webservice * 3. "{plugintype}/{pluginname}": Indicates webservices for a plugin. * * @param string $component The component to look for * @param reference $plugintype If the component represents a plugin, the plugin's type will * be returned via this variable, passed by reference. * @param reference $pluginname If the component represents a plugin, the plugin's name will * be returned via this variable, passed by reference. * @return string Relative path to the component's webservices directory. If the component * is a plugin, this path will be relative the plugin's directory. Otherwise, it'll be * relative to $CFG->docroot. * @throws WebserviceCodingException */ function webservice_component_ws_directory($component, &$plugintype, &$pluginname) { if ($component == WEBSERVICE_DIRECTORY) { $plugintype = false; $pluginname = false; return WEBSERVICE_DIRECTORY; } if ($component == 'local') { $plugintype = false; $pluginname = false; return 'local/' . WEBSERVICE_DIRECTORY; } $bits = explode('/', $component); if (count($bits) == 2) { list($plugintype, $pluginname) = $bits; return WEBSERVICE_DIRECTORY; } throw new WebserviceCodingException("Invalid component name: '{$component}'"); } /** * Returns detailed function information * @param string|object $function name of external function or record from external_function * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; * MUST_EXIST means throw exception if no record or multiple records found * @return object description or false if not found or exception thrown */ function webservice_function_info($function, $strictness=MUST_EXIST, $component = null) { $mustexist = ($strictness === MUST_EXIST); if (!is_object($function)) { if ($component) { $function = get_record('external_functions', 'name', $function, 'component', $component); } else { $function = get_record('external_functions', 'name', $function); } if (!$function) { return false; } $component = $function->component; } //first find and include the ext implementation class if (!class_exists($function->classname)) { $wsdir = webservice_component_ws_directory( $function->component, $plugintype, $pluginname ); if ($plugintype && $pluginname) { // Standard plugin; can use safe_require $foundfile = safe_require_plugin( $plugintype, $pluginname, $wsdir . '/functions/' . $function->classname . '.php', 'require_once', true ); if (!$foundfile) { if ($mustexist) { throw new WebserviceCodingException(get_string('cannotfindimplfile', 'auth.webservice')); } return false; } } else { // Not a plugin, must handle manually $filepath = get_config('docroot') . $wsdir . '/functions/' . $function->classname . '.php'; if (!file_exists($filepath)) { if ($mustexist) { throw new WebserviceCodingException(get_string('cannotfindimplfile', 'auth.webservice')); } return false; } require_once($filepath); } } $function->parameters_method = $function->methodname . '_parameters'; $function->returns_method = $function->methodname . '_returns'; // make sure the implementaion class is ok if (!method_exists($function->classname, $function->methodname)) { if ($mustexist) { throw new WebserviceCodingException(get_string('missingimplofmeth', 'auth.webservice', $function->classname . '::' . $function->methodname)); } return false; } if (!method_exists($function->classname, $function->parameters_method)) { if ($mustexist) { throw new WebserviceCodingException(get_string('missingparamdesc', 'auth.webservice')); } return false; } if (!method_exists($function->classname, $function->returns_method)) { if ($mustexist) { throw new WebserviceCodingException(get_string('missingretvaldesc', 'auth.webservice')); } return false; } // fetch the parameters description $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method)); if (!($function->parameters_desc instanceof external_function_parameters)) { if ($mustexist) { throw new WebserviceCodingException(get_string('invalidparamdesc', 'auth.webservice')); } return false; } // fetch the return values description $function->returns_desc = call_user_func(array($function->classname, $function->returns_method)); // null means void result or result is ignored if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) { if ($mustexist) { throw new WebserviceCodingException(get_string('invalidretdesc', 'auth.webservice')); } return false; } //now get the function description //TODO: use localised lang pack descriptions, it would be nice to have // easy to understand descriptions in admin UI, // on the other hand this is still a bit in a flux and we need to find some new naming // conventions for these descriptions in lang packs $function->description = null; $result = webservice_load_services_file($function->component); $functionlist = $result['functions']; if (isset($functionlist[$function->name]['description'])) { $function->description = $functionlist[$function->name]['description']; } return $function; } /** * Returns a list of all of the webservice connection definitions declared * by all of the installed plugins. */ function webservice_connection_definitions() { $connections = array(); $plugins = array(); $plugins['blocktype'] = array(); foreach (plugin_types() as $plugin) { // this has to happen first because of broken artefact/blocktype ordering $plugins[$plugin] = array(); $plugins[$plugin]['installed'] = array(); $plugins[$plugin]['notinstalled'] = array(); } foreach (array_keys($plugins) as $plugin) { if (table_exists(new XMLDBTable($plugin . '_installed'))) { if ($installed = plugins_installed($plugin, true)) { foreach ($installed as $i) { $key = $i->name; if ($plugin == 'blocktype') { $key = blocktype_single_to_namespaced($i->name, $i->artefactplugin); } if (!safe_require_plugin($plugin, $key)) { continue; } if ($i->active) { $classname = generate_class_name($plugin, $key); if (method_exists($classname, 'define_webservice_connections')) { $conns = call_static_method($classname, 'define_webservice_connections'); if (!empty($conns)) { $connections[$classname] = array('connections' => $conns, 'type' => $plugin, 'key' => $key); } } } if ($plugin == 'artefact') { safe_require('artefact', $key); if ($types = call_static_method(generate_class_name('artefact', $i->name), 'get_artefact_types')) { foreach ($types as $t) { $classname = generate_artefact_class_name($t); if (method_exists($classname, 'define_webservice_connections')) { $conns = call_static_method($classname, 'define_webservice_connections'); if (!empty($conns)) { $connections[$classname] = array('connections' => $conns, 'type' => $plugin, 'key' => $key); } } } } } } } } } return $connections; } /** * General web service library */ class webservice { /** * Get the list of all functions for given service ids * @param array $serviceids * @return array functions */ public function get_external_functions($serviceids) { global $WS_FUNCTIONS; if (!empty($serviceids)) { $where = (count($serviceids) == 1 ? ' = '.array_shift($serviceids) : ' IN (' . implode(',', $serviceids) . ')'); $sql = "SELECT f.* FROM {external_functions} f WHERE f.name IN (SELECT sf.functionname FROM {external_services_functions} sf WHERE sf.externalserviceid $where)"; $functions = get_records_sql_array($sql, array()); } else { $functions = array(); } // stash functions for intro spective RPC calls later $WS_FUNCTIONS = array(); foreach ($functions as $function) { $WS_FUNCTIONS[$function->name] = array('id' => $function->id); } return $functions; } } /** * Base class for external api methods. */ class external_api { private static $contextrestriction; /** * Set context restriction for all following subsequent function calls. * @param stdClass $contex * @return void */ public static function set_context_restriction($context) { self::$contextrestriction = $context; } /** * This method has to be called before every operation * that takes a longer time to finish! * * @param int $seconds max expected time the next operation needs * @return void */ public static function set_timeout($seconds=360) { $seconds = ($seconds < 300) ? 300 : $seconds; set_time_limit($seconds); } /** * Validates submitted function parameters, if anything is incorrect * WebserviceInvalidParameterException is thrown. * This is a simple recursive method which is intended to be called from * each implementation method of external API. * @param external_description $description description of parameters * @param mixed $params the actual parameters * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found */ public static function validate_parameters(external_description $description, $params) { // we need to turn the social profile information into a single string to pass external_value test // because we can either pass in the information as a string 'profiletype|profileurl' or as an array if (isset($params['socialprofile']) && is_array($params['socialprofile'])) { $params['socialprofile'] = (!empty($params['socialprofile']['profiletype']) ? $params['socialprofile']['profiletype'] : '') . '|' . (!empty($params['socialprofile']['profileurl']) ? $params['socialprofile']['profileurl'] : ''); } if ($description instanceof external_value) { if (is_array($params) or is_object($params)) { throw new WebserviceInvalidParameterException(get_string('errorscalartype', 'auth.webservice')); } if ($description->type == PARAM_BOOL) { // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-) if (is_bool($params) or $params === 0 or $params === 1 or $params === '0' or $params === '1') { return (bool)$params; } } return validate_param($params, $description->type, $description->allownull, get_string('errorinvalidparamsapi', 'auth.webservice')); } else if ($description instanceof external_single_structure) { if (!is_array($params)) { throw new WebserviceInvalidParameterException(get_string('erroronlyarray', 'auth.webservice')); } $result = array(); foreach ($description->keys as $key=>$subdesc) { if (!array_key_exists($key, $params)) { if ($subdesc->required == VALUE_REQUIRED) { throw new WebserviceInvalidParameterException(get_string('errormissingkey', 'auth.webservice', $key)); } if ($subdesc->required == VALUE_DEFAULT) { $result[$key] = $subdesc->default; } if ($subdesc->required == VALUE_OPTIONAL) { $result[$key] = null; } } else { try { $result[$key] = self::validate_parameters($subdesc, $params[$key]); } catch (WebserviceInvalidParameterException $e) { //it's ok to display debug info as here the information is useful for ws client/dev throw new WebserviceParameterException(get_string('invalidextparam', 'auth.webservice', "key: $key - " . $e->getMessage() . (isset($e->debuginfo) ? " (debuginfo: " . $e->debuginfo . ") " : ""))); } } unset($params[$key]); } if (!empty($params)) { //list all unexpected keys $keys = ''; $customkeys = ''; foreach ($params as $key => $value) { if (substr($key, 0, 7) === "custom_") { $customkeys .= $key . ','; } else { $keys .= $key . ','; } } if (!empty($customkeys) && !get_config('productionmode')) { log_info(get_string('errorunexpectedcustomkey', 'auth.webservice', $customkeys)); } if (!empty($keys)) { throw new WebserviceInvalidParameterException(get_string('errorunexpectedkey', 'auth.webservice', $keys)); } } return $result; } else if ($description instanceof external_multiple_structure) { if (!is_array($params)) { throw new WebserviceInvalidParameterException(get_string('erroronlyarray', 'auth.webservice')); } $result = array(); foreach ($params as $param) { $result[] = self::validate_parameters($description->content, $param); } return $result; } else { throw new WebserviceInvalidParameterException(get_string('errorinvalidparamsdesc', 'auth.webservice')); } } /** * Clean response * If a response attribute is unknown from the description, we just ignore the attribute. * If a response attribute is incorrect, WebserviceInvalidResponseException is thrown. * Note: this function is similar to validate parameters, however it is distinct because * parameters validation must be distinct from cleaning return values. * @param external_description $description description of the return values * @param mixed $response the actual response * @return mixed response with added defaults for optional items, WebserviceInvalidResponseException thrown if any problem found */ public static function clean_returnvalue(external_description $description, $response) { if ($description instanceof external_value) { if (is_array($response) or is_object($response)) { throw new WebserviceInvalidResponseException(get_string('errorscalartype', 'auth.webservice')); } if ($description->type == PARAM_BOOL) { // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-) if (is_bool($response) or $response === 0 or $response === 1 or $response === '0' or $response === '1') { return (bool)$response; } } return validate_param($response, $description->type, $description->allownull, get_string('errorinvalidresponseapi', 'auth.webservice')); } else if ($description instanceof external_single_structure) { if ($response === null) { if ($description->required == VALUE_REQUIRED) { throw new WebserviceInvalidParameterException(get_string('errormissingkey', 'auth.webservice', $description->type)); } else if ($description->required == VALUE_DEFAULT) { return $description->default; } else { return null; } } if (!is_array($response)) { throw new WebserviceInvalidResponseException(get_string('erroronlyarray', 'auth.webservice')); } $result = array(); foreach ($description->keys as $key=>$subdesc) { if (!array_key_exists($key, $response)) { if ($subdesc->required == VALUE_REQUIRED) { throw new WebserviceParameterException('errorresponsemissingkey', $key); } else if ($subdesc->required == VALUE_DEFAULT) { try { $result[$key] = self::clean_returnvalue($subdesc, $subdesc->default); } catch (Exception $e) { throw new WebserviceParameterException('invalidextresponse',$key . " (" . $e->getMessage() . ")"); } } } else { try { $result[$key] = self::clean_returnvalue($subdesc, $response[$key]); } catch (Exception $e) { //it's ok to display debug info as here the information is useful for ws client/dev throw new WebserviceParameterException('invalidextresponse', $key . " (" . $e->getMessage() . ")"); } } unset($response[$key]); } return $result; } else if ($description instanceof external_multiple_structure) { if ($response === null) { if ($description->required == VALUE_REQUIRED) { throw new WebserviceInvalidParameterException(get_string('errormissingkey', 'auth.webservice', $description->type)); } else if ($description->required == VALUE_DEFAULT) { return $description->default; } else { return null; } } if (!is_array($response)) { throw new WebserviceInvalidResponseException(get_string('erroronlyarray', 'auth.webservice')); } $result = array(); foreach ($response as $param) { $result[] = self::clean_returnvalue($description->content, $param); } return $result; } else { throw new WebserviceInvalidResponseException(get_string('errorinvalidresponsedesc', 'auth.webservice')); } } /** * Returns detailed function information * * @param string|object $function name of external function or record from external_function * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; * MUST_EXIST means throw exception if no record or multiple records found * @return stdClass description or false if not found or exception thrown */ public static function external_function_info($function, $strictness=MUST_EXIST) { if (!is_object($function)) { if (!$function = get_record('external_functions', 'name', $function)) { return false; } } // First try class autoloading. if (!class_exists($function->classname)) { if ($function->classpath == 'webservice') { $function->classpath = get_config('docroot') . $function->classpath . '/functions/' . $function->classname . '.php'; } else { $function->classpath = get_config('docroot') . $function->classpath; if (!preg_match('/\.php$/', $function->classpath)) { $function->classpath .= '/functions/' . $function->classname . '.php'; } } if (!file_exists($function->classpath)) { throw new MaharaException('Cannot find file with external function implementation'); } require_once($function->classpath); if (!class_exists($function->classname)) { throw new MaharaException('Cannot find external class'); } } $function->ajax_method = $function->methodname . '_is_allowed_from_ajax'; $function->parameters_method = $function->methodname . '_parameters'; $function->returns_method = $function->methodname . '_returns'; $function->deprecated_method = $function->methodname . '_is_deprecated'; // Make sure the implementaion class is ok. if (!method_exists($function->classname, $function->methodname)) { throw new MaharaException('Missing implementation method of ' . $function->classname . '::' . $function->methodname); } if (!method_exists($function->classname, $function->parameters_method)) { throw new MaharaException('Missing parameters description'); } if (!method_exists($function->classname, $function->returns_method)) { throw new MaharaException('Missing returned values description'); } if (method_exists($function->classname, $function->deprecated_method)) { if (call_user_func(array($function->classname, $function->deprecated_method)) === true) { $function->deprecated = true; } } $function->allowed_from_ajax = false; // Fetch the parameters description. $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method)); if (!($function->parameters_desc instanceof external_function_parameters)) { throw new MaharaException('Invalid parameters description'); } // Fetch the return values description. $function->returns_desc = call_user_func(array($function->classname, $function->returns_method)); // Null means void result or result is ignored. if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) { throw new MaharaException('Invalid return description'); } return $function; } } /** * Common ancestor of all parameter description classes */ abstract class external_description { /** @property string $description description of element */ public $desc; /** @property bool $required element value required, null not allowed */ public $required; /** @property mixed $default default value */ public $default; /** * Contructor * @param string $desc * @param bool $required * @param mixed $default */ public function __construct($desc, $required, $default) { $this->desc = $desc; $this->required = $required; $this->default = $default; } } /** * Scalar alue description class */ class external_value extends external_description { /** @property mixed $type value type PARAM_XX */ public $type; /** @property bool $allownull allow null values */ public $allownull; /** * Constructor * @param mixed $type * @param string $desc * @param bool $required * @param mixed $default * @param bool $allownull */ public function __construct($type, $desc='', $required=VALUE_REQUIRED, $default=null, $allownull=NULL_ALLOWED) { parent::__construct($desc, $required, $default); $this->type = $type; $this->allownull = $allownull; } } /** * Associative array description class */ class external_single_structure extends external_description { /** @property array $keys description of array keys key=>external_description */ public $keys; /** * Constructor * @param array $keys * @param string $desc * @param bool $required * @param array $default */ public function __construct(array $keys, $desc='', $required=VALUE_REQUIRED, $default=null) { parent::__construct($desc, $required, $default); $this->keys = $keys; } } /** * Bulk array description class. */ class external_multiple_structure extends external_description { /** @property external_description $content */ public $content; /** * Constructor * @param external_description $content * @param string $desc * @param bool $required * @param array $default */ public function __construct(external_description $content, $desc='', $required=VALUE_REQUIRED, $default=null) { parent::__construct($desc, $required, $default); $this->content = $content; } } /** * Description of top level - PHP function parameters. * @author skodak * */ class external_function_parameters extends external_single_structure { } /** * Is protocol enabled? * @param string $protocol name of WS protocol * @return bool */ function webservice_protocol_is_enabled($protocol) { if (!get_config('webservice_provider_enabled')) { return false; } return get_config('webservice_provider_'.$protocol.'_enabled'); } //=== WS classes === /** * Mandatory interface for all test client classes. * @author Petr Skoda (skodak) */ interface webservice_test_client_interface { /** * Execute test client WS request * @param string $serverurl * @param string $function * @param array $params * @return mixed */ public function simpletest($serverurl, $function, $params); } /** * Mandatory interface for all web service protocol classes * @author Petr Skoda (skodak) */ interface webservice_server_interface { /** * Process request from client. * @return void */ public function run(); } /** * Abstract web service base class. * @author Petr Skoda (skodak) */ abstract class webservice_server implements webservice_server_interface { /** @property string $wsname name of the web server plugin */ protected $wsname = null; /** @property string $username name of local user */ protected $username = null; /** @property string $password password of the local user */ protected $password = null; /** @property string $service service for wsdl look up */ protected $service = null; /** @property int $userid the local user */ protected $userid = null; /** @property integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* */ protected $authmethod; /** @property string $token authentication token*/ protected $token = null; /** @property int restrict call to one service id*/ protected $restricted_serviceid = null; /** @property string info to add to logging*/ protected $info = null; /** * Contructor * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* */ public function __construct($authmethod) { $this->authmethod = $authmethod; } /** * Authenticate user using username+password or token. * This function sets up $USER global. * It is safe to use has_capability() after this. * This method also verifies user is allowed to use this * server. * @return void */ protected function authenticate_user() { global $USER, $SESSION, $WEBSERVICE_INSTITUTION, $WEBSERVICE_OAUTH_USER, $WEBSERVICE_OAUTH_SERVERID; if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { $this->auth = 'USER'; //we check that authentication plugin is enabled //it is only required by simple authentication $plugin = get_record('auth_installed', 'name', 'webservice'); if (empty($plugin) || $plugin->active != 1) { throw new WebserviceAccessException(get_string('wsauthnotenabled', 'auth.webservice')); } if (!$this->username) { throw new WebserviceAccessException(get_string('missingusername', 'auth.webservice')); } if (!$this->password) { throw new WebserviceAccessException(get_string('missingpassword', 'auth.webservice')); } // special web service login safe_require('auth', 'webservice'); // get the user $user = get_record('usr', 'username', $this->username); if (empty($user)) { throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice')); } // user account is nolonger validly configured if (!$auth_instance = webservice_validate_user($user)) { throw new WebserviceAccessException(get_string('invalidaccount', 'auth.webservice')); } // set the global for the web service users defined institution $WEBSERVICE_INSTITUTION = $auth_instance->institution; // get the institution from the external user $ext_user = get_record('external_services_users', 'userid', $user->id); if (empty($ext_user)) { throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice')); } // determine the internal auth instance $auth_instance = get_record('auth_instance', 'institution', $ext_user->institution, 'authname', 'webservice', 'active', 1); if (empty($auth_instance)) { throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice')); } // authenticate the user $auth = new AuthWebservice($auth_instance->id); if (!$auth->authenticate_user_account($user, $this->password, 'webservice')) { // log failed login attempts throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice')); } } else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN) { $this->auth = 'TOKEN'; $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT); } else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_OAUTH_TOKEN) { //OAuth $this->auth = 'OAUTH'; // special web service login safe_require('auth', 'webservice'); // get the user - the user that authorised the token $user = $this->authenticate_by_token(EXTERNAL_TOKEN_OAUTH1); $is_site_admin = false; foreach (get_site_admins() as $site_admin) { if ($site_admin->id == $user->id) { $is_site_admin = true; break; } } if (!$is_site_admin) { // check user is member of configured OAuth institution $institutions = array_keys(load_user_institutions($this->oauth_token_details['user_id'])); $auth_instance = get_record('auth_instance', 'id', $user->authinstance, 'active', 1); $institutions[]= $auth_instance->institution; if (!in_array($this->oauth_token_details['institution'], $institutions)) { throw new WebserviceAccessException(get_string('institutiondenied', 'auth.webservice')); } } // set the global for the web service users defined institution $WEBSERVICE_INSTITUTION = $this->oauth_token_details['institution']; // set the note of the OAuth service owner $WEBSERVICE_OAUTH_USER = $this->oauth_token_details['service_user']; // set the OAuth server id $WEBSERVICE_OAUTH_SERVERID = $this->oauth_token_details['id']; } else { $this->auth = 'OTHER'; $user = $this->authenticate_by_token(EXTERNAL_TOKEN_USER); } // now fake user login, the session is completely empty too $USER->reanimate($user->id, $user->authinstance); } /** * Authenticate by token type * * @param $tokentype string tokentype constant * @return $user object */ public function authenticate_by_token($tokentype) { global $WEBSERVICE_INSTITUTION; if ($tokentype == EXTERNAL_TOKEN_OAUTH1) { $user = get_record('usr', 'id', $this->oauth_token_details['user_id']); if (empty($user)) { throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice')); } return $user; } if (empty($this->token)) { // log failed login attempts throw new WebserviceAccessException(get_string('invalidtokennotsupplied', 'auth.webservice')); } $token = get_record('external_tokens', 'token', $this->token); if (!$token) { // log failed login attempts throw new WebserviceAccessException(get_string('invalidtoken', 'auth.webservice')); } // tidy up the auth method - this could be user token or session token if ($token->tokentype != EXTERNAL_TOKEN_PERMANENT) { if ($token->tokentype === EXTERNAL_TOKEN_USER) { // TODO: These should probably be constants, not strings... $this->auth = 'TOKEN_USER'; } else { $this->auth = 'OTHER'; } } /** * check the valid until date */ if ($token->validuntil and $token->validuntil < time()) { delete_records('external_tokens', 'token', $this->token, 'tokentype', $tokentype); throw new WebserviceAccessException(get_string('invalidtimedtoken', 'auth.webservice')); } //assumes that if sid is set then there must be a valid associated session no matter the token type if ($token->sid) { $session = session_get_instance(); if (!$session->session_exists($token->sid)) { delete_records('external_tokens', 'sid', $token->sid); throw new WebserviceAccessException(get_string('invalidtokensession', 'auth.webservice')); } } if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { throw new WebserviceAccessException(get_string('invalidiptoken', 'auth.webservice')); } $this->restricted_serviceid = $token->externalserviceid; $user = get_record('usr', 'id', $token->userid, 'deleted', 0); // log token access set_field('external_tokens', 'mtime', db_format_timestamp(time()), 'id', $token->id); // set the global for the web service users defined institution $WEBSERVICE_INSTITUTION = $token->institution; return $user; } /** * Intercept some maharawssettingXXX $_GET and $_POST parameter * that are related to the web service call and are not the function parameters */ protected function set_web_service_call_settings() { global $CFG; // Default web service settings. // Must be the same XXX key name as the external_settings::set_XXX function. // Must be the same XXX ws parameter name as 'maharawssettingXXX'. $externalsettings = array( 'raw' => false, 'fileurl' => true, 'filter' => false); // Load the external settings with the web service settings. $settings = external_settings::get_instance(); foreach ($externalsettings as $name => $default) { $wsparamname = 'maharawssetting' . $name; // Retrieve and remove the setting parameter from the request. $value = param_variable($wsparamname, $default); unset($_GET[$wsparamname]); unset($_POST[$wsparamname]); $functioname = 'set_' . $name; $settings->$functioname($value); } } /** * Gets information about services the authenticated user is allowed * to access. * @param string $serviceid (Optional) Only look at this service * @param string $functionname (Optional) Services must contain this function * @throws WebserviceInvalidParameterException */ protected function get_allowed_services($serviceid = false, $functionname = false) { global $USER; if ($functionname) { $fncond1 = 'AND sf.functionname = ?'; $fncond2 = 'AND sf.functionname = ?'; } else { $fncond1 = ''; $fncond2 = ''; } if ($serviceid) { $wscond1 = 'AND s.id = ? '; $wscond2 = 'AND s.id = ? '; } else { $wscond1 = ''; $wscond2 = ''; } if ($this->auth === 'TOKEN_USER') { $tokencond = 'AND s.tokenusers = 1'; } else { $tokencond = ''; } // now let's verify access control // Allow access only if: // - restrictedusers = 0 // - OR // - restrictedusers = 1 // - AND user is on the list for the service // - AND user's listing hasn't expired // - AND user's IP matches any restrictions for their listing $sql = " SELECT s.*, NULL AS iprestriction FROM {external_services} s INNER JOIN {external_services_functions} sf ON sf.externalserviceid = s.id AND s.restrictedusers = 0 $fncond1 WHERE s.enabled = 1 $tokencond $wscond1 UNION SELECT s.*, su.iprestriction FROM {external_services} s INNER JOIN {external_services_functions} sf ON sf.externalserviceid = s.id AND s.restrictedusers = 1 $fncond2 INNER JOIN {external_services_users} su ON su.externalserviceid = s.id AND su.userid = ? WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < ?) $tokencond $wscond2 "; $params = array(); $fncond1 && $params[] = $functionname; $wscond1 && $params[]= $serviceid; $fncond2 && $params[]= $functionname; $params[]= $USER->get('id'); $params[]= time(); $wscond2 && $params[]= $serviceid; $rs = get_records_sql_array($sql, $params); $remoteaddr = getremoteaddr(); $serviceids = array(); foreach ($rs as $service) { if ($service->iprestriction && !address_in_subnet($remoteaddr, $service->iprestriction)) { // wrong request source ip, sorry continue; } $serviceids[] = $service->id; } return $serviceids; } } /** * Web Service server base class, this class handles both * simple and token authentication. * @author Petr Skoda (skodak) */ abstract class webservice_base_server extends webservice_server { /** @property array $parameters the function parameters - the real values submitted in the request */ protected $parameters = null; /** @property string $functionname the name of the function that is executed */ protected $functionname = null; /** @property object $function full function description */ protected $function = null; /** @property mixed $returns function return value */ protected $returns = null; /** * This method parses the request input, it needs to get: * 1/ user authentication - username+password or token * 2/ function name * 3/ function parameters * * @return void */ abstract protected function parse_request(); /** * Send the result of function call to the WS client. * @return void */ abstract protected function send_response(); /** * Send the error information to the WS client. * @param exception $ex * @return void */ abstract protected function send_error($ex=null); /** * Process request from client. * @return void */ public function run() { global $WEBSERVICE_FUNCTION_RUN, $USER, $WEBSERVICE_INSTITUTION, $WEBSERVICE_START; $WEBSERVICE_START = microtime(true); // we will probably need a lot of memory in some functions raise_memory_limit('128M'); // set some longer timeout, this script is not sending any output, // this means we need to manually extend the timeout operations // that need longer time to finish external_api::set_timeout(); // set up exception handler first, we want to sent them back in correct format that // the other system understands // we do not need to call the original default handler because this ws handler does everything set_exception_handler(array($this, 'exception_handler')); // init all properties from the request data $this->parse_request(); // authenticate user, this has to be done after the request parsing // this also sets up $USER and $SESSION $this->authenticate_user(); // find all needed function info and make sure user may actually execute the function $this->load_function_info(); // finally, execute the function - any errors are catched by the default exception handler $this->execute(); $time_end = microtime(true); $time_taken = $time_end - $WEBSERVICE_START; //log the web service request $log = (object) array('timelogged' => time(), 'userid' => $USER->get('id'), 'externalserviceid' => $this->restricted_serviceid, 'institution' => $WEBSERVICE_INSTITUTION, 'protocol' => 'REST', 'auth' => $this->auth, 'functionname' => $this->functionname, 'timetaken' => "" . $time_taken, 'uri' => $_SERVER['REQUEST_URI'], 'info' => '', 'ip' => getremoteaddr()); insert_record('external_services_logs', $log, 'id', true); // send the results back in correct format $this->send_response(); // session cleanup $this->session_cleanup(); die; } /** * Specialised exception handler, we can not use the standard one because * it can not just print html to output. * * @param exception $ex * @return void does not return */ public function exception_handler($ex) { global $WEBSERVICE_FUNCTION_RUN, $USER, $WEBSERVICE_INSTITUTION, $WEBSERVICE_START; // detect active db transactions, rollback and log as error db_rollback(); $time_end = microtime(true); $time_taken = $time_end - $WEBSERVICE_START; //log the error on the web service request $log = (object) array('timelogged' => time(), 'userid' => $USER->get('id'), 'externalserviceid' => $this->restricted_serviceid, 'institution' => $WEBSERVICE_INSTITUTION, 'protocol' => 'REST', 'auth' => $this->token, 'functionname' => ($WEBSERVICE_FUNCTION_RUN ? $WEBSERVICE_FUNCTION_RUN : $this->functionname), 'timetaken' => '' . $time_taken, 'uri' => $_SERVER['REQUEST_URI'], 'info' => 'exception: ' . get_class($ex) . ' message: ' . $ex->getMessage() . ' debuginfo: ' . (isset($ex->debuginfo) ? $ex->debuginfo : ''), 'ip' => getremoteaddr()); insert_record('external_services_logs', $log, 'id', true); // some hacks might need a cleanup hook $this->session_cleanup($ex); // now let the plugin send the exception to client $this->send_error($ex); // not much else we can do now, add some logging later exit(1); } /** * Future hook needed for emulated sessions. * @param exception $exception null means normal termination, $exception received when WS call failed * @return void */ protected function session_cleanup($exception=null) { if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { // nothing needs to be done, there is no persistent session } else { // close emulated session if used } } /** * Fetches the function description from database, * verifies user is allowed to use this function and * loads all paremeters and return descriptions. * @return void */ protected function load_function_info() { global $USER; if (empty($this->functionname)) { throw new WebserviceInvalidParameterException(get_string('missingfuncname', 'auth.webservice')); } // function must exist $function = webservice_function_info($this->functionname); if (!$function) { throw new WebserviceInvalidParameterException(get_string('accessextfunctionnotconf', 'auth.webservice')); } // Check that the function is in a service this user is allowed // to access. $serviceids = $this->get_allowed_services($this->restricted_serviceid, $this->functionname); if (!count($serviceids)) { throw new WebserviceAccessException(get_string('accesstofunctionnotallowed', 'auth.webservice', $this->functionname)); } // now get the list of all functions - this triggers the stashing of // functions in the context $wsmanager = new webservice(); $functions = $wsmanager->get_external_functions($serviceids); // we have all we need now $this->function = $function; } /** * Execute previously loaded function using parameters parsed from the request data. * @return void */ protected function execute() { // validate params, this also sorts the params properly, we need the correct order in the next part ksort($this->parameters); $params = call_user_func(array($this->function->classname, 'validate_parameters'), $this->function->parameters_desc, $this->parameters); // execute - yay! log_debug('executing: ' . $this->function->classname . "/" . $this->function->methodname); $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), array_values($params)); } } /** * Delete all service and external functions information defined in the specified component. * @param string $component name of component (mahara, local, etc.) * @return void */ function external_delete_descriptions($component) { $params = array($component); delete_records_select('external_services_users', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params); delete_records_select('external_tokens', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params); delete_records_select('oauth_server_token', "osr_id_ref IN (SELECT id FROM {oauth_server_registry} WHERE externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?))", $params); delete_records_select('oauth_server_config', "oauthserverregistryid IN (SELECT id FROM {oauth_server_registry} WHERE externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?))", $params); delete_records_select('oauth_server_registry', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params); delete_records_select( 'external_services_functions', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)" . " OR functionname IN (SELECT name FROM {external_functions} WHERE component = ?)", array($component, $component) ); delete_records('external_services', 'component', $component); delete_records('external_functions', 'component', $component); } /** * The web services cron callback * clean out the old records that are N seconds old */ function webservice_clean_webservice_logs() { $LOG_AGE = 8 * 24 * 60 * 60; // 8 days delete_records_select('external_services_logs', 'timelogged < ?', array(time() - $LOG_AGE)); } /** * Reload the webservice descriptions for all plugins * * @return bool true = success */ function external_reload_webservices() { // first - prune all components that are nolonger available/installed $dead_components = get_records_sql_array( 'SELECT DISTINCT component AS component FROM {external_functions} WHERE component != \'\' AND component NOT IN ('. implode(', ', array_fill(1, count(get_ws_subsystems()), '?')) .')', get_ws_subsystems() ); if ($dead_components) { foreach ($dead_components as $component) { external_delete_descriptions($component->component); } } foreach (get_ws_subsystems() as $component) { external_reload_component($component); } return true; } /** * Utility function to load up the $services and $functions arrays * from the services.php file for the specified component. (Calling it * from its own function in order to avoid polluting the namespace.) * * @param string $component * @return array [$services, $functions] */ function webservice_load_services_file($component) { $wsdir = webservice_component_ws_directory( $component, $plugintype, $pluginname ); if ($plugintype && $pluginname) { // Standard plugin; can use safe_require $file = safe_require( $plugintype, $pluginname, WEBSERVICE_DIRECTORY . '/services.php', 'include', true, array('services', 'functions') ); $services = $file['services']; $functions = $file['functions']; } else { // Not a plugin, must handle manually $filepath = get_config('docroot') . $wsdir . '/services.php'; if (file_exists($filepath)) { include($filepath); } } if (empty($services)) { $services = array(); } if (empty($functions)) { $functions = array(); } return array( 'services' => $services, 'functions' => $functions ); } /** * Reload the webservice descriptions for a single plugins * * @param string $component ("webservice", "local", or a plugin e.g. * "artefact/internal". * @return bool Whether or not we found webservices for this component */ function external_reload_component($component) { // Load arrays $services and $functions from the plugin or component's // {path_to_plugin}/webservice/services.php file. $result = webservice_load_services_file($component); $services = $result['services']; $functions = $result['functions']; // Does the component have a valid services.php file? if (!$services && !$functions) { external_delete_descriptions($component); return false; } // update all function first $dbfunctions = get_records_array('external_functions', 'component', $component); if (!empty($dbfunctions)) { foreach ($dbfunctions as $dbfunction) { if (empty($functions[$dbfunction->name])) { // the functions is nolonger available for use delete_records('external_services_functions', 'functionname', $dbfunction->name); delete_records('external_functions', 'id', $dbfunction->id); continue; } $function = $functions[$dbfunction->name]; unset($functions[$dbfunction->name]); // Fill in default classpath if (empty($function['classpath'])) { if ($component === WEBSERVICE_DIRECTORY) { $function['classpath'] = WEBSERVICE_DIRECTORY; } else { $function['classpath'] = $component . '/' . WEBSERVICE_DIRECTORY; } } $update = false; if ($dbfunction->classname != $function['classname']) { $dbfunction->classname = $function['classname']; $update = true; } if ($dbfunction->methodname != $function['methodname']) { $dbfunction->methodname = $function['methodname']; $update = true; } if ($dbfunction->classpath != $function['classpath']) { $dbfunction->classpath = $function['classpath']; $update = true; } if ($update) { update_record('external_functions', $dbfunction); } } } foreach ($functions as $fname => $function) { $dbfunction = new stdClass(); $dbfunction->name = $fname; $dbfunction->classname = $function['classname']; $dbfunction->methodname = $function['methodname']; $dbfunction->classpath = empty($function['classpath']) ? null : $function['classpath']; $dbfunction->component = $component; $dbfunction->id = insert_record('external_functions', $dbfunction); } unset($functions); // now deal with services $dbservices = get_records_array('external_services', 'component', $component); if (!empty($dbservices)) { foreach ($dbservices as $dbservice) { if (empty($services[$dbservice->name])) { delete_records('external_services_functions', 'externalserviceid', $dbservice->id); delete_records('external_services_users', 'externalserviceid', $dbservice->id); delete_records('external_tokens', 'externalserviceid', $dbservice->id); delete_records_select('oauth_server_token', "osr_id_ref IN (SELECT id FROM {oauth_server_registry} WHERE externalserviceid = ?)", array($dbservice->id)); delete_records_select('oauth_server_registry', "externalserviceid = ?", array($dbservice->id)); delete_records('external_services', 'id', $dbservice->id); continue; } $service = $services[$dbservice->name]; unset($services[$dbservice->name]); $service['enabled'] = empty($service['enabled']) ? 0 : $service['enabled']; $service['restrictedusers'] = ((isset($service['restrictedusers']) && $service['restrictedusers'] == 1) ? 1 : 0); $service['tokenusers'] = ((isset($service['tokenusers']) && $service['tokenusers'] == 1) ? 1 : 0); $service['shortname'] = (isset($service['shortname']) ? $service['shortname'] : ''); $update = false; if ($dbservice->enabled != $service['enabled']) { $dbservice->enabled = $service['enabled']; $update = true; } if ($dbservice->restrictedusers != $service['restrictedusers']) { $dbservice->restrictedusers = $service['restrictedusers']; $update = true; } if ($dbservice->tokenusers != $service['tokenusers']) { $dbservice->tokenusers = $service['tokenusers']; $update = true; } if ($dbservice->shortname !== $service['shortname']) { $dbservice->shortname = $service['shortname']; $update = true; } // Optional "apiversion" field, to let webservice clients adapt gracefully to changes // in a service over time. $libApiVersion = (int)(isset($service['apiversion']) ? $service['apiversion'] : false); if ($dbservice->apiversion !== $libApiVersion) { $dbservice->apiversion = $libApiVersion; $update = true; } if ($update) { $dbservice->mtime = db_format_timestamp(time()); update_record('external_services', $dbservice); } $functions = get_records_array('external_services_functions', 'externalserviceid', $dbservice->id); if (!empty($functions)) { foreach ($functions as $function) { $key = array_search($function->functionname, $service['functions']); if ($key === false) { delete_records('external_services_functions', 'id', $function->id); } else { unset($service['functions'][$key]); } } } foreach ($service['functions'] as $fname) { $newf = new stdClass(); $newf->externalserviceid = $dbservice->id; $newf->functionname = $fname; insert_record('external_services_functions', $newf); } unset($functions); } } foreach ($services as $name => $service) { $dbservice = new stdClass(); $dbservice->name = $name; $dbservice->shortname = (isset($service['shortname']) ? $service['shortname'] : ''); $dbservice->enabled = empty($service['enabled']) ? 0 : $service['enabled']; $dbservice->restrictedusers = ((isset($service['restrictedusers']) && $service['restrictedusers'] == 1) ? 1 : 0); $dbservice->tokenusers = ((isset($service['tokenusers']) && $service['tokenusers'] == 1) ? 1 : 0); $dbservice->component = $component; $dbservice->ctime = db_format_timestamp(time()); $dbservice->mtime = $dbservice->ctime; $dbservice->id = insert_record('external_services', $dbservice, 'id', true); foreach ($service['functions'] as $fname) { $newf = new stdClass(); $newf->externalserviceid = $dbservice->id; $newf->functionname = $fname; insert_record('external_services_functions', $newf); } } return true; } /** * General System type Exception class for errors thrown inside the core * web service handling code */ class WebserviceException extends MaharaException { public $errorcode = null; /** * Constructor * @param string $errorcode The name of the string to print * @param string $debuginfo optional debugging information * @param integer $errornumber A numerical identifier for the error (optional) */ function __construct($errorcode = null, $debuginfo = '', $errornumber = null) { $this->errorcode = rtrim($errorcode, '0123456789'); if (string_exists($errorcode, 'auth.webservice')) { $count = count_string_args($errorcode, 'auth.webservice'); if ($count) { $message = get_string($errorcode, 'auth.webservice', $debuginfo); } else { $message = get_string($errorcode, 'auth.webservice'); } } else { $message = $errorcode; } if ($debuginfo && !$count) { $message .= ' : ' . $debuginfo; } // In 15.04-16.04, the third parameter to this constructor was // documented as an object. Nothing was done with this object, // so it's unlikely that changing it broke anything. But just // in case, make sure that this param, if provided, is cast // to an integer. if ($errornumber !== null) { $errornumber = (int) $errornumber; } parent::__construct($message, $errornumber); } public function get_error_name() { // Return the error lang string identifier. Trim off any integers // from the end of it, in case we've added one in to notify // translators of a change in the translated string return $this->errorcode; } } /** * Web service parameter exception class * * This exception must be thrown to the web service client when a web service parameter is invalid * The error string is gotten from webservice.php */ class WebserviceParameterException extends WebserviceException {} /** * Exception indicating programming error, must be fixed by a programer. For example * a core API might throw this type of exception if a plugin calls it incorrectly. */ class WebserviceCodingException extends WebserviceException { /** * Constructor * @param string $debuginfo optional debugging information */ function __construct($debuginfo='') { parent::__construct('codingerror', $debuginfo); } } /** * Exception indicating malformed parameter problem. * This exception is not supposed to be thrown when processing * user submitted data in forms. It is more suitable * for WS and other low level stuff. */ class WebserviceInvalidParameterException extends WebserviceException { /** * Constructor * @param string $debuginfo some detailed information */ function __construct($debuginfo='') { parent::__construct('invalidparameter', $debuginfo); } } /** * Exception indicating malformed response problem. * This exception is not supposed to be thrown when processing * user submitted data in forms. It is more suitable * for WS and other low level stuff. */ class WebserviceInvalidResponseException extends WebserviceException { /** * Constructor * @param string $debuginfo some detailed information */ function __construct($debuginfo='') { parent::__construct('invalidresponse', $debuginfo); } } /** * Exception indicating access control problem in web service call */ class WebserviceAccessException extends WebserviceException { /** * Constructor * @param string $debuginfo some detailed information */ function __construct($debuginfo='') { parent::__construct('accessexception', $debuginfo); } } /** * Process the logged-in user's REST-based request for a webservices token. * Checks whether the user has permission to self-generate a token for the * requested service group. Then it issues a new token, or retrieves an * existing one if the user already has an applicable token. * * @param string $serviceshortname Shortname of the desired service group * @param string $servicecomponent The service group's component * @param string $clientname (Optional) Human-readable name of client program using this token * @param string $clientenv (Optional) Human-readable description of device/environment for client * @param string $clientguid (Optional) Unique identifier for the client program * @throws WebserviceException * @return string The token generated */ function webservice_user_token_selfservice($serviceshortname, $servicecomponent, $clientname, $clientenv, $clientguid) { global $USER; // TODO: more granular access controls: Is this user allowed to access webservices at all? // From here, we know that the user has at least logged in, so we can // expose a little bit more information in the error responses. $service = get_record('external_services', 'shortname', $serviceshortname, 'component', $servicecomponent); if (empty($service)) { // will throw exception if no token found throw new WebserviceException( 'servicenotfound', "No service group found with name $servicecomponent/$serviceshortname", 400 ); } else if (!$service->enabled) { throw new WebserviceException( 'servicenotenabled', 'Requested service group is disabled.', 501 ); } // TODO: more granular access controls: Is this user allowed to access this particular service group? //specific checks related to user restricted service if ($service->restrictedusers) { $authoriseduser = get_record( 'external_services_users', 'externalserviceid', $service->id, 'userid', $USER->get('id') ); if (empty($authoriseduser)) { throw new WebserviceException( 'usernotauthorised', 'This service is restricted to authorized users only.', 403 ); } if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) { throw new WebserviceException( 'userauthorisationexpired', 'Your access rights to this service have expired.', 403 ); } require_once(get_config('docroot') . 'webservice/libs/net.php'); if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) { throw new WebserviceException( 'ipnotauthorised', 'This service is restricted to authorized IP ranges only.', 403 ); } } // Check if a token has already been created for this user and this service $tokensql = "SELECT t.id, t.sid, t.token, t.validuntil, t.iprestriction FROM {external_tokens} t WHERE t.userid = ? AND t.externalserviceid = ? AND t.tokentype = ?"; $tokenparams = array( $USER->get('id'), $service->id, EXTERNAL_TOKEN_USER ); // Client specified a GUID; so only re-use that same token. if ($clientname || $clientguid) { $tokensql .= ' AND clientname = ? AND clientguid = ? '; $tokenparams[] = $clientname; $tokenparams[] = $clientguid; } $tokensql .= ' ORDER BY t.ctime ASC'; $tokens = get_records_sql_array($tokensql, $tokenparams); if (!$tokens) { $tokens = array(); } //A bit of sanity checks foreach ($tokens as $key=>$token) { /// Checks related to a specific token. (script execution continue) $unsettoken = false; // Take this opportunity to delete expired tokens // (similar logic to the web service servers // /webservice/lib.php/webservice_server::authenticate_by_token()) if (!empty($token->validuntil) and $token->validuntil < time()) { delete_records('external_tokens', 'id', $token->id); $unsettoken = true; } // remove token if its ip not in whitelist if (isset($token->iprestriction) and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { $unsettoken = true; } if ($unsettoken) { unset($tokens[$key]); } } // if some valid tokens exist then use the most recent if (count($tokens) > 0) { // Retrieve an existing token $token = array_pop($tokens); // log token access set_field( 'external_tokens', 'mtime', db_format_timestamp(time()), 'id', $token->id ); $token = $token->token; } else { // Generate a new token // If you wanted to separately restrict the ability to *generate* // a token, (as opposed to just retrieving one), this would be the // place to do it. $token = webservice_generate_token( EXTERNAL_TOKEN_USER, $service, $USER->get('id'), // token user null, // institution (time() + EXTERNAL_TOKEN_USER_EXPIRES), //expiration null, // iprestriction $clientname, $clientenv, $clientguid ); } return $token; }