Skip to content

Commit

Permalink
MDL-20808 Fixes for amf web services and test client - a web service …
Browse files Browse the repository at this point in the history
…browser.

Includes :

MDL-21552 amf web services need to accept params and return values of proper type

MDL-21553 amf web service : In Flash an array has normally a numeri
  • Loading branch information
jamiepratt committed Feb 10, 2010
1 parent 279e266 commit 94a9b9e
Show file tree
Hide file tree
Showing 9 changed files with 764 additions and 14 deletions.
107 changes: 107 additions & 0 deletions webservice/amf/introspector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
/**
* Moodle - Modular Object-Oriented Dynamic Learning Environment
* http://moodle.org
* Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package moodle
* @author Penny Leach <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL
* @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com
*
* Introspection for amf - figures out where all the services are and
* returns a list of their available methods.
* Requires $CFG->amf_introspection = true for security.
*/


/**
* Provides a function to get details of methods available on another class.
* @author HP
*
*/
class MethodDescriptor {

private $methods;
private $classes;

static public $classnametointrospect;


public function __construct() {
$this->setup();
}

private function setup() {
global $CFG;
if (!empty($this->nothing)) {
return; // we've already tried, no classes.
}
if (!empty($this->classes)) { // we've already done it successfully.
return;
}
/*if (empty($CFG->amf_introspection)) {
throw new Exception(get_string('amfintrospectiondisabled', 'local'));
}*/

//just one class here, possibility for expansion in future
$classes = array(MethodDescriptor::$classnametointrospect);

$hugestructure = array();

foreach ($classes as $c) {
$r = new ReflectionClass($c);

if (!$methods = $r->getMethods()) {
continue;
}
$this->classes[] = $c;
$hugestructure[$c] = array('docs' => $r->getDocComment(), 'methods' => array());
foreach ($methods as $method) {
if (!$method->isPublic()) {
continue;
}
$params = array();
foreach ($method->getParameters() as $param) {
$params[] = array('name' => $param->getName(), 'required' => !$param->isOptional());
}
$hugestructure[$c]['methods'][$method->getName()] = array(
'docs' => $method->getDocComment(),
'params' => $params,
);
}
}
$this->methods = $hugestructure;
if (empty($this->classes)) {
$this->nothing = true;
}
}

public function getMethods() {
$this->setup();
return $this->methods;
}

public function getClasses() {
$this->setup();
return $this->classes;
}

public function isConnected() {
return true;
}
}

118 changes: 118 additions & 0 deletions webservice/amf/locallib.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@
*/

require_once("$CFG->dirroot/webservice/lib.php");
require_once( "{$CFG->dirroot}/webservice/amf/introspector.php");

/**
* Exception indicating an invalid return value from a function.
* Used when an externallib function does not return values of the expected structure.
*/
class invalid_return_value_exception extends moodle_exception {
/**
* Constructor
* @param string $debuginfo some detailed information
*/
function __construct($debuginfo=null) {
parent::__construct('invalidreturnvalue', 'debug', '', null, $debuginfo);
}
}

