if (empty($CFG->enablewsdocumentation)) { echo get_string('wsdocumentationdisable', 'webservice'); die; } //check that the current user is the token user $webservice = new webservice(); $token = $webservice->get_token_by_id($tokenid); if (empty($token) or empty($token->userid) or empty($USER->id) or $token->userid != $USER->id) { throw new moodle_exception('docaccessrefused', 'webservice'); } // get the list of all functions related to the token $functions = $webservice->get_external_functions(array($token->externalserviceid)); // get all the function descriptions $functiondescs = array(); foreach ($functions as $function) { $functiondescs[$function->name] = external_function_info($function); } //get activated protocol $activatedprotocol = array(); $activatedprotocol['rest'] = webservice_protocol_is_enabled('rest'); $activatedprotocol['xmlrpc'] = webservice_protocol_is_enabled('xmlrpc'); /// Check if we are in printable mode $printableformat = false; if (isset($_REQUEST['print'])) { $printableformat = $_REQUEST['print']; } /// OUTPUT echo $OUTPUT->header(); $renderer = $PAGE->get_renderer('core', 'webservice'); echo $renderer->documentation_html($functiondescs, $printableformat, $activatedprotocol, array('id' => $tokenid)); /// trigger browser print operation
/** * Display list of function for a given service * If the service is build-in do not display remove/add operation (read-only) * @param array $functions * @param object $service * @return string the table html + add operation html */ public function admin_service_function_list($functions, $service) { global $CFG; if (!empty($functions)) { $table = new html_table(); $table->head = array(get_string('function', 'webservice'), get_string('description'), get_string('requiredcaps', 'webservice')); $table->align = array('left', 'left', 'left'); $table->size = array('15%', '40%', '40%'); $table->width = '100%'; $table->align[] = 'left'; //display remove function operation (except for build-in service) if (empty($service->component)) { $table->head[] = get_string('edit'); $table->align[] = 'center'; } foreach ($functions as $function) { $function = external_function_info($function); $requiredcaps = html_writer::tag('div', empty($function->capabilities) ? '' : $function->capabilities, array('class' => 'functiondesc')); ; $description = html_writer::tag('div', $function->description, array('class' => 'functiondesc')); //display remove function operation (except for build-in service) if (empty($service->component)) { $removeurl = new moodle_url('/' . $CFG->admin . '/webservice/service_functions.php', array('sesskey' => sesskey(), 'fid' => $function->id, 'id' => $service->id, 'action' => 'delete')); $removelink = html_writer::tag('a', get_string('removefunction', 'webservice'), array('href' => $removeurl)); $table->data[] = array($function->name, $description, $requiredcaps, $removelink); } else { $table->data[] = array($function->name, $description, $requiredcaps); } } $html = html_writer::table($table); } else { $html = get_string('nofunctions', 'webservice') . html_writer::empty_tag('br'); } //display add function operation (except for build-in service) if (empty($service->component)) { $addurl = new moodle_url('/' . $CFG->admin . '/webservice/service_functions.php', array('sesskey' => sesskey(), 'id' => $service->id, 'action' => 'add')); $html .= html_writer::tag('a', get_string('addfunctions', 'webservice'), array('href' => $addurl)); } return $html; }
function definition() { global $CFG; $mform = $this->_form; $data = $this->_customdata; $mform->addElement('header', 'addfunction', get_string('addfunctions', 'webservice')); require_once $CFG->dirroot . "/webservice/lib.php"; $webservicemanager = new webservice(); $functions = $webservicemanager->get_not_associated_external_functions($data['id']); //we add the descriptions to the functions foreach ($functions as $functionid => $functionname) { //retrieve full function information (including the description) $function = external_function_info($functionname); $functions[$functionid] = $function->name . ':' . $function->description; } $mform->addElement('searchableselector', 'fids', get_string('name'), $functions, array('multiple')); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'action'); $mform->setType('action', PARAM_ACTION); $this->add_action_buttons(true, get_string('addfunctions', 'webservice')); $this->set_data($data); }
/** * Returns a virtual method code for a web service function. * * NOTE: The implementation of this method has been mostly copied from webservice_zend_server::get_virtual_method_code(). * @param stdClass $function a record from external_function * @return string The PHP code of the virtual method. * @throws coding_exception * @throws moodle_exception */ protected function get_virtual_method_code($function) { $function = external_function_info($function); // Parameters and their defaults for the method signature. $paramanddefaults = array(); // Parameters for external lib call. $params = array(); $paramdesc = array(); // The method's input parameters and their respective types. $inputparams = array(); // The method's output parameters and their respective types. $outputparams = array(); foreach ($function->parameters_desc->keys as $name => $keydesc) { $param = '$' . $name; $paramanddefault = $param; if ($keydesc->required == VALUE_OPTIONAL) { // It does not make sense to declare a parameter VALUE_OPTIONAL. VALUE_OPTIONAL is used only for array/object key. throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name); } else { if ($keydesc->required == VALUE_DEFAULT) { // Need to generate the default, if there is any. if ($keydesc instanceof external_value) { if ($keydesc->default === null) { $paramanddefault .= ' = null'; } else { switch ($keydesc->type) { case PARAM_BOOL: $default = (int) $keydesc->default; break; case PARAM_INT: $default = $keydesc->default; break; case PARAM_FLOAT: $default = $keydesc->default; break; default: $default = "'{$keydesc->default}'"; } $paramanddefault .= " = {$default}"; } } else { // Accept empty array as default. if (isset($keydesc->default) && is_array($keydesc->default) && empty($keydesc->default)) { $paramanddefault .= ' = array()'; } else { // For the moment we do not support default for other structure types. throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name); } } } } $params[] = $param; $paramanddefaults[] = $paramanddefault; $type = $this->get_phpdoc_type($keydesc); $inputparams[$name]['type'] = $type; $paramdesc[] = '* @param ' . $type . ' $' . $name . ' ' . $keydesc->desc; } $paramanddefaults = implode(', ', $paramanddefaults); $paramdescstr = implode("\n ", $paramdesc); $serviceclassmethodbody = $this->service_class_method_body($function, $params); if (empty($function->returns_desc)) { $return = '* @return void'; } else { $type = $this->get_phpdoc_type($function->returns_desc); $outputparams['return']['type'] = $type; $return = '* @return ' . $type . ' ' . $function->returns_desc->desc; } // Now create the virtual method that calls the ext implementation. $code = <<<EOD /** * {$function->description}. * {$paramdescstr} {$return} */ public function {$function->name}({$paramanddefaults}) { {$serviceclassmethodbody} } EOD; // Prepare the method information. $methodinfo = new stdClass(); $methodinfo->name = $function->name; $methodinfo->inputparams = $inputparams; $methodinfo->outputparams = $outputparams; $methodinfo->description = $function->description; // Add the method information into the list of service methods. $this->servicemethods[] = $methodinfo; return $code; }
$amfclientatag = html_writer::tag('a', get_string('amftestclient', 'webservice'), array('href' => $amfclienturl)); $descparams->amfatag = $amfclientatag; echo get_string('testclientdescription', 'webservice', $descparams); echo $OUTPUT->box_end(); $mform->display(); echo $OUTPUT->footer(); die; } $class = $function . '_form'; $mform = new $class(null, array('authmethod' => $authmethod)); $mform->set_data(array('function' => $function, 'protocol' => $protocol)); if ($mform->is_cancelled()) { redirect('testclient.php'); } else { if ($data = $mform->get_data()) { $functioninfo = external_function_info($function); // first load lib of selected protocol require_once "{$CFG->dirroot}/webservice/{$protocol}/locallib.php"; $testclientclass = "webservice_{$protocol}_test_client"; if (!class_exists($testclientclass)) { throw new coding_exception('Missing WS test class in protocol ' . $protocol); } $testclient = new $testclientclass(); $serverurl = "{$CFG->wwwroot}/webservice/{$protocol}/"; if ($authmethod == 'simple') { $serverurl .= 'simpleserver.php'; $serverurl .= '?wsusername='******'&wspassword='******'token') { $serverurl .= 'server.php';
/** * 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 $DB, $USER, $CFG; if (empty($this->functionname)) { throw new invalid_parameter_exception('Missing function name'); } // function must exist $function = external_function_info($this->functionname); if ($this->restricted_serviceid) { $params = array('sid1' => $this->restricted_serviceid, 'sid2' => $this->restricted_serviceid); $wscond1 = 'AND s.id = :sid1'; $wscond2 = 'AND s.id = :sid2'; } else { $params = array(); $wscond1 = ''; $wscond2 = ''; } // now let's verify access control // now make sure the function is listed in at least one service user is allowed to use // allow access only if: // 1/ entry in the external_services_users table if required // 2/ validuntil not reached // 3/ has capability if specified in service desc // 4/ iprestriction $sql = "SELECT s.*, NULL AS iprestriction\n FROM {external_services} s\n JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1)\n WHERE s.enabled = 1 {$wscond1}\n\n UNION\n\n SELECT s.*, su.iprestriction\n FROM {external_services} s\n JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2)\n JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)\n WHERE s.enabled = 1 AND su.validuntil IS NULL OR su.validuntil < :now {$wscond2}"; $params = array_merge($params, array('userid' => $USER->id, 'name1' => $function->name, 'name2' => $function->name, 'now' => time())); $rs = $DB->get_recordset_sql($sql, $params); // now make sure user may access at least one service $remoteaddr = getremoteaddr(); $allowed = false; foreach ($rs as $service) { if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { continue; // cap required, sorry } if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { continue; // wrong request source ip, sorry } $allowed = true; break; // one service is enough, no need to continue } $rs->close(); if (!$allowed) { throw new webservice_access_exception('Access to external function not allowed'); } // we have all we need now $this->function = $function; }
/** * Test init_service_class(). */ public function test_init_service_class() { global $DB, $USER; $this->resetAfterTest(true); // Set current user. $this->setAdminUser(); // Add a web service. $webservice = new stdClass(); $webservice->name = 'Test web service'; $webservice->enabled = true; $webservice->restrictedusers = false; $webservice->component = 'moodle'; $webservice->timecreated = time(); $webservice->downloadfiles = true; $webservice->uploadfiles = true; $externalserviceid = $DB->insert_record('external_services', $webservice); // Add token. $externaltoken = new stdClass(); $externaltoken->token = 'testtoken'; $externaltoken->tokentype = 0; $externaltoken->userid = $USER->id; $externaltoken->externalserviceid = $externalserviceid; $externaltoken->contextid = 1; $externaltoken->creatorid = $USER->id; $externaltoken->timecreated = time(); $DB->insert_record('external_tokens', $externaltoken); // Add a function to the service. $wsmethod = new stdClass(); $wsmethod->externalserviceid = $externalserviceid; $wsmethod->functionname = 'core_course_get_contents'; $DB->insert_record('external_services_functions', $wsmethod); // Initialise the dummy web service. $dummy = new webservice_dummy(WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN); // Set the token. $dummy->set_token($externaltoken->token); // Run the web service. $dummy->run(); // Get service methods and structs. $servicemethods = $dummy->get_service_methods(); $servicestructs = $dummy->get_service_structs(); $this->assertNotEmpty($servicemethods); // The function core_course_get_contents should be only the only web service function in the moment. $this->assertEquals(1, count($servicemethods)); // The function core_course_get_contents doesn't have a struct class, so the list of service structs should be empty. $this->assertEmpty($servicestructs); // Add other functions to the service. // The function core_comment_get_comments has one struct class in its output. $wsmethod->functionname = 'core_comment_get_comments'; $DB->insert_record('external_services_functions', $wsmethod); // The function core_grades_update_grades has one struct class in its input. $wsmethod->functionname = 'core_grades_update_grades'; $DB->insert_record('external_services_functions', $wsmethod); // Run the web service again. $dummy->run(); // Get service methods and structs. $servicemethods = $dummy->get_service_methods(); $servicestructs = $dummy->get_service_structs(); $this->assertEquals(3, count($servicemethods)); $this->assertEquals(2, count($servicestructs)); // Check the contents of service methods. foreach ($servicemethods as $method) { // Get the external function info. $function = external_function_info($method->name); // Check input params. foreach ($function->parameters_desc->keys as $name => $keydesc) { $this->check_params($method->inputparams[$name]['type'], $keydesc, $servicestructs); } // Check output params. $this->check_params($method->outputparams['return']['type'], $function->returns_desc, $servicestructs); // Check description. $this->assertEquals($function->description, $method->description); } }
/** * @dataProvider all_external_info_provider */ public function test_all_external_info($f) { $desc = external_function_info($f); $this->assertNotEmpty($desc->name); $this->assertNotEmpty($desc->classname); $this->assertNotEmpty($desc->methodname); $this->assertEquals($desc->component, clean_param($desc->component, PARAM_COMPONENT)); $this->assertInstanceOf('external_function_parameters', $desc->parameters_desc); if ($desc->returns_desc != null) { $this->assertInstanceOf('external_description', $desc->returns_desc); } }
/** * Fetches the function description from database, * verifies user is allowed to use this function and * loads all paremeters and return descriptions. */ protected function load_function_info() { global $DB, $USER, $CFG; if (empty($this->functionname)) { throw new invalid_parameter_exception('Missing function name'); } // function must exist $function = external_function_info($this->functionname); if ($this->restricted_serviceid) { $params = array('sid1' => $this->restricted_serviceid, 'sid2' => $this->restricted_serviceid); $wscond1 = 'AND s.id = :sid1'; $wscond2 = 'AND s.id = :sid2'; } else { $params = array(); $wscond1 = ''; $wscond2 = ''; } // now let's verify access control // now make sure the function is listed in at least one service user is allowed to use // allow access only if: // 1/ entry in the external_services_users table if required // 2/ validuntil not reached // 3/ has capability if specified in service desc // 4/ iprestriction $sql = "SELECT s.*, NULL AS iprestriction\n FROM {external_services} s\n JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1)\n WHERE s.enabled = 1 {$wscond1}\n\n UNION\n\n SELECT s.*, su.iprestriction\n FROM {external_services} s\n JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2)\n JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)\n WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) {$wscond2}"; $params = array_merge($params, array('userid' => $USER->id, 'name1' => $function->name, 'name2' => $function->name, 'now' => time())); $rs = $DB->get_recordset_sql($sql, $params); // now make sure user may access at least one service $remoteaddr = getremoteaddr(); $allowed = false; foreach ($rs as $service) { if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { continue; // cap required, sorry } if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { continue; // wrong request source ip, sorry } $allowed = true; break; // one service is enough, no need to continue } $rs->close(); if (!$allowed) { throw new webservice_access_exception('Access to the function ' . $this->functionname . '() is not allowed. There could be multiple reasons for this: 1. The service linked to the user token does not contain the function. 2. The service is user-restricted and the user is not listed. 3. The service is IP-restricted and the user IP is not listed. 4. The service is time-restricted and the time has expired. 5. The token is time-restricted and the time has expired. 6. The service requires a specific capability which the user does not have. 7. The function is called with username/password (no user token is sent) and none of the services has the function to allow the user. These settings can be found in Administration > Site administration > Plugins > Web services > External services and Manage tokens.'); } // we have all we need now $this->function = $function; }
require_once $CFG->libdir . '/externallib.php'; require_sesskey(); $rawjson = file_get_contents('php://input'); $requests = json_decode($rawjson, true); if ($requests === null) { $lasterror = json_last_error_msg(); throw new coding_exception('Invalid json in request: ' . $lasterror); } $responses = array(); foreach ($requests as $request) { $response = array(); $methodname = clean_param($request['methodname'], PARAM_ALPHANUMEXT); $index = clean_param($request['index'], PARAM_INT); $args = $request['args']; try { $externalfunctioninfo = external_function_info($methodname); if (!$externalfunctioninfo->allowed_from_ajax) { throw new moodle_exception('servicenotavailable', 'webservice'); } // Do not allow access to write or delete webservices as a public user. if ($externalfunctioninfo->loginrequired) { if (!isloggedin()) { error_log('This external function is not available to public users. Failed to call "' . $methodname . '"'); throw new moodle_exception('servicenotavailable', 'webservice'); } } // Validate params, this also sorts the params properly, we need the correct order in the next part. $callable = array($externalfunctioninfo->classname, 'validate_parameters'); $params = call_user_func($callable, $externalfunctioninfo->parameters_desc, $args); // Execute - gulp! $callable = array($externalfunctioninfo->classname, $externalfunctioninfo->methodname);
/** * returns virtual method code * * @param stdClass $function a record from external_function * @return string PHP code */ protected function get_virtual_method_code($function) { global $CFG; $function = external_function_info($function); //arguments in function declaration line with defaults. $paramanddefaults = array(); //arguments used as parameters for external lib call. $params = array(); $params_desc = array(); foreach ($function->parameters_desc->keys as $name => $keydesc) { $param = '$' . $name; $paramanddefault = $param; //need to generate the default if there is any if ($keydesc instanceof external_value) { if ($keydesc->required == VALUE_DEFAULT) { if ($keydesc->default === null) { $paramanddefault .= '=null'; } else { switch ($keydesc->type) { case PARAM_BOOL: $paramanddefault .= '=' . $keydesc->default; break; case PARAM_INT: $paramanddefault .= '=' . $keydesc->default; break; case PARAM_FLOAT: $paramanddefault .= '=' . $keydesc->default; break; default: $paramanddefault .= '=\'' . $keydesc->default . '\''; } } } else { if ($keydesc->required == VALUE_OPTIONAL) { //it does make sens to declare a parameter VALUE_OPTIONAL //VALUE_OPTIONAL is used only for array/object key throw new moodle_exception('parametercannotbevalueoptional'); } } } else { //for the moment we do not support default for other structure types if ($keydesc->required == VALUE_DEFAULT) { //accept empty array as default if (isset($keydesc->default) and is_array($keydesc->default) and empty($keydesc->default)) { $paramanddefault .= '=array()'; } else { throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name); } } if ($keydesc->required == VALUE_OPTIONAL) { throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name); } } $params[] = $param; $paramanddefaults[] = $paramanddefault; $type = $this->get_phpdoc_type($keydesc, $function->name . '__' . $name . 'Data'); $params_desc[] = ' * @param ' . $type . ' $' . $name . ' ' . $keydesc->desc; } $params = implode(', ', $params); $paramanddefaults = implode(', ', $paramanddefaults); $params_desc = implode("\n", $params_desc); $serviceclassmethodbody = $this->service_class_method_body($function, $params); if (is_null($function->returns_desc)) { $return = ' * @return void'; } else { $type = $this->get_phpdoc_return_type($function->returns_desc, $function->name . '__returnType'); $return = ' * @return ' . $type . ' ' . $function->returns_desc->desc; } // now crate the virtual method that calls the ext implementation $code = ' /** * ' . $function->description . ' * ' . $params_desc . ' ' . $return . ' */ public function ' . $function->name . '(' . $paramanddefaults . ') { ' . $serviceclassmethodbody . ' } '; return $code; }