/** * @covers \Cougar\RestService\RestService::bindFromObject * @covers \Cougar\RestService\RestService::handleRequest */ public function testHTML() { $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; $_SERVER["REQUEST_METHOD"] = "GET"; $_SERVER["REQUEST_URI"] = "/get/SimpleCase"; $_SERVER["PHP_SELF"] = "/request_handler"; $_SERVER["HTTP_HOST"] = "localhost"; $_SERVER["HTTP_ACCEPT"] = "text/html"; $object = new AnnotatedRestServiceGetTests(); $this->expectOutputString(Xml::toXml($object->simpleCase())->asXML()); $service = new AnnotatedRestService(new Security()); $service->bindFromObject($object); $service->handleRequest(); }
/** * @covers \Cougar\Util\Xml::toObject * @depends testToXmlWithComplexObject */ public function testToObjectWithComplexObject() { $object = new \stdClass(); $object->property1 = "value1"; $object->property2 = "value2"; $object->property3 = "value3"; $object->property4 = "value4"; $object->subClass = new \stdClass(); $object->subClass->subProperty1 = "subvalue1"; $object->subClass->subProperty2 = "subvalue2"; $object->array = array("a", "b", "c", "d", "e"); $object->jaggedArray = array("a", "b", "c", "d", "e", array("x", "y", "z")); $object->associativeArray = $array = array("a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5); $object->jaggedAssociativeArray = array("a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "extra" => array("x" => "foo", "y" => "bar", "z" => "baz")); $xml = Xml::toXml($object); $new_object = Xml::toObject($xml); $object->array = json_decode(json_encode($object->array)); $object->jaggedArray = json_decode(json_encode($object->jaggedArray)); $object->associativeArray = json_decode(json_encode($object->associativeArray)); $object->jaggedAssociativeArray = json_decode(json_encode($object->jaggedAssociativeArray)); $this->assertEquals($object, $new_object); }
/** * Returns the body of the request, optionally parsing it as a specified * type of object. These are: * * XML - Parse the body as XML and return as a SimpleXML object * OBJECT - Parse the body as a JSON, XML or PHP serialized object and * return as an object * ARRAY - Parse the body as a JSON or XML object and return as an assoc. * array * PHP - Parse the body as serialized PHP data * * If parsing fails the call will throw a BadRequestException. * * If no parse type is specified, the body will be returned as a string. * * @history * 2013.09.30: * (AT) Initial release * 2014.08:06: * (AT) Allow conversion of XML to object or array, or PHP to object * * @version 2014.08.06 * @author (AT) Alberto Trevino, Brigham Young Univ. <*****@*****.**> * * @param string $parse_type xml|object|array|php * @throws \Cougar\Exceptions\BadRequestException * @return mixed Body */ public function body($parse_type = null) { if ($this->body === null) { # Get the body if ($this->__testMode) { # In test mode, read the body from the $_BODY variable global $_BODY; $this->body = trim($_BODY); } else { $this->body = trim(file_get_contents("php://input")); } } # See if we will be parsing the data if ($this->body) { switch (strtolower($parse_type)) { case "xml": return new \SimpleXMLElement($this->body); break; case "object": try { return Xml::toObject(new \SimpleXMLElement($this->body)); } catch (\Exception $e) { try { return unserialize($this->body); } catch (\Exception $e) { $object = json_decode($this->body); if ($object === null) { throw new BadRequestException("Body must be a valid JSON, XML or " . "serialized PHP object"); } return $object; } } break; case "array": try { return Xml::toArray(new \SimpleXMLElement($this->body)); } catch (\Exception $e) { $object = json_decode($this->body, true); if ($object === null) { throw new BadRequestException("Body must be a valid JSON or XML object"); } return $object; } break; case "php": return unserialize($this->body); break; default: return $this->body; break; } } else { return $this->body; } }
/** * @covers \Cougar\RestService\RestService::body */ public function testBodyXmlAsObject() { global $_BODY; $xml = new \SimpleXMLElement("<unit_test/>"); $xml->addChild("key", "value"); $_BODY = $xml->asXML(); $this->assertEquals(Xml::toObject($xml), $this->object->body("object")); }
/** * Handles the incoming request with one of the bound objects. This is a * terminal call, meaning that the proper method will be called and will * automatically send the data to the browser. If an error occurs, it will * be caught and sent to the browser. * * @history * 2013.09.30: * (AT) Initial release * 2013.11.21: * (AT) Add __toHtml() and __toXml() support when converting method * response to HTML or XML * 2014.02.26: * (AT) Fix bug where continue would only exit case statement rather than * going to the next binding when evaluating content-type * 2014.03.12: * (AT) Sort bindings by number of parameters, number of literal * parameters and finally by pattern; improves binding accuracy * 2014.03.14: * (AT) Remove single trailing slashes from the URI path * (AT) Use the backtick (`) as the regex delimiter to allow the colon * to be used in regex classes * 2014.03.24: * (AT) Access path and method variables directly from the object * 2014.08.05: * (AT) Handle JSON, XML and PHP Returns types properly * * @version 2014.08.05 * @author (AT) Alberto Trevino, Brigham Young Univ. <*****@*****.**> * * @throws \Cougar\Exceptions\Exception * @throws \Cougar\Exceptions\AuthenticationRequiredException * @throws \Cougar\Exceptions\BadRequestException * @throws \Cougar\Exceptions\MethodNotAllowedException * @throws \Cougar\Exceptions\NotAcceptableException */ public function handleRequest() { # Remove trailing slash from the path if (mb_strlen($this->path) > 1) { $this->path = preg_replace(':/$:', "", $this->path); } # Sort the bindings from most specific to least specific $path_argument_counts = array(); $literal_path_argument_counts = array(); $patterns = array(); foreach ($this->bindings as $pattern => $method_bindings) { $path_argument_counts[$pattern] = $method_bindings[0]->pathArgumentCount; $literal_path_argument_counts[$pattern] = $method_bindings[0]->literalPathArgumentCount; $patterns[$pattern] = $pattern; } array_multisort($path_argument_counts, SORT_DESC, $literal_path_argument_counts, SORT_DESC, $patterns, SORT_NATURAL, $this->bindings); # Go through the bindings and find those which match the URI pattern and # HTTP method $method_list = array(); $http_method_mismatch = false; foreach ($this->bindings as $pattern => $method_bindings) { # See if the pattern matches if (preg_match("`" . $pattern . "`u", $this->path)) { # See if we are dealing with an OPTIONS request if ($this->method == "OPTIONS") { /* Either we need to continue to iterate through all methods * or we need to gather all the information and generate the * response after the loop. Since this was originally * written for CORS support, we will simply return the list * of all methods for now. */ /* # Get the list of methods the end point will support $methods = array(); foreach($method_bindings as $binding) { foreach($binding->http_methods as $method) { if (! in_array($method, $methods)) { $methods[] = $method; } } } */ # Return all basic methods for now $methods = array("GET", "POST", "PUT", "DELETE"); # Return the response $this->sendResponse(204, null, array("Allow" => implode(", ", $methods))); } else { # Go through each binding and get potential candidates foreach ($method_bindings as $binding) { # See if this binding can handle this method if (!in_array($this->method, $binding->http_methods)) { $http_method_mismatch = true; continue; } # See if there is a specific mimetype this method # accepts switch ($binding->accepts) { case "json": if ($this->header("Content-type") != "application/json") { # This binding doesn't accept this type; # go to the next binding continue 2; } break; case "xml": if ($this->header("Content-type") != "application/xml" && $this->header("Content-type") != "text/xml") { # This binding doesn't accept this type; # go to the next binding continue 2; } break; case "php": if ($this->header("Content-type") != "application/vnd.php.serialized") { # This binding doesn't accept this type; # go to the next one continue 2; } break; case "": # No binding specified; allow binding break; default: # Check the content type directly if (!$binding->accepts != $this->header("Content-type")) { # This binding doesn't accept this type; # go to the next binding continue 2; } break; } $method_list[] = $binding; } } } } # See if we have any methods that can respond to our request if (count($method_list) == 0) { # See if we had methods that matched the pattern but couldn't # support the HTTP method if ($http_method_mismatch) { throw new MethodNotAllowedException("The resource does not support " . $this->method . " operations"); } else { # Return a 400 error throw new BadRequestException("Your request could not be mapped to a known resource"); } } # Go through the potential method bindings and extract the response # type; if none is defined, do JSON, XML, HTML # TODO: add version information once version methodology is defined $response_types = array(); foreach ($method_list as $binding) { if ($binding->returns) { switch ($binding->returns) { case "json": $response_types[] = "application/json"; break; case "xml": $response_types[] = "application/xml"; $response_types[] = "text/xml"; break; case "php": $response_types[] = "application/vnd.php.serialized"; break; default: $response_types[] = $binding->returns; break; } } else { if (!in_array("application/json", $response_types)) { $response_types[] = "application/json"; } if (!in_array("application/vnd.php.serialized", $response_types)) { $response_types[] = "application/vnd.php.serialized"; } if (!in_array("application/xml", $response_types)) { $response_types[] = "application/xml"; } if (!in_array("text/html", $response_types)) { $response_types[] = "text/html"; } } } # Negotiate the response $output_response_types = $this->negotiateResponseType($response_types); # Find the binding that best fits # TODO improve detection $binding = null; foreach ($output_response_types as $response_type) { foreach ($method_list as $potential_binding) { if ($potential_binding->returns == $response_type) { $binding = $potential_binding; break 2; } else { if ($potential_binding->returns == "json" && $response_type == "application/json") { $binding = $potential_binding; break 2; } else { if ($potential_binding->returns == "xml" && $response_type == "application/xml") { $binding = $potential_binding; break 2; } else { if ($potential_binding->returns == "xml" && $response_type == "text/xml") { $binding = $potential_binding; break 2; } else { if (!$potential_binding->returns) { $binding = $potential_binding; break 2; } } } } } } } # If we don't have a binding, send a NotAcceptable exception if (!$binding) { throw new NotAcceptableException("The requested resource cannot be represented by any " . "of the acceptable representations requested by the client"); } # If we've made it this far, we have found our optimal binding # Get the object associated with the binding if (!array_key_exists($binding->object, $this->objects)) { throw new Exception("Could not find object!"); } $object = $this->objects[$binding->object]; $r_object = new \ReflectionClass($object); # Get the method associated with the binding if (!$r_object->hasMethod($binding->method)) { throw new Exception("Could not find method!"); } $r_method = $r_object->getMethod($binding->method); # Assemble the method parameters into an array $params = array(); foreach ($r_method->getParameters() as $r_param) { # Get the default value of the parameter (if it has one) $default_param_value = null; if ($r_param->isOptional()) { $default_param_value = $r_param->getDefaultValue(); } # See if we have a binding for this parameter if (array_key_exists($r_param->name, $binding->parameters)) { # Get the parameter information $param_info = $binding->parameters[$r_param->name]; # See where the value is coming from switch ($param_info->source) { case "URI": # See if we need to make an array with the remaining # parameters if ($param_info->array) { $params[] = array_slice($this->uri, $param_info->index); } else { $params[] = $this->uriValue($param_info->index, $param_info->type, $default_param_value); } break; case "GET": if ($param_info->array) { $params[] = $_GET; } else { $params[] = $this->getValue($param_info->index, $param_info->type, $default_param_value); } break; case "POST": if ($param_info->array) { $params[] = $_POST; } else { $params[] = $this->postValue($param_info->index, $param_info->type, $default_param_value); } break; case "BODY": $params[] = $this->body($param_info->type); break; case "QUERY": $params[] = $this->getQuery(); break; case "IDENTITY": break; default: throw new Exception("Invalid parameter source"); } } else { # We don't have a binding; pass the default value $params[] = $default_param_value; } } # See if the call requires authentication switch ($binding->authentication) { case "required": $auth_success = $this->security->authenticate(); if (!$auth_success) { throw new AuthenticationRequiredException(); } break; case "optional": $this->security->authenticate(); break; default: # No need to do anything break; } # Call the method $data = call_user_func_array(array($object, $binding->method), $params); # Send the data in the appropriate data type if ($data !== null) { switch ($response_type) { case "application/json": $this->sendResponse(200, json_encode($data), array(), $response_type); break; case "application/vnd.php.serialized": $this->sendResponse(200, serialize($data), array(), $response_type); break; case "application/xml": case "text/xml": # TODO: Implement XSD # See if we have an object if (is_object($data)) { # See if this is a SimpleXMLElement if ($data instanceof \SimpleXMLElement) { $xml = $data->asXML(); } else { if (method_exists($data, "__toXml")) { $xml = $data->__toXml(); } else { $xml = Xml::toXml($data, $binding->xmlRootElement, $binding->xmlObjectName, $binding->xmlObjectList); } } } else { # Convert data to XML $xml = Xml::toXml($data, $binding->xmlRootElement, $binding->xmlObjectName, $binding->xmlObjectList); } if (is_object($xml)) { if ($xml instanceof \SimpleXMLElement) { $xml = $xml->asXML(); } } # Send the response $this->sendResponse(200, $xml, array(), $response_type); break; case "text/html": # See if this is an object $html = null; $xml = null; if (is_object($data)) { # See if object has __toHtml() method if (method_exists($data, "__toHtml")) { $html = $data->__toHtml(); } else { if ($data instanceof \SimpleXMLElement) { $xml = $data->asXML(); } else { if (method_exists($data, "__toXml")) { $xml = $data->__toXml(); } } } } if ($html === null && $xml === null) { # Convert data to XML $xml = Xml::toXml($data, $binding->xmlRootElement, $binding->xmlObjectName, $binding->xmlObjectList); # See if we have an XSL transform if ($binding->xsl) { $xsl = new \SimpleXMLElement($binding["xsl"]); $xslt = new \XSLTProcessor(); $xslt->importStylesheet($xsl); $html = $xslt->transformToXml($xml); } } # See which kind of response we have if ($html !== null) { $this->sendResponse(200, $html, array(), $response_type); } else { if (is_object($xml)) { if ($xml instanceof \SimpleXMLElement) { $xml = $xml->asXML(); } } $this->sendResponse(200, $xml, array(), "text/xml"); } break; default: $this->sendResponse(200, $data, array(), $response_type); } } else { $this->sendResponse(204, null, array(), $response_type); } }