/**
* AMF service server implementation.
Expand All @@ -39,7 +54,108 @@ public function __construct($simple) {
parent::__construct($simple, 'Zend_Amf_Server');
$this->wsname = 'amf';
}
protected function init_service_class(){
parent::init_service_class();
//allow access to data about methods available.
$this->zend_server->setClass( "MethodDescriptor" );
MethodDescriptor::$classnametointrospect = $this->service_class;
}

protected function service_class_method_body($function, $params){
$params = "webservice_amf_server::cast_objects_to_array($params)";
$externallibcall = $function->classname.'::'.$function->methodname.'('.$params.')';
$descriptionmethod = $function->methodname.'_returns()';
$callforreturnvaluedesc = $function->classname.'::'.$descriptionmethod;
return
' return webservice_amf_server::validate_and_cast_values('.$callforreturnvaluedesc.', '.$externallibcall.', true)';
}
/**
* Validates submitted value, comparing it to a description. If anything is incorrect
* invalid_return_value_exception is thrown. Also casts the values to the type specified in
* the description.
* @param external_description $description description of parameters
* @param mixed $value the actual values
* @param boolean $singleasobject specifies whether a external_single_structure should be cast to a stdClass object
* should always be false for use in validating parameters in externallib functions.
* @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found
*/
public static function validate_and_cast_values(external_description $description, $value) {
if (is_null($description)){
return $value;
}
if ($description instanceof external_value) {
if (is_array($value) or is_object($value)) {
throw new invalid_return_value_exception('Scalar type expected, array or object received.');
}

if ($description->type == PARAM_BOOL) {
// special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
if (is_bool($value) or $value === 0 or $value === 1 or $value === '0' or $value === '1') {
return (bool)$value;
}
}
return validate_param($value, $description->type, $description->allownull, 'Invalid external api parameter');

} else if ($description instanceof external_single_structure) {
if (!is_array($value)) {
throw new invalid_return_value_exception('Only arrays accepted.');
}
$result = array();
foreach ($description->keys as $key=>$subdesc) {
if (!array_key_exists($key, $value)) {
if ($subdesc->required == VALUE_REQUIRED) {
throw new invalid_return_value_exception('Missing required key in single structure: '.$key);
}
if ($subdesc instanceof external_value) {
if ($subdesc->required == VALUE_DEFAULT) {
$result[$key] = self::validate_and_cast_values($subdesc, $subdesc->default);
}
}
} else {
$result[$key] = self::validate_and_cast_values($subdesc, $value[$key]);
}
unset($value[$key]);
}
if (!empty($value)) {
throw new invalid_return_value_exception('Unexpected keys detected in parameter array.');
}
return (object)$result;

} else if ($description instanceof external_multiple_structure) {
if (!is_array($value)) {
throw new invalid_return_value_exception('Only arrays accepted.');
}
$result = array();
foreach ($value as $param) {
$result[] = self::validate_and_cast_values($description->content, $param);
}
return $result;

} else {
throw new invalid_return_value_exception('Invalid external api description.');
}
}
/**
* Recursive function to recurse down into a complex variable and convert all
* objects to arrays. Doesn't recurse down into objects or cast objects other than stdClass
* which is represented in Flash / Flex as an object.
* @param mixed $params value to cast
* @return mixed Cast value
*/
public static function cast_objects_to_array($params){
if ($params instanceof stdClass){
$params = (array)$params;
}
if (is_array($params)){
$toreturn = array();
foreach ($params as $key=> $param){
$toreturn[$key] = self::cast_objects_to_array($param);
}
return $toreturn;
} else {
return $params;
}
}
/**
* Set up zend service class
* @return void
Expand All @@ -50,6 +166,8 @@ protected function init_zend_server() {
//(complete error message displayed into your AMF client)
// TODO: add some exception handling
}


}

// TODO: implement AMF test client somehow, maybe we could use moodle form to feed the data to the flash app somehow
63 changes: 63 additions & 0 deletions webservice/amf/testclient/AMFConnector.as
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package {

import flash.events.Event;
import flash.net.NetConnection;
import flash.net.Responder;

import nl.demonsters.debugger.MonsterDebugger;

/**
* Wrapper class for the NetConnection/Responder instances
*
* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://sam.zoy.org/wtfpl/COPYING for more details.
*
* @author Jordi Boggiano <j[email protected]>
*/
public class AMFConnector extends NetConnection {
private var responder:Responder;
public var data:Object;
public var error:Boolean = false;

public function AMFConnector(url:String) {
responder = new Responder(onSuccess, onError);
connect(url);
}

/**
* executes a command on the remote server, passing all the given arguments along
*/
public function exec(command:String, ... args:Array):void
{
if (!args) args = [];
args.unshift(responder);
args.unshift(command);
(call as Function).apply(this, args);
}

/**
* handles success
*/
protected function onSuccess(result:Object):void {
MonsterDebugger.trace(this, {'result':result});
data = result;
dispatchEvent(new Event(Event.COMPLETE));
data = null;
}

/**
* handles errors
*/
protected function onError(result:Object):void {
data = result;
MonsterDebugger.trace(this, {'result':result});
error = true;
dispatchEvent(new Event(Event.COMPLETE));
error = false;
data = null;
}
}
}
Loading

0 comments on commit 94a9b9e

Please sign in to comment.