From: pkiddie <pk...@us...> - 2005-09-09 10:59:29
|
Update of /cvsroot/stack/stack-1-0/scripts/rqp/old In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv13239/scripts/rqp/old Added Files: nb_easyxml_lite.php nusoap.php rqp.wsdl rqp_calls.php rqp_util.php uni_soap.php uni_soap.php5 Log Message: Modifications to RQP directory structure --- NEW FILE: uni_soap.php5 --- <?php /* $trace indicates if the soap messages should be saved (i.e. if get_soap_messages is used) and should be used only for debugging */ function soap_connect($wsdl, $trace=false) { try { $connection = new SoapClient($wsdl, array('soap_version'=>SOAP_1_1, 'exceptions'=>true, 'trace'=>$trace)); } catch (SoapFault $f) { $connection = $f; } catch (Exception $e) { $connection = new SoapFault('client', 'Could not connect to the service'); } return $connection; } function soap_call($connection, $call, $params) { try { $return = $connection->__call($call, $params); } catch (SoapFault $f) { $return = $f; } catch (Exception $e) { $return = new SoapFault('client', 'Could call the method'); } // return multiple parameters using an object rather than an array if (is_array($return)) { $keys = array_keys($return); $assoc = true; foreach ($keys as $key) { if (!is_string($key)) { $assoc = false; break; } } if ($assoc) $return = (object) $return; } return $return; } function soap_serve($wsdl, $functions) { // create server object $s = new SoapServer($wsdl); // export functions foreach ($functions as $func) $s->addFunction($func); // handle the request $s->handle(); } function make_soap_fault($faultcode, $faultstring, $faultactor='', $detail='', $faultname='', $headerfault='') { return new SoapFault($faultcode, $faultstring, $faultactor, $detail, $faultname, $headerfault); } function get_last_soap_messages($connection) { return array('request'=>$connection->__getLastRequest(), 'response'=>$connection->__getLastResponse()); } // Fix simple type encoding - work around a bug in PHP function soap_encode($value, $name, $type, $namespace, $encode=XSD_STRING) { $value = new SoapVar($value, $encode, $type, $namespace); if ('' === $name) return $value; return new SoapParam($value, $name); } // Fix complex type encoding - work around a bug in PHP function soap_encode_object($value, $name, $type, $namespace) { if (!is_object($value)) return $value; $value = new SoapVar($value, SOAP_ENC_OBJECT, $type, $namespace); if ('' === $name) return $value; return new SoapParam($value, $name); } // Fix array encoding - work around a bug in PHP function soap_encode_array($value, $name, $type, $namespace) { if (!is_array($value)) return $value; $value = new SoapVar($value, SOAP_ENC_ARRAY, 'ArrayOf' . $type, $namespace); if ('' === $name) return $value; return new SoapParam($value, $name); } ?> --- NEW FILE: rqp_util.php --- <?php /** * Stack RQP utility functions * Not part of the RQP Server/Client combo but necessary to interact with RQP * * @package stackRQP * @subpackage Stack */ // Utility functions // /** * Converts an associative array of variables into an array of key-value * objects suitable for RQP * * Creates an array of {@link RQPVar} objects from an associative array * of variables. * @param array $vars Associative array of variables. * @return array Returns an array of objects with the key property set to * the key of the corresponding entry in the vars parameter and the values * property set to the corresponding value. */ function MakeRQPVars($vars) { if (!count($vars)) return array(); array_walk($vars, create_function('&$val, $key', '$obj->identifier = $key; $obj->values = (is_array($val) ? array_values($val) : array($val)); $val = $obj;')); return array_values($vars); } /** * Converts an array of name-value objects as used by RQP into an * associative array of variables. * * Creates an associative array of responses from an array of * {@link RQPInput} objects. * @param array $vars Array of {@link RQPInput} objects. * @return array Returns an associative array with the key of each element * set to the name property of the corresponding object and the value set to * the value property of the corresponding object. */ function FlattenRQPInputs($vars) { if (!is_array($vars)) return array(); $return = array(); foreach ($vars as $var) { $var = (object) $var; // sort out nuSOAP encoding weirdness if (!empty($var->name)) { $return[$var->name] = $var->value; } } return $return; } /** * Converts an array of key-value objects as used by RQP into an * associative array of variables. * * Creates an associative array of variables from an array of {@link RQPVar} * objects. * @param array $vars Array of {@link RQPVar} objects. * @return array Returns an associative array with the key of each element * set to the key property of the corresponding object and the value set to * the values property of the corresponding object. */ function FlattenRQPVars($vars) { if (!is_array($vars)) return array(); $return = array(); foreach ($vars as $var) { $var = (object) $var; // sort out nuSOAP encoding weirdness if (!empty($var->identifier)) { if (1 === count($var->values)) $return[$var->identifier] = $var->values[0]; else $return[$var->identifier] = array_values($var->values); } } return $return; } /** * Interprets source for the all operations which need it. * * @param string $source The item source. * @return array $item The item is returned. */ function source_handler($source, &$format, &$errors, $item_information=FALSE) { global $stack_web_url; $item = NULL; // No source supplied if ('' == trim($source)) { //return MakeRQPFault(MakeRQPError('emptySource', 'No item source supplied.'), 'No item source supplied.'); return 'No item source supplied' } // We need to use the source supplied, and had beter interpret the format. if ('' === $format) { // format not specified; we are asked to make our best guess if (stristr($source,'<assessmentItem>')) { $format = STACK_URI_FORMAT_XML; } else { $format = STACK_URI_FORMAT_FLAT; } } switch ($format) { case STACK_URI_FORMAT_XML: $itemTemp = stack_xml_parse_question_string($source); $item = $itemTemp['assessmentItem']; $questionBody = addslashes(base64_serialize($itemTemp)); break; case STACK_URI_FORMAT_FLAT: $questionBody = $source; $item = stack_db_decodebody($questionBody); break; default: // Invalid format return MakeRQPFault(MakeRQPError('unknownFormat', '`' . $format . '\' is not a format which STACK can use.'), 'Unknown source format.'); } // By this stage we should have an $item. // Validate item $errors = array(); stack_question_validate($item,$errors); // HACK: throwing a SOAP error here will break the itemInformation operation..... if (count($errors) > 0) { // $errstr should be generated on the client side // $errstr = stack_question_errstr($errors); if (!$item_information) return MakeRQPFault(MakeRQPError('invalidSource', 'The item is invalid.', $errors)); } // HACK: Items no longer need GUIDS so just generate one if it is not set if (empty($item['questionGUID'])) { $item['questionGUID'] = stack_generate_guid($stack_web_url); } $item['questionID'] = 0; // For test and RQP questions only return $item; } /** * Creates an RQP error object. * * An {@link RQPError} object is created to represent the error condition. * @param string $id The identifier for the error (this will be prefixed * with the error URI). * @param string $description Human readable description of the error * reprensented by the id. * @param mixed $detail Machine readable information about the error. * @return RQPError The error object created. */ function MakeRQPError($id, $description='', $detail='') { $error = new RQPError; $error->identifier = RQP_URI_ERROR . $id; $error->message = $description; $error->detail = $detail; return $error; } /** * Checks if a value is an RQP error object. * * The value is tested for being an object and if it is an object for being * of class {@link RQPError}. * @param mixed $value The value to be tested. * @return boolean Returns true if the value is an object of class * {@link RQPError}, false otherwise. */ if (floor(phpversion()) > 4) { function IsRQPError($value) { if (is_object($value)) { if ('RQPError' === get_class($value)) { return true; } } return false; } } else { // get_class returns the class name in lowercase under PHP 4 function IsRQPError($value) { if (is_object($value)) { if ('rqperror' === get_class($value)) { return true; } } return false; } } class /** * Creates an RQP style SOAP fault object. * * Creates a SOAP fault object with an RQP error object describing the * error using the SOAP detail field. * @param object $error The {@link RQPError} object describing the error. * @param string $description Human readable description of the error * reprensented by the id field of the error object. * @return SoapFault The SOAP fault object. */ function MakeRQPFault($error, $description='') { $errorID = substr($error->id, strlen(RQP_URI_ERROR)); switch ($errorID) { case 'emptySource': case 'unknownFormat': case 'invalidSource': case 'invalidTemplateVariables': $who = 'Client'; break; default: $who = 'Server'; break; } if (empty($description)) $description = $errorID; return make_soap_fault($who, 'RQP error: '.$description, '', $error); } ?> --- NEW FILE: uni_soap.php --- <?php /** * Web Services Toolkit for PHP * * This script sorts out whether or not to use the native PHP support for * SOAP. * * @package stackRQP * @subpackage Stack */ // Web services wrapper library script if (class_exists('SoapClient')) { // Use the native PHP5 support require_once('uni_soap.php5'); } else{ // Use nuSOAP instead require_once('nusoap.php'); function make_soap_fault($faultcode, $faultstring, $faultactor='', $detail='', $faultname='', $headerfault='') { return new soap_fault($faultcode, $faultactor, $faultstring, $detail); } function is_soap_fault($obj) { if (!is_object($obj)) return false; return (strcasecmp(get_class($obj), 'soap_fault') === 0); } if (class_exists('soap_client')) { function soap_connect($wsdl, $trace=false) { return new soap_client($wsdl, 'wsdl'); } } else { function soap_connect($wsdl, $trace=false) { return new soapclient($wsdl, 'wsdl'); } } function soap_call($connection, $call, $params) { $result = $connection->call($call, $params); if ($connection->fault) { return @make_soap_fault($result['faultcode'], $result['faultstring'], '', $result['detail']); } if ($connection->error_str) { return @make_soap_fault('server', $connection->error_str, '', $connection->response); } /* Fix objects being returned as associative arrays (to fit with PHP5 SOAP support */ return fix_object($result); } function soap_serve($wsdl, $functions) { global $HTTP_RAW_POST_DATA; $s = new soap_server($wsdl); $s->service(isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : ''); } function get_last_soap_messages($connection) { return array('request'=>$connection->request, 'response'=>$connection->response); } /* Fix objects being returned as associative arrays (to fit with PHP5 SOAP support */ function fix_object($value) { if (is_array($value)) { $value = array_map('fix_object', $value); $keys = array_keys($value); /* check for arrays of length 1 (they get given the key "item" rather than 0 by nusoap) */ if (1 === count($value) && 'item' === $keys[0]) { $value = array_values($value); } else { /* cast to object if it is an associative array with at least one string key */ foreach ($keys as $key) { if (is_string($key)) { $value = (object) $value; break; } } } } return $value; } // Fix simple type encoding - not needed for nuSOAP function soap_encode($value, $name, $type, $namespace, $encode=0) { return $value; } // Fix complex type encoding - not needed for nuSOAP function soap_encode_object($value, $name, $type, $namespace) { return $value; } // Fix array encoding - not needed for nuSOAP function soap_encode_array($value, $name, $type, $namespace) { return $value; } } // In both cases... function handle_soap_wsdl_request($wsdlfile, $address=false) { header('Content-type: application/wsdl+xml'); $wsdl = file_get_contents($wsdlfile); if (false !== $address) { if (true === $address) { $address = (($_SERVER['SERVER_PORT'] == 443) ? 'https://' : 'http://') . $_SERVER['SERVER_NAME'] . $_SERVER['SCRIPT_NAME']; } $wsdl = str_replace('###SERVER_ADDRESS###', $address, $wsdl); } echo $wsdl; exit; } ?> --- NEW FILE: nusoap.php --- <?php /** * NuSOAP - Web Services Toolkit for PHP * <br> * Copyright (c) 2002 NuSphere Corporation * Stack RQP item engine implementation * * @package stackRQP * @subpackage Stack */ /* $Id: nusoap.php,v 1.1 2005/09/09 10:59:22 pkiddie Exp $ NuSOAP - Web Services Toolkit for PHP Copyright (c) 2002 NuSphere Corporation This library is free software; you can redistribute it and/or [...6479 lines suppressed...] $this->debug('Update cookie ' . $newName . '=' . $newCookie['value']); break; } if (! $found) { $this->debug('Add cookie ' . $newName . '=' . $newCookie['value']); $this->cookies[] = $newCookie; } } return true; } } // Backwards compatibility definition // This won't work where native PHP SOAP support is present // so the new name should be used whenever possible if (!class_exists('SoapClient')) { class soapclient extends soap_client { } } ?> --- NEW FILE: nb_easyxml_lite.php --- <?PHP /*!< This set of classes and function is to provide a simple method of * dealing with XML that does not depend on optional libraries */ define("START_TAG",1); define("END_TAG",2); define("CLOSED_TAG",3); define("CONTENT",4); define("XML_ERROR",0); define("XML_DECLARATION",5); // <?xml define("XML_DOCTYPE",6); // <!DOCTYPE ... > define("XML_INSTRUCTION",7); // <? define("XML_CDATA",8); // <[CDATA[ ... ]]> define("XML_COMMENT",9); // <!-- ... --> /*! Base class from which tag and content classes are derived. Never used directly. */ class nb_xml_part{ var $type; //!< Indicates the type of entity - tag, content etc. var $depth; //!< Indicates how deep in the xml tree this entity is. //! A 'pure virtual' function. Derived classes impliment their own version. function dump() { return "<font color='red'>This should never be called!</font><br>"; } }; //! Class for tags in the list of the XML. class nb_xml_tag extends nb_xml_part{ var $tagname; //!< The name of the tag, no namespace prefix var $attribs; //!< An associative array containing attributes keyed by name var $cleansource; //!< the tidy source for the tag. // Next four lines added 28Jun05 NSFB var $nsURI; //!< The namespace URI of the tag if it exists var $nsPrefix; //!< The namespace prefix of the tag if it was used // this is only really meaningful in start elements, potentially used in closed though var $nsDetail; //!< an array of namespaces declared here ['!'] used as prefix for default; /*! Constructor takes a XML tag (without < >) sets $tagname, * $type (START_TAG|END_TAG|CLOSED_TAG), and $attribs, * an associative array of attributes. */ function nb_xml_tag($tagsource) { // initialise to default values $this->nsDetail = array(); $this->nsPrefix = ""; $this->nsURI = false; // "" would be empty, false is undefined // tidy up the ends and make sure the < and > are gone. $tagsource=trim($tagsource); if($tagsource[0]=="<") { $tagsource = substr($tagsource,1); } if($tagsource[strlen($tagsource)-1]==">") { $tagsource = substr($tagsource,0,strlen($tagsource)-1); } $tagsource=trim($tagsource); $this->cleansource=$tagsource; // now check what sort of tag it is... // echo $tagsource[0]; // debug switch($tagsource[0]) { case '?': if(substr($tagsource,0,4)=="?xml"){ $this->type = XML_DECLARATION;} else{ $this->type = XML_INSTRUCTION;} break; case '!': if(substr($tagsource,0,8)=="!DOCTYPE"){ $this->type = XML_DOCTYPE;} elseif(substr($tagsource,0,3)=="!--"){ $this->type = XML_COMMENT;} else{ $this->type = XML_ERROR;} break; case '[': if(substr($tagsource,0,7)=="[CDATA["){ $this->type = XML_CDATA;} else{ $this->type = XML_ERROR;} break; case '/': $this->type = END_TAG; $this->tagname = substr($tagsource,1); // split off namespace part if it exists, added 28Jun05 NSFB if(strpos($this->tagname,":") !== FALSE) list($this->nsPrefix,$this->tagname) = explode(":",$this->tagname,2); else $this->nsPrefix = ""; break; default: //it's a normal tag! if($tagsource[strlen($tagsource)-1]=='/'){ $this->type = CLOSED_TAG; $tagsource = substr($tagsource,0,strlen($tagsource)-1); } else{ $this->type = START_TAG;} // need to extract name and attributes here. if(strpos($tagsource," ")==false) { $nm=$tagsource; $attrs=""; } else list($nm,$attrs)=explode(" ",$tagsource,2); $attrs = preg_replace("/\s+/"," ",$attrs); $attrs = str_replace("= \"","=\"",$attrs); $attrs = str_replace(" =\"","=\"",$attrs); $attrs=trim($attrs); while(strlen($attrs)>2) { if((strpos($attrs," ")!==false)&&((strpos($attrs," ")<strpos($attrs,"=")))) //== would indicate end of attribs, no val { list($attrname,$attrs)=explode(" ",$attrs,2); $attrval=true; } else { list($attrname,$attrs)=explode("=",$attrs,2); $echar=$attrs[0]; $attrs=trim($attrs); if(($echar=="\"")||($echar=="'")) { $attrs = substr($attrs,1); list($attrval,$attrs)=explode($echar,$attrs,2); } else list($attrval,$attrs)=explode(" ",$attrs,2); if($attrval=="") $attrval=true; } $this->attribs[trim($attrname)]=$attrval; $attrs=trim($attrs); // do the namespace collecting stuff, added 28Jun05, NSFB if(strpos($attrname,":")!==false) list($attrname, $prefix) = explode(":",$attrname,2); else $prefix = ""; // so there's a key use this instead of ""; if($attrname=="xmlns") { $this->nsDetail[$prefix] = trim($attrval); } // end of namespace collecting stuff } $this->tagname = $nm; if(strpos($this->tagname,":") !== FALSE) list($this->nsPrefix,$this->tagname) = explode(":",$this->tagname,2); else $this->nsPrefix = ""; // alocate namespace URI if possible , added 28Jun05, NSFB if(array_key_exists($this->nsPrefix,$this->nsDetail)) $this->nsURI = $this->nsDetail[$this->nsPrefix]; break; } } /*! Creates a tidy version of the tag for output. Start, end and closed tags are all correctly formated. Attributes will not nessesaraly be in the original order. \return A string containing the well formated tag. */ function dump() { switch($this->type) { case START_TAG: $rstr = "<".$this->tagname; if(is_array($this->attribs)) { while (list($akey, $aval)=each($this->attribs)) { if(strstr($aval,"\"")===false){ $rstr .= " ".$akey."=\"".$aval."\"";} else{ $rstr .= " ".$akey."='".$aval."'";} } } return $rstr.">"; break; case CLOSED_TAG: $rstr = "<".$this->tagname; if(is_array($this->attribs)) { while (list($akey, $aval)=each($this->attribs)) { if(strstr($aval,"\"")===false){ $rstr .= " ".$akey."=\"".$aval."\"";} else{ $rstr .= " ".$akey."='".$aval."'";} } } else { if(strlen($this->attribs)>0){$rstr .= " ".$this->attribs;} } return $rstr."/>"; break; case END_TAG: return "</".$this->tagname.">"; break; default: return "<".$this->cleansource.">"; break; } } }; /*! Class for content in the XML list - content is everything between tags, but does not include child tags, they become the next item in the list. */ class nb_xml_content extends nb_xml_part{ var $content; /*! constructor takes the content and sets the block type. \param $source is the content. */ function nb_xml_content($source) { $this->content = $source; $this->type = CONTENT; } //! returns the content. function dump() { return $this->content; } } /*! The main class of the XML library reads in XML and provides methods for manipulating it. The XML is held as a list of tags and content, each with a type and depth. */ class nb_easyxml { var $entities; var $source; // C++ private: equiv /*! Get a 'block' of xml from $this->source, either a tag or DATA/CDATA starting * from $point, update point to first char after the 'block' */ function getblock(&$point) { if($this->source[$point]=='<') { //print("<br>tag at $point<br>");//debug switch($this->source[$point+1]) { case '?': $estr = '?'.'>'; // split to be nice to PHP parser; break; case '!': if(substr($this->source,$point+1,3)=="!--"){ $estr = "-->";} elseif($this->source[$point+2]=="["){ $estr = "]]>";} else{ $estr = ">";} break; default: $estr = ">"; break; } $epoint = strpos($this->source,$estr,$point); if($epoint === false) { $point = strlen($this->source); return ""; } $epoint += strlen($estr); $block = substr($this->source,$point,$epoint-$point); $point = $epoint; return $block; } else { $epoint = strpos($this->source,'<',$point); if($epoint === false) { $epoint = strlen($this->source); } $block = substr($this->source,$point,$epoint-$point); $point = $epoint; return $block; } } // C++ protected: equiv /*! Parses XML into the list and does a limited amount of checking. Not really intended to be used from outside the library. \param $source is the xml to be parsed. */ function parsein($source) { $this->entities=""; // this clears out old data. $this->source = $source; $prspt=0; // counter for where in the xml the parser has got to (PaRSe PoinT). $level=0; // how far down the xml tree I am! $nsInfo = array(); // namespace allocation array while($prspt < strlen($this->source)) { $block = $this->getblock($prspt); $printblk = htmlentities($block); // echo "<p><b>".$prspt."</b> ".strlen($block)."<br>".$printblk."</p>";//debug if($block[0] == '<') { $tagcls = new nb_xml_tag($block); $tagcls->depth = $level; if($tagcls->type== START_TAG) { $tagatlev[$level]=$tagcls->tagname; //printf("At level $level got tag '$tagcls->tagname'<br>"); // debug // store namespace info $nsInfo[$level] = $tagcls->nsDetail; $level++; $testlev = $level - 1; // no need to test this level as tag parser does it. while(($tagcls->nsURI === false)&&($testlev >= 0)) { if(array_key_exists($tagcls->nsPrefix, $nsInfo[$testlev])) $tagcls->nsURI = $nsInfo[$testlev][$tagcls->nsPrefix]; $testlev--; } //printf("At level $level got tag '$tagcls->tagname' in namespace {$tagcls->nsURI}<br>"); // debug } $this->entities[] = $tagcls; //$sz = sizeof($this->entities); //debug //echo $this->entities[$sz-1]->depth.": tag - ".$tagcls->tagname."<br>"; // debug if($tagcls->type== END_TAG) { $level--; if($tagatlev[$level]!=$tagcls->tagname) { die("XML error - unmatching start and end tags (/".$tagatlev[$level]." expected, found /".$tagcls->tagname.")"); } } } else { $tblock = trim($block); if($tblock != "") // eliminate all whitespace blocks. { $contcls = new nb_xml_content($block); $contcls->depth = $level; $this->entities[] = $contcls; //echo $contcls->depth.": some content<br>"; // debug } } } } /*! Finds the root tag of the xml. \return the index of the root tag. */ function root() { $index=0; while(($index<=sizeof($this->entities))&&($this->entities[$index]->type!=START_TAG)) { $index++; } if($index<=sizeof($this->entities)){ return $index;} else{ return false;} } /*! Finds the index of the first occurence of a tag after an index point. \param $tagnm is the tag name to search for \param $startpt is the point to start the search from (defaults to 0) \return the index of the first occurence of the tag or false */ function findElement($tagnm, $startpt=0, $namespace = false) { $n = $startpt; $found = false; while(($n < sizeof($this->entities))&&($found===false)) { if(($this->entities[$n]->type==START_TAG)&&($this->entities[$n]->tagname==$tagnm)) { if(($namespace==false)||($namespace==$this->entities[$n]->nsURI)) $found=true; } if($found==false) $n++; } if($found===true){ return $n; } else{ return false;} } function getContent($idx) { if($this->entities[$idx]->type==CLOSED_TAG) return ""; elseif($this->entities[$idx]->type==START_TAG) { $n = $idx+1; $rstr = ""; while(($n < sizeof($this->entities))&&($this->entities[$n]->depth > $this->entities[$idx]->depth)) { if($this->entities[$n]->type==CONTENT) //# need to deal with CDATA too { $rstr .= $this->entities[$n]->dump(); } $n++; } return $rstr; } else return false; } /*! Finds either an immediate child or a peer (same depth) tag. If $tagnm is "" (default) the first child or peer tag is returned, otherwise only one with a matching name. This is the basic function for FindChildTag, FindNextPeerTag etc. This should be considered private, nicer functions exist that call this. \return index of the found tag or false. */ function findclosetag($parent, $tagnm="", $child=false, $namespace=false) { if($parent===false) die("false passed as parent in findclosetag"); $n = $parent+1; $seekdepth=$this->entities[$parent]->depth; //echo "$n parent [$parent], depth $seekdepth;<br>"; if($child) $seekdepth++; $found = false; while(($n < sizeof($this->entities))&&($found===false)&&($this->entities[$n]->depth>=$seekdepth)) { if((($this->entities[$n]->type==START_TAG)|| ($this->entities[$n]->type==CLOSED_TAG)) &&($this->entities[$n]->depth==$seekdepth) &&(($tagnm=="")||($this->entities[$n]->tagname==$tagnm)) &&(($namespace==false)||($namespace==$this->entities[$n]->nsURI)) ){ $found=true;} else{ $n++;} } if($found===true){ return $n; } else{ return false;} } /*! Finds imediate child tags of a particular tag (indicated by index). The tag index returned will either be the first child tag, or the first with a particular name. \param $parent is the index of the tag to find children of. \param $tagnm if provided is the tagname of child to search for. \return the index of the child tag or false */ function findChildElement($parent, $tagnm="", $namespace=false) { return $this->findclosetag($parent, $tagnm, true, $namespace); } /*! Finds the next peer tag of a particular tag (indicated by index) The tag index returned will either be the first peer tag, or the first with a particular name. \param $searchfrom is the index of the tag to search from. \param $tagnm is the tagname of peer to search for - if not given returns the first child index \return the index of the child tag or false */ function findNextPeer($searchfrom, $tagnm="", $namespace=false) { return $this->findclosetag($searchfrom, $tagnm, false, $namespace); } /*! Converts a block into a string of XML. The block goes from the start tag index provided to the matching end tag. \param $tagnum is the index of the start tag of the block to be dumped \return a string containing the block of XML */ function dumpblock($tagnum) { if($tagnum===false) return ""; $n = $tagnum+1; $rstr = $this->entities[$tagnum]->dump(); while(($n < sizeof($this->entities))&&($this->entities[$n]->depth > $this->entities[$tagnum]->depth)) { $rstr .= $this->entities[$n]->dump(); $n++; } return $rstr; } function getName($index) { return $this->entities[$index]->tagname; } function getNamespace($index) { return $this->entities[$index]->nsURI; } function getAttribute($index, $attribname) { if((is_array($this->entities[$index]->attribs))&&(array_key_exists($attribname,$this->entities[$index]->attribs))) return $this->entities[$index]->attribs[$attribname]; else return ""; } }; function nb_easyxmldoc($inp) { $dom = new nb_easyxml; $dom->parsein($inp); return $dom; } ?> --- NEW FILE: rqp_calls.php --- <?php /** * Stack RQP item engine core functions. These act as * the implementation of the RQP. * * @package stackRQP * @subpackage Stack */ /** * RQP Error object * * An object containing the error code (a 16 bit unsigned integer), * a human readable string message and additional details. */ class RQPError { public $identifier; public $message; public $detail; } /** * RQP Variable object * * An object containing a key (name for the variable) and an array of * strings giving the encoded value(s). */ class RQPVar { public $identifier; public $values; } /** * RQP Input object * * An object containing a name-value pair, holding a response to an * interaction */ class RQPInput { public $name; public $value; } /** * RQP Output object * * An object containing a fragment of code in the given format (identified * by MIME type) to be inserted in the document the client is building * representing a particular component of the item (identified by URI). */ class RQPOutput { public $identifier; public $encoding; public $output; } /** * RQP namespace URI * * This constant defines the namespace URI used by RQP */ define('RQP_URI_NAMESPACE', 'http://schemas.rqp.org/rqp'); /** * Base RQP URI for RQP-defined identifiers * * RQP defines standard URIs for common values of the parameters. Currently * there is no RQP domain so we define a base URI here so that it can be * changed later. */ define('RQP_URI_BASE', 'http://rqp.org/'); /** * RQP parameter URIs * * RQP defines standard URIs for common values of the parameters. These are * defined in several categories under different directories under the base * URI. */ define('RQP_URI_ERROR', RQP_URI_BASE . 'errors/'); define('RQP_URI_FORMAT', RQP_URI_BASE . 'formats/'); define('RQP_URI_OUTCOME', RQP_URI_BASE . 'outcomes/'); define('RQP_URI_COMPONENT', RQP_URI_BASE . 'components/'); /** * Stack version (date: YYYYMMDDHH) */ define('STACK_VERSION_DATE', '2005033117'); /** * Stack item parameter URIs * * The Stack item type defines a number of custom values for various * RQP parameters. */ define('STACK_URI_BASE', 'http://eee595.bham.ac.uk/~stack/rqp/'); define('STACK_URI_FORMAT_XML', STACK_URI_BASE . 'formats/xml'); define('STACK_URI_FORMAT_FLAT', STACK_URI_BASE . 'formats/flat'); /** * Returns an array of information about the server * * Interface independent information about the server is returned. This * includes the input formats accepted, output formats available and * output format templates supported. * @return array Returns an associative array with the following fields: * - identifier A URI identifying the server system and version. * - name A short name for the server system. * - description A string containing a description of the server * which may include software version numbers and * other information. * - cloning A boolean flag indicating if the server supports * cloning (the RQP_ProcessTemplate and RQP_Clone * operations). * - implicitCloning A boolean flag indicating if the server supports * the use of item templates together with instance * data (template variables or a seed) in the * rendering calls as if they were regular items. * - rendering A boolean flag indicating if the server supports * rendering (all operations other than * RQP_ServerInformation, RQP_ItemInformation and the * cloning operations. * - itemFormat A comma separated list of input formats supported. * - renderFormats An array of URIs giving the supported render formats. * The first entry is the default. */ function stack_rqp_ServerInformationCall() { $itemFormats = array(STACK_URI_FORMAT_XML, STACK_URI_FORMAT_FLAT); $renderFormats = array(RQP_URI_FORMAT . 'xhtml-1.0-web', RQP_URI_FORMAT . 'xhtml-1.0-print', RQP_URI_FORMAT . 'latex-2e', RQP_URI_FORMAT . 'plain'); // Fix return types where required // (works around a bug in early versions of PHP 5) $itemFormats = soap_encode_array($itemFormats, 'itemFormats', 'anyURI', RQP_URI_NAMESPACE); $renderFormats = soap_encode_array($renderFormats, 'renderFormats', 'anyURI', RQP_URI_NAMESPACE); return array('identifier' => STACK_URI_BASE . 'stack.html#' . STACK_VERSION_DATE, 'name' => 'STACK', 'description' => 'STACK server. See http://sourceforge.net/projects/stack', 'cloning' => FALSE, 'implicitCloning' => TRUE, 'rendering' => TRUE, 'itemFormats' => $itemFormats, 'renderFormats' => $renderFormats); } /** * Returns information about the given item source * * This function examines the item source passed to it and returns * information about it. This information includes the type of item * passed, any syntax or similar errors in the source and attributes * such as if the source describes a template, if the item is adaptive, etc. * @param string $itemSource The source for the item or just the RQP tag * with a period in place of the last space character indicating that the * item source should be loaded from the cache. * @return array Returns an associative array with the following fields: * - format A string giving the type of item source passed. * - sourceErrors A string with a human readable description of any * syntax (or similar) errors. * - template A boolean flag indicating if the source is for an * item template. * - adaptive A boolean flag indicating if the item is adaptive. * - timeDependent A boolean flag indicating if the item is time * dependent. * - canComputerScore A boolean flag indicating if the item can be computer * scored. * - solutionAvailable A boolean flag indicating if worked solutions are * available for the item. * - hintAvailable A boolean flag indicating if hints are available for * the item. * - validationPossible A boolean flag indicating if validation makes sense for * the item. * - maxScore An integer giving the maximum obtainable score for the * item. * - length An integer giving the number of items in the package, one * for regular items. */ function stack_rqp_ItemInformationCall($item,$format,$errors) { $options = stack_options_set($item,$errors); // Ensure $errors is an array if (empty($errors)) { $errors = array(); } // Fix return types where required // (works around a bug in early versions of PHP 5) $sourceErrors = soap_encode_array($errors, 'sourceErrors', 'error', RQP_URI_NAMESPACE); return array('format' => $format, 'sourceErrors' => $sourceErrors, 'template' => ( array_key_exists('questionVars', $item ) ? TRUE : FALSE), 'adaptive' => TRUE, 'timeDependent' => FALSE, 'canComputerScore' => TRUE, 'solutionAvailable' => ( array_key_exists('questionSol', $item ) ? TRUE : FALSE), 'hintAvailable' => FALSE, 'validationPossible' => TRUE, 'maxScore' => $options['QuVal'], 'length' => 1); } /** * Performs template processing to instantiate the template variables for * the item * * The supplied item source is parsed. The randomizer is then seeded using * a server generated seed if none is given. The * template variables are then instantiated by performing template * processing. * @param string $item * @return array Returns an associative array with the following fields: * - seed The integer seed used to seed the randomizer. This * will be equal to the seed parameter unless that * parameter was zero. * - templateVars An array of {@link RQPVar} objects containing the * template variables instantiated during template * processing. */ function stack_rqp_ProcessTemplateCall($item) { } // Clone call function stack_rqp_CloneCall($item, $templateVars) { } // SessionInformation call function stack_rqp_SessionInformationCall($item, $templateVars, $persistence) { // (1) $questionGUID = $item['questionGUID']; $persistence = base64_unserialize($persistentData); $templateVars = FlattenRQPVars($templateVars); $options = stack_options_set($item,$errors); // (2) Generate a $seed $useClonedItem = TRUE; list($usec, $sec) = explode(' ', microtime()); $seed = (integer) ((float) $sec + ((float) $usec * 100000)); $useClonedItem = FALSE; // (3) Decide if we should use a cached $itemClone, or generate our own. if ($useClonedItem and stack_db_rqpcache_itemInst_exists($questionGUID,$seed)) { // (3.1) Get it from the database. $itemClone = stack_db_rqpcache_itemInst_get($questionGUID,$seed); } else { // (4) Generate an $itemClone // (4.1) Should we use the questionVars in the item, or the templateVars? if (array_key_exists('questionVars',$item)) { if (is_array($templateVars) and count($templateVars)) { // We have (i) questionVars, and (ii) templateVars // HACK: should only happen if all questionVars are in templateVars! //Substitute the questionVars with templateVars unset($item['questionVars']); foreach ($templateVars as $key => $value) { $item['questionVars'][] = array('key' => $key, 'value' => $value); } } } // (4.2) Make sure we have a cloned item to work with. $itemClone = NULL; $itemClone = stack_question_inst($item,$seed,$options,$errors); // (4.3) if the cloning generated errors, we should stop here. // HACK, Not done. // (4.4) Cache this if required. stack_db_rqpcache_itemInst_add($questionGUID,$seed,$itemClone); } // END of (3) & (4) // (5) Now make sure the $templateVars are recorded. // (5.1) Only record the last value assigned to each 'questionVarsInst' name. if (array_key_exists('questionVarsInst',$itemClone)) { $templateVars = NULL; foreach ($itemClone['questionVarsInst'] as $key => $value) { $templateVars[$value['key']] = $value['value']; } } // (5.2) questionNoteInst if (array_key_exists('questionNoteInst',$itemClone)) { $persistence['questionNoteInst']= trim($itemClone['questionNoteInst']); } // (6) Work out the correct responses (teacher's answer) $correct = ''; // TODO: Is this the right way to do this? if (array_key_exists('questionAnsInst', $itemClone)) { if (array_key_exists('value', $itemClone['questionAnsInst'])) { $correct = trim($itemClone['questionAnsInst']['value']); } } $correctResponses = array((object) array('name' => '_1', 'value' => $correct)); $outcomeVars = MakeRQPVars($outcomeVars); $templateVars = MakeRQPVars($templateVars); // Fix return types where required // (works around a bug in early versions of PHP 5) $templateVars = soap_encode_array($templateVars, 'templateVars', 'variable', RQP_URI_NAMESPACE); $correctResponses = soap_encode_array($correctResponses, 'correctResponses', 'input', RQP_URI_NAMESPACE); return array('templateVars' => $templateVars, 'correctResponses' => $correctResponses); } // Render function stack_rqp_RenderCall($item, $templateVars, $persistentData, $responses, $advanceState, $namePrefix, $appletBase, $mediaBase, $renderFormat, $modalFormat) { // These are the return values $outcomeVars = array(); $stem = ''; $lastAnswer = ''; $validation = ''; $interactions = ''; $feedback = ''; $solution = ''; $errors = NULL; // (1) $questionGUID = $item['questionGUID']; $persistence = base64_unserialize($persistentData); $templateVars = FlattenRQPVars($templateVars); $responses = FlattenRQPInputs($responses); // Get the display option from the render format if (RQP_URI_FORMAT . 'xhtml-1.0-web' == $renderFormat) { $item['questionOptions']['Display'] = 'LaTeX'; $encoding = 'application/xhtml+xml'; } else if (RQP_URI_FORMAT . 'xhtml-1.0-print' == $renderFormat) { $item['questionOptions']['Display'] = 'LaTeX'; $encoding = 'application/xhtml+xml'; } else if (RQP_URI_FORMAT . 'latex-2e' == $renderFormat) { $item['questionOptions']['Display'] = 'LaTeX Source'; // TODO: What is the MIME type for latex? $encoding = 'application/x-latex'; } else if(RQP_URI_FORMAT . 'plain' == $renderFormat) { $item['questionOptions']['Display'] = 'String'; $encoding = 'text/plain'; } else { return MakeRQPFault(MakeRQPError('unknownRenderFormat', 'This server does not support the ' . $renderFormat . 'render format.'), 'Unknown render format.'); } $options = stack_options_set($item,$errors); // (2) Generate $seed $useClonedItem = TRUE; list($usec, $sec) = explode(' ', microtime()); $seed = (integer) ((float) $sec + ((float) $usec * 100000)); $useClonedItem = FALSE; // (3) Decide if we should use a cached $itemClone, or generate our own. if ($useClonedItem and stack_db_rqpcache_itemInst_exists($questionGUID,$seed)) { // (3.1) Get it from the database. $itemClone = stack_db_rqpcache_itemInst_get($questionGUID,$seed); } else { // (4) Generate an $itemClone // (4.1) Should we use the questionVars in the item, or the templateVars? if (array_key_exists('questionVars',$item)) { if (is_array($templateVars) and count($templateVars)) { // We have (i) questionVars, and (ii) templateVars // HACK: should only happen if all questionVars are in templateVars! //Substitute the questionVars with templateVars unset($item['questionVars']); foreach ($templateVars as $key => $value) { $item['questionVars'][] = array('key' => $key, 'value' => $value); } } } // (4.2) Make sure we have a cloned item to work with. $itemClone = NULL; $itemClone = stack_question_inst($item,$seed,$options,$errors); // (4.3) if the cloning generated errors, we should stop here. // HACK, Not done. // (4.4) Cache this if required. //stack_db_rqpcache_itemInst_add($questionGUID,$seed,$itemClone); } // END of (3) & (4) // (5) Now make sure the $templateVars are recorded. // (5.1) Only record the last value assigned to each 'questionVarsInst' name. if (array_key_exists('questionVarsInst',$itemClone)) { $templateVars = NULL; foreach ($itemClone['questionVarsInst'] as $key => $value) { $templateVars[$value['key']] = $value['value']; } } // (5.2) questionNoteInst if (array_key_exists('questionNoteInst',$itemClone)) { $persistence['questionNoteInst']= trim($itemClone['questionNoteInst']); } // (6) Check for and deal with responses // This could mean we process a response. $score = 0; // The score for this attempt, including penalties. $AnsFieldName = $namePrefix.'_1'; // Get the most recent attempt from the history $last_attempt = array('RawAns' => '', 'AnsVal' => '', 'AnsDisp' => '', 'RawMark' => 0, 'Penalty' => 0, 'FeedBack' => ''); if (is_array($persistence)) { if (array_key_exists('attempts', $persistence)) { if (is_array($persistence['attempts'])) { $last_attempt = end($persistence['attempts']); } } } $RawAns = ''; // Holds the student's answer, if there is one. if (array_key_exists($AnsFieldName, $responses)) { $RawAns = $responses[$AnsFieldName]; } else if (array_key_exists('RawAns', $persistence)) { $RawAns = $persisence['RawAns']; } // Has an attempt been made? if ('' != $RawAns) { // Response processing! $this_attempt = stack_question_inst_mark($itemClone,$options,$RawAns,$errors); // Was the response processed sucessfully? if (array_key_exists('Ans',$this_attempt)) { // Make $this_attempt like the persistent data attempts (but with // the full Ans array) $this_attempt['RawAns'] = $RawAns; $this_attempt['AnsVal'] = $this_attempt['Ans']['value']; $this_attempt['AnsDisp'] = $this_attempt['Ans']['display']; } else { $this_attempt = array('RawAns' => '', 'AnsVal' => '', 'AnsDisp' => '', 'RawMark' => 0, 'Penalty' => 0, 'FeedBack' => ''); } } else { // Use the previous attempt $this_attempt = $last_attempt; } // End of (6) // (7) Advance the state (if required) and calculate the overall score // We advance the state regardless here so that the score calculation // works below, then we remove the new entry if $advanceState is false // (7.1) Update the persistent data // We only count an attempt as "new" from the point of view of // marking and penalties if it is interpreted as being different. if ($this_attempt['AnsVal'] != $last_attempt['AnsVal']) { // Append this attempt to the array in the persistent data if (!is_array($persistence)) { $persistence = array(); } if (!array_key_exists('attempts',$persistence)) { $persistence['attempts'] = array(); } // Push the new info on the end. $per_att = $this_attempt; unset($per_att['Ans']); $persistence['attempts'][] = $per_att; unset($per_att); } // (7.2) Calculate the overall score ($score) // This is taken from stack_question_inst_calcmark($questionAttempts,$qval) // We can't use this because we don't record 'mark'/'validate' // I don't think keeping this info out is a good idea, the client has to // deal with the persistence data. // TODO: Remove the array with the complete history of attempts and keep // just the last attempt in the persistent data. Store the score for that // attempt and just update it rather than walking the whole history. if (is_array($persistence)) { if (array_key_exists('attempts',$persistence)) { // It should always exist! $maxmark=0; $cumpen=0; foreach ( $persistence['attempts'] as $val ) { $cumpen += $val['Penalty']; $maxmark= max( ($val['RawMark'] - $cumpen), $maxmark ); } // Now use the value of the question. $maxmark = $maxmark * $options['QuVal']; $score = $maxmark; } } // (7.3) Undo the change to the persistent data if $advanceState is false if (!$advanceState && $this_attempt['AnsVal'] != $last_attempt['AnsVal']) { // Remove the new attempt array_pop($persistence['attempts']); } // (7.4) Save the raw response in the presistent data so that the // interaction can be filled in with the correct value in future if ('' != $RawAns) { $persistence['RawAns'] = $RawAns; } else if (array_key_exists('RawAns', $persistence)) { // We want to fill in the last response (graded or otherwise) in the // interaction in (8) below $RawAns = $persistence['RawAns']; } // (7.5) Update $last_attempt to be this attempt if required if ($advanceState && $this_attempt['AnsVal'] != $last_attempt['AnsVal']) { $last_attempt = $this_attempt; unset($last_attempt['Ans']); } // (8) Construct the question stem, last answer, validation, feedback and // interactions if ( RQP_URI_FORMAT . 'latex-2e' == $renderFormat ) { // We assume LaTeX source // (8.1) Construct the stem $stem .= trim($itemClone['questionStemInst']); // (8.2) Construct the last answer if ('' !== trim($last_attempt['RawAns'])) { $lastAnswer .= "Your last submitted answer was:\n"; $lastAnswer .= '\begin{verbatim}'; $lastAnswer .= trim($last_attempt['RawAns']); $lastAnswer .= "\end{verbatim}\n\n"; $lastAnswer .= "This was interpreted as:\n"; $lastAnswer .= '\begin{center}'; $lastAnswer .= trim($last_attempt['AnsDisp']); $lastAnswer .= "\end{center}\n"; } // (8.3) Construct the validation if ('true' === $this_attempt['Valid']) { $validation .= trim($this_attempt['RawAns']); $validation .= " was interpretted as:\n"; $validation .= trim($this_attempt['AnsDisp']); $validation .= "\n"; } else { $validation .= trim($this_attempt['FeedBack']) . "\n"; } // (8.4) Construct the interactions $interactions .= trim($this_attempt['AnsDisp']); // (8.5) Construct the feedback if ('true' === $this_attempt['Valid']) { $feedback = trim($this_attempt['FeedBack']) . "\n"; } // (8.6) Construct the solution $correct = ''; if (array_key_exists('questionSolInst', $itemClone)) { if (array_key_exists('display', $itemClone['questionSolInst'])) { $correct = trim($itemClone['questionSolInst']['display']); } } // If there is no worked solution just display the correct response if ('' === $correct) { if (array_key_exists('questionAnsInst', $itemClone)) { if (array_key_exists('display', $itemClone['questionAnsInst'])) { $correct .= "A correct answer is:\n"; $correct .= trim($itemClone['questionAnsInst']['display']); } if (array_key_exists('value', $itemClone['questionAnsInst'])) { if (array_key_exists('display', $itemClone['questionAnsInst'])) { $correct .= "\nwhich can be entered as: "; } else { $correct .= 'A correct answer can be entered as: '; } $correct .= trim($itemClone['questionAnsInst']['value']); } } } $solution .= $correct . "\n"; } else if ( RQP_URI_FORMAT . 'plain' == $renderFormat ) { // (8.1) Construct the stem $stem .= trim($itemClone['questionStemInst']); // (8.2) Construct the last answer if ('' !== trim($last_attempt['RawAns'])) { $lastAnswer .= "Your last submitted answer was:\n"; $lastAnswer .= trim($last_attempt['RawAns']); $lastAnswer .= "\n\n"; $lastAnswer .= "This was interpreted as:\n"; $lastAnswer .= trim($this_attempt['AnsDisp']); $lastAnswer .= "\n"; } // (8.3) Construct the validation if ('true' === $this_attempt['Valid']) { $validation .= trim($this_attempt['RawAns']); $validation .= " was interpretted as:\n"; $validation .= trim($this_attempt['AnsDisp']); $validation .= "\n"; } else { $validation .= trim($this_attempt['FeedBack']) . "\n"; } // (8.4) Construct the interactions $interactions .= trim($this_attempt['AnsDisp']); // (8.5) Construct the feedback if ('true' === $this_attempt['Valid']) { $feedback = trim($this_attempt['FeedBack']) . "\n"; } // (8.6) Construct the solution $correct = ''; if (array_key_exists('questionSolInst', $itemClone)) { if (array_key_exists('display', $itemClone['questionSolInst'])) { $correct = trim($itemClone['questionSolInst']['display']); } } // If there is no worked solution just display the correct response if ('' === $correct) { if (array_key_exists('questionAnsInst', $itemClone)) { if (array_key_exists('display', $itemClone['questionAnsInst'])) { $correct .= "A correct answer is:\n"; $correct .= trim($itemClone['questionAnsInst']['display']); } if (array_key_exists('value', $itemClone['questionAnsInst'])) { if (array_key_exists('display', $itemClone['questionAnsInst'])) { $correct .= "\nwhich can be entered as: "; } else { $correct .= 'A correct answer can be entered as: '; } $correct .= trim($itemClone['questionAnsInst']['value']); } } } $solution .= $correct . "\n"; } else { // We assume HTML of some kind. // (8.1) Construct the stem $stem .= trim($itemClone['questionStemInst']); // (8.2) Construct the last answer if ('' !== trim($last_attempt['RawAns'])) { $lastAnswer .= 'Your last submitted answer was: <code>'; $lastAnswer .= htmlspecialchars(trim($last_attempt['RawAns'])); $lastAnswer .= '</code><br />'; $lastAnswer .= 'This was interpreted as:'; $lastAnswer .= trim($last_attempt['AnsDisp']); } // (8.3) Construct the validation if ('true' === $this_attempt['Valid']) { $validation .= '<code>'; $validation .= htmlspecialchars(trim($this_attempt['RawAns'])); $validation .= '</code> was interpreted as:<br />'; $validation .= trim($this_attempt['AnsDisp']); } else { $validation .= trim($this_attempt['FeedBack']); } // (8.4) Construct the interactions if (RQP_URI_FORMAT . 'xhtml-1.0-web' == $renderFormat) { // Use the most recent response (even if not marked) when in // interactive mode $interactions .= stack_question_inst_try_formfrag($RawAns, $AnsFieldName); } else { $interactions .= trim($this_attempt['AnsDisp']); } // (8.5) Construct the feedback if ('true' === $this_attempt['Valid']) { $feedback = trim($this_attempt['FeedBack']); } // (8.6) Construct the solution $correct = ''; if (array_key_exists('questionSolInst', $itemClone)) { if (array_key_exists('display', $itemClone['questionSolInst'])) { $correct = trim($itemClone['questionSolInst']['display']); } } // If there is no worked solution just display the correct response if ('' === $correct) { if (array_key_exists('questionAnsInst', $itemClone)) { if (array_key_exists('display', $itemClone['questionAnsInst'])) { $correct .= 'A correct answer is:<br />'; $correct .= trim($itemClone['questionAnsInst']['display']); } if (array_key_exists('value', $itemClone['questionAnsInst'])) { if (array_key_exists('display', $itemClone['questionAnsInst'])) { $correct .= '<br />which can be entered as: '; } else { $correct .= 'A correct answer can be entered as: '; } $correct .= trim($itemClone['questionAnsInst']['value']); } } } $solution .= $correct; } // (9) Construct the $outcomeVars // (note, it is tempting to just do $outcomeVars = $this_attempt.......) $outcomeVars[RQP_URI_OUTCOME . 'valid'] = ( array_key_exists('questionVars', $item ) ? 'true' : 'false'); $outcomeVars[RQP_URI_OUTCOME . 'grade'] = $score; $outcomeVars[RQP_URI_OUTCOME . 'rawScore'] = $this_attempt['RawMa... [truncated message content] |