MDL-20808 Fixes for amf web services and test client - a web service browser.
authorJamie Pratt <me@jamiep.org>
Wed, 10 Feb 2010 08:44:46 +0000 (08:44 +0000)
committerJamie Pratt <me@jamiep.org>
Wed, 10 Feb 2010 08:44:46 +0000 (08:44 +0000)
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

webservice/amf/introspector.php [new file with mode: 0644]
webservice/amf/locallib.php
webservice/amf/testclient/AMFConnector.as [new file with mode: 0644]
webservice/amf/testclient/AMFTester.mxml [new file with mode: 0644]
webservice/amf/testclient/AMFTester.swf [new file with mode: 0644]
webservice/amf/testclient/customValidators/JSONValidator.as [new file with mode: 0644]
webservice/amf/testclient/flashcompilationinstructions.txt [new file with mode: 0644]
webservice/amf/testclient/index.php
webservice/lib.php

diff --git a/webservice/amf/introspector.php b/webservice/amf/introspector.php
new file mode 100644 (file)
index 0000000..6e8bef7
--- /dev/null
@@ -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 <penny@liip.ch>
+ * @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;
+    }
+}
+
index 73918b3..9715a0b 100644 (file)
  */
 
 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.
@@ -39,7 +54,108 @@ class webservice_amf_server extends webservice_zend_server {
         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
@@ -50,6 +166,8 @@ class webservice_amf_server extends webservice_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
diff --git a/webservice/amf/testclient/AMFConnector.as b/webservice/amf/testclient/AMFConnector.as
new file mode 100644 (file)
index 0000000..683ab3b
--- /dev/null
@@ -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.boggiano@seld.be>
+        */                     
+       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;
+               }
+       }
+}
\ No newline at end of file
diff --git a/webservice/amf/testclient/AMFTester.mxml b/webservice/amf/testclient/AMFTester.mxml
new file mode 100644 (file)
index 0000000..ef7eee6
--- /dev/null
@@ -0,0 +1,396 @@
+<?xml version="1.0" encoding="utf-8"?>
+<mx:Application 
+       xmlns:mx="http://www.adobe.com/2006/mxml" 
+       backgroundColor="white"
+       layout="absolute" 
+       creationPolicy="all" 
+       height="100%" width="100%" 
+       applicationComplete="init()"
+       xmlns:cv="customValidators.*"
+       defaultButton="{call}">
+
+       <mx:Script>
+               <![CDATA[
+                       import mx.events.ValidationResultEvent;
+                       import mx.validators.Validator;
+                       /**
+                        * Main class/dialog
+                        * 
+                        * 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.boggiano@seld.be>
+                        */                     
+                       
+                       import mx.controls.Label;
+                       import mx.controls.Alert;
+                       import mx.messaging.channels.AMFChannel;
+                       import com.adobe.serialization.json.JSON;
+                       
+/*                     // Import the debugger
+                       import nl.demonsters.debugger.MonsterDebugger;
+ */                    
+                       public var api:AMFConnector;
+                       protected var methods:Array;
+                       protected var introspector:String;
+                       
+                       public var rooturl:String;
+                       
+                       [Bindable]
+                       public var argumentToolTip:String = "You can use JSON syntax for method arguments ie. an array is written like this [item1, item2, etc.] objects are written {\"propname\":value, \"propname2\":value2, etc}";
+                       
+                       // Variable to hold the debugger
+//                     private var debugger:MonsterDebugger;
+
+                       /**
+                        * restores the last settings if available
+                        */
+                       public function init():void
+                       {
+                               // Init the debugger
+//                             debugger = new MonsterDebugger(this);
+                               
+                               // Send a simple trace
+//                             MonsterDebugger.trace(this, "Hello World!");
+                               
+                               var so:SharedObject = SharedObject.getLocal('AMFTester');
+                               if (so.data.token) {
+                                       token.text = so.data.token;
+                               }
+                               if (so.data.username) {
+                                       username.text = so.data.username;
+                                       password.text = so.data.password;
+                               }
+                               if (so.data.mode == 'username'){
+                                       loginType.selectedIndex = 1;
+                               }
+                               this.rememberpassword.selected = so.data.rememberpassword;
+                               this.remembertoken.selected = so.data.remembertoken;
+                               this.rooturl = Application.application.parameters.rooturl;
+                               this.urllabel1.text = 'Root URL :'+this.rooturl;
+                               this.urllabel2.text = 'Root URL :'+this.rooturl;
+                               
+                       }
+                       public function doConnectToken():void
+                       {
+                               var url:String = this.rooturl + '/webservice/amf/server.php?'+
+                                                                       'wstoken='+this.token.text;
+                               this.doConnect(url);
+                               // saving settings for next time
+                               var so:SharedObject = SharedObject.getLocal('AMFTester');
+                               if (this.rememberpassword.selected == true ){
+                                       so.setProperty('token', this.token.text);
+                               } else {
+                                       so.setProperty('token', null);//delete shared obj prop
+                               }
+                               so.setProperty('remembertoken', this.remembertoken.selected);
+                               so.setProperty('mode', 'token');
+                               so.flush();
+                       }
+                       public function doConnectUsername():void
+                       {
+                               var url:String = this.rooturl + '/webservice/amf/simpleserver.php?'+
+                                                       'wsusername=' + this.username.text+
+                                                       '&wspassword=' +  this.password.text;
+                               this.doConnect(url);
+                               // saving settings for next time
+                               var so:SharedObject = SharedObject.getLocal('AMFTester');
+                               if (this.rememberpassword.selected == true ){
+                                       so.setProperty('username', this.username.text);
+                                       so.setProperty('password', this.password.text);
+                               } else {
+                                       so.setProperty('username', null);//delete shared obj prop
+                                       so.setProperty('password', null);
+                               }
+                               so.setProperty('rememberpassword', this.rememberpassword.selected);
+                               so.setProperty('mode', 'username');
+                               so.flush();
+                       }
+                       
+                       /**
+                        * initializes the connection
+                        */
+                       private function doConnect(url:String):void
+                       {
+                               api = new AMFConnector(url);
+                               api.exec('MethodDescriptor.getMethods');
+                               api.addEventListener(Event.COMPLETE, handleConnection);
+                               if (!api.hasEventListener(NetStatusEvent.NET_STATUS)) {
+                                       api.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
+                                       api.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
+                                       api.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
+                               }
+                               this.panelDebug.enabled = false;
+                       }
+                       
+                       /**
+                        * initializes the debugger dialog with the method list and everything
+                        */
+                       protected function handleConnection(event:Event):void
+                       {
+                               methods = [];
+                               for (var cls:String in api.data) {
+                                       for (var meth:String in api.data[cls]['methods']) {
+                                               methods.push({label: cls+'.'+meth, docs: api.data[cls]['methods'][meth]['docs'], args: api.data[cls]['methods'][meth]['params']});
+                                       }
+                               }
+                               
+                               this.panelDebug.enabled = true;
+                               this.maintabs.selectedIndex = 1;
+                               func.dataProvider = methods;
+                               api.removeEventListener(Event.COMPLETE, handleConnection);
+                               api.addEventListener(Event.COMPLETE, process);
+                               reloadArgs();
+                               
+                       }
+                       
+                       
+                       /**
+                        * outputs a response from the server
+                        */
+                       protected function process(event:Event):void
+                       {
+                               if (api.error) {
+                                       push(input, time() + ": Exception (code: "+api.data.code+", description: "+api.data.description+", detail: "+api.data.detail+", line: "+api.data.line+")\n");
+                               } else {
+                                       push(input, time() + ": "+JSON.encode(api.data)+"\n");
+                               }
+//                             MonsterDebugger.trace(this, api.data);
+                       }
+                       
+                       /**
+                        * updates the display of arguments when the selected method changes
+                        * 
+                        * it's hardly optimal to do it that way but it was faster to copy paste, I just hope nobody needs more than 7 args
+                        */
+                       protected function reloadArgs():void
+                       {
+                               var i:int;
+                               for (i = 1; i <= 7; i++) {
+                                       this['arg'+i].visible = false;
+                                       this['arg'+i].includeInLayout = false;
+                                       this['larg'+i].visible = false;
+                                       this['larg'+i].includeInLayout = false;
+                                       this['JSONV'+i].enabled = false;
+                               }
+                               i = 1;
+                               for (var arg:String in func.selectedItem.args) {
+                                       (this['arg'+i] as TextInput).visible = true;
+                                       (this['arg'+i] as TextInput).includeInLayout = true;
+                                       (this['larg'+i] as Label).visible = true;
+                                       (this['larg'+i] as Label).includeInLayout = true;
+                                       this['JSONV'+i].enabled = true;
+                                       this['JSONV'+i].required = func.selectedItem.args[arg]['required'];
+                                       
+                                       (this['larg'+i++] as Label).text = func.selectedItem.args[arg]['name'] + (func.selectedItem.args[arg]['required'] ? "*":"");
+                               }
+                               if (func.selectedItem.docs == ""){
+                                       (this.methodDescription as TextArea).text = "";
+                                       (this.methodDescription as TextArea).visible = false;
+                                       (this.methodDescription as TextArea).includeInLayout = false;
+                               } else {
+                                       (this.methodDescription as TextArea).text = func.selectedItem.docs.replace(/[\n\r\f]+/g, "\n");
+                                       (this.methodDescription as TextArea).visible = true;
+                                       (this.methodDescription as TextArea).includeInLayout = true;
+                               }
+                       }
+                       
+                       /**
+                        * calls a method on the server
+                        */
+                       protected function execute():void
+                       {
+                               var input:TextInput;
+                               var argumentArray:Array = [];
+                               var argumentErrors:Array = Validator.validateAll(argumentValidators);
+                               if (argumentErrors.length != 0){
+//                                     MonsterDebugger.trace(this, argumentErrors);
+                                       return;
+                               }
+                               for(var i:int = 1; i < 8; i++)
+                               {
+                                       input = this['arg' +i] as TextInput;
+                                       if(input)
+                                       {
+                                               if (input.text.indexOf("{") == 0 || input.text.indexOf("[") == 0)
+                                                       try {
+                                                               argumentArray.push(JSON.decode(input.text));
+                                                       } catch (err:Error){
+                                                               return;
+                                                       }
+                                               else
+                                                       argumentArray.push(input.text as String);
+                                       }
+                               }
+                               
+                               
+                               api.exec(func.selectedLabel, argumentArray[0], argumentArray[1], argumentArray[2], argumentArray[3], argumentArray[4], argumentArray[5], argumentArray[6]);
+//                             MonsterDebugger.trace(this, [func.selectedLabel, argumentArray[0], argumentArray[1], argumentArray[2], argumentArray[3], argumentArray[4], argumentArray[5], argumentArray[6]]);
+                               push(output, time() + ": Calling "+func.selectedLabel+" with arguments - "+JSON.encode(argumentArray));
+                       }
+                       
+                       /**
+                        * clears debug consoles
+                        */
+                       protected function clear():void
+                       {
+                               input.text = output.text = "";
+                       }
+                       
+                       /**
+                        * refreshes the method list
+                        */
+                       protected function refresh():void
+                       {
+                               api.removeEventListener(Event.COMPLETE, process);
+                               api.addEventListener(Event.COMPLETE, handleConnection);
+                               api.exec(introspector);
+                       }
+                       
+                       /**
+                        * returns timestamp string
+                        */
+                       protected function time():String
+                       {
+                               var d:Date = new Date();
+                               var ret:String = d.hours+":"+d.minutes+":"+d.seconds+"."+d.milliseconds;
+                               return ret + "000000000000".substring(ret.length);
+                       }
+
+                       /**
+                        * handler for specific net events
+                        */
+                       public function netStatusHandler(event:NetStatusEvent):void 
+                       {
+                               push(input, time() + ": Error("+event.type+"): "+event.info.code+", "+event.info.description+", "+event.info.details);
+                       }
+                       
+                       /**
+                        * handler for security errors
+                        */
+                       public function securityErrorHandler(event:SecurityErrorEvent):void
+                       {
+                               push(input, time() + ": Error("+event.type+"): "+event.text);
+                       }
+               
+                       /**
+                        * handler for io errors
+                        */
+                       public function ioErrorHandler(event:IOErrorEvent):void 
+                       {
+                               push(input, time() + ": Error("+event.type+"): "+event.text);
+                       }
+                       
+                       /**
+                        * pushes text into a console and scrolls it down automatically
+                        */
+                       public function push(target:TextArea, text:String):void
+                       {
+                               target.text += text + "\n";
+                               target.verticalScrollPosition = target.maxVerticalScrollPosition;
+                       }
+
+               ]]>
+       </mx:Script>
+       <mx:Array id="argumentValidators">
+               <cv:JSONValidator id="JSONV1" required="false"  source="{arg1}"  property="text" />
+               <cv:JSONValidator id="JSONV2" required="false"  source="{arg2}"  property="text" />
+               <cv:JSONValidator id="JSONV3" required="false"  source="{arg3}"  property="text" />
+               <cv:JSONValidator id="JSONV4" required="false"  source="{arg4}"  property="text" />
+               <cv:JSONValidator id="JSONV5" required="false"  source="{arg5}"  property="text" />
+               <cv:JSONValidator id="JSONV6" required="false"  source="{arg6}"  property="text" />
+               <cv:JSONValidator id="JSONV7" required="false"  source="{arg7}"  property="text" />
+       </mx:Array>
+               
+
+       
+    <mx:TabNavigator id="maintabs" height="100%" width="100%" >
+       
+           <mx:TabNavigator label="Connect" id="loginType" borderStyle="solid" height="100%" width="100%">
+                       <mx:Panel label="Use Token" id="panelConnectToken">
+                               <mx:HBox width="100%">
+                                       <mx:Label text="Token"/>
+                                       <mx:TextInput id="token" text=""  width="100%"/>
+                               </mx:HBox>
+                               <mx:HBox width="100%">
+                                       <mx:Label text="Remember"/>
+                                       <mx:CheckBox id="remembertoken" width="100%"/>
+                               </mx:HBox>
+                               <mx:Label id="urllabel1" text="URL :" />
+                               <mx:HBox width="100%">
+                                       <mx:Spacer width="100%" />
+                                       <mx:Button label="Connect" click="doConnectToken()"/>
+                                       <mx:Spacer width="100%" />
+                               </mx:HBox>
+                       </mx:Panel>
+                       <mx:Panel label="Use Username and Password" id="panelConnectUsername">
+                               <mx:HBox width="100%">
+                                       <mx:Label text="Username"/>
+                                       <mx:TextInput id="username" text=""  width="100%"/>
+                               </mx:HBox>
+               
+                               <mx:HBox width="100%">
+                                       <mx:Label text="Password"/>
+                                       <mx:TextInput id="password" text="" displayAsPassword="true"  width="100%"/>
+                               </mx:HBox>
+                               <mx:HBox width="100%">
+                                       <mx:Label text="Remember"/>
+                                       <mx:CheckBox id="rememberpassword" width="100%"/>
+                               </mx:HBox>
+                               <mx:Label id="urllabel2" text="URL :" />
+               
+                               <mx:HBox width="100%">
+                                       <mx:Spacer width="100%" />
+                                       <mx:Button label="Connect" click="doConnectUsername()"/>
+                                       <mx:Spacer width="100%" />
+                               </mx:HBox>
+                       </mx:Panel>
+               </mx:TabNavigator>
+               <mx:Panel label="Service Browser" width="100%" height="100%" layout="vertical" title="Moodle AMF Service Browser" enabled="false" id="panelDebug">
+                       <mx:HBox width="100%">
+                               <mx:Label text="Func "/>
+                               <mx:ComboBox id="func" change="reloadArgs()">
+                               </mx:ComboBox>
+                       </mx:HBox>
+                       <mx:TextArea id="methodDescription" text="" width="100%" height="120"/>
+                       <mx:HBox width="100%">
+                               <mx:Label id="larg1" text="Arg 1"/>
+                               <mx:TextInput id="arg1" toolTip="{argumentToolTip}"/>
+                       </mx:HBox>
+                       <mx:HBox width="100%">
+                               <mx:Label id="larg2" text="Arg 2"/>
+                               <mx:TextInput id="arg2" toolTip="{argumentToolTip}"/>
+                       </mx:HBox>
+                       <mx:HBox width="100%">
+                               <mx:Label id="larg3" text="Arg 3"/>
+                               <mx:TextInput id="arg3" toolTip="{argumentToolTip}"/>
+                       </mx:HBox>
+                       <mx:HBox width="100%">
+                               <mx:Label id="larg4" text="Arg 4"/>
+                               <mx:TextInput id="arg4" toolTip="{argumentToolTip}"/>
+                       </mx:HBox>
+                       <mx:HBox width="100%">
+                               <mx:Label id="larg5" text="Arg 5"/>
+                               <mx:TextInput id="arg5" toolTip="{argumentToolTip}"/>
+                       </mx:HBox>
+                       <mx:HBox width="100%">
+                               <mx:Label id="larg6" text="Arg 6"/>
+                               <mx:TextInput id="arg6" toolTip="{argumentToolTip}"/>
+                       </mx:HBox>
+                       <mx:HBox width="100%">
+                               <mx:Label id="larg7" text="Arg 7"/>
+                               <mx:TextInput id="arg7" toolTip="{argumentToolTip}"/>
+                       </mx:HBox>
+                       <mx:HBox width="100%">
+                               <mx:Button id="call" label="Call" click="execute()"/>
+                               <mx:Button label="Clear" click="clear()"/>
+                       </mx:HBox>
+                       <mx:TextArea id="output"  width="100%" height="100"/>
+                       <mx:TextArea id="input"  width="100%" height="300"/>
+               </mx:Panel>
+    </mx:TabNavigator>
+       
+</mx:Application>
diff --git a/webservice/amf/testclient/AMFTester.swf b/webservice/amf/testclient/AMFTester.swf
new file mode 100644 (file)
index 0000000..8819ff3
Binary files /dev/null and b/webservice/amf/testclient/AMFTester.swf differ
diff --git a/webservice/amf/testclient/customValidators/JSONValidator.as b/webservice/amf/testclient/customValidators/JSONValidator.as
new file mode 100644 (file)
index 0000000..39d4982
--- /dev/null
@@ -0,0 +1,40 @@
+package customValidators
+{
+       import com.adobe.serialization.json.JSON;
+       import com.adobe.serialization.json.JSONParseError;
+       
+       import mx.validators.ValidationResult;
+       import mx.validators.Validator;
+       
+       import nl.demonsters.debugger.MonsterDebugger;
+
+       public class JSONValidator extends Validator
+       {
+        // Define Array for the return value of doValidation().
+        private var errors:Array;
+
+               public function JSONValidator()
+               {
+                       super();
+               }
+        
+        override protected function doValidation(value:Object):Array {
+               var JSONstring:String = String(value);
+            errors = [];
+            if (JSONstring != ''){
+                               try {
+                                       JSON.decode(JSONstring);
+                               } catch (err:Error){
+                                       errors.push(new ValidationResult(true, null, "JSON decode failed", 
+                           "Not able to decode this JSON."));
+                               }
+            }
+            if (this.required && JSONstring == ''){
+               errors.push(new ValidationResult(true, null, "Required", 
+                           "You must enter a value for this argument."));
+            }
+            return errors;
+        }
+               
+       }
+}
\ No newline at end of file
diff --git a/webservice/amf/testclient/flashcompilationinstructions.txt b/webservice/amf/testclient/flashcompilationinstructions.txt
new file mode 100644 (file)
index 0000000..b82c6ee
--- /dev/null
@@ -0,0 +1,9 @@
+AMFTester.mxml can be compiled as a Flex application in Flex builder or using the Flex SDK.
+
+Copy the following into a Flex project source folder :
+
+* customValidators folder and contents
+* AMFConnector.as
+* AMFTester.mxml
+
+Then you need to use either the compiled Flex library or the source for the open source library as3corelib available here : http://code.google.com/p/as3corelib/downloads/list
index e6c663b..918e378 100644 (file)
@@ -1,20 +1,23 @@
 <?php
 require "../../../config.php";
 
