https://wiki.mahara.org/api.php?action=feedcontributions&user=PiersHarding&feedformat=atomMahara Wiki - User contributions [en-gb]2024-03-29T02:22:44ZUser contributionsMediaWiki 1.35.10https://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2660Plugins/Auth/WebServices2011-10-18T17:59:52Z<p>PiersHarding: </p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==== Ideas for APIs ====<br />
[https://wiki.mahara.org/index.php/Plugins/Artefact/WebServices/APIs Plugins/Artefact/WebServices/APIs]<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
[[File:Webservice-logs.png|border|Example of Web Services logs]]<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
Or even simpler if you use User Tokens - this example polls for the token owners user record:<br />
<br />
GET -UsSe -H 'Host: your.mahara.local.net' 'https://your.mahara.local.net/artefact/webservice/rest/server.php?alt=json&wsfunction=mahara_user_get_my_user&wstoken=a0ec4c23ff7bc5d9afd33da0019a4d87'<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/server.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/server.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/server.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices/APIs&diff=2605Plugins/Auth/WebServices/APIs2011-10-05T08:30:48Z<p>PiersHarding: /* artefact/webservice APIs */</p>
<hr />
<div>== artefact/webservice APIs ==<br />
<br />
=== Ideas for APIs ===<br />
<br />
A list of ideas for core APIs that could be implemented:<br />
<br />
* suggestion from dobedobedoh<br />
<br />
I guess one thing which might be helpful is the ability to copy pages<br />
both from a user to become a template, and from user to user (e.g. administrator to all members of a group)</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2604Plugins/Auth/WebServices2011-10-05T08:29:39Z<p>PiersHarding: /* Ideas for APIs */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==== Ideas for APIs ====<br />
[https://wiki.mahara.org/index.php/Plugins/Artefact/WebServices/APIs Plugins/Artefact/WebServices/APIs]<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
[[File:Webservice-logs.png|border|Example of Web Services logs]]<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
Or even simpler if you use User Tokens - this example polls for the token owners user record:<br />
<br />
GET -UsSe -H 'Host: your.mahara.local.net' 'https://your.mahara.local.net/artefact/webservice/rest/server.php?alt=json&wsfunction=mahara_user_get_my_user&wstoken=a0ec4c23ff7bc5d9afd33da0019a4d87'<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2603Plugins/Auth/WebServices2011-10-05T08:29:19Z<p>PiersHarding: /* Ideas for APIs */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==== Ideas for APIs ====<br />
[https://wiki.mahara.org/index.php/Plugins/Artefact/WebServices/APIs]<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
[[File:Webservice-logs.png|border|Example of Web Services logs]]<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
Or even simpler if you use User Tokens - this example polls for the token owners user record:<br />
<br />
GET -UsSe -H 'Host: your.mahara.local.net' 'https://your.mahara.local.net/artefact/webservice/rest/server.php?alt=json&wsfunction=mahara_user_get_my_user&wstoken=a0ec4c23ff7bc5d9afd33da0019a4d87'<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2602Plugins/Auth/WebServices2011-10-05T08:28:16Z<p>PiersHarding: /* Ideas for APIs */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==== Ideas for APIs ====<br />
[Plugins/Artefact/WebServices/APIs]<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
[[File:Webservice-logs.png|border|Example of Web Services logs]]<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
Or even simpler if you use User Tokens - this example polls for the token owners user record:<br />
<br />
GET -UsSe -H 'Host: your.mahara.local.net' 'https://your.mahara.local.net/artefact/webservice/rest/server.php?alt=json&wsfunction=mahara_user_get_my_user&wstoken=a0ec4c23ff7bc5d9afd33da0019a4d87'<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2601Plugins/Auth/WebServices2011-10-05T08:27:58Z<p>PiersHarding: /* Web Services support for Mahara */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==== Ideas for APIs ====<br />
Plugins/Artefact/WebServices/APIs<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
[[File:Webservice-logs.png|border|Example of Web Services logs]]<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
Or even simpler if you use User Tokens - this example polls for the token owners user record:<br />
<br />
GET -UsSe -H 'Host: your.mahara.local.net' 'https://your.mahara.local.net/artefact/webservice/rest/server.php?alt=json&wsfunction=mahara_user_get_my_user&wstoken=a0ec4c23ff7bc5d9afd33da0019a4d87'<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices/APIs&diff=2600Plugins/Auth/WebServices/APIs2011-10-05T08:27:06Z<p>PiersHarding: Created page with "== artefact/webservice APIs == === Ideas for APIs === * suggestion from dobedobedoh I guess one thing which might be helpful is the ability to copy pages both from a user to…"</p>
<hr />
<div>== artefact/webservice APIs ==<br />
<br />
=== Ideas for APIs ===<br />
<br />
* suggestion from dobedobedoh<br />
<br />
I guess one thing which might be helpful is the ability to copy pages<br />
both from a user to become a template, and from user to user (e.g. administrator to all members of a group)</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Developer_Area/Developer_Meetings/2011-10-05&diff=2595Developer Area/Developer Meetings/2011-10-052011-10-03T20:34:30Z<p>PiersHarding: /* Agenda for the 11th Mahara Developer Meeting */</p>
<hr />
<div>===Agenda for the 11th Mahara Developer Meeting===<br />
Chair: Andrew Nicols<br />
<br />
# Items from previous meeting<br />
#* dan_p LUNS to investigate adding a new mahara integration project to run selenium tests<br />
# Where should we put unit tests? [Francois and Andrew]<br />
# Wiki [zzmonty]:<br />
#* adding standard extensions to the Mahara Wiki (http://en.wikipedia.org/wiki/Special:Version) so that it will make adding wiki pages easier.<br />
#* the LocalSettings.php file from the Mahara Wiki needs to be updated, so that file extensions like .zip can be uploaded. Not fixing this will affect where to place user contributed themes, extensions, unit tests, etc.<br />
# Web Services - an update, and request for API ideas [pxh]<br />
# Next meeting and Chair<br />
# Any other business</div>PiersHardinghttps://wiki.mahara.org/index.php?title=File:Webservices-enable-protocols.png&diff=2582File:Webservices-enable-protocols.png2011-09-30T22:28:22Z<p>PiersHarding: uploaded a new version of "File:Webservices-enable-protocols.png"</p>
<hr />
<div>Web Services activate/deactivate protocols</div>PiersHardinghttps://wiki.mahara.org/index.php?title=File:Webservices-config.png&diff=2581File:Webservices-config.png2011-09-30T22:26:13Z<p>PiersHarding: uploaded a new version of "File:Webservices-config.png"</p>
<hr />
<div>Web Services configuration main page</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Artefact/WebServices/WebServicesConfiguration&diff=2580Plugins/Artefact/WebServices/WebServicesConfiguration2011-09-30T21:23:49Z<p>PiersHarding: /* Service Groups */</p>
<hr />
<div>=Web Services Configuration=<br />
<br />
All [[Plugins/Artefact/WebServices| Web Services]] configuration hangs off the plugin administration page http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
The main page:<br />
<br />
[[File:Webservices-config.png|border|Web Services main configuration page]]<br />
<br/><br />
<br />
From here, you can carry out all the main administrative tasks:<br />
<br />
* Globally activate/deactivate web services<br />
* activate/deactivate individual protocols (REST, SOAP, XML-RPC)<br />
* Create Services Groups of Functions that can be accessed<br />
* Generate access tokens, and allocate users, and services groups<br />
* Grant access to users for simple authentication access, and allocate service groups<br />
* look up API descriptions for functions<br />
<br />
<br />
== Global activation ==<br />
<br />
Globally activate or deactivate all web service access for when you need to shut off access completely<br />
<br />
[[File:Webservices-enable-services.png|border|Global activation/deactivation of all web services]]<br />
<br />
<br />
== Activating Protocols ==<br />
<br />
Individual protocols can be activated or deactivated. Once activated, the protocol is available to all configured functions/users/tokens.<br />
<br />
Available protocols are:<br />
* SOAP<br />
* REST (a basic HTTP and forms POST based interface)<br />
* XML-RPC<br />
* OAuth<br />
<br />
[[File:Webservices-enable-protocols.png|border|activate and deactivate inidividual protocols]]<br />
<br />
== Service Groups ==<br />
<br />
Service groups are the unit of allocation of access to a user (simple auth) or user token. They are a collection of functions.<br />
<br />
[[File:Webservices-manage-service-groups.png|border|Web Services service groups]]<br />
<br />
<br />
Give a service group a name, and then specify what functions are to be included. You must also decide what form of authentication can access this service group - web service token, or user simple auth (user and password), and User Token authentication. User Tokens are personal tokens that individual users can have access to - this enables all the widest possible Mahara user base to make use of the API, and should generally be restricted to Service Groups that give access to individual-user focused functions like mahara_user_get_my_user for the current users own details.<br />
<br />
From here, and individual service group can be deactivated for all users.<br />
<br />
[[File:Webservices-service-group.png|border|edit service group]]<br />
<br />
== Function API Descriptions ==<br />
<br />
From the Service Group editing view(above), there is a link to each API function that will take you to a description of the functions API - input/output parameters, and error handling.<br />
<br />
[[File:Webservices-group-delete-api.png|border|Web Services Group Delete API description]]<br />
<br />
=Authentication=<br />
<br />
There are currently two forms of authentication for web service access, which are available for all protocols:<br />
* Web Service Tokens<br />
* User/Password based simple authentication<br />
<br />
In both cases, the authentication data is passed as query string parameters:<br />
* Token - http://your.mahara.local.net/artefact/webservice/rest/server.php?wstoken=484dfea8715ed427b4d95796c3013f7d<br />
* simple auth - http://your.mahara.local.net/artefact/webservice/rest/simpleserver.php?wsusername=blah3&wspassword=blahblah<br />
<br />
Note: that the application changes for each - server.php vs simpleserver.php<br />
<br />
There is one exception, for SOAP, where the user and password can be passed as part of the Web Services Security Extension SOAP headers (wsse), as plaintext username and password. eg:<br />
<env:Header><br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>bean</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">blahblah</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
</env:Header><br />
<br />
<br />
<br />
== Token Based Access Control ==<br />
<br />
Each token is generated, and allocated to a user. You then choose a service group that the given user is allowed to access with this token. Only service groups that have been enabled for tokens can be selected.<br />
<br />
[[File:Webservices-access-tokens.png|border|Web services generate an access token]]<br />
<br />
== Personal User Token Based Access Control ==<br />
<br />
Individual Users can have Personal Access Tokens. These are a specific type of access token that enables access to a Service Group that has been enabled for User Token Access.<br />
These appear in the same list under the users Settings page -&gt; Application connections.<br />
<br />
The purpose of these tokens, and the Service Groups restricted to 'User Token' is to provide individuals access to API calls that are restricted to their own data. For instance - a Service Group can be created 'User Token User Query' with the functions mahara_user_get_context, and mahara_user_get_my_user which enable a user to query their institution connection context (just the internal institution name), and their own user master details.<br />
<br />
[[File:ApplicationConnections.png|border|Web services Application Connections]]<br />
<br />
== Simple User Authentication Access Control ==<br />
<br />
To configure a user for simple authentication access, choose a user, and then allocate a service group. Only service groups that are enabled for simple user authentication can be used.<br />
<br />
A selected user must have the "webservice" authentication method. This authentication method will only allow access via Web Services, restricting the user form normal login.<br />
<br />
[[File:Webservices-user-access.png|border|Web Service allocate access to a user for simple authentication]]<br />
<br />
<br />
== OAuth Authentication Access Control ==<br />
<br />
OAuth service access is configured separately. This enables the selection of an institution and service definition, for which a consumer key, and secret will be automatically generated.<br />
<br />
[[File:Oauth-service-configuration.png|border|Web Service access configuration for OAuth]]<br />
<br />
When an external application attempts to access the Mahara API via OAuth, it will be challenged requiring the user to login and accept the application linking. This creates a Mahara specific user access token. Mahara users can inspect and maintain their own access tokens:<br />
<br />
[[File:Oauth-user-tokens.png|border|User OAuth access tokens]]</div>PiersHardinghttps://wiki.mahara.org/index.php?title=File:Webservices-service-group.png&diff=2579File:Webservices-service-group.png2011-09-30T21:20:17Z<p>PiersHarding: uploaded a new version of "File:Webservices-service-group.png":&#32;Mahara Web Services - Service Group configuration</p>
<hr />
<div>Web Services - edit service group</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Artefact/WebServices/WebServicesConfiguration&diff=2578Plugins/Artefact/WebServices/WebServicesConfiguration2011-09-30T21:15:36Z<p>PiersHarding: /* Personal User Token Based Access Control */</p>
<hr />
<div>=Web Services Configuration=<br />
<br />
All [[Plugins/Artefact/WebServices| Web Services]] configuration hangs off the plugin administration page http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
The main page:<br />
<br />
[[File:Webservices-config.png|border|Web Services main configuration page]]<br />
<br/><br />
<br />
From here, you can carry out all the main administrative tasks:<br />
<br />
* Globally activate/deactivate web services<br />
* activate/deactivate individual protocols (REST, SOAP, XML-RPC)<br />
* Create Services Groups of Functions that can be accessed<br />
* Generate access tokens, and allocate users, and services groups<br />
* Grant access to users for simple authentication access, and allocate service groups<br />
* look up API descriptions for functions<br />
<br />
<br />
== Global activation ==<br />
<br />
Globally activate or deactivate all web service access for when you need to shut off access completely<br />
<br />
[[File:Webservices-enable-services.png|border|Global activation/deactivation of all web services]]<br />
<br />
<br />
== Activating Protocols ==<br />
<br />
Individual protocols can be activated or deactivated. Once activated, the protocol is available to all configured functions/users/tokens.<br />
<br />
Available protocols are:<br />
* SOAP<br />
* REST (a basic HTTP and forms POST based interface)<br />
* XML-RPC<br />
* OAuth<br />
<br />
[[File:Webservices-enable-protocols.png|border|activate and deactivate inidividual protocols]]<br />
<br />
== Service Groups ==<br />
<br />
Service groups are the unit of allocation of access to a user (simple auth) or user token. They are a collection of functions.<br />
<br />
[[File:Webservices-manage-service-groups.png|border|Web Services service groups]]<br />
<br />
<br />
Give a service group a name, and then specify what functions are to be included. You must also decide what form of authentication can access this service group - web service token, or user simple auth (user and password).<br />
<br />
From here, and individual service group can be deactivated for all users.<br />
<br />
[[File:Webservices-service-group.png|border|edit service group]]<br />
<br />
<br />
<br />
== Function API Descriptions ==<br />
<br />
From the Service Group editing view(above), there is a link to each API function that will take you to a description of the functions API - input/output parameters, and error handling.<br />
<br />
[[File:Webservices-group-delete-api.png|border|Web Services Group Delete API description]]<br />
<br />
=Authentication=<br />
<br />
There are currently two forms of authentication for web service access, which are available for all protocols:<br />
* Web Service Tokens<br />
* User/Password based simple authentication<br />
<br />
In both cases, the authentication data is passed as query string parameters:<br />
* Token - http://your.mahara.local.net/artefact/webservice/rest/server.php?wstoken=484dfea8715ed427b4d95796c3013f7d<br />
* simple auth - http://your.mahara.local.net/artefact/webservice/rest/simpleserver.php?wsusername=blah3&wspassword=blahblah<br />
<br />
Note: that the application changes for each - server.php vs simpleserver.php<br />
<br />
There is one exception, for SOAP, where the user and password can be passed as part of the Web Services Security Extension SOAP headers (wsse), as plaintext username and password. eg:<br />
<env:Header><br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>bean</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">blahblah</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
</env:Header><br />
<br />
<br />
<br />
== Token Based Access Control ==<br />
<br />
Each token is generated, and allocated to a user. You then choose a service group that the given user is allowed to access with this token. Only service groups that have been enabled for tokens can be selected.<br />
<br />
[[File:Webservices-access-tokens.png|border|Web services generate an access token]]<br />
<br />
== Personal User Token Based Access Control ==<br />
<br />
Individual Users can have Personal Access Tokens. These are a specific type of access token that enables access to a Service Group that has been enabled for User Token Access.<br />
These appear in the same list under the users Settings page -&gt; Application connections.<br />
<br />
The purpose of these tokens, and the Service Groups restricted to 'User Token' is to provide individuals access to API calls that are restricted to their own data. For instance - a Service Group can be created 'User Token User Query' with the functions mahara_user_get_context, and mahara_user_get_my_user which enable a user to query their institution connection context (just the internal institution name), and their own user master details.<br />
<br />
[[File:ApplicationConnections.png|border|Web services Application Connections]]<br />
<br />
== Simple User Authentication Access Control ==<br />
<br />
To configure a user for simple authentication access, choose a user, and then allocate a service group. Only service groups that are enabled for simple user authentication can be used.<br />
<br />
A selected user must have the "webservice" authentication method. This authentication method will only allow access via Web Services, restricting the user form normal login.<br />
<br />
[[File:Webservices-user-access.png|border|Web Service allocate access to a user for simple authentication]]<br />
<br />
<br />
== OAuth Authentication Access Control ==<br />
<br />
OAuth service access is configured separately. This enables the selection of an institution and service definition, for which a consumer key, and secret will be automatically generated.<br />
<br />
[[File:Oauth-service-configuration.png|border|Web Service access configuration for OAuth]]<br />
<br />
When an external application attempts to access the Mahara API via OAuth, it will be challenged requiring the user to login and accept the application linking. This creates a Mahara specific user access token. Mahara users can inspect and maintain their own access tokens:<br />
<br />
[[File:Oauth-user-tokens.png|border|User OAuth access tokens]]</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Artefact/WebServices/WebServicesConfiguration&diff=2577Plugins/Artefact/WebServices/WebServicesConfiguration2011-09-30T21:11:38Z<p>PiersHarding: /* Token Based Access Control */</p>
<hr />
<div>=Web Services Configuration=<br />
<br />
All [[Plugins/Artefact/WebServices| Web Services]] configuration hangs off the plugin administration page http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
The main page:<br />
<br />
[[File:Webservices-config.png|border|Web Services main configuration page]]<br />
<br/><br />
<br />
From here, you can carry out all the main administrative tasks:<br />
<br />
* Globally activate/deactivate web services<br />
* activate/deactivate individual protocols (REST, SOAP, XML-RPC)<br />
* Create Services Groups of Functions that can be accessed<br />
* Generate access tokens, and allocate users, and services groups<br />
* Grant access to users for simple authentication access, and allocate service groups<br />
* look up API descriptions for functions<br />
<br />
<br />
== Global activation ==<br />
<br />
Globally activate or deactivate all web service access for when you need to shut off access completely<br />
<br />
[[File:Webservices-enable-services.png|border|Global activation/deactivation of all web services]]<br />
<br />
<br />
== Activating Protocols ==<br />
<br />
Individual protocols can be activated or deactivated. Once activated, the protocol is available to all configured functions/users/tokens.<br />
<br />
Available protocols are:<br />
* SOAP<br />
* REST (a basic HTTP and forms POST based interface)<br />
* XML-RPC<br />
* OAuth<br />
<br />
[[File:Webservices-enable-protocols.png|border|activate and deactivate inidividual protocols]]<br />
<br />
== Service Groups ==<br />
<br />
Service groups are the unit of allocation of access to a user (simple auth) or user token. They are a collection of functions.<br />
<br />
[[File:Webservices-manage-service-groups.png|border|Web Services service groups]]<br />
<br />
<br />
Give a service group a name, and then specify what functions are to be included. You must also decide what form of authentication can access this service group - web service token, or user simple auth (user and password).<br />
<br />
From here, and individual service group can be deactivated for all users.<br />
<br />
[[File:Webservices-service-group.png|border|edit service group]]<br />
<br />
<br />
<br />
== Function API Descriptions ==<br />
<br />
From the Service Group editing view(above), there is a link to each API function that will take you to a description of the functions API - input/output parameters, and error handling.<br />
<br />
[[File:Webservices-group-delete-api.png|border|Web Services Group Delete API description]]<br />
<br />
=Authentication=<br />
<br />
There are currently two forms of authentication for web service access, which are available for all protocols:<br />
* Web Service Tokens<br />
* User/Password based simple authentication<br />
<br />
In both cases, the authentication data is passed as query string parameters:<br />
* Token - http://your.mahara.local.net/artefact/webservice/rest/server.php?wstoken=484dfea8715ed427b4d95796c3013f7d<br />
* simple auth - http://your.mahara.local.net/artefact/webservice/rest/simpleserver.php?wsusername=blah3&wspassword=blahblah<br />
<br />
Note: that the application changes for each - server.php vs simpleserver.php<br />
<br />
There is one exception, for SOAP, where the user and password can be passed as part of the Web Services Security Extension SOAP headers (wsse), as plaintext username and password. eg:<br />
<env:Header><br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>bean</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">blahblah</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
</env:Header><br />
<br />
<br />
<br />
== Token Based Access Control ==<br />
<br />
Each token is generated, and allocated to a user. You then choose a service group that the given user is allowed to access with this token. Only service groups that have been enabled for tokens can be selected.<br />
<br />
[[File:Webservices-access-tokens.png|border|Web services generate an access token]]<br />
<br />
== Personal User Token Based Access Control ==<br />
<br />
Individual Users can have Personal Access Tokens. These are a specific type of access token that enables access to a Service Group that has been enabled for User Token Access.<br />
These appear in the same list under the users Settings page -&gt; Application connections.<br />
<br />
[[File:ApplicationConnections.png|border|Web services Application Connections]]<br />
<br />
== Simple User Authentication Access Control ==<br />
<br />
To configure a user for simple authentication access, choose a user, and then allocate a service group. Only service groups that are enabled for simple user authentication can be used.<br />
<br />
A selected user must have the "webservice" authentication method. This authentication method will only allow access via Web Services, restricting the user form normal login.<br />
<br />
[[File:Webservices-user-access.png|border|Web Service allocate access to a user for simple authentication]]<br />
<br />
<br />
== OAuth Authentication Access Control ==<br />
<br />
OAuth service access is configured separately. This enables the selection of an institution and service definition, for which a consumer key, and secret will be automatically generated.<br />
<br />
[[File:Oauth-service-configuration.png|border|Web Service access configuration for OAuth]]<br />
<br />
When an external application attempts to access the Mahara API via OAuth, it will be challenged requiring the user to login and accept the application linking. This creates a Mahara specific user access token. Mahara users can inspect and maintain their own access tokens:<br />
<br />
[[File:Oauth-user-tokens.png|border|User OAuth access tokens]]</div>PiersHardinghttps://wiki.mahara.org/index.php?title=File:ApplicationConnections.png&diff=2576File:ApplicationConnections.png2011-09-30T21:11:05Z<p>PiersHarding: Mahara Web Services - Application Connections</p>
<hr />
<div>Mahara Web Services - Application Connections</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2437Plugins/Auth/WebServices2011-09-15T01:55:39Z<p>PiersHarding: /* Example Clients */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
[[File:Webservice-logs.png|border|Example of Web Services logs]]<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
Or even simpler if you use User Tokens - this example polls for the token owners user record:<br />
<br />
GET -UsSe -H 'Host: your.mahara.local.net' 'https://your.mahara.local.net/artefact/webservice/rest/server.php?alt=json&wsfunction=mahara_user_get_my_user&wstoken=a0ec4c23ff7bc5d9afd33da0019a4d87'<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2436Plugins/Auth/WebServices2011-09-15T01:54:55Z<p>PiersHarding: /* Example Clients */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
[[File:Webservice-logs.png|border|Example of Web Services logs]]<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
Or even simpler if you use User Tokens - this example pools for the token owners user record:<br />
<br />
GET -UsSe -H 'Host: your.mahara.local.net' 'https://your.mahara.local.net/artefact/webservice/rest/server.php?alt=json&wsfunction=mahara_user_get_my_user&wstoken=a0ec4c23ff7bc5d9afd33da0019a4d87'<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Artefact/WebServices/WebServicesConfiguration&diff=2428Plugins/Artefact/WebServices/WebServicesConfiguration2011-09-13T21:44:11Z<p>PiersHarding: /* Simple User Authentication Access Control */</p>
<hr />
<div>=Web Services Configuration=<br />
<br />
All [[Plugins/Artefact/WebServices| Web Services]] configuration hangs off the plugin administration page http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
The main page:<br />
<br />
[[File:Webservices-config.png|border|Web Services main configuration page]]<br />
<br/><br />
<br />
From here, you can carry out all the main administrative tasks:<br />
<br />
* Globally activate/deactivate web services<br />
* activate/deactivate individual protocols (REST, SOAP, XML-RPC)<br />
* Create Services Groups of Functions that can be accessed<br />
* Generate access tokens, and allocate users, and services groups<br />
* Grant access to users for simple authentication access, and allocate service groups<br />
* look up API descriptions for functions<br />
<br />
<br />
== Global activation ==<br />
<br />
Globally activate or deactivate all web service access for when you need to shut off access completely<br />
<br />
[[File:Webservices-enable-services.png|border|Global activation/deactivation of all web services]]<br />
<br />
<br />
== Activating Protocols ==<br />
<br />
Individual protocols can be activated or deactivated. Once activated, the protocol is available to all configured functions/users/tokens.<br />
<br />
Available protocols are:<br />
* SOAP<br />
* REST (a basic HTTP and forms POST based interface)<br />
* XML-RPC<br />
* OAuth<br />
<br />
[[File:Webservices-enable-protocols.png|border|activate and deactivate inidividual protocols]]<br />
<br />
== Service Groups ==<br />
<br />
Service groups are the unit of allocation of access to a user (simple auth) or user token. They are a collection of functions.<br />
<br />
[[File:Webservices-manage-service-groups.png|border|Web Services service groups]]<br />
<br />
<br />
Give a service group a name, and then specify what functions are to be included. You must also decide what form of authentication can access this service group - web service token, or user simple auth (user and password).<br />
<br />
From here, and individual service group can be deactivated for all users.<br />
<br />
[[File:Webservices-service-group.png|border|edit service group]]<br />
<br />
<br />
<br />
== Function API Descriptions ==<br />
<br />
From the Service Group editing view(above), there is a link to each API function that will take you to a description of the functions API - input/output parameters, and error handling.<br />
<br />
[[File:Webservices-group-delete-api.png|border|Web Services Group Delete API description]]<br />
<br />
=Authentication=<br />
<br />
There are currently two forms of authentication for web service access, which are available for all protocols:<br />
* Web Service Tokens<br />
* User/Password based simple authentication<br />
<br />
In both cases, the authentication data is passed as query string parameters:<br />
* Token - http://your.mahara.local.net/artefact/webservice/rest/server.php?wstoken=484dfea8715ed427b4d95796c3013f7d<br />
* simple auth - http://your.mahara.local.net/artefact/webservice/rest/simpleserver.php?wsusername=blah3&wspassword=blahblah<br />
<br />
Note: that the application changes for each - server.php vs simpleserver.php<br />
<br />
There is one exception, for SOAP, where the user and password can be passed as part of the Web Services Security Extension SOAP headers (wsse), as plaintext username and password. eg:<br />
<env:Header><br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>bean</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">blahblah</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
</env:Header><br />
<br />
<br />
<br />
== Token Based Access Control ==<br />
<br />
Each token is generated, and allocated to a user. You then choose a service group that the given user is allowed to access with this token. Only service groups that have been enabled for tokens can be selected.<br />
<br />
[[File:Webservices-access-tokens.png|border|Web services generate an access token]]<br />
<br />
<br />
<br />
== Simple User Authentication Access Control ==<br />
<br />
To configure a user for simple authentication access, choose a user, and then allocate a service group. Only service groups that are enabled for simple user authentication can be used.<br />
<br />
A selected user must have the "webservice" authentication method. This authentication method will only allow access via Web Services, restricting the user form normal login.<br />
<br />
[[File:Webservices-user-access.png|border|Web Service allocate access to a user for simple authentication]]<br />
<br />
<br />
== OAuth Authentication Access Control ==<br />
<br />
OAuth service access is configured separately. This enables the selection of an institution and service definition, for which a consumer key, and secret will be automatically generated.<br />
<br />
[[File:Oauth-service-configuration.png|border|Web Service access configuration for OAuth]]<br />
<br />
When an external application attempts to access the Mahara API via OAuth, it will be challenged requiring the user to login and accept the application linking. This creates a Mahara specific user access token. Mahara users can inspect and maintain their own access tokens:<br />
<br />
[[File:Oauth-user-tokens.png|border|User OAuth access tokens]]</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Artefact/WebServices/WebServicesConfiguration&diff=2427Plugins/Artefact/WebServices/WebServicesConfiguration2011-09-13T21:38:00Z<p>PiersHarding: /* Activating Protocols */</p>
<hr />
<div>=Web Services Configuration=<br />
<br />
All [[Plugins/Artefact/WebServices| Web Services]] configuration hangs off the plugin administration page http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
The main page:<br />
<br />
[[File:Webservices-config.png|border|Web Services main configuration page]]<br />
<br/><br />
<br />
From here, you can carry out all the main administrative tasks:<br />
<br />
* Globally activate/deactivate web services<br />
* activate/deactivate individual protocols (REST, SOAP, XML-RPC)<br />
* Create Services Groups of Functions that can be accessed<br />
* Generate access tokens, and allocate users, and services groups<br />
* Grant access to users for simple authentication access, and allocate service groups<br />
* look up API descriptions for functions<br />
<br />
<br />
== Global activation ==<br />
<br />
Globally activate or deactivate all web service access for when you need to shut off access completely<br />
<br />
[[File:Webservices-enable-services.png|border|Global activation/deactivation of all web services]]<br />
<br />
<br />
== Activating Protocols ==<br />
<br />
Individual protocols can be activated or deactivated. Once activated, the protocol is available to all configured functions/users/tokens.<br />
<br />
Available protocols are:<br />
* SOAP<br />
* REST (a basic HTTP and forms POST based interface)<br />
* XML-RPC<br />
* OAuth<br />
<br />
[[File:Webservices-enable-protocols.png|border|activate and deactivate inidividual protocols]]<br />
<br />
== Service Groups ==<br />
<br />
Service groups are the unit of allocation of access to a user (simple auth) or user token. They are a collection of functions.<br />
<br />
[[File:Webservices-manage-service-groups.png|border|Web Services service groups]]<br />
<br />
<br />
Give a service group a name, and then specify what functions are to be included. You must also decide what form of authentication can access this service group - web service token, or user simple auth (user and password).<br />
<br />
From here, and individual service group can be deactivated for all users.<br />
<br />
[[File:Webservices-service-group.png|border|edit service group]]<br />
<br />
<br />
<br />
== Function API Descriptions ==<br />
<br />
From the Service Group editing view(above), there is a link to each API function that will take you to a description of the functions API - input/output parameters, and error handling.<br />
<br />
[[File:Webservices-group-delete-api.png|border|Web Services Group Delete API description]]<br />
<br />
=Authentication=<br />
<br />
There are currently two forms of authentication for web service access, which are available for all protocols:<br />
* Web Service Tokens<br />
* User/Password based simple authentication<br />
<br />
In both cases, the authentication data is passed as query string parameters:<br />
* Token - http://your.mahara.local.net/artefact/webservice/rest/server.php?wstoken=484dfea8715ed427b4d95796c3013f7d<br />
* simple auth - http://your.mahara.local.net/artefact/webservice/rest/simpleserver.php?wsusername=blah3&wspassword=blahblah<br />
<br />
Note: that the application changes for each - server.php vs simpleserver.php<br />
<br />
There is one exception, for SOAP, where the user and password can be passed as part of the Web Services Security Extension SOAP headers (wsse), as plaintext username and password. eg:<br />
<env:Header><br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>bean</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">blahblah</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
</env:Header><br />
<br />
<br />
<br />
== Token Based Access Control ==<br />
<br />
Each token is generated, and allocated to a user. You then choose a service group that the given user is allowed to access with this token. Only service groups that have been enabled for tokens can be selected.<br />
<br />
[[File:Webservices-access-tokens.png|border|Web services generate an access token]]<br />
<br />
<br />
<br />
== Simple User Authentication Access Control ==<br />
<br />
To configure a user for simple authentication access, choose a user, and then allocate a service group. Only service groups that are enabled for simple user authentication can be used.<br />
<br />
A selected user must have the "webservice" authentication method. This authentication method will only allow access via Web Services, restricting the user form normal login.<br />
<br />
[[File:Webservices-user-access.png|border|Web Service allocate access to a user for simple authentication]]</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2426Plugins/Auth/WebServices2011-09-13T21:36:51Z<p>PiersHarding: </p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
[[File:Webservice-logs.png|border|Example of Web Services logs]]<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2425Plugins/Auth/WebServices2011-09-13T21:35:31Z<p>PiersHarding: </p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Logging==<br />
<br />
Logs are maintained of all web service access. The logs show what functions were accessed, by who, and when. If there was an error in the call, then this is logged too.<br />
<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=File:Webservice-logs.png&diff=2424File:Webservice-logs.png2011-09-13T21:34:04Z<p>PiersHarding: Web service logs example</p>
<hr />
<div>Web service logs example</div>PiersHardinghttps://wiki.mahara.org/index.php?title=File:Oauth-user-tokens.png&diff=2423File:Oauth-user-tokens.png2011-09-13T21:33:36Z<p>PiersHarding: OAuth User token inspection/deletion</p>
<hr />
<div>OAuth User token inspection/deletion</div>PiersHardinghttps://wiki.mahara.org/index.php?title=File:Oauth-service-configuration.png&diff=2422File:Oauth-service-configuration.png2011-09-13T21:32:51Z<p>PiersHarding: OAuth Service configuration</p>
<hr />
<div>OAuth Service configuration</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2421Plugins/Auth/WebServices2011-09-13T21:24:38Z<p>PiersHarding: /* REST with OAuth authentication */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and [http://tools.ietf.org/html/rfc5849#section-2.1 OOB] flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2420Plugins/Auth/WebServices2011-09-13T21:17:51Z<p>PiersHarding: /* REST with OAuth authentication */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and OOB flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://your.mahara.local.net/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://your.mahara.local.net/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2419Plugins/Auth/WebServices2011-09-13T21:16:32Z<p>PiersHarding: </p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON, and OAuth authentication.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
<br />
=== REST with OAuth authentication ===<br />
<br />
The OAuth authentication supports both Standard and OOB flows.<br />
<br />
OAuth applcation connections are configured under http://your.mahara.local.net/artefact/webservice/oauthv1sregister.php, and individual user tokens can be examined and removed by going to http://your.mahara.local.net/artefact/webservice/apptokens.php .<br />
<br />
Example PHP web app using [http://code.google.com/p/oauth-php/ oauth-php] :<br />
<br />
<?php<br />
include_once "../../library/OAuthStore.php";<br />
include_once "../../library/OAuthRequester.php";<br />
$options = array(<br />
'consumer_key' => '513c8d4eb56e0beae4d68e5c6249b53e04e2bd2ef',<br />
'consumer_secret' => '69d90f55d921892dcc71fa669034e0d4',<br />
'server_uri' => 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php',<br />
'request_token_uri' => 'http://mahara.local.net/maharadev/artefact/webservice/oauthv1.php/request_token',<br />
'authorize_uri' => 'http://mahara.local.net/maharadev/artefact/webservice/oauthv1.php/authorize',<br />
'access_token_uri' => 'http://mahara.local.net/maharadev/artefact/webservice/oauthv1.php/access_token',<br />
);<br />
// Note: do not use "Session" storage in production. Prefer a database<br />
// storage, such as MySQL.<br />
$store = OAuthStore::instance("Session", $options);<br />
<br />
try {<br />
// STEP 1: If we do not have an OAuth token yet, go get one<br />
if (empty($_GET["oauth_token"]))<br />
{<br />
$getAuthTokenParams = array(<br />
'xoauth_displayname' => 'Oauth test',<br />
'oauth_callback' => 'http://oauthclient.local.net/maharatest.php' // << this is us!<br />
);<br />
// get a request token<br />
$tokenResultParams = OAuthRequester::requestRequestToken($options['consumer_key'], 0, $getAuthTokenParams);<br />
// redirect to the Mahara authorization page, they will redirect back<br />
header("Location: " . $options['authorize_uri'] . "?oauth_token=" . $tokenResultParams['token']);<br />
}<br />
else {<br />
// STEP 2: Get an access token<br />
$oauthToken = $_GET["oauth_token"];<br />
$tokenResultParams = $_GET;<br />
try {<br />
OAuthRequester::requestAccessToken($options['consumer_key'], $oauthToken, 0, 'POST', $_GET);<br />
}<br />
catch (OAuthException2 $e) {<br />
// Something wrong with the oauth_token.<br />
var_dump($e);<br />
return;<br />
}<br />
// make the docs requestrequest.<br />
$secrets = $store->getSecretsForSignature('', 1);<br />
$body = '{"wsfunction":"mahara_user_get_users_by_id","users":[{"id":1}]}';<br />
$request = new OAuthRequester($options['server_uri'].'?alt=json', 'POST', array(), $body);<br />
$result = $request->doRequest(0);<br />
if ($result['code'] == 200) {<br />
var_dump($result['body']);<br />
}<br />
else {<br />
echo 'Error';<br />
}<br />
}<br />
}<br />
catch(OAuthException2 $e) {<br />
echo "OAuthException: " . $e->getMessage();<br />
}<br />
?><br />
<br />
<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2326Plugins/Auth/WebServices2011-08-18T02:34:52Z<p>PiersHarding: /* SOAP with Encryption, Signatures, and optionally WSSE */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2325Plugins/Auth/WebServices2011-08-18T02:33:17Z<p>PiersHarding: /* SOAP with Encryption, Signatures, and optionally WSSE */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2324Plugins/Auth/WebServices2011-08-18T02:32:17Z<p>PiersHarding: </p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2323Plugins/Auth/WebServices2011-08-18T02:06:40Z<p>PiersHarding: /* SOAP with WSSE */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
<br />
=== SOAP with Encryption, Signatures, and optionally WSSE ===<br />
<br />
To add in Encryption and Signatures, is a similar process to above. This essentially creates to two wrapper documents - one inside the other - encapsulating the underlying SOAP call.<br />
<br />
Note: This MUST use token based authentication to at least start the process, as the token is tied to the Web Services user account that has the partner X509 Public Key certificate for the call. It is also possible to use the WSSE extensions as above, to switch the user account that is used for the actual execution of the function call.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/server.php';<br />
$token = '1285154226b75a39fa98b3570f030734';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
$my_key = '-----BEGIN RSA PRIVATE KEY-----<br />
MIICXAIBAAKBgQCWWzSIAjlNjWI8v6IPMiRMUp8pF/1zvbIlBkytWysuVC5YIw5U<br />
...<br />
n+xcTCeSEt5tbS+mSR1V35KhviNYQQt3r+zv+h6qeOE=<br />
-----END RSA PRIVATE KEY-----';<br />
<br />
$my_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIIEfjCCA+egAwIBAgIBADANBgkqhkiG9w0BAQQFADCB4DELMAkGA1UEBhMCTlox<br />
...<br />
m9Y=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
$mahara_server_crt = '<br />
-----BEGIN CERTIFICATE-----<br />
MIID3DCCA0WgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqzELMAkGA1UEBhMCTlox<br />
ot/klADixf5lzrnDzudQhfTXKV0iuTscoinHp3NUt1w=<br />
-----END CERTIFICATE-----<br />
';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
private $key;<br />
private $crt;<br />
private $server_crt;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setMyCertificate($key, $crt, $server_crt) {<br />
$this->key = $key;<br />
$this->crt = $crt;<br />
$this->server_crt = $server_crt;<br />
}<br />
<br />
private function signMessage($message) {<br />
$digest = sha1($message);<br />
$privatekey = $this->key;<br />
$publickey = $this->crt;<br />
<br />
// If the user hasn't supplied a private key (for example, one of our older,<br />
// expired private keys, we get the current default private key and use that.<br />
if ($privatekey == null) {<br />
throw new Exception('Must have a private key');<br />
}<br />
if ($publickey == null) {<br />
throw new Exception('Must have a public key');<br />
}<br />
<br />
// The '$sig' value below is returned by reference.<br />
// We initialize it first to stop my IDE from complaining.<br />
$sig = '';<br />
$privatekey = openssl_pkey_get_private($privatekey);<br />
$bool = openssl_sign($message, $sig, $privatekey);<br />
if (!$bool) {<br />
throw new Exception('Reading private key failed');<br />
}<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<signedMessage><br />
<Signature Id="DocumentSignature" xmlns="http://www.w3.org/2000/09/xmldsig#"><br />
<SignedInfo><br />
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><br />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><br />
<Reference URI="#XMLRPC-MSG"><br />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><br />
<DigestValue>'.$digest.'</DigestValue><br />
</Reference><br />
</SignedInfo><br />
<SignatureValue>'.base64_encode($sig).'</SignatureValue><br />
<KeyInfo><br />
<X509Data>'.$publickey.'</X509Data><br />
</KeyInfo><br />
</Signature><br />
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object><br />
<timestamp>'.time().'</timestamp><br />
</signedMessage>';<br />
return $message;<br />
}<br />
<br />
private function encryptMessage($message) {<br />
// Generate a key resource from the remote_certificate text string<br />
$publickey = openssl_get_publickey($this->server_crt);<br />
<br />
if ( gettype($publickey) != 'resource' ) {<br />
// Remote certificate is faulty.<br />
throw new Exception('remote certificate is faulty');<br />
}<br />
<br />
// Initialize vars<br />
$encryptedstring = '';<br />
$symmetric_keys = array();<br />
<br />
// passed by ref -> &$encryptedstring &$symmetric_keys<br />
$bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));<br />
$message = $encryptedstring;<br />
$symmetrickey = array_pop($symmetric_keys);<br />
<br />
$message = '<?xml version="1.0" encoding="iso-8859-1"?><br />
<encryptedMessage><br />
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/><br />
<ds:KeyName>XMLENC</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($message).'</CipherValue><br />
</CipherData><br />
</EncryptedData><br />
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#"><br />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/><br />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><br />
<ds:KeyName>SSLKEY</ds:KeyName><br />
</ds:KeyInfo><br />
<CipherData><br />
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue><br />
</CipherData><br />
<ReferenceList><br />
<DataReference URI="#ED"/><br />
</ReferenceList><br />
<CarriedKeyName>XMLENC</CarriedKeyName><br />
</EncryptedKey><br />
</encryptedMessage>';<br />
return $message;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
<br />
function __doRequest($request, $location, $action, $version) {<br />
$request = $this->signMessage($request);<br />
$request = $this->encryptMessage($request);<br />
$result = parent::__doRequest($request, $location, $action, $version);<br />
// parse out the response<br />
try {<br />
$xml = new SimpleXMLElement($result);<br />
} catch (Exception $e) {<br />
throw new Exception('doRequest failed: bad XML: '.$e->getMessage());<br />
}<br />
<br />
// decrypt and verify the signature<br />
try {<br />
if ($xml->getName() == 'encryptedMessage') {<br />
$data = base64_decode($xml->EncryptedData->CipherData->CipherValue);<br />
$key = base64_decode($xml->EncryptedKey->CipherData->CipherValue);<br />
$payload = ''; // Initialize payload var<br />
$isOpen = openssl_open($data, $payload, $key, $this->key);<br />
if (!$isOpen) {<br />
throw new Exception('An error occurred while trying to decrypt your message');<br />
}<br />
$xml = new SimpleXMLElement($payload);<br />
}<br />
<br />
if ($xml->getName() == 'signedMessage') {<br />
$signature = base64_decode($xml->Signature->SignatureValue);<br />
$payload = base64_decode($xml->object);<br />
$timestamp = $xml->timestamp;<br />
<br />
// Does the signature match the data and the public cert?<br />
$signature_verified = openssl_verify($payload, $signature, $this->server_crt);<br />
if ($signature_verified == 1) {<br />
// Parse the XML<br />
try {<br />
$xml = new SimpleXMLElement($payload);<br />
} catch (Exception $e) {<br />
throw new Exception('Signed payload is not a valid XML document');<br />
}<br />
}<br />
else {<br />
throw new Exception('An error occurred while trying to verify your message signature');<br />
}<br />
}<br />
$result = $payload;<br />
}<br />
catch (CryptException $e) {<br />
throw new Exception('Invalid key');<br />
}<br />
return $result;<br />
}<br />
<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$soapClient->setMyCertificate($my_key, $my_crt, $server_crt);<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2058Plugins/Auth/WebServices2011-06-29T20:56:30Z<p>PiersHarding: /* Interactive testing */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
<br/><br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2057Plugins/Auth/WebServices2011-06-29T20:55:57Z<p>PiersHarding: /* Interactive testing */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
[[File:Webservices-testclient.png|border|Example run of Interactive Test client]]<br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=File:Webservices-testclient.png&diff=2056File:Webservices-testclient.png2011-06-29T20:54:00Z<p>PiersHarding: Web Services test client example</p>
<hr />
<div>Web Services test client example</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2055Plugins/Auth/WebServices2011-06-29T20:48:00Z<p>PiersHarding: /* Example Clients */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
<br />
===Interactive testing===<br />
<br />
An interactive test client has been developed that is linked off your [https://your.mahara.local.net/artefact/webservice/pluginconfig.php plugin config] at [https://your.mahara.local.net/artefact/webservice/testclient.php testclient]. This enables token, and username/password authentication, and REST/SOAP/XML-RPC interaction testing via a simple parameter interface. Good for debugging authorisation issues. Be careful, as this executes functions for real, so if it says delete account then it means it.<br />
<br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2015Plugins/Auth/WebServices2011-06-28T05:12:35Z<p>PiersHarding: /* Core Function Interfaces */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites - get lists of favourites for all users within the authenticated institution, selected by favourites list shortname<br />
* mahara_user_get_favourites - get lists of favourites for a specified set of users (specify by id or username) for a selected favourites list shortname (restricted by the authenticated institution)<br />
* mahara_user_update_favourites - set favourites lists for a series of users (specified by id or username) for a selected favourites list shortname<br />
* mahara_user_get_users - get details for all users for the authenticated institution<br />
* mahara_user_get_users_by_id - get details for a selection of users (specified by id or username) for the authenticated institution<br />
* mahara_user_create_users - create one or more users within the authenticated institution<br />
* mahara_user_delete_users - delete one or more users (specified by id or username) within the authenticated institution<br />
* mahara_user_update_users - update one or more users (specified by id or username) within the authenticated institution<br />
<br />
=== Group ===<br />
* mahara_group_get_groups - get all available groups (including members) within the authenticated institution<br />
* mahara_group_get_groups_by_id - get one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_create_groups - create one or more groups (and allocate members) within the authenticated institution<br />
* mahara_group_delete_groups - delete one or more groups (specified by id or shortname/institution) within the authenticated institution<br />
* mahara_group_update_groups - update one or more groups including members (specified by id or shortname/institution) within the authenticated institution<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members - add a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_remove_members - remove a list of users (specified by id or username) from a specified institution<br />
* mahara_institution_invite_members - invite a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_decline_members - decline membership to a list of users (specified by id or username) to a specified institution<br />
* mahara_institution_get_members - get the list of members for a specified institution<br />
* mahara_institution_get_requests - get the list of outstanding requests for a specified institution</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2012Plugins/Auth/WebServices2011-06-27T02:12:36Z<p>PiersHarding: /* PHP Clients */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2011Plugins/Auth/WebServices2011-06-27T02:11:57Z<p>PiersHarding: </p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients | exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ | Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2010Plugins/Auth/WebServices2011-06-27T02:11:28Z<p>PiersHarding: /* Example Clients */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
There is a whole set of [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/exampleclients | exampleclients] available as part of the download. These PHP examples are based on [http://www.zend.com/en/downloads/ | Zend data services], and require inparticular Zend_Soap_Client (which works automatically with the artefact/webservice tar ball).<br />
<br />
To run these examples, cd to artefact/webservice/exampleclients and run each of example_user_api.php, example_group_api.php, example_institution_api.php by going: <br />
php example_user_api.php --username='< your web service username>' --password='the password' <br />
<br />
A transcript of running a test is:<br />
$ php example_user_api.php --username=blah3 --password=blahblah<br />
Enter Mahara username (blah3): <br />
Enter Mahara password (blahblah): <br />
Enter Mahara servicegroup (Simple User Provisioning): <br />
Enter Mahara Mahara Web Services URL (http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php): <br />
web services url: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php<br />
service group: Simple User Provisioning<br />
username; blah3<br />
password: blahblah<br />
WSDL URL: http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php?wsservice=Simple User Provisioning&wsdl=1 <br />
Select one of functions to execute:<br />
0. mahara_user_create_users<br />
1. mahara_user_update_users<br />
2. mahara_user_delete_users<br />
3. mahara_user_update_favourites<br />
4. mahara_user_get_favourites<br />
5. mahara_user_get_users_by_id<br />
6. mahara_user_get_users<br />
Enter your choice (0..6 or x for exit):5<br />
Chosen function: mahara_user_get_users_by_id<br />
Parameters used for execution are: array (<br />
'users' => <br />
array (<br />
0 => <br />
array (<br />
'username' => 'veryimprobabletestusername1',<br />
),<br />
),<br />
)<br />
Results are: array (<br />
0 => <br />
array (<br />
'id' => 3512,<br />
'username' => 'veryimprobabletestusername1',<br />
'firstname' => 'testfirstname1',<br />
'lastname' => 'testlastname1',<br />
'email' => 'testemail1@hogwarts.school.nz',<br />
'auth' => 'internal',<br />
'studentid' => 'testidnumber1',<br />
'institution' => 'mahara',<br />
'preferredname' => 'Hello World!',<br />
'introduction' => '',<br />
'country' => '',<br />
'city' => '',<br />
'address' => '',<br />
'town' => '',<br />
'homenumber' => '',<br />
'businessnumber' => '',<br />
'mobilenumber' => '',<br />
'faxnumber' => '',<br />
'officialwebsite' => '',<br />
'personalwebsite' => '',<br />
'blogaddress' => '',<br />
'aimscreenname' => '',<br />
'icqnumber' => '',<br />
'msnnumber' => '',<br />
'yahoochat' => '',<br />
'skypeusername' => '',<br />
'jabberusername' => '',<br />
'occupation' => '',<br />
'industry' => '',<br />
),<br />
)<br />
<br />
<br />
<br />
<br />
<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2009Plugins/Auth/WebServices2011-06-26T21:13:01Z<p>PiersHarding: /* Testing */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebserviceuser.php<br />
sudo -u www-data php testwebservicegroup.php<br />
sudo -u www-data php testwebserviceinstitution.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group|institution) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
Note: It is a REALLY REALLY good idea to use a separate test instance/Mahara database to run the tests in as it repeatedly creates, and deletes data.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2008Plugins/Auth/WebServices2011-06-26T21:06:05Z<p>PiersHarding: /* Example Clients */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebservice.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D%5Bid%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2007Plugins/Auth/WebServices2011-06-26T20:46:34Z<p>PiersHarding: /* Example Clients */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebservice.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "users%5B0%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(array('id' => 1)));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'users' => array(array('id' => 1)), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=2006Plugins/Auth/WebServices2011-06-26T20:42:37Z<p>PiersHarding: /* SOAP */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebservice.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "userids%5B0%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(array('id' => 1))); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
$userids = array(0=>1);<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(0=>1));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'userids' => array(1), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=1997Plugins/Auth/WebServices2011-06-26T00:47:14Z<p>PiersHarding: /* REST with simple authentication */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebservice.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "userids%5B0%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
$userids = array(0=>1);<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(0=>1));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'userids' => array(1), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump ($response->getBody());<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
You can now POST and receive REST based function calls using JSON:<br />
<br />
...<br />
$client = new Zend_Http_Client($serverurl);<br />
$client->setHeaders('Content-Type', 'application/json');<br />
<br />
try {<br />
$client->setParameterPost(json_encode($params));<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=1978Plugins/Auth/WebServices2011-06-23T18:20:28Z<p>PiersHarding: /* Core Function Interfaces */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebservice.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "userids%5B0%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
$userids = array(0=>1);<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(0=>1));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php?alt=json';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'userids' => array(1), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
//$client->setHeaders('Accept', 'application/jsonrequest');<br />
<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, Institution and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=1977Plugins/Auth/WebServices2011-06-23T18:19:47Z<p>PiersHarding: /* Institution */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebservice.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "userids%5B0%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
$userids = array(0=>1);<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(0=>1));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php?alt=json';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'userids' => array(1), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
//$client->setHeaders('Accept', 'application/jsonrequest');<br />
<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members<br />
* mahara_institution_get_members<br />
* mahara_institution_get_requests</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=1976Plugins/Auth/WebServices2011-06-23T18:13:14Z<p>PiersHarding: /* Core Function Interfaces */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebservice.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "userids%5B0%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
$userids = array(0=>1);<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(0=>1));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php?alt=json';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'userids' => array(1), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
//$client->setHeaders('Accept', 'application/jsonrequest');<br />
<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, and Group administration functions.<br />
<br />
=== User ===<br />
* mahara_user_get_all_favourites<br />
* mahara_user_get_favourites<br />
* mahara_user_update_favourites<br />
* mahara_user_get_users<br />
* mahara_user_get_users_by_id<br />
* mahara_user_create_users<br />
* mahara_user_delete_users<br />
* mahara_user_update_users<br />
<br />
=== Group ===<br />
* mahara_group_get_groups<br />
* mahara_group_get_groups_by_id<br />
* mahara_group_create_groups<br />
* mahara_group_delete_groups<br />
* mahara_group_update_groups<br />
<br />
=== Institution ===<br />
* mahara_institution_add_members<br />
* mahara_institution_remove_members<br />
* mahara_institution_invite_members<br />
* mahara_institution_decline_members</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Artefact/WebServices/WebServicesConfiguration&diff=1968Plugins/Artefact/WebServices/WebServicesConfiguration2011-06-23T01:26:50Z<p>PiersHarding: /* Web Services Configuration */</p>
<hr />
<div>=Web Services Configuration=<br />
<br />
All [[Plugins/Artefact/WebServices| Web Services]] configuration hangs off the plugin administration page http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
The main page:<br />
<br />
[[File:Webservices-config.png|border|Web Services main configuration page]]<br />
<br/><br />
<br />
From here, you can carry out all the main administrative tasks:<br />
<br />
* Globally activate/deactivate web services<br />
* activate/deactivate individual protocols (REST, SOAP, XML-RPC)<br />
* Create Services Groups of Functions that can be accessed<br />
* Generate access tokens, and allocate users, and services groups<br />
* Grant access to users for simple authentication access, and allocate service groups<br />
* look up API descriptions for functions<br />
<br />
<br />
== Global activation ==<br />
<br />
Globally activate or deactivate all web service access for when you need to shut off access completely<br />
<br />
[[File:Webservices-enable-services.png|border|Global activation/deactivation of all web services]]<br />
<br />
<br />
== Activating Protocols ==<br />
<br />
Individual protocols can be activated or deactivated. Once activated, the protocol is available to all configured functions/users/tokens.<br />
<br />
Available protocols are:<br />
* SOAP<br />
* REST (a basic HTTP and forms POST based interface)<br />
* XML-RPC<br />
<br />
[[File:Webservices-enable-protocols.png|border|activate and deactivate inidividual protocols]]<br />
<br />
<br />
== Service Groups ==<br />
<br />
Service groups are the unit of allocation of access to a user (simple auth) or user token. They are a collection of functions.<br />
<br />
[[File:Webservices-manage-service-groups.png|border|Web Services service groups]]<br />
<br />
<br />
Give a service group a name, and then specify what functions are to be included. You must also decide what form of authentication can access this service group - web service token, or user simple auth (user and password).<br />
<br />
From here, and individual service group can be deactivated for all users.<br />
<br />
[[File:Webservices-service-group.png|border|edit service group]]<br />
<br />
<br />
<br />
== Function API Descriptions ==<br />
<br />
From the Service Group editing view(above), there is a link to each API function that will take you to a description of the functions API - input/output parameters, and error handling.<br />
<br />
[[File:Webservices-group-delete-api.png|border|Web Services Group Delete API description]]<br />
<br />
=Authentication=<br />
<br />
There are currently two forms of authentication for web service access, which are available for all protocols:<br />
* Web Service Tokens<br />
* User/Password based simple authentication<br />
<br />
In both cases, the authentication data is passed as query string parameters:<br />
* Token - http://your.mahara.local.net/artefact/webservice/rest/server.php?wstoken=484dfea8715ed427b4d95796c3013f7d<br />
* simple auth - http://your.mahara.local.net/artefact/webservice/rest/simpleserver.php?wsusername=blah3&wspassword=blahblah<br />
<br />
Note: that the application changes for each - server.php vs simpleserver.php<br />
<br />
There is one exception, for SOAP, where the user and password can be passed as part of the Web Services Security Extension SOAP headers (wsse), as plaintext username and password. eg:<br />
<env:Header><br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>bean</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">blahblah</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
</env:Header><br />
<br />
<br />
<br />
== Token Based Access Control ==<br />
<br />
Each token is generated, and allocated to a user. You then choose a service group that the given user is allowed to access with this token. Only service groups that have been enabled for tokens can be selected.<br />
<br />
[[File:Webservices-access-tokens.png|border|Web services generate an access token]]<br />
<br />
<br />
<br />
== Simple User Authentication Access Control ==<br />
<br />
To configure a user for simple authentication access, choose a user, and then allocate a service group. Only service groups that are enabled for simple user authentication can be used.<br />
<br />
A selected user must have the "webservice" authentication method. This authentication method will only allow access via Web Services, restricting the user form normal login.<br />
<br />
[[File:Webservices-user-access.png|border|Web Service allocate access to a user for simple authentication]]</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Artefact/WebServices/WebServicesConfiguration&diff=1966Plugins/Artefact/WebServices/WebServicesConfiguration2011-06-23T01:25:40Z<p>PiersHarding: /* Authentication */</p>
<hr />
<div>=Web Services Configuration=<br />
<br />
All Web Services configuration hangs off the plugin administration page http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
The main page:<br />
<br />
[[File:Webservices-config.png|border|Web Services main configuration page]]<br />
<br/><br />
<br />
From here, you can carry out all the main administrative tasks:<br />
<br />
* Globally activate/deactivate web services<br />
* activate/deactivate individual protocols (REST, SOAP, XML-RPC)<br />
* Create Services Groups of Functions that can be accessed<br />
* Generate access tokens, and allocate users, and services groups<br />
* Grant access to users for simple authentication access, and allocate service groups<br />
* look up API descriptions for functions<br />
<br />
<br />
== Global activation ==<br />
<br />
Globally activate or deactivate all web service access for when you need to shut off access completely<br />
<br />
[[File:Webservices-enable-services.png|border|Global activation/deactivation of all web services]]<br />
<br />
<br />
== Activating Protocols ==<br />
<br />
Individual protocols can be activated or deactivated. Once activated, the protocol is available to all configured functions/users/tokens.<br />
<br />
Available protocols are:<br />
* SOAP<br />
* REST (a basic HTTP and forms POST based interface)<br />
* XML-RPC<br />
<br />
[[File:Webservices-enable-protocols.png|border|activate and deactivate inidividual protocols]]<br />
<br />
<br />
== Service Groups ==<br />
<br />
Service groups are the unit of allocation of access to a user (simple auth) or user token. They are a collection of functions.<br />
<br />
[[File:Webservices-manage-service-groups.png|border|Web Services service groups]]<br />
<br />
<br />
Give a service group a name, and then specify what functions are to be included. You must also decide what form of authentication can access this service group - web service token, or user simple auth (user and password).<br />
<br />
From here, and individual service group can be deactivated for all users.<br />
<br />
[[File:Webservices-service-group.png|border|edit service group]]<br />
<br />
<br />
<br />
== Function API Descriptions ==<br />
<br />
From the Service Group editing view(above), there is a link to each API function that will take you to a description of the functions API - input/output parameters, and error handling.<br />
<br />
[[File:Webservices-group-delete-api.png|border|Web Services Group Delete API description]]<br />
<br />
<br />
=Authentication=<br />
<br />
There are currently two forms of authentication for web service access, which are available for all protocols:<br />
* Web Service Tokens<br />
* User/Password based simple authentication<br />
<br />
In both cases, the authentication data is passed as query string parameters:<br />
* Token - http://your.mahara.local.net/artefact/webservice/rest/server.php?wstoken=484dfea8715ed427b4d95796c3013f7d<br />
* simple auth - http://your.mahara.local.net/artefact/webservice/rest/simpleserver.php?wsusername=blah3&wspassword=blahblah<br />
<br />
Note: that the application changes for each - server.php vs simpleserver.php<br />
<br />
There is one exception, for SOAP, where the user and password can be passed as part of the Web Services Security Extension SOAP headers (wsse), as plaintext username and password. eg:<br />
<env:Header><br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>bean</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">blahblah</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
</env:Header><br />
<br />
<br />
<br />
== Token Based Access Control ==<br />
<br />
Each token is generated, and allocated to a user. You then choose a service group that the given user is allowed to access with this token. Only service groups that have been enabled for tokens can be selected.<br />
<br />
[[File:Webservices-access-tokens.png|border|Web services generate an access token]]<br />
<br />
<br />
<br />
== Simple User Authentication Access Control ==<br />
<br />
To configure a user for simple authentication access, choose a user, and then allocate a service group. Only service groups that are enabled for simple user authentication can be used.<br />
<br />
A selected user must have the "webservice" authentication method. This authentication method will only allow access via Web Services, restricting the user form normal login.<br />
<br />
[[File:Webservices-user-access.png|border|Web Service allocate access to a user for simple authentication]]</div>PiersHardinghttps://wiki.mahara.org/index.php?title=Plugins/Auth/WebServices&diff=1965Plugins/Auth/WebServices2011-06-23T01:24:29Z<p>PiersHarding: /* Configuration */</p>
<hr />
<div>=Web Services support for Mahara=<br />
<br />
==Artefact Webservices==<br />
<br />
[https://gitorious.org/mahara-contrib/artefact-webservice Web Services] plugin based support for Mahara.<br />
<br />
The Web Services support is modelled on the webservices subsystem for [http://docs.moodle.org/dev/Web_services Moodle] and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.<br />
<br />
The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.<br />
<br />
==Installation Instructions==<br />
This requires the simple authentication module auth/webservice <br />
See https://gitorious.org/mahara-contrib/auth-webservice<br />
<br />
To install you need to download both modules:<br />
<br />
get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master<br />
<br />
and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master<br />
<br />
Then do the following:<br />
<br />
cd /path/to/mahara/htdocs/auth<br />
tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz<br />
mv mahara-contrib-auth-webservice webservice<br />
cd /path/to/mahara/htdocs/artefact<br />
tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz<br />
mv mahara-contrib-artefact-webservice webservice<br />
<br />
You should now have the two necessary modules in place.<br />
<br />
Now login as admin to Mahara, and go through the upgrade process to complete the install.<br />
In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.<br />
<br />
It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!<br />
<br />
==Configuration==<br />
<br />
The configuration interface can be found under [http://your.mahara.local.net/admin/extensions/plugins.php Site administration] for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php<br />
<br />
Read on for more [[Plugins/Artefact/WebServices/WebServicesConfiguration| configuration details]].<br />
<br />
==Testing==<br />
<br />
The testing framework and examples are based on [http://www.simpletest.org/ simpletest] unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:<br />
<br />
# for a debian based install<br />
cd /path/to/artefact/webservice/simpletest<br />
sudo -u www-data php testwebservice.php<br />
<br />
This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.<br />
<br />
If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.<br />
<br />
==Developing Functions for Services==<br />
<br />
Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:<br />
* admin<br />
* local<br />
* api<br />
* artefact/webservice<br />
<br />
Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:<br />
* artefact/webservice/serviceplugins/user<br />
* artefact/webservice/serviceplugins/group<br />
<br />
Each plugin must have two files service.php and externallib.php.<br />
<br />
===externallib.php===<br />
<br />
This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the [https://wiki.mahara.org/index.php/Artefact/WebServices/WebServicesConfiguration#Function_API_Descriptions web services configuration]. The following is boiler plate code from the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/externallib.php mahara_group_create_groups] function API:<br />
<br />
class mahara_group_external extends external_api {<br />
...<br />
/**<br />
* Returns description of method parameters<br />
* @return external_function_parameters<br />
*/<br />
public static function create_groups_parameters() {<br />
<br />
$group_types = group_get_grouptypes();<br />
return new external_function_parameters(<br />
array(<br />
'groups' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'name' => new external_value(PARAM_RAW, 'Group name'),<br />
'shortname' => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),<br />
'description' => new external_value(PARAM_NOTAGS, 'Group description'),<br />
'institution' => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),<br />
'grouptype' => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),<br />
'jointype' => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),<br />
'category' => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),<br />
'public' => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),<br />
'usersautoadded' => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),<br />
'members' => new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),<br />
'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),<br />
'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')<br />
), 'Group membership')<br />
),<br />
)<br />
)<br />
)<br />
)<br />
);<br />
}<br />
<br />
/**<br />
* Create one or more group<br />
*<br />
* @param array $groups An array of groups to create.<br />
* @return array An array of arrays<br />
*/<br />
public static function create_groups($groups) {<br />
global $USER, $WEBSERVICE_INSTITUTION;<br />
<br />
// Do basic automatic PARAM checks on incoming data, using params description<br />
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));<br />
...<br />
<br />
return $groupids;<br />
}<br />
<br />
/**<br />
* Returns description of method result value<br />
* @return external_description<br />
*/<br />
public static function create_groups_returns() {<br />
return new external_multiple_structure(<br />
new external_single_structure(<br />
array(<br />
'id' => new external_value(PARAM_INT, 'group id'),<br />
'name' => new external_value(PARAM_RAW, 'group name'),<br />
)<br />
)<br />
);<br />
}<br />
...<br />
}<br />
<br />
=== service.php ===<br />
<br />
This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the [https://gitorious.org/mahara-contrib/artefact-webservice/blobs/master/serviceplugins/group/services.php mahara_group_external] services:<br />
<br />
$functions = array(<br />
...<br />
'mahara_group_create_groups' => array(<br />
'classname' => 'mahara_group_external',<br />
'methodname' => 'create_groups',<br />
'classpath' => 'artefact/webservice/serviceplugins/group',<br />
'description' => 'Create groups',<br />
'type' => 'write',<br />
),<br />
...<br />
);<br />
<br />
$services = array(<br />
...<br />
'Simple Group Provisioning' => array(<br />
'functions' => array ('mahara_group_create_groups', ... ),<br />
'enabled'=>1,<br />
'restrictedusers'=>1,<br />
),<br />
...<br />
);<br />
<br />
==Example Clients==<br />
<br />
Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:<br />
<br />
echo "userids%5B0%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net' https://mahara.local.net/artefact/webservice/rest/server.php<br />
<br />
===PHP Clients===<br />
<br />
=== SOAP ===<br />
Zend provide a convenient web services interface:<br />
<br />
// pull in the Zend libraries<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
// specfy the URL for looking up the WSDL interface definition<br />
$remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';<br />
<br />
//create the soap client instance<br />
$client = new Zend_Soap_Client($wsdl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should be the admin account<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== SOAP with WSSE ===<br />
<br />
The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.<br />
<br />
Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';<br />
$servicegroup = 'User Provisioning';<br />
$wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';<br />
<br />
$userids = array(0=>1);<br />
<br />
//create the soap client instance<br />
class WSSoapClient extends Zend_Soap_Client_Common {<br />
private $username;<br />
private $password;<br />
<br />
/*Generates the WSSecurity header*/<br />
private function wssecurity_header() {<br />
$timestamp = gmdate('Y-m-d\TH:i:s\Z');<br />
$nonce = mt_rand();<br />
$passdigest = base64_encode(pack('H*', sha1(<br />
pack('H*', $nonce) . pack('a*',$timestamp).<br />
pack('a*',$this->password))));<br />
$auth = '<br />
<wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.<br />
'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><br />
<wsse:UsernameToken><br />
<wsse:Username>'.$this->username.'</wsse:Username><br />
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.<br />
'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password><br />
</wsse:UsernameToken><br />
</wsse:Security><br />
';<br />
$authvalues = new SoapVar($auth,XSD_ANYXML);<br />
$header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);<br />
return $header;<br />
}<br />
<br />
/* set the username and password for the wsse header */<br />
public function setUsernameToken($username, $password) {<br />
$this->username = $username;<br />
$this->password = $password;<br />
}<br />
<br />
/* Overwrites the original method adding the security header */<br />
public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {<br />
return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());<br />
}<br />
}<br />
<br />
$client = new Zend_Soap_Client($wsdl);<br />
$soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());<br />
$soapClient->setUsernameToken('bean', 'blahblah');<br />
$client->setSoapClient($soapClient);<br />
<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should always be the admin user<br />
echo "result: ";<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
echo "exception: ";<br />
var_dump($e);<br />
}<br />
<br />
=== XML-RPC ===<br />
<br />
The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
$remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
$serverurl = $remotemaharaurl. '?wstoken=' . $token;<br />
<br />
//create the xmlrpc client instance<br />
require_once 'Zend/XmlRpc/Client.php';<br />
$client = new Zend_XmlRpc_Client($serverurl);<br />
<br />
//make the web service call<br />
try {<br />
$result = $client->call('mahara_user_get_users_by_id', array(0=>1));<br />
var_dump($result);<br />
} catch (Exception $e) {<br />
var_dump($e);<br />
}<br />
<br />
=== REST with simple authentication ===<br />
<br />
The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.<br />
<br />
Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.<br />
<br />
include 'Zend/Loader/Autoloader.php';<br />
Zend_Loader_Autoloader::autoload('Zend_Loader');<br />
<br />
//set web service url server<br />
$serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php?alt=json';<br />
$token = '91525eca3e3cb11c8f5d94dc0b3c8d82';<br />
<br />
$params = array(<br />
'userids' => array(1), // the params to passed to the function<br />
'wsfunction' => 'mahara_user_get_users_by_id', // the function to be called<br />
'wstoken' => $token, //token need to be passed in the url<br />
);<br />
<br />
$client = new Zend_Http_Client($serverurl);<br />
//$client->setHeaders('Accept', 'application/jsonrequest');<br />
<br />
try {<br />
$client->setParameterPost($params);<br />
$response = $client->request('POST');<br />
var_dump (json_decode($response->getBody()));<br />
} catch (exception $exception) {<br />
var_dump ($exception);<br />
}<br />
<br />
==Core Function Interfaces==<br />
<br />
The base set of APIs are implmented in [https://gitorious.org/mahara-contrib/artefact-webservice/trees/master/serviceplugins artefact/webservice/serviceplugins/].<br />
<br />
These cover User, Favourite, and Group administration functions.</div>PiersHarding