-die('TODO');
 
-$args['movie'] = $CFG->wwwroot.'/webservice/amf/testclient/moodleclient.swf';
-$args['width'] = '100%';
-$args['height'] = 500;
-$args['majorversion'] = 9;
-$args['build'] = 0;
-$args['allowscriptaccess'] = 'never';
-$args['quality'] = 'high';
-$args['flashvars'] = 'amfurl='.$CFG->wwwroot.'/webservice/amf/server.php';
-$args['setcontainercss'] = 'true';
+$flashvars = new object();
+$flashvars->rooturl =$CFG->wwwroot;
 
-$PAGE->requires->js('/lib/ufo.js');
-$PAGE->requires->js_function_call('M.util.create_UFO_object', array('moodletestclient', $args));
+
+$PAGE->requires->js('/lib/swfobject/swfobject.js', true);
+
+$PAGE->requires->js_function_call('swfobject.embedSWF', 
+                               array($CFG->wwwroot.'/webservice/amf/testclient/AMFTester.swf', //movie
+                                       'moodletestclient', // div id
+                                       '100%', // width
+                                       '1000', // height
+                                       '9.0', // version
+                                       false,//no express install swf
+                                       $flashvars), //flash vars
+                               true
+                       );
 
 $PAGE->set_title('Test Client');
 $PAGE->set_heading('Test Client');
index fd74060..9d29c5e 100644 (file)
@@ -408,6 +408,8 @@ class '.$classname.' {
         }
         $params      = implode(', ', $params);
         $params_desc = implode("\n", $params_desc);
+        
+        $serviceclassmethodbody = $this->service_class_method_body($function, $params);
 
         if (is_null($function->returns_desc)) {
             $return = '     * @return void';
@@ -441,12 +443,24 @@ class '.$classname.' {
 '.$return.'
      */
     public function '.$function->name.'('.$params.') {
-        return '.$function->classname.'::'.$function->methodname.'('.$params.');
+'.$serviceclassmethodbody.';
     }
 ';
         return $code;
     }
-
+    
+    /**
+     * You can override this function in your child class to add extra code into the dynamically
+     * created service class. For example it is used in the amf server to cast types of parameters and to
+     * cast the return value to the types as specified in the return value description.
+     * @param unknown_type $function
+     * @param unknown_type $params
+     * @return string body of the method for $function ie. everything within the {} of the method declaration.
+     */
+    protected function service_class_method_body($function, $params){
+       return '        return '.$function->classname.'::'.$function->methodname.'('.$params.')';
+    }
+    
     /**
      * Set up zend service class
      * @return void