diff --git a/.gitignore b/.gitignore
index 7742743..2a0b5bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ php-cs-fixer.phar
*.Identifier
/tests/.phpunit.cache
/.vscode
+tests/WebFiori/Tests/Http/output-stream.txt
+/OpenAPI_files
diff --git a/WebFiori/Http/APIFilter.php b/WebFiori/Http/APIFilter.php
index ea9551a..19fddfa 100644
--- a/WebFiori/Http/APIFilter.php
+++ b/WebFiori/Http/APIFilter.php
@@ -21,6 +21,7 @@
*
*/
class APIFilter {
+ private $requestParameters = [];
/**
* A constant that indicates a given value is invalid.
*
@@ -103,6 +104,10 @@ public function addRequestParameter(RequestParameter $reqParam) {
$attribute[$filterIdx][] = FILTER_DEFAULT;
}
$this->paramDefs[] = $attribute;
+ $this->requestParameters[] = $reqParam;
+ }
+ public function getParameters() : array {
+ return $this->requestParameters;
}
/**
* Clears the arrays that are used to store filtered and not-filtered variables.
@@ -118,6 +123,7 @@ public function clearInputs() {
*/
public function clearParametersDef() {
$this->paramDefs = [];
+ $this->requestParameters = [];
}
/**
* Filter the values of an associative array.
diff --git a/WebFiori/Http/APITestCase.php b/WebFiori/Http/APITestCase.php
index 30c9208..e64ffc6 100644
--- a/WebFiori/Http/APITestCase.php
+++ b/WebFiori/Http/APITestCase.php
@@ -23,13 +23,38 @@
*/
class APITestCase extends TestCase {
const NL = "\r\n";
- const DEFAULT_OUTPUT_STREAM = __DIR__.DIRECTORY_SEPARATOR.'outputStream.txt';
+ const DEFAULT_OUTPUT_STREAM = __DIR__.DIRECTORY_SEPARATOR.'output-stream.txt';
/**
* The path to the output stream file.
*
* @var string
*/
private $outputStreamPath;
+ /**
+ * Backup of global variables.
+ *
+ * @var array
+ */
+ private $globalsBackup;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->globalsBackup = [
+ 'GET' => $_GET,
+ 'POST' => $_POST,
+ 'FILES' => $_FILES,
+ 'SERVER' => $_SERVER
+ ];
+ }
+
+ protected function tearDown(): void {
+ $_GET = $this->globalsBackup['GET'];
+ $_POST = $this->globalsBackup['POST'];
+ $_FILES = $this->globalsBackup['FILES'];
+ $_SERVER = $this->globalsBackup['SERVER'];
+ SecurityContext::clear();
+ parent::tearDown();
+ }
/**
* Sets the path to the file which is used to store API output temporarily.
*
@@ -74,7 +99,7 @@ public function addFile(string $fileIdx, string $filePath, bool $reset = false)
$_FILES[$fileIdx]['error'] = [];
}
$info = $this->extractPathAndName($filePath);
- $path = $info['path'].DS.$info['name'];
+ $path = $info['path'].DIRECTORY_SEPARATOR.$info['name'];
$_FILES[$fileIdx]['name'][] = $info['name'];
$_FILES[$fileIdx]['type'][] = mime_content_type($path);
@@ -102,47 +127,109 @@ public function addFile(string $fileIdx, string $filePath, bool $reset = false)
* @param array $httpHeaders An optional associative array that can be used
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
+ *
+ * @param UserInterface|null $user Optional user to authenticate the request with.
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
*
* @return string The method will return the output of the endpoint.
*/
- public function callEndpoint(WebServicesManager $manager, string $requestMethod, string $apiEndpointName, array $parameters = [], array $httpHeaders = []) : string {
- $manager->setOutputStream(fopen($this->getOutputFile(),'w'));
+ public function callEndpoint(WebServicesManager $manager, string $requestMethod, string $apiEndpointName, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
$method = strtoupper($requestMethod);
- putenv('REQUEST_METHOD='.$method);
+ $serviceName = $this->resolveServiceName($apiEndpointName);
+
+ $this->setupRequest($method, $serviceName, $parameters, $httpHeaders);
+
+ $manager->setOutputStream(fopen($this->getOutputFile(), 'w'));
+ $manager->setRequest(Request::createFromGlobals());
+ SecurityContext::setCurrentUser($user);
+ $manager->process();
+
+ $result = $manager->readOutputStream();
- if (class_exists($apiEndpointName)) {
- $service = new $apiEndpointName();
+ if (file_exists($this->getOutputFile())) {
+ unlink($this->getOutputFile());
+ }
+
+ return $this->formatOutput($result);
+ }
+
+ /**
+ * Resolves service name from class name or returns the name as-is.
+ *
+ * @param string $nameOrClass Service name or class name
+ *
+ * @return string The resolved service name
+ */
+ private function resolveServiceName(string $nameOrClass): string {
+ if (class_exists($nameOrClass)) {
+ $reflection = new \ReflectionClass($nameOrClass);
- if ($service instanceof AbstractWebService) {
- $apiEndpointName = $service->getName();
+ if ($reflection->isSubclassOf(WebService::class)) {
+ $constructor = $reflection->getConstructor();
+
+ if ($constructor && $constructor->getNumberOfRequiredParameters() === 0) {
+ $service = $reflection->newInstance();
+ return $service->getName();
+ }
}
}
- if ($method == RequestMethod::POST || $method == RequestMethod::PUT || $method == RequestMethod::PATCH) {
- foreach ($parameters as $key => $val) {
- $_POST[$key] = $this->parseVal($val);
- }
- $_POST['service'] = $apiEndpointName;
- $_SERVER['CONTENT_TYPE'] = 'multipart/form-data';
- $this->unset($_POST, $parameters, $manager, $httpHeaders);
+
+ return $nameOrClass;
+ }
+
+ /**
+ * Sets up the request environment.
+ *
+ * @param string $method HTTP method
+ * @param string $serviceName Service name
+ * @param array $parameters Request parameters
+ * @param array $httpHeaders An optional associative array that can be used
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
+ * @param UserInterface|null $user Optional user to authenticate the request with.
+ */
+ private function setupRequest(string $method, string $serviceName, array $parameters, array $httpHeaders) {
+ putenv('REQUEST_METHOD=' . $method);
+
+ // Normalize header names to lowercase for case-insensitive comparison
+ $normalizedHeaders = [];
+ foreach ($httpHeaders as $name => $value) {
+ $normalizedHeaders[strtolower($name)] = $value;
+ }
+
+ if (in_array($method, [RequestMethod::POST, RequestMethod::PUT, RequestMethod::PATCH])) {
+ $_POST = $parameters;
+ $_POST['service'] = $serviceName;
+ $_SERVER['CONTENT_TYPE'] = $normalizedHeaders['content-type'] ?? 'application/x-www-form-urlencoded';
} else {
- foreach ($parameters as $key => $val) {
- $_GET[$key] = $this->parseVal($val);
- }
- $_GET['service'] = $apiEndpointName;
- $this->unset($_GET, $parameters, $manager, $httpHeaders);
+ $_GET = $parameters;
+ $_GET['service'] = $serviceName;
}
-
- $retVal = $manager->readOutputStream();
- unlink($this->getOutputFile());
+ foreach ($normalizedHeaders as $name => $value) {
+ if ($name !== 'content-type') {
+ $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $name))] = $value;
+ }
+ }
+ }
+
+ /**
+ * Formats the output, attempting to pretty-print JSON if possible.
+ *
+ * @param string $output Raw output
+ *
+ * @return string Formatted output
+ */
+ private function formatOutput(string $output): string {
try {
- $json = Json::decode($retVal);
+ $json = Json::decode($output);
$json->setIsFormatted(true);
- return $json.'';
+ return $json . '';
} catch (JsonException $ex) {
- return $retVal;
+ return $output;
}
-
}
/**
* Creates a formatted string from calling an API.
@@ -166,26 +253,6 @@ public function format(string $output) {
echo ". '$expl[$x]]'".$nl;
}
}
- private function parseVal($val) {
- $type = gettype($val);
-
- if ($type == 'array') {
- $array = [];
-
- foreach ($val as $arrVal) {
- if (gettype($val) == 'string') {
- $array[] = "'".$arrVal."'";
- } else {
- $array[] = $arrVal;
- }
- }
-
- return implode(',', $array);
- } else if ($type == 'boolean') {
- return $type === true ? 'y' : 'n';
- }
- return $val;
- }
/**
* Sends a DELETE request to specific endpoint.
*
@@ -200,11 +267,15 @@ private function parseVal($val) {
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
+ * @param UserInterface|null $user Optional user to authenticate the request with.
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function deletRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string {
- return $this->callEndpoint($manager, RequestMethod::DELETE, $endpoint, $parameters, $httpHeaders);
+ public function deleteRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ return $this->callEndpoint($manager, RequestMethod::DELETE, $endpoint, $parameters, $httpHeaders, $user);
}
/**
* Sends a GET request to specific endpoint.
@@ -219,8 +290,8 @@ public function deletRequest(WebServicesManager $manager, string $endpoint, arra
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function getRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string {
- return $this->callEndpoint($manager, RequestMethod::GET, $endpoint, $parameters, $httpHeaders);
+ public function getRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ return $this->callEndpoint($manager, RequestMethod::GET, $endpoint, $parameters, $httpHeaders, $user);
}
/**
* Sends a POST request to specific endpoint.
@@ -236,11 +307,15 @@ public function getRequest(WebServicesManager $manager, string $endpoint, array
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
+ * @param UserInterface|null $user Optional user to authenticate the request with.
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function postRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string {
- return $this->callEndpoint($manager, RequestMethod::POST, $endpoint, $parameters, $httpHeaders);
+ public function postRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ return $this->callEndpoint($manager, RequestMethod::POST, $endpoint, $parameters, $httpHeaders, $user);
}
/**
* Sends a PUT request to specific endpoint.
@@ -256,11 +331,87 @@ public function postRequest(WebServicesManager $manager, string $endpoint, array
* to mimic HTTP request headers. The keys of the array are names of headers
* and the value of each key represents the value of the header.
*
+ * @param UserInterface|null $user Optional user to authenticate the request with.
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
+ * @return string The method will return the output that was produced by
+ * the endpoint as string.
+ */
+ public function putRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ return $this->callEndpoint($manager, RequestMethod::PUT, $endpoint, $parameters, $httpHeaders, $user);
+ }
+ /**
+ * Sends a PATCH request to specific endpoint.
+ *
+ * @param WebServicesManager $manager The manager which is used to manage the endpoint.
+ *
+ * @param string $endpoint The name of the endpoint.
+ *
+ * @param array $parameters An optional array of request parameters that can be
+ * passed to the endpoint.
+ *
+ * @param array $httpHeaders An optional associative array that can be used
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
+ * @param UserInterface|null $user Optional user to authenticate the request with.
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
* @return string The method will return the output that was produced by
* the endpoint as string.
*/
- public function putRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string {
- return $this->callEndpoint($manager, RequestMethod::PUT, $endpoint, $parameters, $httpHeaders);
+ public function patchRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ return $this->callEndpoint($manager, RequestMethod::PATCH, $endpoint, $parameters, $httpHeaders, $user);
+ }
+ /**
+ * Sends an OPTIONS request to specific endpoint.
+ *
+ * @param WebServicesManager $manager The manager which is used to manage the endpoint.
+ *
+ * @param string $endpoint The name of the endpoint.
+ *
+ * @param array $parameters An optional array of request parameters that can be
+ * passed to the endpoint.
+ *
+ * @param array $httpHeaders An optional associative array that can be used
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
+ * @param UserInterface|null $user Optional user to authenticate the request with.
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
+ * @return string The method will return the output that was produced by
+ * the endpoint as string.
+ */
+ public function optionsRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ return $this->callEndpoint($manager, RequestMethod::OPTIONS, $endpoint, $parameters, $httpHeaders, $user);
+ }
+ /**
+ * Sends a HEAD request to specific endpoint.
+ *
+ * @param WebServicesManager $manager The manager which is used to manage the endpoint.
+ *
+ * @param string $endpoint The name of the endpoint.
+ *
+ * @param array $parameters An optional array of request parameters that can be
+ * passed to the endpoint.
+ *
+ * @param array $httpHeaders An optional associative array that can be used
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
+ * @param UserInterface|null $user Optional user to authenticate the request with.
+ * to mimic HTTP request headers. The keys of the array are names of headers
+ * and the value of each key represents the value of the header.
+ *
+ * @return string The method will return the output that was produced by
+ * the endpoint as string.
+ */
+ public function headRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string {
+ return $this->callEndpoint($manager, RequestMethod::HEAD, $endpoint, $parameters, $httpHeaders, $user);
}
private function extractPathAndName($absPath): array {
$DS = DIRECTORY_SEPARATOR;
@@ -286,24 +437,4 @@ private function extractPathAndName($absPath): array {
'path' => ''
];
}
- private function unset(array &$arr, array $params, WebServicesManager $m, array $httpHeaders) {
- foreach ($httpHeaders as $header => $value) {
- $trHeader = trim($header.'');
- $trVal = trim($value.'');
- if (strlen($trHeader) != 0) {
- $_SERVER['HTTP_'.strtoupper($trHeader)] = $trVal;
- }
- }
- $m->setRequest(Request::createFromGlobals());
- $m->process();
-
- foreach ($params as $key => $val) {
- unset($arr[$key]);
- }
-
- foreach ($httpHeaders as $header => $value) {
- $trHeader = trim($header.'');
- unset($_SERVER['HTTP_'.strtoupper($trHeader)]);
- }
- }
}
diff --git a/WebFiori/Http/AbstractWebService.php b/WebFiori/Http/AbstractWebService.php
index f6b97bb..516be9a 100644
--- a/WebFiori/Http/AbstractWebService.php
+++ b/WebFiori/Http/AbstractWebService.php
@@ -1,844 +1,8 @@
- *
It can contain the letters [A-Z] and [a-z].
- *
It can contain the numbers [0-9].
- *
It can have the character '-' and the character '_'.
- *
- * If The given name is invalid, the name of the service will be set to 'new-service'.
- *
- * @param string $name The name of the web service.
- *
- * @param WebServicesManager|null $owner The manager which is used to
- * manage the web service.
- */
- public function __construct(string $name) {
- if (!$this->setName($name)) {
- $this->setName('new-service');
- }
- $this->reqMethods = [];
- $this->parameters = [];
- $this->responses = [];
- $this->requireAuth = true;
- $this->sinceVersion = '1.0.0';
- $this->serviceDesc = '';
- $this->request = Request::createFromGlobals();
- }
- /**
- * Returns an array that contains all possible requests methods at which the
- * service can be called with.
- *
- * The array will contain strings like 'GET' or 'POST'. If no request methods
- * where added, the array will be empty.
- *
- * @return array An array that contains all possible requests methods at which the
- * service can be called using.
- *
- */
- public function &getRequestMethods() : array {
- return $this->reqMethods;
- }
- /**
- * Returns an array that contains an objects of type RequestParameter.
- *
- * @return array an array that contains an objects of type RequestParameter.
- *
- */
- public final function &getParameters() : array {
- return $this->parameters;
- }
- /**
- *
- * @return string
- *
- */
- public function __toString() {
- $retVal = "APIAction[\n";
- $retVal .= " Name => '".$this->getName()."',\n";
- $retVal .= " Description => '".$this->getDescription()."',\n";
- $since = $this->getSince() === null ? 'null' : $this->getSince();
- $retVal .= " Since => '$since',\n";
- $reqMethodsStr = "[\n";
- $comma = ',';
-
- for ($x = 0, $count = count($this->getRequestMethods()) ; $x < $count ; $x++) {
- $meth = $this->getRequestMethods()[$x];
-
- if ($x + 1 == $count) {
- $comma = '';
- }
- $reqMethodsStr .= " $meth$comma\n";
- }
- $reqMethodsStr .= " ],\n";
- $retVal .= " Request Methods => $reqMethodsStr";
- $paramsStr = "[\n";
-
- $comma = ',';
-
- for ($x = 0 , $count = count($this->getParameters()); $x < $count ; $x++) {
- $param = $this->getParameters()[$x];
- $paramsStr .= " ".$param->getName()." => [\n";
- $paramsStr .= " Type => '".$param->getType()."',\n";
- $descStr = $param->getDescription() === null ? 'null' : $param->getDescription();
- $paramsStr .= " Description => '$descStr',\n";
- $isOptional = $param->isOptional() ? 'true' : 'false';
- $paramsStr .= " Is Optional => '$isOptional',\n";
- $defaultStr = $param->getDefault() === null ? 'null' : $param->getDefault();
- $paramsStr .= " Default => '$defaultStr',\n";
- $min = $param->getMinValue() === null ? 'null' : $param->getMinValue();
- $paramsStr .= " Minimum Value => '$min',\n";
- $max = $param->getMaxValue() === null ? 'null' : $param->getMaxValue();
-
- if ($x + 1 == $count) {
- $comma = '';
- }
- $paramsStr .= " Maximum Value => '$max'\n ]$comma\n";
- }
- $paramsStr .= " ],\n";
- $retVal .= " Parameters => $paramsStr";
- $responsesStr = "[\n";
- $count = count($this->getResponsesDescriptions());
- $comma = ',';
-
- for ($x = 0 ; $x < $count ; $x++) {
- if ($x + 1 == $count) {
- $comma = '';
- }
- $responsesStr .= " Response #$x => '".$this->getResponsesDescriptions()[$x]."'".$comma."\n";
- }
- $responsesStr .= " ]\n";
-
- return $retVal." Responses Descriptions => $responsesStr]\n";
- }
- /**
- * Adds new request parameter to the service.
- *
- * The parameter will only be added if no parameter which has the same
- * name as the given one is added before.
- *
- * @param RequestParameter|array $param The parameter that will be added. It
- * can be an object of type 'RequestParameter' or an associative array of
- * options. The array can have the following indices:
- *
- *
name: The name of the parameter. It must be provided.
- *
type: The datatype of the parameter. If not provided, 'string' is used.
- *
optional: A boolean. If set to true, it means the parameter is
- * optional. If not provided, 'false' is used.
- *
min: Minimum value of the parameter. Applicable only for
- * numeric types.
- *
max: Maximum value of the parameter. Applicable only for
- * numeric types.
- *
allow-empty: A boolean. If the type of the parameter is string or string-like
- * type and this is set to true, then empty strings will be allowed. If
- * not provided, 'false' is used.
- *
custom-filter: A PHP function that can be used to filter the
- * parameter even further
- *
default: An optional default value to use if the parameter is
- * not provided and is optional.
- *
description: The description of the attribute.
- *
- *
- * @return bool If the given request parameter is added, the method will
- * return true. If it was not added for any reason, the method will return
- * false.
- *
- */
- public function addParameter($param) : bool {
- if (gettype($param) == 'array') {
- $param = RequestParameter::create($param);
- }
-
- if ($param instanceof RequestParameter && !$this->hasParameter($param->getName())) {
- $this->parameters[] = $param;
-
- return true;
- }
-
- return false;
- }
- /**
- * Adds multiple parameters to the web service in one batch.
- *
- * @param array $params An associative or indexed array. If the array is indexed,
- * each index should hold an object of type 'RequestParameter'. If it is associative,
- * then the key will represent the name of the web service and the value of the
- * key should be a sub-associative array that holds parameter options.
- *
- */
- public function addParameters(array $params) {
- foreach ($params as $paramIndex => $param) {
- if ($param instanceof RequestParameter) {
- $this->addParameter($param);
- } else if (gettype($param) == 'array') {
- $param['name'] = $paramIndex;
- $this->addParameter(RequestParameter::create($param));
- }
- }
- }
- /**
- * Adds new request method.
- *
- * The value that will be passed to this method can be any string
- * that represents HTTP request method (e.g. 'get', 'post', 'options' ...). It
- * can be in upper case or lower case.
- *
- * @param string $method The request method.
- *
- * @return bool true in case the request method is added. If the given
- * request method is already added or the method is unknown, the method
- * will return false.
- *
- */
- public final function addRequestMethod(string $method) : bool {
- $uMethod = strtoupper(trim($method));
-
- if (in_array($uMethod, RequestMethod::getAll()) && !in_array($uMethod, $this->reqMethods)) {
- $this->reqMethods[] = $uMethod;
-
- return true;
- }
-
- return false;
- }
- /**
- * Adds response description.
- *
- * It is used to describe the API for front-end developers and help them
- * identify possible responses if they call the API using the specified service.
- *
- * @param string $description A paragraph that describes one of
- * the possible responses due to calling the service.
- *
- */
- public final function addResponseDescription(string $description) {
- $trimmed = trim($description);
-
- if (strlen($trimmed) != 0) {
- $this->responses[] = $trimmed;
- }
- }
- /**
- * Returns an object that contains the value of the header 'authorization'.
- *
- * @return AuthHeader|null The object will have two primary attributes, the first is
- * the 'scheme' and the second one is 'credentials'. The 'scheme'
- * will contain the name of the scheme which is used to authenticate
- * ('basic', 'bearer', 'digest', etc...). The 'credentials' will contain
- * the credentials which can be used to authenticate the client.
- *
- */
- public function getAuthHeader() {
- if ($this->request !== null) {
- return $this->request->getAuthHeader();
- }
- return null;
- }
-
- /**
- * Sets the request instance for the service.
- *
- * @param mixed $request The request instance (Request, etc.)
- */
- public function setRequest($request) {
- $this->request = $request;
- }
- /**
- * Returns the description of the service.
- *
- * @return string The description of the service. Default is empty string.
- *
- */
- public final function getDescription() : string {
- return $this->serviceDesc;
- }
- /**
- * Returns an associative array or an object of type Json of filtered request inputs.
- *
- * The indices of the array will represent request parameters and the
- * values of each index will represent the value which was set in
- * request body. The values will be filtered and might not be exactly the same as
- * the values passed in request body. Note that if a parameter is optional and not
- * provided in request body, its value will be set to 'null'. Note that
- * if request content type is 'application/json', only basic filtering will
- * be applied. Also, parameters in this case don't apply.
- *
- * @return array|Json|null An array of filtered request inputs. This also can
- * be an object of type 'Json' if request content type was 'application/json'.
- * If no manager was associated with the service, the method will return null.
- *
- */
- public function getInputs() {
- $manager = $this->getManager();
-
- if ($manager !== null) {
- return $manager->getInputs();
- }
-
- return null;
- }
- /**
- * Returns the manager which is used to manage the web service.
- *
- * @return WebServicesManager|null If set, it is returned as an object.
- * Other than that, null is returned.
- */
- public function getManager() {
- return $this->owner;
- }
- /**
- * Returns the name of the service.
- *
- * @return string The name of the service.
- *
- */
- public final function getName() : string {
- return $this->name;
- }
- /**
- * Map service parameter to specific instance of a class.
- *
- * This method assumes that every parameter in the request has a method
- * that can be called to set attribute value. For example, if a parameter
- * has the name 'user-last-name', the mapping method should have the name
- * 'setUserLastName' for mapping to work correctly.
- *
- * @param string $clazz The class that service parameters will be mapped
- * to.
- *
- * @param array $settersMap An optional array that can have custom
- * setters map. The indices of the array should be parameters names
- * and the values are the names of setter methods in the class.
- *
- * @return object The Method will return an instance of the class with
- * all its attributes set to request parameter's values.
- */
- public function getObject(string $clazz, array $settersMap = []) {
- $mapper = new ObjectMapper($clazz, $this);
-
- foreach ($settersMap as $param => $method) {
- $mapper->addSetterMap($param, $method);
- }
-
- return $mapper->map($this->getInputs());
- }
- /**
- * Returns one of the parameters of the service given its name.
- *
- * @param string $paramName The name of the parameter.
- *
- * @return RequestParameter|null Returns an objects of type RequestParameter if
- * a parameter with the given name was found. null if nothing is found.
- *
- */
- public final function getParameterByName(string $paramName) {
- $trimmed = trim($paramName);
-
- if (strlen($trimmed) != 0) {
- foreach ($this->parameters as $param) {
- if ($param->getName() == $trimmed) {
- return $param;
- }
- }
- }
-
- return null;
- }
- /**
- * Returns the value of request parameter given its name.
- *
- * @param string $paramName The name of request parameter as specified when
- * it was added to the service.
- *
- * @return mixed|null If the parameter is found and its value is set, the
- * method will return its value. Other than that, the method will return null.
- * For optional parameters, if a default value is set for it, the method will
- * return that value.
- *
- */
- public function getParamVal(string $paramName) {
- $inputs = $this->getInputs();
- $trimmed = trim($paramName);
-
- if ($inputs !== null) {
- if ($inputs instanceof Json) {
- return $inputs->get($trimmed);
- } else {
- return $inputs[$trimmed] ?? null;
- }
- }
-
- return null;
- }
- /**
- * Returns an indexed array that contains information about possible responses.
- *
- * It is used to describe the API for front-end developers and help them
- * identify possible responses if they call the API using the specified service.
- *
- * @return array An array that contains information about possible responses.
- *
- */
- public final function getResponsesDescriptions() : array {
- return $this->responses;
- }
- /**
- * Returns version number or name at which the service was added to the API.
- *
- * Version number is set based on the version number which was set in the
- * class WebAPI.
- *
- * @return string The version number at which the service was added to the API.
- * Default is '1.0.0'.
- *
- */
- public final function getSince() : string {
- return $this->sinceVersion;
- }
- /**
- * Checks if the service has a specific request parameter given its name.
- *
- * Note that the name of the parameter is case-sensitive. This means that
- * 'get-profile' is not the same as 'Get-Profile'.
- *
- * @param string $name The name of the parameter.
- *
- * @return bool If a request parameter which has the given name is added
- * to the service, the method will return true. Otherwise, the method will return
- * false.
- *
- */
- public function hasParameter(string $name) : bool {
- $trimmed = trim($name);
-
- if (strlen($name) != 0) {
- foreach ($this->getParameters() as $param) {
- if ($param->getName() == $trimmed) {
- return true;
- }
- }
- }
-
- return false;
- }
- /**
- * Checks if the client is authorized to use the service or not.
- *
- * The developer should implement this method in a way it returns a boolean.
- * If the method returns true, it means the client is allowed to use the service.
- * If the method returns false, then he is not authorized and a 401 error
- * code will be sent back. If the method returned nothing, then it means the
- * user is authorized to call the API. If WebFiori framework is used, it is
- * possible to perform the functionality of this method using middleware.
- *
- */
- public function isAuthorized() {
- }
- /**
- * Returns the value of the property 'requireAuth'.
- *
- * The property is used to tell if the authorization step will be skipped
- * or not when the service is called.
- *
- * @return bool The method will return true if authorization step required.
- * False if the authorization step will be skipped. Default return value is true.
- *
- */
- public function isAuthRequired() : bool {
- return $this->requireAuth;
- }
-
- /**
- * Validates the name of a web service or request parameter.
- *
- * @param string $name The name of the service or parameter.
- *
- * @return bool If valid, true is returned. Other than that, false is returned.
- */
- public static function isValidName(string $name): bool {
- $trimmedName = trim($name);
- $len = strlen($trimmedName);
-
- if ($len != 0) {
- for ($x = 0 ; $x < $len ; $x++) {
- $ch = $trimmedName[$x];
-
- if (!($ch == '_' || $ch == '-' || ($ch >= 'a' && $ch <= 'z') || ($ch >= 'A' && $ch <= 'Z') || ($ch >= '0' && $ch <= '9'))) {
- return false;
- }
- }
-
- return true;
- }
-
- return false;
- }
- /**
- * Process client's request.
- *
- * This method must be implemented in a way it sends back a response after
- * processing the request.
- *
- */
- abstract function processRequest();
- /**
- * Removes a request parameter from the service given its name.
- *
- * @param string $paramName The name of the parameter (case-sensitive).
- *
- * @return null|RequestParameter If a parameter which has the given name
- * was removed, the method will return an object of type 'RequestParameter'
- * that represents the removed parameter. If nothing is removed, the
- * method will return null.
- *
- */
- public function removeParameter(string $paramName) {
- $trimmed = trim($paramName);
- $params = &$this->getParameters();
- $index = -1;
- $count = count($params);
-
- for ($x = 0 ; $x < $count ; $x++) {
- if ($params[$x]->getName() == $trimmed) {
- $index = $x;
- break;
- }
- }
- $retVal = null;
-
- if ($index != -1) {
- if ($count == 1) {
- $retVal = $params[0];
- unset($params[0]);
- } else {
- $retVal = $params[$index];
- $params[$index] = $params[$count - 1];
- unset($params[$count - 1]);
- }
- }
-
- return $retVal;
- }
- /**
- * Removes a request method from the previously added ones.
- *
- * @param string $method The request method (e.g. 'get', 'post', 'options' ...). It
- * can be in upper case or lower case.
- *
- * @return bool If the given request method is remove, the method will
- * return true. Other than that, the method will return true.
- *
- */
- public function removeRequestMethod(string $method): bool {
- $uMethod = strtoupper(trim($method));
- $allowedMethods = &$this->getRequestMethods();
-
- if (in_array($uMethod, $allowedMethods)) {
- $count = count($allowedMethods);
- $methodIndex = -1;
-
- for ($x = 0 ; $x < $count ; $x++) {
- if ($this->getRequestMethods()[$x] == $uMethod) {
- $methodIndex = $x;
- break;
- }
- }
-
- if ($count == 1) {
- unset($allowedMethods[0]);
- } else {
- $allowedMethods[$methodIndex] = $allowedMethods[$count - 1];
- unset($allowedMethods[$count - 1]);
- }
-
- return true;
- }
-
- return false;
- }
- /**
- * Sends Back a data using specific content type and specific response code.
- *
- * @param string $contentType Response content type (such as 'application/json')
- *
- * @param mixed $data Any data to send back. Mostly, it will be a string.
- *
- * @param int $code HTTP response code that will be used to send the data.
- * Default is HTTP code 200 - Ok.
- *
- */
- public function send(string $contentType, $data, int $code = 200) {
- $manager = $this->getManager();
-
- if ($manager !== null) {
- $manager->send($contentType, $data, $code);
- }
- }
- /**
- * Sends a JSON response to the client.
- *
- * The basic format of the message will be as follows:
- *
- * Where EXTRA_INFO can be a simple string or any JSON data.
- *
- * @param string $message The message to send back.
- *
- * @param string $type A string that tells the client what is the type of
- * the message. The developer can specify his own message types such as
- * 'debug', 'info' or any string. If it is empty string, it will be not
- * included in response payload.
- *
- * @param int $code Response code (such as 404 or 200). Default is 200.
- *
- * @param mixed $otherInfo Any other data to send back it can be a simple
- * string, an object... . If null is given, the parameter 'more-info'
- * will be not included in response. Default is empty string. Default is null.
- *
- */
- public function sendResponse(string $message, int $code = 200, string $type = '', mixed $otherInfo = '') {
- $manager = $this->getManager();
-
- if ($manager !== null) {
- $manager->sendResponse($message, $code, $type, $otherInfo);
- }
- }
- /**
- * Sets the description of the service.
- *
- * Used to help front-end to identify the use of the service.
- *
- * @param string $desc Action description.
- *
- */
- public final function setDescription(string $desc) {
- $this->serviceDesc = trim($desc);
- }
- /**
- * Sets the value of the property 'requireAuth'.
- *
- * The property is used to tell if the authorization step will be skipped
- * or not when the service is called.
- *
- * @param bool $bool True to make authorization step required. False to
- * skip the authorization step.
- *
- */
- public function setIsAuthRequired(bool $bool) {
- $this->requireAuth = $bool;
- }
- /**
- * Associate the web service with a manager.
- *
- * The developer does not have to use this method. It is used when a
- * service is added to a manager.
- *
- * @param WebServicesManager|null $manager The manager at which the service
- * will be associated with. If null is given, the association will be removed if
- * the service was associated with a manager.
- *
- */
- public function setManager(?WebServicesManager $manager) {
- if ($manager === null) {
- $this->owner = null;
- } else {
- $this->owner = $manager;
- }
- }
- /**
- * Sets the name of the service.
- *
- * A valid service name must follow the following rules:
- *
- *
It can contain the letters [A-Z] and [a-z].
- *
It can contain the numbers [0-9].
- *
It can have the character '-' and the character '_'.
- *
- *
- * @param string $name The name of the web service.
- *
- * @return bool If the given name is valid, the method will return
- * true once the name is set. false is returned if the given
- * name is invalid.
- *
- */
- public final function setName(string $name) : bool {
- if (self::isValidName($name)) {
- $this->name = trim($name);
-
- return true;
- }
-
- return false;
- }
- /**
- * Adds multiple request methods as one group.
- *
- * @param array $methods
- */
- public function setRequestMethods(array $methods) {
- foreach ($methods as $m) {
- $this->addRequestMethod($m);
- }
- }
- /**
- * Sets version number or name at which the service was added to a manager.
- *
- * This method is called automatically when the service is added to any services manager.
- * The developer does not have to use this method.
- *
- * @param string $sinceAPIv The version number at which the service was added to the API.
- *
- */
- public final function setSince(string $sinceAPIv) {
- $this->sinceVersion = $sinceAPIv;
- }
- /**
- * Returns a Json object that represents the service.
- *
- * The generated JSON string from the returned Json object will have
- * the following format:
- *
*
* @return Json An object of type Json.
*
@@ -697,60 +692,114 @@ public function setType(string $type) : bool {
public function toJSON() : Json {
$json = new Json();
$json->add('name', $this->getName());
- $json->add('type', $this->getType());
- $json->add('description', $this->getDescription());
- $json->add('is-optional', $this->isOptional());
- $json->add('default-value', $this->getDefault());
- $json->add('min-val', $this->getMinValue());
- $json->add('max-val', $this->getMaxValue());
- $json->add('min-length', $this->getMinLength());
- $json->add('max-length', $this->getMaxLength());
-
+
+ $methods = $this->getMethods();
+ // Default to 'query' for GET/DELETE, 'body' for others
+ if (count($methods) === 0 || in_array(RequestMethod::GET, $methods) || in_array(RequestMethod::DELETE, $methods)) {
+ $json->add('in', 'query');
+ } else {
+ $json->add('in', 'body');
+ }
+
+ $json->add('required', !$this->isOptional());
+
+ if ($this->getDescription() !== null) {
+ $json->add('description', $this->getDescription());
+ }
+
+ $json->add('schema', $this->getSchema());
+
return $json;
}
+ private function getSchema() : Json {
+ return Schema::fromRequestParameter($this)->toJson();
+ }
+
/**
*
* @param RequestParameter $param
* @param array $options
*/
private static function checkParamAttrs(RequestParameter $param, array $options) {
- $isOptional = $options['optional'] ?? false;
+ $isOptional = $options[ParamOption::OPTIONAL] ?? false;
$param->setIsOptional($isOptional);
- if (isset($options['custom-filter'])) {
- $param->setCustomFilterFunction($options['custom-filter']);
+ if (isset($options[ParamOption::FILTER])) {
+ $param->setCustomFilterFunction($options[ParamOption::FILTER]);
}
- if (isset($options['min'])) {
- $param->setMinValue($options['min']);
+ if (isset($options[ParamOption::MIN])) {
+ $param->setMinValue($options[ParamOption::MIN]);
}
- if (isset($options['max'])) {
- $param->setMaxValue($options['max']);
+ if (isset($options[ParamOption::MAX])) {
+ $param->setMaxValue($options[ParamOption::MAX]);
}
- if (isset($options['min-length'])) {
- $param->setMinLength($options['min-length']);
+ if (isset($options[ParamOption::MIN_LENGTH])) {
+ $param->setMinLength($options[ParamOption::MIN_LENGTH]);
}
- if (isset($options['max-length'])) {
- $param->setMaxLength($options['max-length']);
+ if (isset($options[ParamOption::MAX_LENGTH])) {
+ $param->setMaxLength($options[ParamOption::MAX_LENGTH]);
}
- if (isset($options['allow-empty'])) {
- $param->setIsEmptyStringAllowed($options['allow-empty']);
+ if (isset($options[ParamOption::EMPTY])) {
+ $param->setIsEmptyStringAllowed($options[ParamOption::EMPTY]);
}
- if (isset($options['custom-filter'])) {
- $param->setCustomFilterFunction($options['custom-filter']);
+ if (isset($options[ParamOption::METHODS])) {
+ $type = gettype($options[ParamOption::METHODS]);
+ if ($type == 'string') {
+ $param->addMethod($options[ParamOption::METHODS]);
+ } else if ($type == 'array') {
+ $param->addMethods($options[ParamOption::METHODS]);
+ }
}
- if (isset($options['default'])) {
- $param->setDefault($options['default']);
+ if (isset($options[ParamOption::DEFAULT])) {
+ $param->setDefault($options[ParamOption::DEFAULT]);
}
- if (isset($options['description'])) {
- $param->setDescription($options['description']);
+ if (isset($options[ParamOption::DESCRIPTION])) {
+ $param->setDescription($options[ParamOption::DESCRIPTION]);
+ }
+ }
+ /**
+ * Returns an array of request methods at which the parameter must exist.
+ *
+ * @return array An array of request method names (e.g., ['GET', 'POST']).
+ */
+ public function getMethods(): array {
+ return $this->methods;
+ }
+
+ /**
+ * Adds a request method to the parameter.
+ *
+ * @param string $requestMethod The request method name (e.g., 'GET', 'POST').
+ *
+ * @return RequestParameter Returns self for method chaining.
+ */
+ public function addMethod(string $requestMethod): RequestParameter {
+ $method = strtoupper(trim($requestMethod));
+ if (!in_array($method, $this->methods) && in_array($method, RequestMethod::getAll())) {
+ $this->methods[] = $method;
+ }
+ return $this;
+ }
+
+ /**
+ * Adds multiple request methods to the parameter.
+ *
+ * @param array $arr An array of request method names.
+ *
+ * @return RequestParameter Returns self for method chaining.
+ */
+ public function addMethods(array $arr): RequestParameter {
+ foreach ($arr as $method) {
+ $this->addMethod($method);
}
+ return $this;
}
}
diff --git a/WebFiori/Http/SecurityContext.php b/WebFiori/Http/SecurityContext.php
new file mode 100644
index 0000000..1cb8165
--- /dev/null
+++ b/WebFiori/Http/SecurityContext.php
@@ -0,0 +1,267 @@
+getRoles();
+ self::$authorities = $user->getAuthorities();
+ } else {
+ self::$roles = [];
+ self::$authorities = [];
+ }
+ }
+
+ /**
+ * Get the current authenticated user.
+ *
+ * @return UserInterface|null User object or null if not authenticated
+ */
+ public static function getCurrentUser(): ?UserInterface {
+ return self::$currentUser;
+ }
+
+ /**
+ * Set user roles.
+ *
+ * @param array $roles Array of role names
+ * Example: ['USER', 'ADMIN', 'MODERATOR']
+ */
+ public static function setRoles(array $roles): void {
+ self::$roles = $roles;
+ }
+
+ /**
+ * Get user roles.
+ *
+ * @return array Array of role names
+ */
+ public static function getRoles(): array {
+ return self::$roles;
+ }
+
+ /**
+ * Set user authorities/permissions.
+ *
+ * @param array $authorities Array of authority names
+ * Example: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'REPORT_VIEW']
+ */
+ public static function setAuthorities(array $authorities): void {
+ self::$authorities = $authorities;
+ }
+
+ /**
+ * Get user authorities/permissions.
+ *
+ * @return array Array of authority names
+ */
+ public static function getAuthorities(): array {
+ return self::$authorities;
+ }
+
+ /**
+ * Check if user has a specific role.
+ *
+ * @param string $role Role name to check
+ * Example: 'ADMIN', 'USER', 'MODERATOR'
+ * @return bool True if user has the role
+ */
+ public static function hasRole(string $role): bool {
+ return in_array($role, self::$roles);
+ }
+
+ /**
+ * Check if user has a specific authority/permission.
+ *
+ * @param string $authority Authority name to check
+ * Example: 'USER_CREATE', 'USER_DELETE', 'REPORT_VIEW'
+ * @return bool True if user has the authority
+ */
+ public static function hasAuthority(string $authority): bool {
+ return in_array($authority, self::$authorities);
+ }
+
+ /**
+ * Check if a user is currently authenticated.
+ *
+ * @return bool True if user is authenticated and active
+ */
+ public static function isAuthenticated(): bool {
+ return self::$currentUser !== null && self::$currentUser->isActive();
+ }
+
+ /**
+ * Clear all security context data.
+ */
+ public static function clear(): void {
+ self::$currentUser = null;
+ self::$roles = [];
+ self::$authorities = [];
+ }
+
+ /**
+ * Evaluate security expression.
+ *
+ * @param string $expression Security expression to evaluate
+ *
+ * Simple expressions:
+ * - "hasRole('ADMIN')" - Check single role
+ * - "hasAuthority('USER_CREATE')" - Check single authority
+ * - "isAuthenticated()" - Check if user is logged in
+ * - "permitAll()" - Always allow access
+ *
+ * Multiple values:
+ * - "hasAnyRole('ADMIN', 'MODERATOR')" - Check any of multiple roles
+ * - "hasAnyAuthority('USER_CREATE', 'USER_UPDATE')" - Check any of multiple authorities
+ *
+ * Complex boolean expressions:
+ * - "hasRole('ADMIN') && hasAuthority('USER_CREATE')" - Both conditions must be true
+ * - "hasRole('ADMIN') || hasRole('MODERATOR')" - Either condition can be true
+ * - "isAuthenticated() && hasAnyRole('USER', 'ADMIN')" - Authenticated with any role
+ *
+ * @return bool True if expression evaluates to true
+ * @throws \InvalidArgumentException If expression is invalid
+ */
+ public static function evaluateExpression(string $expression): bool {
+ $expression = trim($expression);
+
+ if (empty($expression)) {
+ throw new \InvalidArgumentException('Security expression cannot be empty');
+ }
+
+ // Handle complex boolean expressions with && and ||
+ if (strpos($expression, '&&') !== false) {
+ return self::evaluateAndExpression($expression);
+ }
+
+ if (strpos($expression, '||') !== false) {
+ return self::evaluateOrExpression($expression);
+ }
+
+ // Handle single expressions
+ return self::evaluateSingleExpression($expression);
+ }
+
+ /**
+ * Evaluate AND expression (all conditions must be true).
+ */
+ private static function evaluateAndExpression(string $expression): bool {
+ $parts = array_map('trim', explode('&&', $expression));
+
+ foreach ($parts as $part) {
+ if (!self::evaluateSingleExpression($part)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Evaluate OR expression (at least one condition must be true).
+ */
+ private static function evaluateOrExpression(string $expression): bool {
+ $parts = array_map('trim', explode('||', $expression));
+
+ foreach ($parts as $part) {
+ if (self::evaluateSingleExpression($part)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Evaluate single security expression.
+ */
+ private static function evaluateSingleExpression(string $expression): bool {
+ // Handle hasRole('ROLE_NAME')
+ if (preg_match("/hasRole\('([^']+)'\)/", $expression, $matches)) {
+ return self::hasRole($matches[1]);
+ }
+
+ // Handle hasAnyRole('ROLE1', 'ROLE2', ...)
+ if (preg_match("/hasAnyRole\(([^)]+)\)/", $expression, $matches)) {
+ $roles = self::parseArgumentList($matches[1]);
+ foreach ($roles as $role) {
+ if (self::hasRole($role)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Handle hasAuthority('AUTHORITY_NAME')
+ if (preg_match("/hasAuthority\('([^']+)'\)/", $expression, $matches)) {
+ return self::hasAuthority($matches[1]);
+ }
+
+ // Handle hasAnyAuthority('AUTH1', 'AUTH2', ...)
+ if (preg_match("/hasAnyAuthority\(([^)]+)\)/", $expression, $matches)) {
+ $authorities = self::parseArgumentList($matches[1]);
+ foreach ($authorities as $authority) {
+ if (self::hasAuthority($authority)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Handle isAuthenticated()
+ if ($expression === 'isAuthenticated()') {
+ return self::isAuthenticated();
+ }
+
+ // Handle permitAll()
+ if ($expression === 'permitAll()') {
+ return true;
+ }
+
+ throw new \InvalidArgumentException("Invalid security expression: '$expression'");
+ }
+
+ /**
+ * Parse comma-separated argument list from function call.
+ */
+ private static function parseArgumentList(string $args): array {
+ $result = [];
+ $parts = explode(',', $args);
+
+ foreach ($parts as $part) {
+ $part = trim($part);
+ if (preg_match("/^'([^']+)'$/", $part, $matches)) {
+ $result[] = $matches[1];
+ } else {
+ throw new \InvalidArgumentException("Invalid argument format: '$part'");
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/WebFiori/Http/UserInterface.php b/WebFiori/Http/UserInterface.php
new file mode 100644
index 0000000..982468c
--- /dev/null
+++ b/WebFiori/Http/UserInterface.php
@@ -0,0 +1,37 @@
+
+ *
It can contain the letters [A-Z] and [a-z].
+ *
It can contain the numbers [0-9].
+ *
It can have the character '-' and the character '_'.
+ *
+ * If The given name is invalid, the name of the service will be set to 'new-service'.
+ *
+ * @param string $name The name of the web service.
+ *
+ * @param WebServicesManager|null $owner The manager which is used to
+ * manage the web service.
+ */
+ public function __construct(string $name = '') {
+ $this->reqMethods = [];
+ $this->parameters = [];
+ $this->responses = [];
+ $this->responsesByMethod = [];
+ $this->requireAuth = true;
+ $this->sinceVersion = '1.0.0';
+ $this->serviceDesc = '';
+ $this->request = Request::createFromGlobals();
+
+ $this->configureFromAnnotations($name);
+ }
+
+ /**
+ * Configure service from annotations if present.
+ */
+ private function configureFromAnnotations(string $fallbackName): void {
+ $reflection = new \ReflectionClass($this);
+ $attributes = $reflection->getAttributes(\WebFiori\Http\Annotations\RestController::class);
+
+ if (!empty($attributes)) {
+ $restController = $attributes[0]->newInstance();
+ $serviceName = $restController->name ?: $fallbackName;
+ $description = $restController->description;
+ } else {
+ $serviceName = $fallbackName;
+ $description = '';
+ }
+
+ if (!$this->setName($serviceName)) {
+ $this->setName('new-service');
+ }
+
+ if ($description) {
+ $this->setDescription($description);
+ }
+
+ $this->configureMethodMappings();
+ $this->configureAuthentication();
+ }
+
+ /**
+ * Process the web service request with auto-processing support.
+ * This method should be called instead of processRequest() for auto-processing.
+ */
+ public function processWithAutoHandling(): void {
+ $targetMethod = $this->getTargetMethod();
+
+ if ($targetMethod && $this->hasResponseBodyAnnotation($targetMethod)) {
+ // Check method-level authorization first
+ if (!$this->checkMethodAuthorization()) {
+ $this->sendResponse('Access denied', 403, 'error');
+ return;
+ }
+
+ try {
+ // Call the target method and process its return value
+ $result = $this->$targetMethod();
+ $this->handleMethodResponse($result, $targetMethod);
+ } catch (HttpException $e) {
+ // Handle HTTP exceptions automatically
+ $this->handleException($e);
+ } catch (\Exception $e) {
+ // Handle other exceptions as 500 Internal Server Error
+ $this->sendResponse($e->getMessage(), 500, 'error');
+ }
+ } else {
+ // Fall back to traditional processRequest() approach
+ $this->processRequest();
+ }
+ }
+
+ /**
+ * Check if a method has the ResponseBody annotation.
+ *
+ * @param string $methodName The method name to check
+ * @return bool True if the method has ResponseBody annotation
+ */
+ public function hasResponseBodyAnnotation(string $methodName): bool {
+ try {
+ $reflection = new \ReflectionMethod($this, $methodName);
+ return !empty($reflection->getAttributes(ResponseBody::class));
+ } catch (\ReflectionException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Handle HTTP exceptions by converting them to appropriate responses.
+ *
+ * @param HttpException $exception The HTTP exception to handle
+ */
+ protected function handleException(HttpException $exception): void {
+ $this->sendResponse(
+ $exception->getMessage(),
+ $exception->getStatusCode(),
+ $exception->getResponseType()
+ );
+ }
+
+ /**
+ * Configure parameters dynamically for a specific method.
+ *
+ * @param string $methodName The method name to configure parameters for
+ */
+ public function configureParametersForMethod(string $methodName): void {
+ try {
+ $reflection = new \ReflectionMethod($this, $methodName);
+ $this->configureParametersFromMethod($reflection);
+ } catch (\ReflectionException $e) {
+ // Method doesn't exist, ignore
+ }
+ }
+
+ /**
+ * Configure parameters for all methods with RequestParam annotations.
+ */
+ private function configureAllAnnotatedParameters(): void {
+ $reflection = new \ReflectionClass($this);
+ foreach ($reflection->getMethods() as $method) {
+ $paramAttributes = $method->getAttributes(\WebFiori\Http\Annotations\RequestParam::class);
+ if (!empty($paramAttributes)) {
+ $this->configureParametersFromMethod($method);
+ }
+ }
+ }
+
+ /**
+ * Configure parameters for methods with specific HTTP method mapping.
+ *
+ * @param string $httpMethod HTTP method (GET, POST, PUT, DELETE, etc.)
+ */
+ private function configureParametersForHttpMethod(string $httpMethod): void {
+ $reflection = new \ReflectionClass($this);
+ $httpMethod = strtoupper($httpMethod);
+
+ foreach ($reflection->getMethods() as $method) {
+ // Check if method has HTTP method mapping annotation
+ $mappingFound = false;
+
+ // Check for specific HTTP method annotations
+ $annotations = [
+ 'GET' => \WebFiori\Http\Annotations\GetMapping::class,
+ 'POST' => \WebFiori\Http\Annotations\PostMapping::class,
+ 'PUT' => \WebFiori\Http\Annotations\PutMapping::class,
+ 'DELETE' => \WebFiori\Http\Annotations\DeleteMapping::class,
+ 'PATCH' => \WebFiori\Http\Annotations\PatchMapping::class,
+ ];
+
+ if (isset($annotations[$httpMethod])) {
+ $mappingFound = !empty($method->getAttributes($annotations[$httpMethod]));
+ }
+
+ if ($mappingFound) {
+ $this->configureParametersFromMethod($method);
+ }
+ }
+ }
+
+ /**
+ * Configure authentication from annotations.
+ */
+ private function configureAuthentication(): void {
+ $reflection = new \ReflectionClass($this);
+
+ // Check class-level authentication
+ $classAuth = $this->getAuthenticationFromClass($reflection);
+
+ // If class has AllowAnonymous, disable auth requirement
+ if ($classAuth['allowAnonymous']) {
+ $this->setIsAuthRequired(false);
+ } else if ($classAuth['requiresAuth'] || $classAuth['preAuthorize']) {
+ $this->setIsAuthRequired(true);
+ }
+ }
+
+ /**
+ * Get authentication configuration from class annotations.
+ */
+ private function getAuthenticationFromClass(\ReflectionClass $reflection): array {
+ return [
+ 'allowAnonymous' => !empty($reflection->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class)),
+ 'requiresAuth' => !empty($reflection->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class)),
+ 'preAuthorize' => $reflection->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class)
+ ];
+ }
+
+ /**
+ * Check method-level authorization before processing.
+ */
+ public function checkMethodAuthorization(): bool {
+ $reflection = new \ReflectionClass($this);
+ $method = $this->getCurrentProcessingMethod() ?: $this->getTargetMethod();
+
+ if (!$method) {
+ return $this->isAuthorized();
+ }
+
+ $reflectionMethod = $reflection->getMethod($method);
+
+ // Check AllowAnonymous first
+ if (!empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class))) {
+ return true;
+ }
+
+ // Check RequiresAuth
+ if (!empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class))) {
+ if (!SecurityContext::isAuthenticated()) {
+ return false;
+ }
+ }
+
+ // Check PreAuthorize
+ $preAuthAttributes = $reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class);
+ if (!empty($preAuthAttributes)) {
+ $preAuth = $preAuthAttributes[0]->newInstance();
+
+ return SecurityContext::evaluateExpression($preAuth->expression);
+ }
+
+ return $this->isAuthorized();
+ }
+
+ /**
+ * Check if the method has any authorization annotations.
+ */
+ public function hasMethodAuthorizationAnnotations(): bool {
+ $reflection = new \ReflectionClass($this);
+ $method = $this->getCurrentProcessingMethod() ?: $this->getTargetMethod();
+
+ if (!$method) {
+ return false;
+ }
+
+ $reflectionMethod = $reflection->getMethod($method);
+
+ return !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class)) ||
+ !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class)) ||
+ !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class));
+ }
+
+ /**
+ * Get the current processing method name (to be overridden by subclasses if needed).
+ */
+ protected function getCurrentProcessingMethod(): ?string {
+ return null; // Default implementation
+ }
+
+ /**
+ * Get the target method name based on current HTTP request.
+ *
+ * @return string|null The method name that should handle this request, or null if none found
+ */
+ public function getTargetMethod(): ?string {
+ $httpMethod = $this->getManager() ?
+ $this->getManager()->getRequest()->getMethod() :
+ ($_SERVER['REQUEST_METHOD'] ?? 'GET');
+
+ // First try to get method from getCurrentProcessingMethod (if implemented)
+ $currentMethod = $this->getCurrentProcessingMethod();
+ if ($currentMethod) {
+ $reflection = new \ReflectionClass($this);
+ try {
+ $method = $reflection->getMethod($currentMethod);
+ if ($this->methodHandlesHttpMethod($method, $httpMethod)) {
+ return $currentMethod;
+ }
+ } catch (\ReflectionException $e) {
+ // Method doesn't exist, continue with discovery
+ }
+ }
+
+ // Fall back to finding first method that matches HTTP method
+ $reflection = new \ReflectionClass($this);
+ foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
+ if ($this->methodHandlesHttpMethod($method, $httpMethod)) {
+ return $method->getName();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Check if a method handles the specified HTTP method.
+ *
+ * @param \ReflectionMethod $method The method to check
+ * @param string $httpMethod The HTTP method (GET, POST, etc.)
+ * @return bool True if the method handles this HTTP method
+ */
+ private function methodHandlesHttpMethod(\ReflectionMethod $method, string $httpMethod): bool {
+ $methodMappings = [
+ GetMapping::class => RequestMethod::GET,
+ PostMapping::class => RequestMethod::POST,
+ PutMapping::class => RequestMethod::PUT,
+ DeleteMapping::class => RequestMethod::DELETE
+ ];
+
+ foreach ($methodMappings as $annotationClass => $mappedMethod) {
+ if ($httpMethod === $mappedMethod && !empty($method->getAttributes($annotationClass))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle method response by auto-converting return values to HTTP responses.
+ *
+ * @param mixed $result The return value from the method
+ * @param string $methodName The name of the method that was called
+ * @return void
+ */
+ protected function handleMethodResponse(mixed $result, string $methodName): void {
+ $reflection = new \ReflectionMethod($this, $methodName);
+ $responseBodyAttrs = $reflection->getAttributes(ResponseBody::class);
+
+ if (empty($responseBodyAttrs)) {
+ return; // No auto-processing, method should handle response manually
+ }
+
+ $responseBody = $responseBodyAttrs[0]->newInstance();
+
+ // Auto-convert return value to response
+ if ($result === null) {
+ // Null return = empty response with configured status
+ $this->sendResponse('', $responseBody->status, $responseBody->type);
+ } elseif (is_array($result) || is_object($result)) {
+ // Array/object = JSON response
+ $this->sendResponse('Success', $responseBody->status, $responseBody->type, $result);
+ } else {
+ // String/scalar = plain response
+ $this->sendResponse($result, $responseBody->status, $responseBody->type);
+ }
+ }
+
+ /**
+ * Configure allowed HTTP methods from method annotations.
+ */
+ private function configureMethodMappings(): void {
+ $reflection = new \ReflectionClass($this);
+ $httpMethodToMethods = [];
+
+ foreach ($reflection->getMethods() as $method) {
+ $methodMappings = [
+ GetMapping::class => RequestMethod::GET,
+ PostMapping::class => RequestMethod::POST,
+ PutMapping::class => RequestMethod::PUT,
+ DeleteMapping::class => RequestMethod::DELETE
+ ];
+
+ foreach ($methodMappings as $annotationClass => $httpMethod) {
+ $attributes = $method->getAttributes($annotationClass);
+ if (!empty($attributes)) {
+ if (!isset($httpMethodToMethods[$httpMethod])) {
+ $httpMethodToMethods[$httpMethod] = [];
+ }
+ $httpMethodToMethods[$httpMethod][] = $method->getName();
+ }
+ }
+ }
+
+ // Check for duplicates only if getCurrentProcessingMethod is not overridden
+ $hasCustomRouting = $reflection->getMethod('getCurrentProcessingMethod')->getDeclaringClass()->getName() !== self::class;
+
+ if (!$hasCustomRouting) {
+ foreach ($httpMethodToMethods as $httpMethod => $methods) {
+ if (count($methods) > 1) {
+ throw new Exceptions\DuplicateMappingException(
+ "HTTP method $httpMethod is mapped to multiple methods: " . implode(', ', $methods)
+ );
+ }
+ }
+ }
+
+ if (!empty($httpMethodToMethods)) {
+ $this->setRequestMethods(array_keys($httpMethodToMethods));
+ }
+ }
+
+ /**
+ * Configure parameters from method RequestParam annotations.
+ */
+ private function configureParametersFromMethod(\ReflectionMethod $method): void {
+ $paramAttributes = $method->getAttributes(\WebFiori\Http\Annotations\RequestParam::class);
+
+ foreach ($paramAttributes as $attribute) {
+ $param = $attribute->newInstance();
+
+ $this->addParameters([
+ $param->name => [
+ \WebFiori\Http\ParamOption::TYPE => $this->mapParamType($param->type),
+ \WebFiori\Http\ParamOption::OPTIONAL => $param->optional,
+ \WebFiori\Http\ParamOption::DEFAULT => $param->default,
+ \WebFiori\Http\ParamOption::DESCRIPTION => $param->description
+ ]
+ ]);
+ }
+ }
+
+ /**
+ * Map string type to ParamType constant.
+ */
+ private function mapParamType(string $type): string {
+ return match(strtolower($type)) {
+ 'int', 'integer' => \WebFiori\Http\ParamType::INT,
+ 'float', 'double' => \WebFiori\Http\ParamType::DOUBLE,
+ 'bool', 'boolean' => \WebFiori\Http\ParamType::BOOL,
+ 'email' => \WebFiori\Http\ParamType::EMAIL,
+ 'url' => \WebFiori\Http\ParamType::URL,
+ 'array' => \WebFiori\Http\ParamType::ARR,
+ 'json' => \WebFiori\Http\ParamType::JSON_OBJ,
+ default => \WebFiori\Http\ParamType::STRING
+ };
+ } /**
+ * Returns an array that contains all possible requests methods at which the
+ * service can be called with.
+ *
+ * The array will contain strings like 'GET' or 'POST'. If no request methods
+ * where added, the array will be empty.
+ *
+ * @return array An array that contains all possible requests methods at which the
+ * service can be called using.
+ *
+ */
+ public function &getRequestMethods() : array {
+ return $this->reqMethods;
+ }
+ /**
+ * Returns an array that contains an objects of type RequestParameter.
+ *
+ * @return array an array that contains an objects of type RequestParameter.
+ *
+ */
+ public final function &getParameters() : array {
+ return $this->parameters;
+ }
+ /**
+ *
+ * @return string
+ *
+ */
+ public function __toString() {
+ return $this->toJSON().'';
+ }
+ /**
+ * Adds new request parameter to the service.
+ *
+ * The parameter will only be added if no parameter which has the same
+ * name as the given one is added before.
+ *
+ * @param RequestParameter|array $param The parameter that will be added. It
+ * can be an object of type 'RequestParameter' or an associative array of
+ * options. The array can have the following indices:
+ *
+ *
name: The name of the parameter. It must be provided.
+ *
type: The datatype of the parameter. If not provided, 'string' is used.
+ *
optional: A boolean. If set to true, it means the parameter is
+ * optional. If not provided, 'false' is used.
+ *
min: Minimum value of the parameter. Applicable only for
+ * numeric types.
+ *
max: Maximum value of the parameter. Applicable only for
+ * numeric types.
+ *
allow-empty: A boolean. If the type of the parameter is string or string-like
+ * type and this is set to true, then empty strings will be allowed. If
+ * not provided, 'false' is used.
+ *
custom-filter: A PHP function that can be used to filter the
+ * parameter even further
+ *
default: An optional default value to use if the parameter is
+ * not provided and is optional.
+ *
description: The description of the attribute.
+ *
+ *
+ * @return bool If the given request parameter is added, the method will
+ * return true. If it was not added for any reason, the method will return
+ * false.
+ *
+ */
+ public function addParameter($param) : bool {
+ if (gettype($param) == 'array') {
+ $param = RequestParameter::create($param);
+ }
+
+ if ($param instanceof RequestParameter && !$this->hasParameter($param->getName())) {
+ $this->parameters[] = $param;
+
+ return true;
+ }
+
+ return false;
+ }
+ /**
+ * Adds multiple parameters to the web service in one batch.
+ *
+ * @param array $params An associative or indexed array. If the array is indexed,
+ * each index should hold an object of type 'RequestParameter'. If it is associative,
+ * then the key will represent the name of the web service and the value of the
+ * key should be a sub-associative array that holds parameter options.
+ *
+ */
+ public function addParameters(array $params) {
+ foreach ($params as $paramIndex => $param) {
+ if ($param instanceof RequestParameter) {
+ $this->addParameter($param);
+ } else if (gettype($param) == 'array') {
+ $param['name'] = $paramIndex;
+ $this->addParameter(RequestParameter::create($param));
+ }
+ }
+ }
+ /**
+ * Adds new request method.
+ *
+ * The value that will be passed to this method can be any string
+ * that represents HTTP request method (e.g. 'get', 'post', 'options' ...). It
+ * can be in upper case or lower case.
+ *
+ * @param string $method The request method.
+ *
+ * @return bool true in case the request method is added. If the given
+ * request method is already added or the method is unknown, the method
+ * will return false.
+ *
+ */
+ public final function addRequestMethod(string $method) : bool {
+ $uMethod = strtoupper(trim($method));
+
+ if (in_array($uMethod, RequestMethod::getAll()) && !in_array($uMethod, $this->reqMethods)) {
+ $this->reqMethods[] = $uMethod;
+
+ return true;
+ }
+
+ return false;
+ }
+ /**
+ * Adds response description.
+ *
+ * It is used to describe the API for front-end developers and help them
+ * identify possible responses if they call the API using the specified service.
+ *
+ * @param string $description A paragraph that describes one of
+ * the possible responses due to calling the service.
+ */
+ public function addResponse(string $method, string $statusCode, OpenAPI\ResponseObj|string $response): WebService {
+ $method = strtoupper($method);
+
+ if (!isset($this->responsesByMethod[$method])) {
+ $this->responsesByMethod[$method] = new OpenAPI\ResponsesObj();
+ }
+
+ $this->responsesByMethod[$method]->addResponse($statusCode, $response);
+ return $this;
+ }
+
+ public final function addResponseDescription(string $description) {
+ $trimmed = trim($description);
+
+ if (strlen($trimmed) != 0) {
+ $this->responses[] = $trimmed;
+ }
+ }
+ public function getResponsesForMethod(string $method): ?OpenAPI\ResponsesObj {
+ $method = strtoupper($method);
+ return $this->responsesByMethod[$method] ?? null;
+ }
+ /**
+ * Sets all responses for a specific HTTP method.
+ *
+ * @param string $method HTTP method.
+ * @param OpenAPI\ResponsesObj $responses Responses object.
+ *
+ * @return WebService Returns self for method chaining.
+ */
+ public function setResponsesForMethod(string $method, OpenAPI\ResponsesObj $responses): WebService {
+ $this->responsesByMethod[strtoupper($method)] = $responses;
+ return $this;
+ }
+
+ /**
+ * Gets all responses mapped by HTTP method.
+ *
+ * @return array Map of methods to responses.
+ */
+ public function getAllResponses(): array {
+ return $this->responsesByMethod;
+ }
+
+ /**
+ * Converts this web service to an OpenAPI PathItemObj.
+ *
+ * Each HTTP method supported by this service becomes an operation in the path item.
+ *
+ * @return OpenAPI\PathItemObj The PathItemObj representation of this service.
+ */
+ public function toPathItemObj(): OpenAPI\PathItemObj {
+ $pathItem = new OpenAPI\PathItemObj();
+
+ foreach ($this->getRequestMethods() as $method) {
+ $responses = $this->getResponsesForMethod($method);
+
+ if ($responses === null) {
+ $responses = new OpenAPI\ResponsesObj();
+ $responses->addResponse('200', 'Successful operation');
+ }
+
+ $operation = new OpenAPI\OperationObj($responses);
+
+ switch ($method) {
+ case RequestMethod::GET:
+ $pathItem->setGet($operation);
+ break;
+ case RequestMethod::POST:
+ $pathItem->setPost($operation);
+ break;
+ case RequestMethod::PUT:
+ $pathItem->setPut($operation);
+ break;
+ case RequestMethod::DELETE:
+ $pathItem->setDelete($operation);
+ break;
+ case RequestMethod::PATCH:
+ $pathItem->setPatch($operation);
+ break;
+ }
+
+
+ }return $pathItem;}
+ /**
+ * Returns an object that contains the value of the header 'authorization'.
+ *
+ * @return AuthHeader|null The object will have two primary attributes, the first is
+ * the 'scheme' and the second one is 'credentials'. The 'scheme'
+ * will contain the name of the scheme which is used to authenticate
+ * ('basic', 'bearer', 'digest', etc...). The 'credentials' will contain
+ * the credentials which can be used to authenticate the client.
+ *
+ */
+ public function getAuthHeader() {
+ if ($this->request !== null) {
+ return $this->request->getAuthHeader();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the request instance for the service.
+ *
+ * @param mixed $request The request instance (Request, etc.)
+ */
+ public function setRequest($request) {
+ $this->request = $request;
+ }
+ /**
+ * Returns the description of the service.
+ *
+ * @return string The description of the service. Default is empty string.
+ *
+ */
+ public final function getDescription() : string {
+ return $this->serviceDesc;
+ }
+ /**
+ * Returns an associative array or an object of type Json of filtered request inputs.
+ *
+ * The indices of the array will represent request parameters and the
+ * values of each index will represent the value which was set in
+ * request body. The values will be filtered and might not be exactly the same as
+ * the values passed in request body. Note that if a parameter is optional and not
+ * provided in request body, its value will be set to 'null'. Note that
+ * if request content type is 'application/json', only basic filtering will
+ * be applied. Also, parameters in this case don't apply.
+ *
+ * @return array|Json|null An array of filtered request inputs. This also can
+ * be an object of type 'Json' if request content type was 'application/json'.
+ * If no manager was associated with the service, the method will return null.
+ *
+ */
+ public function getInputs() {
+ $manager = $this->getManager();
+
+ if ($manager !== null) {
+ return $manager->getInputs();
+ }
+
+ return null;
+ }
+ /**
+ * Returns the manager which is used to manage the web service.
+ *
+ * @return WebServicesManager|null If set, it is returned as an object.
+ * Other than that, null is returned.
+ */
+ public function getManager() {
+ return $this->owner;
+ }
+ /**
+ * Returns the name of the service.
+ *
+ * @return string The name of the service.
+ *
+ */
+ public final function getName() : string {
+ return $this->name;
+ }
+ /**
+ * Map service parameter to specific instance of a class.
+ *
+ * This method assumes that every parameter in the request has a method
+ * that can be called to set attribute value. For example, if a parameter
+ * has the name 'user-last-name', the mapping method should have the name
+ * 'setUserLastName' for mapping to work correctly.
+ *
+ * @param string $clazz The class that service parameters will be mapped
+ * to.
+ *
+ * @param array $settersMap An optional array that can have custom
+ * setters map. The indices of the array should be parameters names
+ * and the values are the names of setter methods in the class.
+ *
+ * @return object The Method will return an instance of the class with
+ * all its attributes set to request parameter's values.
+ */
+ public function getObject(string $clazz, array $settersMap = []) {
+ $mapper = new ObjectMapper($clazz, $this);
+
+ foreach ($settersMap as $param => $method) {
+ $mapper->addSetterMap($param, $method);
+ }
+
+ return $mapper->map($this->getInputs());
+ }
+ /**
+ * Returns one of the parameters of the service given its name.
+ *
+ * @param string $paramName The name of the parameter.
+ *
+ * @return RequestParameter|null Returns an objects of type RequestParameter if
+ * a parameter with the given name was found. null if nothing is found.
+ *
+ */
+ public final function getParameterByName(string $paramName, ?string $httpMethod = null) {
+ // Configure parameters if HTTP method specified
+ if ($httpMethod !== null) {
+ $this->configureParametersForHttpMethod($httpMethod);
+ } else {
+ // Configure parameters for all methods with annotations
+ $this->configureAllAnnotatedParameters();
+ }
+
+ $trimmed = trim($paramName);
+
+ if (strlen($trimmed) != 0) {
+ foreach ($this->parameters as $param) {
+ if ($param->getName() == $trimmed) {
+ return $param;
+ }
+ }
+ }
+
+ return null;
+ }
+ /**
+ * Returns the value of request parameter given its name.
+ *
+ * @param string $paramName The name of request parameter as specified when
+ * it was added to the service.
+ *
+ * @return mixed|null If the parameter is found and its value is set, the
+ * method will return its value. Other than that, the method will return null.
+ * For optional parameters, if a default value is set for it, the method will
+ * return that value.
+ *
+ */
+ public function getParamVal(string $paramName) {
+ $inputs = $this->getInputs();
+ $trimmed = trim($paramName);
+
+ if ($inputs !== null) {
+ if ($inputs instanceof Json) {
+ return $inputs->get($trimmed);
+ } else {
+ return $inputs[$trimmed] ?? null;
+ }
+ }
+
+ return null;
+ }
+ /**
+ * Returns an indexed array that contains information about possible responses.
+ *
+ * It is used to describe the API for front-end developers and help them
+ * identify possible responses if they call the API using the specified service.
+ *
+ * @return array An array that contains information about possible responses.
+ *
+ */
+ public final function getResponsesDescriptions() : array {
+ return $this->responses;
+ }
+ /**
+ * Returns version number or name at which the service was added to the API.
+ *
+ * Version number is set based on the version number which was set in the
+ * class WebAPI.
+ *
+ * @return string The version number at which the service was added to the API.
+ * Default is '1.0.0'.
+ *
+ */
+ public final function getSince() : string {
+ return $this->sinceVersion;
+ }
+ /**
+ * Checks if the service has a specific request parameter given its name.
+ *
+ * Note that the name of the parameter is case-sensitive. This means that
+ * 'get-profile' is not the same as 'Get-Profile'.
+ *
+ * @param string $name The name of the parameter.
+ *
+ * @return bool If a request parameter which has the given name is added
+ * to the service, the method will return true. Otherwise, the method will return
+ * false.
+ *
+ */
+ public function hasParameter(string $name) : bool {
+ $trimmed = trim($name);
+
+ if (strlen($name) != 0) {
+ foreach ($this->getParameters() as $param) {
+ if ($param->getName() == $trimmed) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+ /**
+ * Checks if the client is authorized to use the service or not.
+ *
+ * The developer should implement this method in a way it returns a boolean.
+ * If the method returns true, it means the client is allowed to use the service.
+ * If the method returns false, then he is not authorized and a 401 error
+ * code will be sent back. If the method returned nothing, then it means the
+ * user is authorized to call the API. If WebFiori framework is used, it is
+ * possible to perform the functionality of this method using middleware.
+ *
+ * @return bool True if the user is allowed to perform the action. False otherwise.
+ *
+ */
+ public function isAuthorized() : bool {return false;}
+ /**
+ * Returns the value of the property 'requireAuth'.
+ *
+ * The property is used to tell if the authorization step will be skipped
+ * or not when the service is called.
+ *
+ * @return bool The method will return true if authorization step required.
+ * False if the authorization step will be skipped. Default return value is true.
+ *
+ */
+ public function isAuthRequired() : bool {
+ return $this->requireAuth;
+ }
+
+ /**
+ * Validates the name of a web service or request parameter.
+ *
+ * @param string $name The name of the service or parameter.
+ *
+ * @return bool If valid, true is returned. Other than that, false is returned.
+ */
+ public static function isValidName(string $name): bool {
+ $trimmedName = trim($name);
+ $len = strlen($trimmedName);
+
+ if ($len != 0) {
+ for ($x = 0 ; $x < $len ; $x++) {
+ $ch = $trimmedName[$x];
+
+ if (!($ch == '_' || $ch == '-' || ($ch >= 'a' && $ch <= 'z') || ($ch >= 'A' && $ch <= 'Z') || ($ch >= '0' && $ch <= '9'))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ /**
+ * Process client's request.
+ */
+ public function processRequest() {}
+ /**
+ * Removes a request parameter from the service given its name.
+ *
+ * @param string $paramName The name of the parameter (case-sensitive).
+ *
+ * @return null|RequestParameter If a parameter which has the given name
+ * was removed, the method will return an object of type 'RequestParameter'
+ * that represents the removed parameter. If nothing is removed, the
+ * method will return null.
+ *
+ */
+ public function removeParameter(string $paramName) {
+ $trimmed = trim($paramName);
+ $params = &$this->getParameters();
+ $index = -1;
+ $count = count($params);
+
+ for ($x = 0 ; $x < $count ; $x++) {
+ if ($params[$x]->getName() == $trimmed) {
+ $index = $x;
+ break;
+ }
+ }
+ $retVal = null;
+
+ if ($index != -1) {
+ if ($count == 1) {
+ $retVal = $params[0];
+ unset($params[0]);
+ } else {
+ $retVal = $params[$index];
+ $params[$index] = $params[$count - 1];
+ unset($params[$count - 1]);
+ }
+ }
+
+ return $retVal;
+ }
+ /**
+ * Removes a request method from the previously added ones.
+ *
+ * @param string $method The request method (e.g. 'get', 'post', 'options' ...). It
+ * can be in upper case or lower case.
+ *
+ * @return bool If the given request method is remove, the method will
+ * return true. Other than that, the method will return true.
+ *
+ */
+ public function removeRequestMethod(string $method): bool {
+ $uMethod = strtoupper(trim($method));
+ $allowedMethods = &$this->getRequestMethods();
+
+ if (in_array($uMethod, $allowedMethods)) {
+ $count = count($allowedMethods);
+ $methodIndex = -1;
+
+ for ($x = 0 ; $x < $count ; $x++) {
+ if ($this->getRequestMethods()[$x] == $uMethod) {
+ $methodIndex = $x;
+ break;
+ }
+ }
+
+ if ($count == 1) {
+ unset($allowedMethods[0]);
+ } else {
+ $allowedMethods[$methodIndex] = $allowedMethods[$count - 1];
+ unset($allowedMethods[$count - 1]);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ /**
+ * Sends Back a data using specific content type and specific response code.
+ *
+ * @param string $contentType Response content type (such as 'application/json')
+ *
+ * @param mixed $data Any data to send back. Mostly, it will be a string.
+ *
+ * @param int $code HTTP response code that will be used to send the data.
+ * Default is HTTP code 200 - Ok.
+ *
+ */
+ public function send(string $contentType, $data, int $code = 200) {
+ $manager = $this->getManager();
+
+ if ($manager !== null) {
+ $manager->send($contentType, $data, $code);
+ }
+ }
+ /**
+ * Sends a JSON response to the client.
+ *
+ * The basic format of the message will be as follows:
+ *
+ * Where EXTRA_INFO can be a simple string or any JSON data.
+ *
+ * @param string $message The message to send back.
+ *
+ * @param string $type A string that tells the client what is the type of
+ * the message. The developer can specify his own message types such as
+ * 'debug', 'info' or any string. If it is empty string, it will be not
+ * included in response payload.
+ *
+ * @param int $code Response code (such as 404 or 200). Default is 200.
+ *
+ * @param mixed $otherInfo Any other data to send back it can be a simple
+ * string, an object... . If null is given, the parameter 'more-info'
+ * will be not included in response. Default is empty string. Default is null.
+ *
+ */
+ public function sendResponse(string $message, int $code = 200, string $type = '', mixed $otherInfo = '') {
+ $manager = $this->getManager();
+
+ if ($manager !== null) {
+ $manager->sendResponse($message, $code, $type, $otherInfo);
+ }
+ }
+ /**
+ * Sets the description of the service.
+ *
+ * Used to help front-end to identify the use of the service.
+ *
+ * @param string $desc Action description.
+ *
+ */
+ public final function setDescription(string $desc) {
+ $this->serviceDesc = trim($desc);
+ }
+ /**
+ * Sets the value of the property 'requireAuth'.
+ *
+ * The property is used to tell if the authorization step will be skipped
+ * or not when the service is called.
+ *
+ * @param bool $bool True to make authorization step required. False to
+ * skip the authorization step.
+ *
+ */
+ public function setIsAuthRequired(bool $bool) {
+ $this->requireAuth = $bool;
+ }
+ /**
+ * Associate the web service with a manager.
+ *
+ * The developer does not have to use this method. It is used when a
+ * service is added to a manager.
+ *
+ * @param WebServicesManager|null $manager The manager at which the service
+ * will be associated with. If null is given, the association will be removed if
+ * the service was associated with a manager.
+ *
+ */
+ public function setManager(?WebServicesManager $manager) {
+ if ($manager === null) {
+ $this->owner = null;
+ } else {
+ $this->owner = $manager;
+ }
+ }
+ /**
+ * Sets the name of the service.
+ *
+ * A valid service name must follow the following rules:
+ *
+ *
It can contain the letters [A-Z] and [a-z].
+ *
It can contain the numbers [0-9].
+ *
It can have the character '-' and the character '_'.
+ *
+ *
+ * @param string $name The name of the web service.
+ *
+ * @return bool If the given name is valid, the method will return
+ * true once the name is set. false is returned if the given
+ * name is invalid.
+ *
+ */
+ public final function setName(string $name) : bool {
+ if (self::isValidName($name)) {
+ $this->name = trim($name);
+
+ return true;
+ }
+
+ return false;
+ }
+ /**
+ * Adds multiple request methods as one group.
+ *
+ * @param array $methods
+ */
+ public function setRequestMethods(array $methods) {
+ foreach ($methods as $m) {
+ $this->addRequestMethod($m);
+ }
+ }
+ /**
+ * Sets version number or name at which the service was added to a manager.
+ *
+ * This method is called automatically when the service is added to any services manager.
+ * The developer does not have to use this method.
+ *
+ * @param string $sinceAPIv The version number at which the service was added to the API.
+ *
+ */
+ public final function setSince(string $sinceAPIv) {
+ $this->sinceVersion = $sinceAPIv;
+ }
+ /**
+ * Returns a Json object that represents the service.
+ *
+ * @return Json an object of type Json.
+ *
+ */
+ public function toJSON() : Json {
+ return $this->toPathItemObj()->toJSON();
+ }
+}
diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php
index c504092..1fe6c5f 100644
--- a/WebFiori/Http/WebServicesManager.php
+++ b/WebFiori/Http/WebServicesManager.php
@@ -96,6 +96,12 @@ class WebServicesManager implements JsonI {
*
*/
private $services;
+ /**
+ * The base path for all services in this manager.
+ *
+ * @var string
+ */
+ private string $basePath = '';
private $request;
/**
* The response object used to send output.
@@ -147,11 +153,11 @@ public function getResponse() : Response {
/**
* Adds new web service to the set of web services.
*
- * @param AbstractWebService $service The web service that will be added.
+ * @param WebService $service The web service that will be added.
*
*
*/
- public function addService(AbstractWebService $service) : WebServicesManager {
+ public function addService(WebService $service) : WebServicesManager {
return $this->addAction($service);
}
/**
@@ -175,7 +181,7 @@ public function addService(AbstractWebService $service) : WebServicesManager {
public function contentTypeNotSupported(string $cType = '') {
$j = new Json();
$j->add('request-content-type', $cType);
- $this->sendResponse(ResponseMessage::get('415'), 415, AbstractWebService::E, $j);
+ $this->sendResponse(ResponseMessage::get('415'), 415, WebService::E, $j);
}
/**
* Returns the name of the service which is being called.
@@ -203,6 +209,29 @@ public function getCalledServiceName() {
public function getDescription() {
return $this->apiDesc;
}
+ /**
+ * Sets the base path for all services in this manager.
+ *
+ * The base path will be prepended to each service name when generating paths.
+ * For example, if base path is "/api/v1" and service name is "user",
+ * the final path will be "/api/v1/user".
+ *
+ * @param string $basePath The base path (e.g., "/api/v1"). Leading/trailing slashes are handled automatically.
+ *
+ * @return WebServicesManager Returns self for method chaining.
+ */
+ public function setBasePath(string $basePath): WebServicesManager {
+ $this->basePath = rtrim($basePath, '/');
+ return $this;
+ }
+ /**
+ * Returns the base path for all services.
+ *
+ * @return string The base path.
+ */
+ public function getBasePath(): string {
+ return $this->basePath;
+ }
/**
* Returns an associative array or an object of type Json of filtered request inputs.
*
@@ -281,7 +310,7 @@ public function getOutputStreamPath() {
*
* @param string $serviceName The name of the service.
*
- * @return AbstractWebService|null The method will return an object of type 'WebService'
+ * @return WebService|null The method will return an object of type 'WebService'
* if the service is found. If no service was found which has the given name,
* The method will return null.
*
@@ -342,7 +371,7 @@ public function invParams() {
}
$i++;
}
- $this->sendResponse(ResponseMessage::get('404-1').$val.'.', 404, AbstractWebService::E, new Json([
+ $this->sendResponse(ResponseMessage::get('404-1').$val.'.', 404, WebService::E, new Json([
'invalid' => $paramsNamesArr
]));
}
@@ -398,7 +427,7 @@ public function missingParams() {
}
$i++;
}
- $this->sendResponse(ResponseMessage::get('404-2').$val.'.', 404, AbstractWebService::E, new Json([
+ $this->sendResponse(ResponseMessage::get('404-2').$val.'.', 404, WebService::E, new Json([
'missing' => $paramsNamesArr
]));
}
@@ -417,7 +446,7 @@ public function missingParams() {
*
*/
public function missingServiceName() {
- $this->sendResponse(ResponseMessage::get('404-3'), 404, AbstractWebService::E);
+ $this->sendResponse(ResponseMessage::get('404-3'), 404, WebService::E);
}
/**
* Sends a response message to indicate that a user is not authorized call a
@@ -434,7 +463,7 @@ public function missingServiceName() {
*
*/
public function notAuth() {
- $this->sendResponse(ResponseMessage::get('401'), 401, AbstractWebService::E);
+ $this->sendResponse(ResponseMessage::get('401'), 401, WebService::E);
}
/**
@@ -452,20 +481,32 @@ public final function process() {
if ($this->isContentTypeSupported()) {
if ($this->_checkAction()) {
$actionObj = $this->getServiceByName($this->getCalledServiceName());
+
+ // Configure parameters for ResponseBody services before getting them
+ if ($this->serviceHasResponseBodyMethods($actionObj)) {
+ $this->configureServiceParameters($actionObj);
+ }
+
+ $params = $actionObj->getParameters();
$params = $actionObj->getParameters();
$this->filter->clearParametersDef();
$this->filter->clearInputs();
-
+ $requestMethod = $this->getRequest()->getRequestMethod();
+
foreach ($params as $param) {
- $this->filter->addRequestParameter($param);
+ $paramMethods = $param->getMethods();
+
+ if (count($paramMethods) == 0 || in_array($requestMethod, $paramMethods)) {
+ $this->filter->addRequestParameter($param);
+ }
}
$this->filterInputsHelper();
$i = $this->getInputs();
if (!($i instanceof Json)) {
- $this->_processNonJson($params);
+ $this->_processNonJson($this->filter->getParameters());
} else {
- $this->_processJson($params);
+ $this->_processJson($this->filter->getParameters());
}
}
} else {
@@ -501,7 +542,7 @@ public function readOutputStream() {
*
* @param string $name The name of the service.
*
- * @return AbstractWebService|null If a web service which has the given name was found
+ * @return WebService|null If a web service which has the given name was found
* and removed, the method will return an object that represent the removed
* service. Other than that, the method will return null.
*
@@ -542,7 +583,7 @@ public function removeServices() {
*
*/
public function requestMethodNotAllowed() {
- $this->sendResponse(ResponseMessage::get('405'), 405, AbstractWebService::E);
+ $this->sendResponse(ResponseMessage::get('405'), 405, WebService::E);
}
/**
* Sends Back a data using specific content type and specific response code.
@@ -646,7 +687,7 @@ public function sendResponse(string $message, int $code = 200, string $type = ''
*
*/
public function serviceNotImplemented() {
- $this->sendResponse(ResponseMessage::get('404-4'), 404, AbstractWebService::E);
+ $this->sendResponse(ResponseMessage::get('404-4'), 404, WebService::E);
}
/**
* Sends a response message to indicate that called web service is not supported by the API.
@@ -662,7 +703,7 @@ public function serviceNotImplemented() {
*
*/
public function serviceNotSupported() {
- $this->sendResponse(ResponseMessage::get('404-5'), 404, AbstractWebService::E);
+ $this->sendResponse(ResponseMessage::get('404-5'), 404, WebService::E);
}
/**
* Sets the description of the web services set.
@@ -759,6 +800,32 @@ public final function setVersion(string $val) : bool {
return false;
}
+ /**
+ * Converts the services manager to an OpenAPI document.
+ *
+ * This method generates a complete OpenAPI 3.1.0 specification document
+ * from the registered services. Each service becomes a path in the document.
+ *
+ * @return OpenAPI\OpenAPIObj The OpenAPI document.
+ */
+ public function toOpenAPI(): OpenAPI\OpenAPIObj {
+ $info = new OpenAPI\InfoObj(
+ $this->getDescription(),
+ $this->getVersion()
+ );
+
+ $openapi = new OpenAPI\OpenAPIObj($info);
+
+ $paths = new OpenAPI\PathsObj();
+ foreach ($this->getServices() as $service) {
+ $path = $this->basePath . '/' . $service->getName();
+ $paths->addPath($path, $service->toPathItemObj());
+ }
+
+ $openapi->setPaths($paths);
+
+ return $openapi;
+ }
/**
* Returns Json object that represents services set.
*
@@ -843,7 +910,7 @@ private function _checkAction(): bool {
return true;
}
} else {
- $this->sendResponse(ResponseMessage::get('404-6'), 404, AbstractWebService::E);
+ $this->sendResponse(ResponseMessage::get('404-6'), 404, WebService::E);
}
} else {
$this->serviceNotSupported();
@@ -892,12 +959,12 @@ private function _processNonJson($params) {
/**
* Adds new web service to the set of web services.
*
- * @param AbstractWebService $service The web service that will be added.
+ * @param WebService $service The web service that will be added.
*
*
* @deprecated since version 1.4.7 Use WebservicesSet::addService()
*/
- private function addAction(AbstractWebService $service) : WebServicesManager {
+ private function addAction(WebService $service) : WebServicesManager {
$this->services[$service->getName()] = $service;
$service->setManager($this);
return $this;
@@ -976,8 +1043,17 @@ private function getAction() {
return $retVal;
}
- private function isAuth(AbstractWebService $service) {
+ private function isAuth(WebService $service) {
+ $isAuth = false;
+
if ($service->isAuthRequired()) {
+ // Check if method has authorization annotations
+ if ($service->hasMethodAuthorizationAnnotations()) {
+ // Use annotation-based authorization
+ return $service->checkMethodAuthorization();
+ }
+
+ // Fall back to legacy HTTP-method-specific authorization
$isAuthCheck = 'isAuthorized'.$this->getRequest()->getMethod();
if (!method_exists($service, $isAuthCheck)) {
@@ -989,7 +1065,15 @@ private function isAuth(AbstractWebService $service) {
return true;
}
- private function processService(AbstractWebService $service) {
+ private function processService(WebService $service) {
+ // Try auto-processing only if service has ResponseBody methods
+ if ($this->serviceHasResponseBodyMethods($service)) {
+ // Configure parameters for the target method before processing
+ $this->configureServiceParameters($service);
+ $service->processWithAutoHandling();
+ return;
+ }
+
$processMethod = 'process'.$this->getRequest()->getMethod();
if (!method_exists($service, $processMethod)) {
@@ -998,7 +1082,35 @@ private function processService(AbstractWebService $service) {
$service->$processMethod();
}
}
-
+ /**
+ * Check if service has any methods with ResponseBody annotation.
+ */
+ private function serviceHasResponseBodyMethods(WebService $service): bool {
+ $reflection = new \ReflectionClass($service);
+
+ foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
+ $attributes = $method->getAttributes(\WebFiori\Http\Annotations\ResponseBody::class);
+ if (!empty($attributes)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Configure parameters for the target method of a service.
+ */
+ private function configureServiceParameters(WebService $service): void {
+ if (method_exists($service, 'getTargetMethod')) {
+ $targetMethod = $service->getTargetMethod();
+ if ($targetMethod && method_exists($service, 'configureParametersForMethod')) {
+ $reflection = new \ReflectionMethod($service, 'configureParametersForMethod');
+ $reflection->setAccessible(true);
+ $reflection->invoke($service, $targetMethod);
+ }
+ }
+ }
private function setOutputStreamHelper($trimmed, $mode) : bool {
$tempStream = fopen($trimmed, $mode);
diff --git a/examples/AnnotatedService.php b/examples/AnnotatedService.php
new file mode 100644
index 0000000..542d83b
--- /dev/null
+++ b/examples/AnnotatedService.php
@@ -0,0 +1,22 @@
+setRequestMethods([RequestMethod::GET]);
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $this->sendResponse('Hello from annotated service!');
+ }
+}
+
diff --git a/examples/AuthTestService.php b/examples/AuthTestService.php
new file mode 100644
index 0000000..4103d18
--- /dev/null
+++ b/examples/AuthTestService.php
@@ -0,0 +1,19 @@
+ 'You have super admin access!'];
+ }
+}
diff --git a/examples/AuthenticatedController.php b/examples/AuthenticatedController.php
new file mode 100644
index 0000000..93562a9
--- /dev/null
+++ b/examples/AuthenticatedController.php
@@ -0,0 +1,142 @@
+sendResponse('This is public information - no authentication required');
+ }
+
+ #[GetMapping]
+ #[RequiresAuth]
+ public function getProfile() {
+ $user = SecurityContext::getCurrentUser();
+ $this->sendResponse('User profile', 200, 'success', [
+ 'user' => $user,
+ 'roles' => SecurityContext::getRoles(),
+ 'authorities' => SecurityContext::getAuthorities()
+ ]);
+ }
+
+ #[PostMapping]
+ #[PreAuthorize("hasRole('ADMIN')")]
+ public function adminOperation() {
+ $this->sendResponse('Admin operation completed successfully');
+ }
+
+ #[PostMapping]
+ #[PreAuthorize("hasAuthority('USER_MANAGE')")]
+ public function manageUsers() {
+ $this->sendResponse('User management operation completed');
+ }
+
+ public function isAuthorized(): bool {
+ // This is the fallback authorization check
+ // In a real application, you might check JWT tokens, session, etc.
+ return true;
+ }
+
+ public function processRequest() {
+ // Check method-level authorization first
+ if (!$this->checkMethodAuthorization()) {
+ $this->sendResponse('Access denied', 403, 'error');
+ return;
+ }
+
+ $action = $_GET['action'] ?? 'public';
+
+ switch ($action) {
+ case 'public':
+ $this->getPublicInfo();
+ break;
+ case 'profile':
+ $this->getProfile();
+ break;
+ case 'admin':
+ $this->adminOperation();
+ break;
+ case 'manage':
+ $this->manageUsers();
+ break;
+ default:
+ $this->sendResponse('Unknown action', 400, 'error');
+ }
+ }
+
+ protected function getCurrentProcessingMethod(): ?string {
+ $action = $_GET['action'] ?? 'public';
+ return match($action) {
+ 'public' => 'getPublicInfo',
+ 'profile' => 'getProfile',
+ 'admin' => 'adminOperation',
+ 'manage' => 'manageUsers',
+ default => null
+ };
+ }
+}
+
+// Demo usage
+// echo "=== Authentication Demo ===\n";
+
+// $controller = new AuthenticatedController();
+
+// // Test 1: Public access (no auth required)
+// echo "\n1. Testing public access:\n";
+// $_GET['action'] = 'public';
+// $controller->processRequest();
+
+// // Test 2: Private access without authentication
+// echo "\n2. Testing private access without auth:\n";
+// $_GET['action'] = 'profile';
+// $controller->processRequest();
+
+// Test 3: Set up authentication
+// echo "\n3. Setting up authentication:\n";
+// SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com']);
+// SecurityContext::setRoles(['USER']);
+// SecurityContext::setAuthorities(['USER_READ']);
+
+// echo "User authenticated: " . (SecurityContext::isAuthenticated() ? 'Yes' : 'No') . "\n";
+// echo "Roles: " . implode(', ', SecurityContext::getRoles()) . "\n";
+// echo "Authorities: " . implode(', ', SecurityContext::getAuthorities()) . "\n";
+
+// // Test 4: Private access with authentication
+// echo "\n4. Testing private access with auth:\n";
+// $_GET['action'] = 'profile';
+// $controller->processRequest();
+
+// // Test 5: Admin access without admin role
+// echo "\n5. Testing admin access without admin role:\n";
+// $_GET['action'] = 'admin';
+// $controller->processRequest();
+
+// // Test 6: Grant admin role and try again
+// echo "\n6. Granting admin role and testing admin access:\n";
+// SecurityContext::setRoles(['USER', 'ADMIN']);
+// $controller->processRequest();
+
+// // Test 7: Authority-based access
+// echo "\n7. Testing authority-based access:\n";
+// $_GET['action'] = 'manage';
+// $controller->processRequest();
+
+// // Test 8: Grant required authority
+// echo "\n8. Granting USER_MANAGE authority:\n";
+// SecurityContext::setAuthorities(['USER_READ', 'USER_MANAGE']);
+// $controller->processRequest();
+
+// // Cleanup
+// SecurityContext::clear();
+// unset($_GET['action']);
diff --git a/examples/CompleteApiDemo.php b/examples/CompleteApiDemo.php
new file mode 100644
index 0000000..16f4e4a
--- /dev/null
+++ b/examples/CompleteApiDemo.php
@@ -0,0 +1,124 @@
+getParamVal('id');
+
+ if ($id) {
+ // Get specific user
+ if ($id <= 0) {
+ throw new BadRequestException('Invalid user ID');
+ }
+
+ if ($id === 404) {
+ throw new NotFoundException('User not found');
+ }
+
+ return [
+ 'user' => [
+ 'id' => $id,
+ 'name' => 'John Doe',
+ 'email' => 'john@example.com'
+ ]
+ ];
+ } else {
+ // Get all users
+ return [
+ 'users' => [
+ ['id' => 1, 'name' => 'John Doe'],
+ ['id' => 2, 'name' => 'Jane Smith']
+ ],
+ 'total' => 2
+ ];
+ }
+ }
+
+ #[PostMapping]
+ #[ResponseBody(status: 201)]
+ #[RequestParam('name', 'string')]
+ #[RequestParam('email', 'email')]
+ #[PreAuthorize("hasAuthority('USER_CREATE')")]
+ public function createUser(): array {
+ $name = $this->getParamVal('name');
+ $email = $this->getParamVal('email');
+
+ if (empty($name)) {
+ throw new BadRequestException('Name is required');
+ }
+
+ return [
+ 'message' => 'User created successfully',
+ 'user' => [
+ 'id' => rand(1000, 9999),
+ 'name' => $name,
+ 'email' => $email,
+ 'created_at' => date('Y-m-d H:i:s')
+ ]
+ ];
+ }
+
+ #[PutMapping]
+ #[ResponseBody]
+ #[RequestParam('id', 'int')]
+ #[RequestParam('name', 'string', true)]
+ #[RequestParam('email', 'email', true)]
+ #[PreAuthorize("hasAuthority('USER_UPDATE')")]
+ public function updateUser(): array {
+ $id = $this->getParamVal('id');
+ $name = $this->getParamVal('name');
+ $email = $this->getParamVal('email');
+
+ if ($id === 404) {
+ throw new NotFoundException('User not found');
+ }
+
+ $updates = array_filter([
+ 'name' => $name,
+ 'email' => $email
+ ]);
+
+ return [
+ 'message' => 'User updated successfully',
+ 'user' => [
+ 'id' => $id,
+ 'updates' => $updates,
+ 'updated_at' => date('Y-m-d H:i:s')
+ ]
+ ];
+ }
+
+ #[DeleteMapping]
+ #[ResponseBody(status: 204)]
+ #[RequestParam('id', 'int')]
+ #[PreAuthorize("hasRole('ADMIN')")]
+ public function deleteUser(): null {
+ $id = $this->getParamVal('id');
+
+ if ($id === 404) {
+ throw new NotFoundException('User not found');
+ }
+
+ // Simulate deletion
+ return null; // Auto-converts to 204 No Content
+ }
+}
diff --git a/examples/GetRandomService.php b/examples/GetRandomService.php
index ae48520..7e1cc8d 100644
--- a/examples/GetRandomService.php
+++ b/examples/GetRandomService.php
@@ -1,43 +1,33 @@
setRequestMethods([
- RequestMethod::GET,
- RequestMethod::POST
- ]);
+ parent::__construct('get-random');
+ $this->setRequestMethods([RequestMethod::GET, RequestMethod::POST]);
+ $this->setDescription('Returns a random integer. If no range is specified, the method will return a number between 0 and getrandmax().');
$this->addParameters([
'min' => [
ParamOption::TYPE => ParamType::INT,
- ParamOption::OPTIONAL => true
+ ParamOption::OPTIONAL => true,
+ ParamOption::DESCRIPTION => 'Minimum value for the random number.'
],
'max' => [
ParamOption::TYPE => ParamType::INT,
- ParamOption::OPTIONAL => true
+ ParamOption::OPTIONAL => true,
+ ParamOption::DESCRIPTION => 'Maximum value for the random number.'
]
]);
}
- public function isAuthorized() {
-// $authHeader = $this->getAuthHeader();
-//
-// if ($authHeader === null) {
-// return false;
-// }
-//
-// $scheme = $authHeader->getScheme();
-// $credentials = $authHeader->getCredentials();
-
- //Verify credentials based on auth scheme (e.g. 'Basic', 'Barear'
+ public function isAuthorized(): bool {
+ return true;
}
public function processRequest() {
@@ -49,6 +39,9 @@ public function processRequest() {
} else {
$random = rand();
}
- $this->sendResponse($random);
+
+ $this->sendResponse('Random number generated', 'success', 200, [
+ 'number' => $random
+ ]);
}
}
diff --git a/examples/HelloWithAuthService.php b/examples/HelloWithAuthService.php
index 40fdd2a..2585571 100644
--- a/examples/HelloWithAuthService.php
+++ b/examples/HelloWithAuthService.php
@@ -2,13 +2,13 @@
require 'loader.php';
-use WebFiori\Http\AbstractWebService;
+use WebFiori\Http\WebService;
use WebFiori\Http\ParamOption;
use WebFiori\Http\ParamType;
use WebFiori\Http\RequestMethod;
use WebFiori\Http\ResponseMessage;
-class HelloWithAuthService extends AbstractWebService {
+class HelloWithAuthService extends WebService {
public function __construct() {
parent::__construct('hello-with-auth');
$this->setRequestMethods([RequestMethod::GET]);
@@ -20,7 +20,7 @@ public function __construct() {
]
]);
}
- public function isAuthorized() {
+ public function isAuthorized(): bool {
//Change default response message to custom one
ResponseMessage::set('401', 'Not authorized to use this API.');
diff --git a/examples/HelloWorldService.php b/examples/HelloWorldService.php
index 44233e8..23dbd9c 100644
--- a/examples/HelloWorldService.php
+++ b/examples/HelloWorldService.php
@@ -1,25 +1,27 @@
setRequestMethods([RequestMethod::GET]);
+ $this->setDescription('Returns a greeting message.');
$this->addParameters([
'my-name' => [
ParamOption::TYPE => ParamType::STRING,
- ParamOption::OPTIONAL => true
+ ParamOption::OPTIONAL => true,
+ ParamOption::DESCRIPTION => 'Your name to include in the greeting.'
]
]);
}
- public function isAuthorized() {
+ public function isAuthorized(): bool {
+ return true;
}
public function processRequest() {
@@ -27,7 +29,8 @@ public function processRequest() {
if ($name !== null) {
$this->sendResponse("Hello '$name'.");
+ } else {
+ $this->sendResponse('Hello World!');
}
- $this->sendResponse('Hello World!');
}
}
diff --git a/examples/ProductController.php b/examples/ProductController.php
new file mode 100644
index 0000000..760edaf
--- /dev/null
+++ b/examples/ProductController.php
@@ -0,0 +1,91 @@
+getParamVal('page');
+ $limit = $this->getParamVal('limit');
+
+ $this->sendResponse('Products retrieved', 'success', 200, [
+ 'page' => $page,
+ 'limit' => $limit,
+ 'products' => []
+ ]);
+ }
+
+ #[PostMapping]
+ #[RequestParam('name', 'string', false, null, 'Product name')]
+ #[RequestParam('price', 'float', false, null, 'Product price')]
+ #[RequestParam('category', 'string', true, 'General', 'Product category')]
+ public function createProduct() {
+ $name = $this->getParamVal('name');
+ $price = $this->getParamVal('price');
+ $category = $this->getParamVal('category');
+
+ $this->sendResponse('Product created', 'success', 201, [
+ 'id' => 123,
+ 'name' => $name,
+ 'price' => $price,
+ 'category' => $category
+ ]);
+ }
+
+ #[PutMapping]
+ #[RequestParam('id', 'int', false, null, 'Product ID')]
+ #[RequestParam('name', 'string', true)]
+ #[RequestParam('price', 'float', true)]
+ public function updateProduct() {
+ $id = $this->getParamVal('id');
+ $name = $this->getParamVal('name');
+ $price = $this->getParamVal('price');
+
+ $this->sendResponse('Product updated', 'success', 200, [
+ 'id' => $id,
+ 'name' => $name,
+ 'price' => $price
+ ]);
+ }
+
+ #[DeleteMapping]
+ #[RequestParam('id', 'int', false, null, 'Product ID to delete')]
+ public function deleteProduct() {
+ $id = $this->getParamVal('id');
+ $this->sendResponse('Product deleted', 'success', 200, ['id' => $id]);
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
+
+ switch ($method) {
+ case \WebFiori\Http\RequestMethod::GET:
+ $this->getProducts();
+ break;
+ case \WebFiori\Http\RequestMethod::POST:
+ $this->createProduct();
+ break;
+ case \WebFiori\Http\RequestMethod::PUT:
+ $this->updateProduct();
+ break;
+ case \WebFiori\Http\RequestMethod::DELETE:
+ $this->deleteProduct();
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/UserController.php b/examples/UserController.php
new file mode 100644
index 0000000..391766c
--- /dev/null
+++ b/examples/UserController.php
@@ -0,0 +1,58 @@
+sendResponse('Retrieved all users', 'success', 200, ['users' => []]);
+ }
+
+ #[PostMapping]
+ public function createUser() {
+ $this->sendResponse('User created', 'success', 201, ['id' => 123]);
+ }
+
+ #[PutMapping]
+ public function updateUser() {
+ $this->sendResponse('User updated', 'success', 200);
+ }
+
+ #[DeleteMapping]
+ public function deleteUser() {
+ $this->sendResponse('User deleted', 'success', 200);
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
+
+ switch ($method) {
+ case \WebFiori\Http\RequestMethod::GET:
+ $this->getUsers();
+ break;
+ case \WebFiori\Http\RequestMethod::POST:
+ $this->createUser();
+ break;
+ case \WebFiori\Http\RequestMethod::PUT:
+ $this->updateUser();
+ break;
+ case \WebFiori\Http\RequestMethod::DELETE:
+ $this->deleteUser();
+ break;
+ default:
+ $this->sendResponse('Method not allowed', 'error', 405);
+ }
+ }
+}
diff --git a/examples/index.php b/examples/index.php
index 0bbe98e..537e386 100644
--- a/examples/index.php
+++ b/examples/index.php
@@ -4,14 +4,32 @@
require 'HelloWorldService.php';
require 'GetRandomService.php';
require 'HelloWithAuthService.php';
+require 'CompleteApiDemo.php';
+require 'ProductController.php';
+require 'AuthenticatedController.php';
+require 'UserController.php';
+require 'AuthTestService.php';
use HelloWorldService;
use GetRandomService;
use HelloWithAuthService;
use WebFiori\Http\WebServicesManager;
+use WebFiori\Http\SecurityContext;
+
+// Set up authentication context
+SecurityContext::setCurrentUser(['id' => 1, 'name' => 'Demo User']);
+SecurityContext::setRoles(['USER', 'ADMIN']);
+SecurityContext::setCurrentUser(['id' => 1, 'name' => 'Demo User']);
+SecurityContext::setRoles(['USER', 'ADMIN']);
+SecurityContext::setAuthorities(['USER_CREATE', 'USER_UPDATE', 'USER_DELETE']);
$manager = new WebServicesManager();
$manager->addService(new HelloWorldService());
$manager->addService(new GetRandomService());
$manager->addService(new HelloWithAuthService());
+$manager->addService(new CompleteApiDemo());
+$manager->addService(new ProductController());
+$manager->addService(new AuthenticatedController());
+$manager->addService(new UserController());
+$manager->addService(new AuthTestService());
$manager->process();
diff --git a/examples/loader.php b/examples/loader.php
index 3290d0c..a83718c 100644
--- a/examples/loader.php
+++ b/examples/loader.php
@@ -4,4 +4,4 @@
ini_set('display_errors', 1);
error_reporting(-1);
-require_once '../vendor/autoload.php';
+require_once __DIR__ . '/../vendor/autoload.php';
diff --git a/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php b/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php
new file mode 100644
index 0000000..9345a3f
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php
@@ -0,0 +1,88 @@
+assertFalse($service->isAuthRequired());
+ }
+
+ public function testSecurityContextAuthentication() {
+ // Test unauthenticated state
+ $this->assertFalse(SecurityContext::isAuthenticated());
+
+ // Set user and roles
+ SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN', 'USER'], ['USER_CREATE', 'USER_READ'], true));
+
+ $this->assertTrue(SecurityContext::isAuthenticated());
+ $this->assertTrue(SecurityContext::hasRole('ADMIN'));
+ $this->assertTrue(SecurityContext::hasAuthority('USER_CREATE'));
+ $this->assertFalse(SecurityContext::hasRole('GUEST'));
+ }
+
+ public function testMethodLevelAuthorization() {
+ $service = new SecureService();
+
+ // Test public method (AllowAnonymous)
+ $_GET['action'] = 'public';
+ $this->assertTrue($service->checkMethodAuthorization());
+
+ // Test private method without auth (RequiresAuth)
+ $_GET['action'] = 'private';
+ $this->assertFalse($service->checkMethodAuthorization());
+
+ // Test private method with auth
+ SecurityContext::setCurrentUser(new TestUser(1, [], [], true));
+ $this->assertTrue($service->checkMethodAuthorization());
+
+ // Test admin method without admin role
+ $_GET['action'] = 'admin';
+ SecurityContext::setRoles(['USER']);
+ $this->assertFalse($service->checkMethodAuthorization());
+
+ // Test admin method with admin role
+ SecurityContext::setRoles(['ADMIN']);
+ $this->assertTrue($service->checkMethodAuthorization());
+
+ // Test authority-based method
+ $_GET['action'] = 'create';
+ SecurityContext::setAuthorities(['USER_READ']);
+ $this->assertFalse($service->checkMethodAuthorization());
+
+ SecurityContext::setAuthorities(['USER_CREATE']);
+ $this->assertTrue($service->checkMethodAuthorization());
+ }
+
+ public function testSecurityExpressions() {
+ SecurityContext::clear();
+
+ // Test without authentication
+ $this->assertFalse(SecurityContext::evaluateExpression("hasRole('ADMIN')"));
+ $this->assertFalse(SecurityContext::evaluateExpression('isAuthenticated()'));
+ $this->assertTrue(SecurityContext::evaluateExpression('permitAll()'));
+
+ // Test with authentication and roles
+ SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN'], ['USER_CREATE'], true));
+
+ $this->assertTrue(SecurityContext::evaluateExpression("hasRole('ADMIN')"));
+ $this->assertFalse(SecurityContext::evaluateExpression("hasRole('GUEST')"));
+ $this->assertTrue(SecurityContext::evaluateExpression("hasAuthority('USER_CREATE')"));
+ $this->assertTrue(SecurityContext::evaluateExpression('isAuthenticated()'));
+ }
+
+ protected function tearDown(): void {
+ SecurityContext::clear();
+ unset($_GET['action']);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/ExceptionHandlingTest.php b/tests/WebFiori/Tests/Http/ExceptionHandlingTest.php
new file mode 100644
index 0000000..7087c63
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/ExceptionHandlingTest.php
@@ -0,0 +1,109 @@
+assertEquals(404, $notFound->getStatusCode());
+ $this->assertEquals('error', $notFound->getResponseType());
+ $this->assertEquals('Resource not found', $notFound->getMessage());
+
+ $badRequest = new BadRequestException('Invalid input');
+ $this->assertEquals(400, $badRequest->getStatusCode());
+ $this->assertEquals('error', $badRequest->getResponseType());
+
+ $unauthorized = new UnauthorizedException('Login required');
+ $this->assertEquals(401, $unauthorized->getStatusCode());
+ $this->assertEquals('error', $unauthorized->getResponseType());
+
+ $forbidden = new ForbiddenException('Access denied');
+ $this->assertEquals(403, $forbidden->getStatusCode());
+ $this->assertEquals('error', $forbidden->getResponseType());
+ }
+
+ public function testExceptionDefaults() {
+ $notFound = new NotFoundException();
+ $this->assertEquals('Not Found', $notFound->getMessage());
+
+ $badRequest = new BadRequestException();
+ $this->assertEquals('Bad Request', $badRequest->getMessage());
+
+ $unauthorized = new UnauthorizedException();
+ $this->assertEquals('Unauthorized', $unauthorized->getMessage());
+
+ $forbidden = new ForbiddenException();
+ $this->assertEquals('Forbidden', $forbidden->getMessage());
+ }
+
+ public function testServiceExceptionHandling() {
+ $service = new ExceptionTestService();
+
+ // Test that method has ResponseBody annotation
+ $this->assertTrue($service->hasResponseBodyAnnotation('getUser'));
+
+ // Test exception throwing with test parameter
+ $_GET['test_id'] = 404;
+ $this->expectException(NotFoundException::class);
+ $this->expectExceptionMessage('User not found');
+ $service->getUser();
+ }
+
+ public function testDifferentExceptionTypes() {
+ $service = new ExceptionTestService();
+
+ // Test BadRequestException
+ $_GET['test_id'] = 400;
+ $this->expectException(BadRequestException::class);
+ $this->expectExceptionMessage('Invalid user ID');
+ $service->getUser();
+ }
+
+ public function testUnauthorizedException() {
+ $service = new ExceptionTestService();
+
+ $this->expectException(UnauthorizedException::class);
+ $this->expectExceptionMessage('Authentication required');
+ $service->createUser();
+ }
+
+ public function testGenericException() {
+ $service = new ExceptionTestService();
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Generic error');
+ $service->getError();
+ }
+
+ public function testHandleExceptionMethod() {
+ $service = new ExceptionTestService();
+ $exception = new NotFoundException('Test not found');
+
+ // Test the handleException method directly
+ $reflection = new \ReflectionClass($service);
+ $method = $reflection->getMethod('handleException');
+ $method->setAccessible(true);
+
+ // The method should not throw an exception
+ $this->expectNotToPerformAssertions();
+ $method->invoke($service, $exception);
+ }
+
+ protected function tearDown(): void {
+ unset($_GET['action']);
+ unset($_GET['test_id']);
+ unset($_SERVER['REQUEST_METHOD']);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php b/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php
new file mode 100644
index 0000000..3b85632
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php
@@ -0,0 +1,83 @@
+setName('manual-service');
+ $service->addRequestMethod(\WebFiori\Http\RequestMethod::GET);
+ $service->addParameters([
+ 'test' => [
+ \WebFiori\Http\ParamOption::TYPE => ParamType::STRING
+ ]
+ ]);
+
+ $this->assertEquals('manual-service', $service->getName());
+ $this->assertContains(\WebFiori\Http\RequestMethod::GET, $service->getRequestMethods());
+ $this->assertNotNull($service->getParameterByName('test'));
+ }
+
+ public function testAnnotationOverridesManualConfiguration() {
+ $service = new #[\WebFiori\Http\Annotations\RestController('annotated-override')]
+ class extends \WebFiori\Http\WebService {
+ public function __construct() {
+ parent::__construct('manual-name'); // This should be overridden
+ }
+
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function getData() {}
+
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+
+ $this->assertEquals('annotated-override', $service->getName());
+ $this->assertContains(\WebFiori\Http\RequestMethod::GET, $service->getRequestMethods());
+ }
+
+ public function testMixedConfigurationApproach() {
+ $service = new #[\WebFiori\Http\Annotations\RestController('mixed-service')]
+ class extends \WebFiori\Http\WebService {
+ public function __construct() {
+ parent::__construct();
+ // Add manual configuration after annotation processing
+ $this->addRequestMethod(\WebFiori\Http\RequestMethod::PATCH);
+ $this->addParameters([
+ 'manual_param' => [
+ \WebFiori\Http\ParamOption::TYPE => ParamType::STRING
+ ]
+ ]);
+ }
+
+ #[\WebFiori\Http\Annotations\PostMapping]
+ #[\WebFiori\Http\Annotations\RequestParam('annotated_param', 'int')]
+ public function createData() {}
+
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+
+ $this->assertEquals('mixed-service', $service->getName());
+
+ $methods = $service->getRequestMethods();
+ $this->assertContains(\WebFiori\Http\RequestMethod::POST, $methods); // From annotation
+ $this->assertContains(\WebFiori\Http\RequestMethod::PATCH, $methods); // Manual addition
+
+ $this->assertNotNull($service->getParameterByName('annotated_param', 'POST')); // From annotation
+ $this->assertNotNull($service->getParameterByName('manual_param')); // Manual addition
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/HttpCookieTest.php b/tests/WebFiori/Tests/Http/HttpCookieTest.php
index 4e8402a..6304a03 100644
--- a/tests/WebFiori/Tests/Http/HttpCookieTest.php
+++ b/tests/WebFiori/Tests/Http/HttpCookieTest.php
@@ -96,7 +96,8 @@ public function testRemainingTime00() {
$cookie->setExpires(1);
$this->assertEquals(60, $cookie->getRemainingTime());
sleep(3);
- $this->assertEquals(57, $cookie->getRemainingTime());
+ $this->assertGreaterThanOrEqual(55, $cookie->getRemainingTime());
+ $this->assertLessThanOrEqual(57, $cookie->getRemainingTime());
}
/**
* @test
diff --git a/tests/WebFiori/Tests/Http/ManagerInfoServiceTest.php b/tests/WebFiori/Tests/Http/ManagerInfoServiceTest.php
new file mode 100644
index 0000000..6e6531b
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/ManagerInfoServiceTest.php
@@ -0,0 +1,33 @@
+addService($service);
+
+ $this->assertEquals('api-docs', $service->getName());
+ $this->assertStringContainsString('information about all end points', $service->getDescription());
+ $this->assertContains(RequestMethod::GET, $service->getRequestMethods());
+
+ // Test that processRequest sends JSON
+ $this->assertNotNull($service->getManager());
+ $this->assertSame($manager, $service->getManager());
+ }
+}
+
+class TestManagerInfoService extends ManagerInfoService {
+ public function isAuthorized(): bool {
+ return true;
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/MethodMappingTest.php b/tests/WebFiori/Tests/Http/MethodMappingTest.php
new file mode 100644
index 0000000..160bb45
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/MethodMappingTest.php
@@ -0,0 +1,59 @@
+getRequestMethods();
+
+ $this->assertContains(RequestMethod::GET, $methods);
+ $this->assertContains(RequestMethod::POST, $methods);
+ $this->assertCount(2, $methods);
+ }
+
+ public function testAllMethodMappings() {
+ $service = new AllMethodsService();
+ $methods = $service->getRequestMethods();
+
+ $this->assertContains(RequestMethod::GET, $methods);
+ $this->assertContains(RequestMethod::POST, $methods);
+ $this->assertContains(RequestMethod::PUT, $methods);
+ $this->assertContains(RequestMethod::DELETE, $methods);
+ $this->assertCount(4, $methods);
+ }
+
+ public function testServiceWithoutMethodAnnotations() {
+ $service = new class extends \WebFiori\Http\WebService {
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+
+ $methods = $service->getRequestMethods();
+ $this->assertEmpty($methods);
+ }
+
+ public function testMixedAnnotationAndManualConfiguration() {
+ $service = new class extends \WebFiori\Http\WebService {
+ public function __construct() {
+ parent::__construct('mixed-service');
+ $this->addRequestMethod(RequestMethod::PATCH); // Manual addition
+ }
+
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function getData() {}
+
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+
+ $methods = $service->getRequestMethods();
+ $this->assertContains(RequestMethod::GET, $methods); // From annotation
+ $this->assertContains(RequestMethod::PATCH, $methods); // Manual addition
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/OpenAPITest.php b/tests/WebFiori/Tests/Http/OpenAPITest.php
new file mode 100644
index 0000000..5560736
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/OpenAPITest.php
@@ -0,0 +1,525 @@
+assertEquals('My API', $info->getTitle());
+ $this->assertEquals('1.0.0', $info->getVersion());
+
+ $info->setSummary('API Summary');
+ $this->assertEquals('API Summary', $info->getSummary());
+
+ $info->setDescription('API Description');
+ $this->assertEquals('API Description', $info->getDescription());
+
+ $info->setTermsOfService('https://example.com/terms');
+ $this->assertEquals('https://example.com/terms', $info->getTermsOfService());
+
+ $contact = new ContactObj();
+ $info->setContact($contact);
+ $this->assertSame($contact, $info->getContact());
+
+ $license = new LicenseObj('MIT');
+ $info->setLicense($license);
+ $this->assertSame($license, $info->getLicense());
+
+ $json = $info->toJSON();
+ $this->assertEquals('My API', $json->get('title'));
+ $this->assertEquals('1.0.0', $json->get('version'));
+ $this->assertEquals('API Summary', $json->get('summary'));
+ }
+
+ /**
+ * @test
+ */
+ public function testLicenseObj() {
+ $license = new LicenseObj('Apache 2.0');
+ $this->assertEquals('Apache 2.0', $license->getName());
+
+ $license->setIdentifier('Apache-2.0');
+ $this->assertEquals('Apache-2.0', $license->getIdentifier());
+ $this->assertNull($license->getUrl());
+
+ $license->setUrl('https://www.apache.org/licenses/LICENSE-2.0.html');
+ $this->assertEquals('https://www.apache.org/licenses/LICENSE-2.0.html', $license->getUrl());
+ $this->assertNull($license->getIdentifier());
+
+ $json = $license->toJSON();
+ $this->assertEquals('Apache 2.0', $json->get('name'));
+ $this->assertEquals('https://www.apache.org/licenses/LICENSE-2.0.html', $json->get('url'));
+ }
+
+ /**
+ * @test
+ */
+ public function testContactObj() {
+ $contact = new ContactObj();
+ $this->assertNull($contact->getName());
+
+ $contact->setName('API Support');
+ $this->assertEquals('API Support', $contact->getName());
+
+ $contact->setUrl('https://example.com/support');
+ $this->assertEquals('https://example.com/support', $contact->getUrl());
+
+ $contact->setEmail('support@example.com');
+ $this->assertEquals('support@example.com', $contact->getEmail());
+
+ $json = $contact->toJSON();
+ $this->assertEquals('API Support', $json->get('name'));
+ $this->assertEquals('https://example.com/support', $json->get('url'));
+ $this->assertEquals('support@example.com', $json->get('email'));
+ }
+
+ /**
+ * @test
+ */
+ public function testOpenAPIObj() {
+ $info = new InfoObj('Test API', '2.0.0');
+ $openapi = new OpenAPIObj($info);
+
+ $this->assertEquals('3.1.0', $openapi->getOpenapi());
+ $this->assertSame($info, $openapi->getInfo());
+
+ $openapi->setOpenapi('3.0.0');
+ $this->assertEquals('3.0.0', $openapi->getOpenapi());
+
+ $paths = new PathsObj();
+ $openapi->setPaths($paths);
+ $this->assertSame($paths, $openapi->getPaths());
+
+ $json = $openapi->toJSON();
+ $this->assertEquals('3.0.0', $json->get('openapi'));
+ $this->assertNotNull($json->get('info'));
+ }
+
+ /**
+ * @test
+ */
+ public function testServerObj() {
+ $server = new ServerObj('https://api.example.com');
+ $this->assertEquals('https://api.example.com', $server->getUrl());
+
+ $server->setDescription('Production server');
+ $this->assertEquals('Production server', $server->getDescription());
+
+ $json = $server->toJSON();
+ $this->assertEquals('https://api.example.com', $json->get('url'));
+ $this->assertEquals('Production server', $json->get('description'));
+ }
+
+ /**
+ * @test
+ */
+ public function testTagObj() {
+ $tag = new TagObj('users');
+ $this->assertEquals('users', $tag->getName());
+
+ $tag->setDescription('User operations');
+ $this->assertEquals('User operations', $tag->getDescription());
+
+ $externalDocs = new ExternalDocObj('https://docs.example.com');
+ $tag->setExternalDocs($externalDocs);
+ $this->assertSame($externalDocs, $tag->getExternalDocs());
+
+ $json = $tag->toJSON();
+ $this->assertEquals('users', $json->get('name'));
+ $this->assertEquals('User operations', $json->get('description'));
+ }
+
+ /**
+ * @test
+ */
+ public function testExternalDocObj() {
+ $doc = new ExternalDocObj('https://docs.example.com');
+ $this->assertEquals('https://docs.example.com', $doc->getUrl());
+
+ $doc->setDescription('External documentation');
+ $this->assertEquals('External documentation', $doc->getDescription());
+
+ $json = $doc->toJSON();
+ $this->assertEquals('https://docs.example.com', $json->get('url'));
+ $this->assertEquals('External documentation', $json->get('description'));
+ }
+
+ /**
+ * @test
+ */
+ public function testPathsObj() {
+ $paths = new PathsObj();
+ $this->assertEmpty($paths->getPaths());
+
+ $pathItem = new PathItemObj();
+ $paths->addPath('/users', $pathItem);
+
+ $allPaths = $paths->getPaths();
+ $this->assertCount(1, $allPaths);
+ $this->assertSame($pathItem, $allPaths['/users']);
+
+ $json = $paths->toJSON();
+ $this->assertNotNull($json->get('/users'));
+ }
+
+ /**
+ * @test
+ */
+ public function testPathItemObj() {
+ $pathItem = new PathItemObj();
+ $this->assertNull($pathItem->getGet());
+ $this->assertNull($pathItem->getPost());
+
+ $responses = new ResponsesObj();
+ $responses->addResponse('200', 'Success');
+
+ $getOp = new OperationObj($responses);
+ $pathItem->setGet($getOp);
+ $this->assertSame($getOp, $pathItem->getGet());
+
+ $postOp = new OperationObj($responses);
+ $pathItem->setPost($postOp);
+ $this->assertSame($postOp, $pathItem->getPost());
+
+ $putOp = new OperationObj($responses);
+ $pathItem->setPut($putOp);
+ $this->assertSame($putOp, $pathItem->getPut());
+
+ $deleteOp = new OperationObj($responses);
+ $pathItem->setDelete($deleteOp);
+ $this->assertSame($deleteOp, $pathItem->getDelete());
+
+ $patchOp = new OperationObj($responses);
+ $pathItem->setPatch($patchOp);
+ $this->assertSame($patchOp, $pathItem->getPatch());
+
+ $json = $pathItem->toJSON();
+ $this->assertNotNull($json->get('get'));
+ $this->assertNotNull($json->get('post'));
+ $this->assertNotNull($json->get('put'));
+ $this->assertNotNull($json->get('delete'));
+ $this->assertNotNull($json->get('patch'));
+ }
+
+ /**
+ * @test
+ */
+ public function testOperationObj() {
+ $responses = new ResponsesObj();
+ $responses->addResponse('200', 'Success');
+
+ $operation = new OperationObj($responses);
+ $this->assertSame($responses, $operation->getResponses());
+
+ $newResponses = new ResponsesObj();
+ $operation->setResponses($newResponses);
+ $this->assertSame($newResponses, $operation->getResponses());
+
+ $json = $operation->toJSON();
+ $this->assertNotNull($json->get('responses'));
+ }
+
+ /**
+ * @test
+ */
+ public function testResponsesObj() {
+ $responses = new ResponsesObj();
+ $this->assertEmpty($responses->getResponses());
+
+ $responses->addResponse('200', 'Success');
+ $responses->addResponse('404', 'Not found');
+
+ $allResponses = $responses->getResponses();
+ $this->assertCount(2, $allResponses);
+ $this->assertInstanceOf(ResponseObj::class, $allResponses['200']);
+ $this->assertEquals('Success', $allResponses['200']->getDescription());
+
+ $json = $responses->toJSON();
+ $this->assertNotNull($json->get('200'));
+ $this->assertNotNull($json->get('404'));
+ }
+
+ /**
+ * @test
+ */
+ public function testResponseObj() {
+ $response = new ResponseObj('Operation successful');
+ $this->assertEquals('Operation successful', $response->getDescription());
+
+ $response->setDescription('Updated description');
+ $this->assertEquals('Updated description', $response->getDescription());
+
+ $json = $response->toJSON();
+ $this->assertEquals('Updated description', $json->get('description'));
+ }
+
+ /**
+ * @test
+ */
+ public function testParameterObj() {
+ $param = new ParameterObj('userId', 'path');
+ $this->assertEquals('userId', $param->getName());
+ $this->assertEquals('path', $param->getIn());
+
+ $param->setDescription('User ID parameter');
+ $this->assertEquals('User ID parameter', $param->getDescription());
+
+ $param->setRequired(true);
+ $this->assertTrue($param->getRequired());
+
+ $param->setDeprecated(true);
+ $this->assertTrue($param->getDeprecated());
+
+ $param->setAllowEmptyValue(true);
+ $this->assertTrue($param->getAllowEmptyValue());
+
+ $param->setStyle('simple');
+ $this->assertEquals('simple', $param->getStyle());
+
+ $param->setExplode(false);
+ $this->assertFalse($param->getExplode());
+
+ $param->setAllowReserved(true);
+ $this->assertTrue($param->getAllowReserved());
+
+ $param->setSchema(['type' => 'integer']);
+ $this->assertEquals(['type' => 'integer'], $param->getSchema());
+
+ $param->setExample(123);
+ $this->assertEquals(123, $param->getExample());
+
+ $param->setExamples(['example1' => ['value' => 123]]);
+ $this->assertEquals(['example1' => ['value' => 123]], $param->getExamples());
+
+ $json = $param->toJSON();
+ $this->assertEquals('userId', $json->get('name'));
+ $this->assertEquals('path', $json->get('in'));
+ $this->assertEquals('User ID parameter', $json->get('description'));
+ }
+
+ /**
+ * @test
+ */
+ public function testHeaderObj() {
+ $header = new HeaderObj();
+ $this->assertNull($header->getDescription());
+
+ $header->setDescription('Custom header');
+ $this->assertEquals('Custom header', $header->getDescription());
+
+ $header->setRequired(true);
+ $this->assertTrue($header->getRequired());
+
+ $header->setDeprecated(true);
+ $this->assertTrue($header->getDeprecated());
+
+ $header->setStyle('simple');
+ $this->assertEquals('simple', $header->getStyle());
+
+ $header->setExplode(true);
+ $this->assertTrue($header->getExplode());
+
+ $header->setSchema(['type' => 'string']);
+ $this->assertEquals(['type' => 'string'], $header->getSchema());
+
+ $header->setExample('example-value');
+ $this->assertEquals('example-value', $header->getExample());
+
+ $header->setExamples(['ex1' => ['value' => 'test']]);
+ $this->assertEquals(['ex1' => ['value' => 'test']], $header->getExamples());
+
+ $json = $header->toJSON();
+ $this->assertEquals('Custom header', $json->get('description'));
+ }
+
+ /**
+ * @test
+ */
+ public function testMediaTypeObj() {
+ $mediaType = new MediaTypeObj();
+ $this->assertNull($mediaType->getSchema());
+
+ $mediaType->setSchema(['type' => 'object']);
+ $this->assertEquals(['type' => 'object'], $mediaType->getSchema());
+
+ $json = $mediaType->toJSON();
+ $this->assertNotNull($json->get('schema'));
+ }
+
+ /**
+ * @test
+ */
+ public function testReferenceObj() {
+ $ref = new ReferenceObj('#/components/schemas/User');
+ $this->assertEquals('#/components/schemas/User', $ref->getRef());
+
+ $ref->setSummary('User reference');
+ $this->assertEquals('User reference', $ref->getSummary());
+
+ $ref->setDescription('Reference to User schema');
+ $this->assertEquals('Reference to User schema', $ref->getDescription());
+
+ $json = $ref->toJSON();
+ $this->assertEquals('#/components/schemas/User', $json->get('$ref'));
+ $this->assertEquals('User reference', $json->get('summary'));
+ }
+
+ /**
+ * @test
+ */
+ public function testSecuritySchemeObj() {
+ $scheme = new SecuritySchemeObj('http');
+ $this->assertEquals('http', $scheme->getType());
+
+ $scheme->setDescription('HTTP Basic Auth');
+ $this->assertEquals('HTTP Basic Auth', $scheme->getDescription());
+
+ $scheme->setName('Authorization');
+ $this->assertEquals('Authorization', $scheme->getName());
+
+ $scheme->setIn('header');
+ $this->assertEquals('header', $scheme->getIn());
+
+ $scheme->setScheme('basic');
+ $this->assertEquals('basic', $scheme->getScheme());
+
+ $scheme->setBearerFormat('JWT');
+ $this->assertEquals('JWT', $scheme->getBearerFormat());
+
+ $flows = new OAuthFlowsObj();
+ $scheme->setFlows($flows);
+ $this->assertSame($flows, $scheme->getFlows());
+
+ $scheme->setOpenIdConnectUrl('https://example.com/.well-known/openid-configuration');
+ $this->assertEquals('https://example.com/.well-known/openid-configuration', $scheme->getOpenIdConnectUrl());
+
+ $json = $scheme->toJSON();
+ $this->assertEquals('http', $json->get('type'));
+ $this->assertEquals('HTTP Basic Auth', $json->get('description'));
+ }
+
+ /**
+ * @test
+ */
+ public function testOAuthFlowObj() {
+ $flow = new OAuthFlowObj();
+ $this->assertEmpty($flow->getScopes());
+
+ $flow->setAuthorizationUrl('https://example.com/oauth/authorize');
+ $this->assertEquals('https://example.com/oauth/authorize', $flow->getAuthorizationUrl());
+
+ $flow->setTokenUrl('https://example.com/oauth/token');
+ $this->assertEquals('https://example.com/oauth/token', $flow->getTokenUrl());
+
+ $flow->setRefreshUrl('https://example.com/oauth/refresh');
+ $this->assertEquals('https://example.com/oauth/refresh', $flow->getRefreshUrl());
+
+ $flow->addScope('read', 'Read access');
+ $flow->addScope('write', 'Write access');
+
+ $scopes = $flow->getScopes();
+ $this->assertCount(2, $scopes);
+ $this->assertEquals('Read access', $scopes['read']);
+
+ $json = $flow->toJSON();
+ $this->assertNotNull($json->get('scopes'));
+ }
+
+ /**
+ * @test
+ */
+ public function testOAuthFlowsObj() {
+ $flows = new OAuthFlowsObj();
+ $this->assertNull($flows->getImplicit());
+
+ $implicit = new OAuthFlowObj();
+ $flows->setImplicit($implicit);
+ $this->assertSame($implicit, $flows->getImplicit());
+
+ $password = new OAuthFlowObj();
+ $flows->setPassword($password);
+ $this->assertSame($password, $flows->getPassword());
+
+ $clientCredentials = new OAuthFlowObj();
+ $flows->setClientCredentials($clientCredentials);
+ $this->assertSame($clientCredentials, $flows->getClientCredentials());
+
+ $authCode = new OAuthFlowObj();
+ $flows->setAuthorizationCode($authCode);
+ $this->assertSame($authCode, $flows->getAuthorizationCode());
+
+ $json = $flows->toJSON();
+ $this->assertNotNull($json->get('implicit'));
+ $this->assertNotNull($json->get('password'));
+ }
+
+ /**
+ * @test
+ */
+ public function testSecurityRequirementObj() {
+ $requirement = new SecurityRequirementObj();
+ $this->assertEmpty($requirement->getRequirements());
+
+ $requirement->addRequirement('api_key', []);
+ $requirement->addRequirement('oauth2', ['read', 'write']);
+
+ $reqs = $requirement->getRequirements();
+ $this->assertCount(2, $reqs);
+ $this->assertEmpty($reqs['api_key']);
+ $this->assertEquals(['read', 'write'], $reqs['oauth2']);
+
+ $json = $requirement->toJSON();
+ $this->assertNotNull($json->get('api_key'));
+ $this->assertNotNull($json->get('oauth2'));
+ }
+
+ /**
+ * @test
+ */
+ public function testComponentsObj() {
+ $components = new ComponentsObj();
+ $this->assertEmpty($components->getSchemas());
+ $this->assertEmpty($components->getSecuritySchemes());
+
+ $components->addSchema('User', ['type' => 'object']);
+ $schemas = $components->getSchemas();
+ $this->assertCount(1, $schemas);
+ $this->assertEquals(['type' => 'object'], $schemas['User']);
+
+ $securityScheme = new SecuritySchemeObj('http');
+ $components->addSecurityScheme('basicAuth', $securityScheme);
+ $schemes = $components->getSecuritySchemes();
+ $this->assertCount(1, $schemes);
+ $this->assertSame($securityScheme, $schemes['basicAuth']);
+
+ $json = $components->toJSON();
+ $this->assertNotNull($json->get('schemas'));
+ $this->assertNotNull($json->get('securitySchemes'));
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/ParameterMappingTest.php b/tests/WebFiori/Tests/Http/ParameterMappingTest.php
new file mode 100644
index 0000000..860cb01
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/ParameterMappingTest.php
@@ -0,0 +1,57 @@
+getParameterByName('id', 'GET');
+ $service->getParameterByName('email', 'POST');
+
+ $parameters = $service->getParameters();
+
+ $this->assertCount(4, $parameters); // id, name, email, age
+
+ // Check 'id' parameter
+ $idParam = $service->getParameterByName('id');
+ $this->assertNotNull($idParam);
+ $this->assertEquals(ParamType::INT, $idParam->getType());
+ $this->assertFalse($idParam->isOptional());
+ $this->assertEquals('User ID', $idParam->getDescription());
+
+ // Check 'name' parameter
+ $nameParam = $service->getParameterByName('name');
+ $this->assertNotNull($nameParam);
+ $this->assertEquals(ParamType::STRING, $nameParam->getType());
+ $this->assertTrue($nameParam->isOptional());
+ $this->assertEquals('Anonymous', $nameParam->getDefault());
+
+ // Check 'email' parameter
+ $emailParam = $service->getParameterByName('email');
+ $this->assertNotNull($emailParam);
+ $this->assertEquals(ParamType::EMAIL, $emailParam->getType());
+ $this->assertFalse($emailParam->isOptional());
+
+ // Check 'age' parameter
+ $ageParam = $service->getParameterByName('age');
+ $this->assertNotNull($ageParam);
+ $this->assertEquals(ParamType::INT, $ageParam->getType());
+ $this->assertTrue($ageParam->isOptional());
+ $this->assertEquals(18, $ageParam->getDefault());
+ }
+
+ public function testHttpMethodsFromParameterAnnotations() {
+ $service = new ParameterMappedService();
+ $methods = $service->getRequestMethods();
+
+ $this->assertContains(\WebFiori\Http\RequestMethod::GET, $methods);
+ $this->assertContains(\WebFiori\Http\RequestMethod::POST, $methods);
+ $this->assertCount(2, $methods);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/RequestParameterTest.php b/tests/WebFiori/Tests/Http/RequestParameterTest.php
index 1962c57..4822eab 100644
--- a/tests/WebFiori/Tests/Http/RequestParameterTest.php
+++ b/tests/WebFiori/Tests/Http/RequestParameterTest.php
@@ -598,8 +598,7 @@ public function testSetMinLength05() {
*/
public function testToJson00($reqParam) {
$reqParam->setDescription('Test Parameter.');
- $this->assertEquals('{"name":"a-parameter","type":"string","description":"Test Parameter.",'
- .'"is-optional":false,"default-value":null,"min-val":null,"max-val":null,"min-length":null,"max-length":null}',$reqParam->toJSON().'');
+ $this->assertEquals('{"name":"a-parameter","in":"query","required":true,"description":"Test Parameter.","schema":{"type":"string"}}',$reqParam->toJSON().'');
}
/**
* @test
@@ -608,8 +607,7 @@ public function testToJson00($reqParam) {
*/
public function testToJson01($reqParam) {
$reqParam->setDescription('Test Parameter.');
- $this->assertEquals('{"name":"valid","type":"integer","description":"Test Parameter.",'
- .'"is-optional":true,"default-value":null,"min-val":'.~PHP_INT_MAX.',"max-val":'.PHP_INT_MAX.',"min-length":null,"max-length":null}',$reqParam->toJSON().'');
+ $this->assertEquals('{"name":"valid","in":"query","required":false,"description":"Test Parameter.","schema":{"type":"integer","minimum":'.~PHP_INT_MAX.',"maximum":'.PHP_INT_MAX.'}}',$reqParam->toJSON().'');
}
/**
*
@@ -662,5 +660,17 @@ public function testToString01() {
." Minimum Length => 'null',\n"
." Maximum Length => 'null'\n"
."]\n",$rp.'');
+
+ }
+ /**
+ * @test
+ */
+ public function testRequestMethod00() {
+ $rp = new RequestParameter('user-id','integer');
+ $this->assertEquals([], $rp->getMethods());
+ $rp->addMethod('get');
+ $this->assertEquals(['GET'], $rp->getMethods());
+ $rp->addMethods(['geT', 'PoSt ']);
+ $this->assertEquals(['GET', 'POST'], $rp->getMethods());
}
}
diff --git a/tests/WebFiori/Tests/Http/ResponseBodyTest.php b/tests/WebFiori/Tests/Http/ResponseBodyTest.php
new file mode 100644
index 0000000..23734cd
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/ResponseBodyTest.php
@@ -0,0 +1,153 @@
+getTargetMethod();
+ $this->assertEquals('getArrayData', $targetMethod);
+
+ // Test ResponseBody annotation detection
+ $this->assertTrue($service->hasResponseBodyAnnotation('getArrayData'));
+
+ // Test return value processing
+ $result = $service->getArrayData();
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('users', $result);
+ }
+
+ public function testStringReturnValue() {
+ $service = new ResponseBodyTestService();
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+
+ // Test specific method
+ $this->assertTrue($service->hasResponseBodyAnnotation('getStringData'));
+
+ $result = $service->getStringData();
+ $this->assertIsString($result);
+ $this->assertEquals('Resource created successfully', $result);
+ }
+
+ public function testNullReturnValue() {
+ $service = new ResponseBodyTestService();
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+
+ // Test method discovery for POST
+ $targetMethod = $service->getTargetMethod();
+ $this->assertEquals('deleteData', $targetMethod);
+
+ $result = $service->deleteData();
+ $this->assertNull($result);
+ }
+
+ public function testObjectReturnValue() {
+ $service = new ResponseBodyTestService();
+
+ // Test specific method
+ $this->assertTrue($service->hasResponseBodyAnnotation('getObjectData'));
+
+ $result = $service->getObjectData();
+ $this->assertIsObject($result);
+ $this->assertTrue(property_exists($result, 'message'), "The object should have the attribute 'message'.");
+ }
+
+ public function testMethodWithoutResponseBody() {
+ $service = new ResponseBodyTestService();
+
+ // Should not have ResponseBody annotation
+ $this->assertFalse($service->hasResponseBodyAnnotation('getManualData'));
+ }
+
+ public function testMethodWithParameters() {
+ $service = new ResponseBodyTestService();
+
+ // Test that method has ResponseBody annotation
+ $this->assertTrue($service->hasResponseBodyAnnotation('createUser'));
+
+ // Test method has POST mapping
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $targetMethod = $service->getTargetMethod();
+ $this->assertContains($targetMethod, ['deleteData', 'createUser']); // Either POST method
+ }
+
+ public function testMixedServiceWithAuthentication() {
+ $service = new MixedResponseService();
+
+ // Test methods have correct annotations
+ $this->assertTrue($service->hasResponseBodyAnnotation('getSecureData'));
+ $this->assertTrue($service->hasResponseBodyAnnotation('getPublicData'));
+ $this->assertFalse($service->hasResponseBodyAnnotation('traditionalMethod'));
+
+ // Test with authentication
+ SecurityContext::setCurrentUser(new TestUser(1, ['USER']));
+
+ // The service should be authorized since we set up proper authentication
+ $this->assertTrue($service->checkMethodAuthorization());
+ }
+
+ public function testLegacyServiceCompatibility() {
+ $service = new LegacyService();
+
+ // Should find the GET method
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $targetMethod = $service->getTargetMethod();
+ $this->assertEquals('getData', $targetMethod);
+
+ // Method should not have ResponseBody annotation
+ $this->assertFalse($service->hasResponseBodyAnnotation('getData'));
+ }
+
+ public function testProcessWithAutoHandling() {
+ $service = new ResponseBodyTestService();
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+
+ // Test the auto-processing logic
+ $targetMethod = $service->getTargetMethod();
+ $hasResponseBody = $service->hasResponseBodyAnnotation($targetMethod);
+
+ $this->assertTrue($hasResponseBody);
+ $this->assertEquals('getArrayData', $targetMethod);
+ }
+
+ public function testResponseBodyAnnotationConfiguration() {
+ $service = new ResponseBodyTestService();
+
+ // Test default ResponseBody annotation
+ $reflection = new \ReflectionMethod($service, 'getArrayData');
+ $attributes = $reflection->getAttributes(\WebFiori\Http\Annotations\ResponseBody::class);
+ $this->assertNotEmpty($attributes);
+
+ $responseBody = $attributes[0]->newInstance();
+ $this->assertEquals(200, $responseBody->status);
+ $this->assertEquals('success', $responseBody->type);
+
+ // Test custom ResponseBody annotation
+ $reflection = new \ReflectionMethod($service, 'getStringData');
+ $attributes = $reflection->getAttributes(\WebFiori\Http\Annotations\ResponseBody::class);
+ $responseBody = $attributes[0]->newInstance();
+ $this->assertEquals(201, $responseBody->status);
+ $this->assertEquals('created', $responseBody->type);
+ }
+
+ protected function tearDown(): void {
+ SecurityContext::clear();
+ unset($_GET['action']);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/RestControllerTest.php b/tests/WebFiori/Tests/Http/RestControllerTest.php
new file mode 100644
index 0000000..9ffcc7a
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/RestControllerTest.php
@@ -0,0 +1,163 @@
+assertEquals('annotated-service', $service->getName());
+ $this->assertEquals('A service configured via annotations', $service->getDescription());
+ $this->assertEquals(['GET', 'DELETE'], $service->getRequestMethods());
+ }
+
+ public function testNonAnnotatedService() {
+ $service = new NonAnnotatedService();
+ $this->assertEquals('non-annotated', $service->getName());
+ $this->assertEquals('A traditional service', $service->getDescription());
+ }
+
+ public function testAnnotationWithEmptyName() {
+ $service = new class extends \WebFiori\Http\WebService {
+ public function __construct() {
+ parent::__construct('fallback-name');
+ }
+ };
+
+ $this->assertEquals('fallback-name', $service->getName());
+ }
+
+ public function testAnnotationWithoutFallback() {
+ $service = new class extends \WebFiori\Http\WebService {
+ };
+
+ $this->assertEquals('new-service', $service->getName());
+ }
+
+ public function testAnnotatedServiceWithManager() {
+ $manager = new WebServicesManager();
+ $service = new AnnotatedService();
+ $manager->addService($service);
+
+ $retrievedService = $manager->getServiceByName('annotated-service');
+ $this->assertNotNull($retrievedService);
+ $this->assertEquals('A service configured via annotations', $retrievedService->getDescription());
+
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Method Not Allowed.",'.self::NL
+ . ' "type":"error",'.self::NL
+ . ' "http-code":405'.self::NL
+ . '}', $this->postRequest($manager, 'annotated-service'));
+
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Hi user!",'.self::NL
+ . ' "type":"success",'.self::NL
+ . ' "http-code":200'.self::NL
+ . '}', $this->getRequest($manager, 'annotated-service'));
+
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Hi Ibrahim!",'.self::NL
+ . ' "type":"success",'.self::NL
+ . ' "http-code":200'.self::NL
+ . '}', $this->getRequest($manager, 'annotated-service', [
+ 'name' => 'Ibrahim'
+ ]));
+ }
+ public function testAnnotatedServiceMethodNotAllowed() {
+ $manager = new WebServicesManager();
+ $service = new AnnotatedService();
+ $manager->addService($service);
+
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Method Not Allowed.",'.self::NL
+ . ' "type":"error",'.self::NL
+ . ' "http-code":405'.self::NL
+ . '}', $this->postRequest($manager, 'annotated-service'));
+
+ }
+ public function testAnnotatedGet() {
+ $manager = new WebServicesManager();
+ $service = new AnnotatedService();
+ $manager->addService($service);
+
+ $retrievedService = $manager->getServiceByName('annotated-service');
+ $this->assertNotNull($retrievedService);
+ $this->assertEquals('A service configured via annotations', $retrievedService->getDescription());
+
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Hi user!",'.self::NL
+ . ' "type":"success",'.self::NL
+ . ' "http-code":200'.self::NL
+ . '}', $this->getRequest($manager, 'annotated-service'));
+
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Hi Ibrahim!",'.self::NL
+ . ' "type":"success",'.self::NL
+ . ' "http-code":200'.self::NL
+ . '}', $this->getRequest($manager, 'annotated-service', [
+ 'name' => 'Ibrahim'
+ ]));
+ }
+ public function testAnnotatedDelete() {
+ $manager = new WebServicesManager();
+ $service = new AnnotatedService();
+ $manager->addService($service);
+ //Missing param
+ $this->assertEquals('{'.self::NL
+ . ' "message":"The following required parameter(s) where missing from the request body: \'id\'.",'.self::NL
+ . ' "type":"error",'.self::NL
+ . ' "http-code":404,'.self::NL
+ . ' "more-info":{'.self::NL
+ . ' "missing":['.self::NL
+ . ' "id"'.self::NL
+ . ' ]'.self::NL
+ . ' }'.self::NL
+ . '}', $this->deleteRequest($manager, 'annotated-service'));
+ //No auth user
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Not Authorized.",'.self::NL
+ . ' "type":"error",'.self::NL
+ . ' "http-code":401'.self::NL
+ . '}', $this->deleteRequest($manager, 'annotated-service', [
+ 'id' => 1
+ ]));
+ //User with no roles
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Not Authorized.",'.self::NL
+ . ' "type":"error",'.self::NL
+ . ' "http-code":401'.self::NL
+ . '}', $this->deleteRequest($manager, 'annotated-service', [
+ 'id' => 1
+ ], [], new TestUser(1, [''], [''], true)));
+ //user with no authorites
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Not Authorized.",'.self::NL
+ . ' "type":"error",'.self::NL
+ . ' "http-code":401'.self::NL
+ . '}', $this->deleteRequest($manager, 'annotated-service', [
+ 'id' => 1
+ ], [], new TestUser(1, ['ADMIN'], [''], true)));
+ // Inactive user
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Not Authorized.",'.self::NL
+ . ' "type":"error",'.self::NL
+ . ' "http-code":401'.self::NL
+ . '}', $this->deleteRequest($manager, 'annotated-service', [
+ 'id' => 1
+ ], [], new TestUser(1, ['ADMIN'], ['USER_DELETE'], false)));
+ //valid user
+ $this->assertEquals('{'.self::NL
+ . ' "message":"Delete user with ID: 1",'.self::NL
+ . ' "type":"success",'.self::NL
+ . ' "http-code":200'.self::NL
+ . '}', $this->deleteRequest($manager, 'annotated-service', [
+ 'id' => 1
+ ], [], new TestUser(1, ['ADMIN'], ['USER_DELETE'], true)));
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php b/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php
index ca7f33a..b11abf6 100644
--- a/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php
+++ b/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php
@@ -1,7 +1,7 @@
addParameter(new RequestParameter('pass','string'));
}
- public function isAuthorized() {
+ public function isAuthorized(): bool {
$inputs = $this->getInputs();
if ($inputs instanceof \webfiori\json\Json) {
$pass = $inputs->get('pass');
diff --git a/tests/WebFiori/Tests/Http/TestServices/AllMethodsService.php b/tests/WebFiori/Tests/Http/TestServices/AllMethodsService.php
new file mode 100644
index 0000000..ba2d1df
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/AllMethodsService.php
@@ -0,0 +1,33 @@
+sendResponse('All methods service');
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php
new file mode 100644
index 0000000..098cec6
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php
@@ -0,0 +1,36 @@
+getParamVal('name');
+
+ if ($name !== null) {
+ return "Hi ".$name.'!';
+ }
+ return "Hi user!";
+ }
+ #[DeleteMapping]
+ #[ResponseBody]
+ #[RequestParam('id', ParamType::INT)]
+ #[PreAuthorize("isAuthenticated() && hasRole('ADMIN') && hasAuthority('USER_DELETE')")]
+ public function delete() {
+ $id = $this->getParamVal('id');
+ return "Delete user with ID: ".$id;
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/ClassLevelAuthService.php b/tests/WebFiori/Tests/Http/TestServices/ClassLevelAuthService.php
new file mode 100644
index 0000000..2a4ad81
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/ClassLevelAuthService.php
@@ -0,0 +1,19 @@
+sendResponse('Public service - no auth required');
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php b/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php
new file mode 100644
index 0000000..003785f
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php
@@ -0,0 +1,72 @@
+addRequestMethod(RequestMethod::POST);
+ $this->addRequestMethod(RequestMethod::GET);
+ $this->addParameters([
+ 'id' => [
+ ParamOption::TYPE => ParamType::INT,
+ ParamOption::METHODS => [RequestMethod::GET]
+ ],
+ 'name' => [
+ ParamOption::TYPE => ParamType::STRING,
+ ParamOption::METHODS => [RequestMethod::POST]
+ ],
+ 'username' => [
+ ParamOption::TYPE => ParamType::STRING,
+ ParamOption::METHODS => [RequestMethod::POST]
+ ],
+ 'x' => [
+ ParamOption::TYPE => ParamType::INT,
+ ParamOption::OPTIONAL => true,
+ ParamOption::DEFAULT => 3
+ ]
+ ]);
+ }
+ public function isAuthorized(): bool {
+ return true;
+ }
+ public function processRequest() {
+
+ }
+ public function processGET() {
+ $j = new Json();
+ $userObj = new TestUserObj();
+ $userObj->setFullName('Ibx');
+ $userObj->setId($this->getParamVal('id'));
+ $j->addObject('user', $userObj);
+ $this->send('application/json', $j);
+ }
+ public function processPOST() {
+ try {
+ $userObj = $this->getObject('not\\Exist', [
+ 'name' => 'setFullName'
+ ]);
+ } catch (Exception $ex) {
+ $userObj = $this->getObject(TestUserObj::class, [
+ 'name' => 'setFullName',
+ 'x' => 'setId'
+ ]);
+ }
+ $j = new Json();
+ $j->addObject('user', $userObj);
+ $this->send('application/json', $j);
+ }
+
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php b/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php
new file mode 100644
index 0000000..42ccd87
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php
@@ -0,0 +1,79 @@
+getParamVal('id');
+
+ // For testing, check if id is set via test
+ if (!$id && isset($_GET['test_id'])) {
+ $id = (int)$_GET['test_id'];
+ }
+
+ if ($id === 404) {
+ throw new NotFoundException('User not found');
+ }
+
+ if ($id === 400) {
+ throw new BadRequestException('Invalid user ID');
+ }
+
+ return ['user' => ['id' => $id, 'name' => 'Test User']];
+ }
+
+ #[PostMapping]
+ #[ResponseBody]
+ public function createUser(): array {
+ throw new UnauthorizedException('Authentication required');
+ }
+
+ #[GetMapping]
+ #[ResponseBody]
+ public function getError(): array {
+ throw new \Exception('Generic error');
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $action = $_GET['action'] ?? 'get';
+ switch ($action) {
+ case 'get':
+ $this->getUser();
+ break;
+ case 'create':
+ $this->createUser();
+ break;
+ case 'error':
+ $this->getError();
+ break;
+ }
+ }
+
+ protected function getCurrentProcessingMethod(): ?string {
+ $action = $_GET['action'] ?? 'get';
+ return match($action) {
+ 'get' => 'getUser',
+ 'create' => 'createUser',
+ 'error' => 'getError',
+ default => null
+ };
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/IntegrationTestService.php b/tests/WebFiori/Tests/Http/TestServices/IntegrationTestService.php
new file mode 100644
index 0000000..aa4b8cd
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/IntegrationTestService.php
@@ -0,0 +1,26 @@
+ 'Auto-processing via WebServicesManager'];
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ // This should not be called when using auto-processing
+ $this->sendResponse('Manual processing fallback', 200, 'info');
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/LegacyService.php b/tests/WebFiori/Tests/Http/TestServices/LegacyService.php
new file mode 100644
index 0000000..9861305
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/LegacyService.php
@@ -0,0 +1,24 @@
+sendResponse('Legacy service response', 200, 'success', ['legacy' => true]);
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $this->getData();
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/MappedMethodsService.php b/tests/WebFiori/Tests/Http/TestServices/MappedMethodsService.php
new file mode 100644
index 0000000..d23bf4b
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/MappedMethodsService.php
@@ -0,0 +1,34 @@
+sendResponse('GET users');
+ }
+
+ #[PostMapping]
+ public function createUser() {
+ $this->sendResponse('POST user');
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $method = $this->getManager()->getRequestMethod();
+ if ($method === \WebFiori\Http\RequestMethod::GET) {
+ $this->getUsers();
+ } elseif ($method === \WebFiori\Http\RequestMethod::POST) {
+ $this->createUser();
+ }
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/MixedResponseService.php b/tests/WebFiori/Tests/Http/TestServices/MixedResponseService.php
new file mode 100644
index 0000000..878fc5e
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/MixedResponseService.php
@@ -0,0 +1,60 @@
+ 'data', 'user' => 'authenticated'];
+ }
+
+ // ResponseBody method without authentication
+ #[GetMapping]
+ #[ResponseBody]
+ #[AllowAnonymous]
+ public function getPublicData(): array {
+ return ['public' => 'data', 'access' => 'open'];
+ }
+
+ // Traditional method (no ResponseBody)
+ #[PostMapping]
+ #[AllowAnonymous]
+ public function traditionalMethod(): void {
+ $this->sendResponse('Traditional method response', 200, 'success', ['method' => 'traditional']);
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $action = $_GET['action'] ?? 'traditional';
+ if ($action === 'traditional') {
+ $this->traditionalMethod();
+ } else {
+ $this->sendResponse('Unknown action', 400, 'error');
+ }
+ }
+
+ protected function getCurrentProcessingMethod(): ?string {
+ $action = $_GET['action'] ?? 'traditional';
+ return match($action) {
+ 'secure' => 'getSecureData',
+ 'public' => 'getPublicData',
+ 'traditional' => 'traditionalMethod',
+ default => null
+ };
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php b/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php
index 8080339..0386417 100644
--- a/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php
+++ b/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php
@@ -2,14 +2,14 @@
namespace WebFiori\Tests\Http\TestServices;
-use WebFiori\Http\AbstractWebService;
+use WebFiori\Http\WebService;
use WebFiori\Http\RequestMethod;
use WebFiori\Http\RequestParameter;
/**
*
* @author Ibrahim
*/
-class MulNubmersService extends AbstractWebService {
+class MulNubmersService extends WebService {
public function __construct() {
parent::__construct('mul-two-integers');
$this->setDescription('Returns a JSON string that has the multiplication of two integers.');
@@ -19,10 +19,15 @@ public function __construct() {
$this->addParameter(new RequestParameter('second-number', 'integer'));
}
+ public function isAuthorized(): bool {
+ return true;
+ }
+
public function isAuthorizedGET() {
if ($this->getParamVal('first-number') < 0) {
return false;
}
+ return true;
}
public function processGet() {
diff --git a/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php b/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php
index a109560..80f5c36 100644
--- a/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php
+++ b/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php
@@ -2,20 +2,20 @@
namespace WebFiori\Tests\Http\TestServices;
-use WebFiori\Http\AbstractWebService;
+use WebFiori\Http\WebService;
use WebFiori\Http\RequestMethod;
/**
* Description of NoAuthService
*
* @author Ibrahim
*/
-class NoAuthService extends AbstractWebService {
+class NoAuthService extends WebService {
public function __construct() {
parent::__construct('ok-service');
$this->setIsAuthRequired(false);
$this->addRequestMethod(RequestMethod::GET);
}
- public function isAuthorized() {
+ public function isAuthorized(): bool {
return false;
}
diff --git a/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php b/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php
new file mode 100644
index 0000000..13679bb
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php
@@ -0,0 +1,11 @@
+setDescription('A traditional service');
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/NotImplService.php b/tests/WebFiori/Tests/Http/TestServices/NotImplService.php
index 4180ab5..e88fd17 100644
--- a/tests/WebFiori/Tests/Http/TestServices/NotImplService.php
+++ b/tests/WebFiori/Tests/Http/TestServices/NotImplService.php
@@ -2,7 +2,7 @@
namespace WebFiori\Tests\Http\TestServices;
-use WebFiori\Http\AbstractWebService;
+use WebFiori\Http\WebService;
use WebFiori\Http\RequestMethod;
/**
@@ -10,13 +10,13 @@
*
* @author Ibrahim
*/
-class NotImplService extends AbstractWebService {
+class NotImplService extends WebService {
public function __construct() {
parent::__construct('not-implemented');
$this->addRequestMethod(RequestMethod::POST);
}
- public function isAuthorized() {
-
+ public function isAuthorized(): bool {
+ return true;
}
public function processRequest() {
diff --git a/tests/WebFiori/Tests/Http/TestServices/ParameterMappedService.php b/tests/WebFiori/Tests/Http/TestServices/ParameterMappedService.php
new file mode 100644
index 0000000..2ba39af
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/ParameterMappedService.php
@@ -0,0 +1,43 @@
+getParamVal('id');
+ $name = $this->getParamVal('name');
+ $this->sendResponse("User $id: $name");
+ }
+
+ #[PostMapping]
+ #[RequestParam('email', 'email', false)]
+ #[RequestParam('age', 'int', true, 18)]
+ public function createUser() {
+ $email = $this->getParamVal('email');
+ $age = $this->getParamVal('age');
+ $this->sendResponse("Created user: $email, age: $age");
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
+ if ($method === \WebFiori\Http\RequestMethod::GET) {
+ $this->getUser();
+ } elseif ($method === \WebFiori\Http\RequestMethod::POST) {
+ $this->createUser();
+ }
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php b/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php
new file mode 100644
index 0000000..31053ef
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php
@@ -0,0 +1,87 @@
+ [['id' => 1, 'name' => 'John'], ['id' => 2, 'name' => 'Jane']]];
+ }
+
+ // Test 2: Return string with custom status
+ #[GetMapping]
+ #[ResponseBody(status: 201, type: 'created')]
+ public function getStringData(): string {
+ return 'Resource created successfully';
+ }
+
+ // Test 3: Return null (should be empty response)
+ #[PostMapping]
+ #[ResponseBody(status: 204)]
+ public function deleteData(): ?object {
+ // Simulate deletion
+ return null;
+ }
+
+ // Test 4: Return object
+ #[GetMapping]
+ #[ResponseBody]
+ public function getObjectData(): object {
+ return (object)['message' => 'Hello World', 'timestamp' => time()];
+ }
+
+ // Test 5: Method without ResponseBody (manual handling)
+ #[GetMapping]
+ #[RequestParam('manual', 'string', true, 'false')]
+ public function getManualData(): void {
+ $manual = $this->getParamVal('manual');
+ $this->sendResponse('Manual response: ' . $manual, 200, 'success');
+ }
+
+ // Test 6: Method with parameters and ResponseBody
+ #[PostMapping]
+ #[ResponseBody]
+ #[RequestParam('name', 'string')]
+ #[RequestParam('age', 'int', true, 25)]
+ public function createUser(): array {
+ return [
+ 'user' => [
+ 'name' => $this->getParamVal('name'),
+ 'age' => $this->getParamVal('age'),
+ 'created_at' => date('Y-m-d H:i:s')
+ ]
+ ];
+ }
+
+ public function isAuthorized(): bool {
+ return true;
+ }
+
+ public function processRequest() {
+ // This should not be called for ResponseBody methods
+ $this->sendResponse('Fallback processRequest called', 200, 'info');
+ }
+
+ protected function getCurrentProcessingMethod(): ?string {
+ $action = $_GET['action'] ?? 'array';
+ return match($action) {
+ 'array' => 'getArrayData',
+ 'string' => 'getStringData',
+ 'null' => 'deleteData',
+ 'object' => 'getObjectData',
+ 'manual' => 'getManualData',
+ 'create' => 'createUser',
+ default => null
+ };
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/SampleServicesManager.php b/tests/WebFiori/Tests/Http/TestServices/SampleServicesManager.php
index 4916228..75b3cb3 100644
--- a/tests/WebFiori/Tests/Http/TestServices/SampleServicesManager.php
+++ b/tests/WebFiori/Tests/Http/TestServices/SampleServicesManager.php
@@ -17,6 +17,7 @@ public function __construct() {
$this->addService(new SumNumbersService());
$this->addService(new GetUserProfileService());
+ $this->addService(new CreateUserProfileServiceV2());
$this->addService(new CreateUserProfileService());
$this->addService(new MulNubmersService());
}
diff --git a/tests/WebFiori/Tests/Http/TestServices/SecureService.php b/tests/WebFiori/Tests/Http/TestServices/SecureService.php
new file mode 100644
index 0000000..5e98b18
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestServices/SecureService.php
@@ -0,0 +1,80 @@
+sendResponse('Public data - no auth required');
+ }
+
+ #[GetMapping]
+ #[RequiresAuth]
+ public function getPrivateData() {
+ $user = SecurityContext::getCurrentUser();
+ $this->sendResponse('Private data for: ' . ($user['name'] ?? 'unknown'));
+ }
+
+ #[PostMapping]
+ #[PreAuthorize("hasRole('ADMIN')")]
+ public function adminOnly() {
+ $this->sendResponse('Admin-only operation');
+ }
+
+ #[PostMapping]
+ #[PreAuthorize("hasAuthority('USER_CREATE')")]
+ public function createUser() {
+ $this->sendResponse('User created');
+ }
+
+ public function isAuthorized(): bool {
+ return true; // Default fallback
+ }
+
+ public function processRequest() {
+ if (!$this->checkMethodAuthorization()) {
+ $this->sendResponse('Unauthorized', 'error', 401);
+ return;
+ }
+
+ $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
+ $action = $_GET['action'] ?? 'public';
+
+ switch ($action) {
+ case 'public':
+ $this->getPublicData();
+ break;
+ case 'private':
+ $this->getPrivateData();
+ break;
+ case 'admin':
+ $this->adminOnly();
+ break;
+ case 'create':
+ $this->createUser();
+ break;
+ }
+ }
+
+ protected function getCurrentProcessingMethod(): ?string {
+ $action = $_GET['action'] ?? 'public';
+ return match($action) {
+ 'public' => 'getPublicData',
+ 'private' => 'getPrivateData',
+ 'admin' => 'adminOnly',
+ 'create' => 'createUser',
+ default => null
+ };
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php b/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php
index 4153c8a..ed43921 100644
--- a/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php
+++ b/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php
@@ -2,19 +2,19 @@
namespace WebFiori\Tests\Http\TestServices;
-use WebFiori\Http\AbstractWebService;
+use WebFiori\Http\WebService;
/**
* Description of TestServiceObj
*
* @author Ibrahim
*/
-class TestServiceObj extends AbstractWebService {
+class TestServiceObj extends WebService {
public function __construct($name) {
parent::__construct($name);
}
//put your code here
- public function isAuthorized() {
- return parent::isAuthorized();
+ public function isAuthorized(): bool {
+ return true;
}
public function processRequest() {
diff --git a/tests/WebFiori/Tests/Http/TestUser.php b/tests/WebFiori/Tests/Http/TestUser.php
new file mode 100644
index 0000000..51190e4
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/TestUser.php
@@ -0,0 +1,37 @@
+id = $id;
+ $this->roles = $roles;
+ $this->authorities = $authorities;
+ $this->active = $active;
+ }
+
+ public function getId(): int|string {
+ return $this->id;
+ }
+
+ public function getRoles(): array {
+ return $this->roles;
+ }
+
+ public function getAuthorities(): array {
+ return $this->authorities;
+ }
+
+ public function isActive(): bool {
+ return $this->active;
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/WebServiceTest.php b/tests/WebFiori/Tests/Http/WebServiceTest.php
index 8f3a393..535f905 100644
--- a/tests/WebFiori/Tests/Http/WebServiceTest.php
+++ b/tests/WebFiori/Tests/Http/WebServiceTest.php
@@ -16,7 +16,7 @@ class WebServiceTest extends TestCase {
public function testGetAuthHeaders00() {
$service = new TestServiceObj('Hello');
$this->assertNull($service->getAuthHeader());
- $this->assertNull($service->isAuthorized());
+ $this->assertTrue($service->isAuthorized());
}
/**
*
@@ -108,7 +108,8 @@ public function testAddParameters02() {
'password' => [
ParamOption::OPTIONAL => true,
ParamOption::DEFAULT => 1234,
- ParamOption::TYPE => 'integer'
+ ParamOption::TYPE => 'integer',
+ ParamOption::METHODS => 'get'
]
]);
$this->assertEquals(2,count($action->getParameters()));
@@ -119,6 +120,7 @@ public function testAddParameters02() {
$this->assertEquals('integer', $param2->getType());
$this->assertTrue($param2->isOptional());
$this->assertEquals(1234, $param2->getDefault());
+ $this->assertEquals(['GET'], $param2->getMethods());
}
/**
* @test
@@ -252,79 +254,27 @@ public function testRemoveRequestMethod($action) {
*/
public function testToJson00() {
$action = new TestServiceObj('login');
- $this->assertEquals(''
- .'{"name":"login",'
- .'"since":"1.0.0",'
- .'"description":"",'
- .'"request-methods":[],'
- .'"parameters":[],'
- .'"responses":[]}',$action->toJSON().'');
+ $this->assertEquals('{}',$action->toJSON().'');
$action->setSince('1.0.1');
$action->setDescription('Allow the user to login to the system.');
- $this->assertEquals(''
- .'{"name":"login",'
- .'"since":"1.0.1",'
- .'"description":"Allow the user to login to the system.",'
- .'"request-methods":[],'
- .'"parameters":[],'
- .'"responses":[]}',$action->toJSON().'');
+ $this->assertEquals('{}',$action->toJSON().'');
$action->setRequestMethods([RequestMethod::GET, RequestMethod::POST, RequestMethod::PUT]);
$this->assertEquals(''
- .'{"name":"login",'
- .'"since":"1.0.1",'
- .'"description":"Allow the user to login to the system.",'
- .'"request-methods":["GET","POST","PUT"],'
- .'"parameters":[],'
- .'"responses":[]}',$action->toJSON().'');
+ .'{"get":{"responses":{"200":{"description":"Successful operation"}}},'
+ .'"post":{"responses":{"200":{"description":"Successful operation"}}},'
+ .'"put":{"responses":{"200":{"description":"Successful operation"}}}}',$action->toJSON().'');
$action->removeRequestMethod('put');
$action->addParameter(new RequestParameter('username'));
$this->assertEquals(''
- .'{"name":"login",'
- .'"since":"1.0.1",'
- .'"description":"Allow the user to login to the system.",'
- .'"request-methods":["GET","POST"],'
- .'"parameters":['
- .'{"name":"username",'
- .'"type":"string",'
- .'"description":null,'
- .'"is-optional":false,'
- .'"default-value":null,'
- .'"min-val":null,'
- .'"max-val":null,'
- .'"min-length":null,'
- .'"max-length":null}'
- .'],'
- .'"responses":[]}',$action->toJSON().'');
+ .'{"get":{"responses":{"200":{"description":"Successful operation"}}},'
+ .'"post":{"responses":{"200":{"description":"Successful operation"}}}}',$action->toJSON().'');
$action->addParameter(new RequestParameter('password', 'integer'));
$action->getParameterByName('password')->setDescription('The password of the user.');
$action->getParameterByName('password')->setMinValue(1000000);
$this->assertEquals(''
- .'{"name":"login",'
- .'"since":"1.0.1",'
- .'"description":"Allow the user to login to the system.",'
- .'"request-methods":["GET","POST"],'
- .'"parameters":['
- .'{"name":"username",'
- .'"type":"string",'
- .'"description":null,'
- .'"is-optional":false,'
- .'"default-value":null,'
- .'"min-val":null,'
- .'"max-val":null,'
- .'"min-length":null,'
- .'"max-length":null},'
- .'{"name":"password",'
- .'"type":"integer",'
- .'"description":"The password of the user.",'
- .'"is-optional":false,'
- .'"default-value":null,'
- .'"min-val":1000000,'
- .'"max-val":'.PHP_INT_MAX.','
- .'"min-length":null,'
- .'"max-length":null}'
- .'],'
- .'"responses":[]}',$action->toJSON().'');
+ .'{"get":{"responses":{"200":{"description":"Successful operation"}}},'
+ .'"post":{"responses":{"200":{"description":"Successful operation"}}}}',$action->toJSON().'');
}
/**
* @test
@@ -335,26 +285,7 @@ public function testToString00() {
$action->addParameter(new RequestParameter('user-id', 'integer'));
$action->getParameterByName('user-id')->setDescription('The ID of the user.');
$action->setDescription('Returns a JSON string which holds user profile info.');
- $this->assertEquals("APIAction[\n"
- ." Name => 'get-user',\n"
- ." Description => 'Returns a JSON string which holds user profile info.',\n"
- ." Since => '1.0.0',\n"
- ." Request Methods => [\n"
- ." GET\n"
- ." ],\n"
- ." Parameters => [\n"
- ." user-id => [\n"
- ." Type => 'integer',\n"
- ." Description => 'The ID of the user.',\n"
- ." Is Optional => 'false',\n"
- ." Default => 'null',\n"
- ." Minimum Value => '".~PHP_INT_MAX."',\n"
- ." Maximum Value => '".PHP_INT_MAX."'\n"
- ." ]\n"
- ." ],\n"
- ." Responses Descriptions => [\n"
- ." ]\n"
- ."]\n",$action.'');
+ $this->assertEquals('{"get":{"responses":{"200":{"description":"Successful operation"}}}}',$action.'');
}
/**
* @test
@@ -367,38 +298,119 @@ public function testToString01() {
$action->getParameterByName('username')->setDescription('The username of the user.');
$action->getParameterByName('email')->setDescription('The email address of the user.');
$action->setDescription('Adds new user profile to the system.');
- $action->addResponseDescription('If the user is added, a 201 HTTP response is send with a JSON string that contains user ID.');
- $action->addResponseDescription('If a user is already exist wich has the given email, a 404 code is sent back.');
- $this->assertEquals("APIAction[\n"
- ." Name => 'add-user',\n"
- ." Description => 'Adds new user profile to the system.',\n"
- ." Since => '1.0.0',\n"
- ." Request Methods => [\n"
- ." POST,\n"
- ." PUT\n"
- ." ],\n"
- ." Parameters => [\n"
- ." username => [\n"
- ." Type => 'string',\n"
- ." Description => 'The username of the user.',\n"
- ." Is Optional => 'false',\n"
- ." Default => 'null',\n"
- ." Minimum Value => 'null',\n"
- ." Maximum Value => 'null'\n"
- ." ],\n"
- ." email => [\n"
- ." Type => 'string',\n"
- ." Description => 'The email address of the user.',\n"
- ." Is Optional => 'false',\n"
- ." Default => 'null',\n"
- ." Minimum Value => 'null',\n"
- ." Maximum Value => 'null'\n"
- ." ]\n"
- ." ],\n"
- ." Responses Descriptions => [\n"
- ." Response #0 => 'If the user is added, a 201 HTTP response is send with a JSON string that contains user ID.',\n"
- ." Response #1 => 'If a user is already exist wich has the given email, a 404 code is sent back.'\n"
- ." ]\n"
- ."]\n",$action.'');
+ $action->addResponse(RequestMethod::POST, '201', 'User created successfully');
+ $action->addResponse(RequestMethod::PUT, '200', 'User updated successfully');
+ $this->assertEquals('{"post":{"responses":{"201":{"description":"User created successfully"}}},'
+ .'"put":{"responses":{"200":{"description":"User updated successfully"}}}}',$action.'');
+ }
+ /**
+ * @test
+ */
+ public function testDuplicateGetMappingThrowsException() {
+ $this->expectException(\WebFiori\Http\Exceptions\DuplicateMappingException::class);
+ $this->expectExceptionMessage('HTTP method GET is mapped to multiple methods: getUsers, getUsersAgain');
+
+ new class extends \WebFiori\Http\WebService {
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function getUsers() {}
+
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function getUsersAgain() {}
+
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+ }
+
+ /**
+ * @test
+ */
+ public function testDuplicatePostMappingThrowsException() {
+ $this->expectException(\WebFiori\Http\Exceptions\DuplicateMappingException::class);
+ $this->expectExceptionMessage('HTTP method POST is mapped to multiple methods: createUser, addUser');
+
+ new class extends \WebFiori\Http\WebService {
+ #[\WebFiori\Http\Annotations\PostMapping]
+ public function createUser() {}
+
+ #[\WebFiori\Http\Annotations\PostMapping]
+ public function addUser() {}
+
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+ }
+
+ /**
+ * @test
+ */
+ public function testDuplicateMixedMappingsThrowsException() {
+ $this->expectException(\WebFiori\Http\Exceptions\DuplicateMappingException::class);
+ $this->expectExceptionMessage('HTTP method PUT is mapped to multiple methods: updateUser, modifyUser');
+
+ new class extends \WebFiori\Http\WebService {
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function getUser() {}
+
+ #[\WebFiori\Http\Annotations\PutMapping]
+ public function updateUser() {}
+
+ #[\WebFiori\Http\Annotations\PutMapping]
+ public function modifyUser() {}
+
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+ }
+
+ /**
+ * @test
+ */
+ public function testMultipleDuplicateGetMappingThrowsException() {
+ $this->expectException(\WebFiori\Http\Exceptions\DuplicateMappingException::class);
+ $this->expectExceptionMessage('HTTP method GET is mapped to multiple methods: getUsers, getUsersAgain, fetchUsers');
+
+ new class extends \WebFiori\Http\WebService {
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function getUsers() {}
+
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function getUsersAgain() {}
+
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function fetchUsers() {}
+
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+ }
+
+ /**
+ * @test
+ */
+ public function testNoDuplicateMappingsDoesNotThrowException() {
+ $service = new class extends \WebFiori\Http\WebService {
+ #[\WebFiori\Http\Annotations\GetMapping]
+ public function getUser() {}
+
+ #[\WebFiori\Http\Annotations\PostMapping]
+ public function createUser() {}
+
+ #[\WebFiori\Http\Annotations\PutMapping]
+ public function updateUser() {}
+
+ #[\WebFiori\Http\Annotations\DeleteMapping]
+ public function deleteUser() {}
+
+ public function isAuthorized(): bool { return true; }
+ public function processRequest() {}
+ };
+
+ $methods = $service->getRequestMethods();
+ $this->assertContains('GET', $methods);
+ $this->assertContains('POST', $methods);
+ $this->assertContains('PUT', $methods);
+ $this->assertContains('DELETE', $methods);
+ $this->assertCount(4, $methods);
}
}
diff --git a/tests/WebFiori/Tests/Http/WebServicesManagerIntegrationTest.php b/tests/WebFiori/Tests/Http/WebServicesManagerIntegrationTest.php
new file mode 100644
index 0000000..b8e74f5
--- /dev/null
+++ b/tests/WebFiori/Tests/Http/WebServicesManagerIntegrationTest.php
@@ -0,0 +1,109 @@
+addService($service);
+
+ // Test that service has processWithAutoHandling method
+ $this->assertTrue(method_exists($service, 'processWithAutoHandling'));
+
+ // Test that service has ResponseBody methods
+ $this->assertTrue($service->hasResponseBodyAnnotation('getData'));
+ }
+
+ public function testManagerProcessesAutoHandlingServices() {
+ $service = new IntegrationTestService();
+
+ // Test that service has ResponseBody methods
+ $this->assertTrue($service->hasResponseBodyAnnotation('getData'));
+
+ // Test method return value
+ $result = $service->getData();
+ $this->assertIsArray($result);
+ $this->assertEquals('Auto-processing via WebServicesManager', $result['message']);
+ }
+
+ public function testLegacyServiceStillWorks() {
+ $service = new LegacyService();
+
+ // Legacy service should not have ResponseBody methods
+ $this->assertFalse($service->hasResponseBodyAnnotation('getData'));
+
+ // Should have traditional methods
+ $this->assertTrue(method_exists($service, 'processRequest'));
+ }
+
+ public function testMixedServiceTypes() {
+ $manager = new WebServicesManager();
+
+ // Add both new and legacy services
+ $manager->addService(new IntegrationTestService());
+ $manager->addService(new LegacyService());
+
+ // Both should be registered
+ $this->assertNotNull($manager->getServiceByName('integration-test'));
+ $this->assertNotNull($manager->getServiceByName('legacy-service'));
+ }
+
+ public function testResponseBodyWithExceptionHandling() {
+ $service = new ResponseBodyTestService();
+
+ // Test that service has ResponseBody methods
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $targetMethod = $service->getTargetMethod();
+ $this->assertTrue($service->hasResponseBodyAnnotation($targetMethod));
+
+ // Test method return value
+ $result = $service->getArrayData();
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('users', $result);
+ }
+
+ public function testServiceMethodDiscovery() {
+ $service = new ResponseBodyTestService();
+
+ // Test GET method discovery
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $targetMethod = $service->getTargetMethod();
+ $this->assertEquals('getArrayData', $targetMethod);
+
+ // Test POST method discovery
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $targetMethod = $service->getTargetMethod();
+ $this->assertContains($targetMethod, ['deleteData', 'createUser']);
+ }
+
+ public function testBackwardCompatibility() {
+ // Test that all existing functionality still works
+ $legacyService = new LegacyService();
+ $newService = new IntegrationTestService();
+
+ // Both should have processRequest method
+ $this->assertTrue(method_exists($legacyService, 'processRequest'));
+ $this->assertTrue(method_exists($newService, 'processRequest'));
+
+ // Both should have processWithAutoHandling (inherited from WebService)
+ $this->assertTrue(method_exists($legacyService, 'processWithAutoHandling'));
+ $this->assertTrue(method_exists($newService, 'processWithAutoHandling'));
+ }
+
+ protected function tearDown(): void {
+ unset($_GET['service']);
+ unset($_SERVER['REQUEST_METHOD']);
+ }
+}
diff --git a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php
index 3a819ad..852fccb 100644
--- a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php
+++ b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php
@@ -1,7 +1,7 @@
addService(new NoAuthService());
@@ -144,7 +144,7 @@ public function testJson04() {
public function testRemoveService00(WebServicesManager $manager) {
$this->assertNull($manager->removeService('xyz'));
$service = $manager->removeService('ok-service');
- $this->assertTrue($service instanceof AbstractWebService);
+ $this->assertTrue($service instanceof WebService);
$this->assertEquals(0, count($manager->getServices()));
$this->assertNull($service->getManager());
}
@@ -160,9 +160,9 @@ public function testConstructor00() {
$this->assertEquals('1.0.1',$api->getVersion());
$this->assertEquals('NO DESCRIPTION',$api->getDescription());
$api->setDescription('Test API.');
- $this->assertEquals(5,count($api->getServices()));
+ $this->assertEquals(6,count($api->getServices()));
$this->assertEquals('Test API.',$api->getDescription());
- $this->assertTrue($api->getServiceByName('sum-array') instanceof AbstractWebService);
+ $this->assertTrue($api->getServiceByName('sum-array') instanceof WebService);
$this->assertNull($api->getServiceByName('request-info'));
$this->assertNull($api->getServiceByName('api-info-2'));
@@ -300,6 +300,61 @@ public function testCreateUser01() {
$manager->process();
$this->assertEquals('{"user":{"Id":3,"FullName":"Me","Username":"Cpool"}}', $manager->readOutputStream());
}
+ /**
+ * @test
+ */
+ public function testCreateUser02() {
+ $this->clrearVars();
+ putenv('REQUEST_METHOD=GET');
+ $_GET['service'] = 'user-profile';
+ $_GET['id'] = '99';
+ $api = new SampleServicesManager();
+ $api->setOutputStream($this->outputStreamName);
+ $api->process();
+ $this->assertEquals('{"user":{"Id":99,"FullName":"Ibx"}}', $api->readOutputStream());
+ }
+ /**
+ * @test
+ */
+ public function testCreateUser03() {
+ $this->clrearVars();
+ putenv('REQUEST_METHOD=POST');
+ $_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
+ $_POST['service'] = 'user-profile';
+ $_POST['id'] = '99';
+ $api = new SampleServicesManager();
+ $api->setOutputStream($this->outputStreamName);
+ $api->process();
+ $this->assertEquals('{"message":"The following required parameter(s) where missing from the request body: \'name\', \'username\'.","type":"error","http-code":404,"more-info":{"missing":["name","username"]}}', $api->readOutputStream());
+ }
+ /**
+ * @test
+ */
+ public function testCreateUser04() {
+ $this->clrearVars();
+ putenv('REQUEST_METHOD=POST');
+ $_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
+ $_POST['service'] = 'user-profile';
+ $_POST['name'] = '99';
+ $_POST['username'] = 'Cool';
+ $api = new SampleServicesManager();
+ $api->setOutputStream($this->outputStreamName);
+ $api->process();
+ $this->assertEquals('{"user":{"Id":3,"FullName":"99","Username":"Cool"}}', $api->readOutputStream());
+ }
+ /**
+ * @test
+ */
+ public function testCreateUser05() {
+ $this->clrearVars();
+ putenv('REQUEST_METHOD=GET');
+ $_GET['service'] = 'user-profile';
+
+ $api = new SampleServicesManager();
+ $api->setOutputStream($this->outputStreamName);
+ $api->process();
+ $this->assertEquals('{"message":"The following required parameter(s) where missing from the request body: \'id\'.","type":"error","http-code":404,"more-info":{"missing":["id"]}}', $api->readOutputStream());
+ }
/**
* @test
*/
@@ -595,10 +650,10 @@ public function testSetOutputStream02() {
*/
public function testSetOutputStream03() {
$api = new SampleServicesManager();
- $this->assertTrue($api->setOutputStream(__DIR__.DIRECTORY_SEPARATOR.'outputStream.txt', true));
+ $this->assertTrue($api->setOutputStream(__DIR__.DIRECTORY_SEPARATOR.'output-stream.txt', true));
$this->assertNotNull($api->getOutputStream());
$this->assertNotNull($api->getOutputStreamPath());
- $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR.'outputStream.txt', $api->getOutputStreamPath());
+ $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR.'output-stream.txt', $api->getOutputStreamPath());
}
private function clrearVars() {
foreach ($_GET as $k => $v) {
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index 58b4d7e..dbfd68a 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -1,11 +1,10 @@
-
+
-
- ../WebFiori/Http/AbstractWebService.php
- ../WebFiori/Http/APIFilter.php
+ ../WebFiori/Http/AbstractWebService.php
+ ../WebFiori/Http/WebService.php../WebFiori/Http/APIFilter.php../WebFiori/Http/RequestParameter.php../WebFiori/Http/WebServicesManager.php../WebFiori/Http/Request.php
@@ -17,10 +16,9 @@
../WebFiori/Http/UriParameter.php../WebFiori/Http/ObjectMapper.php../WebFiori/Http/AuthHeader.php
-
-
+ ../WebFiori/Http/OpenAPI/ComponentsObj.php../WebFiori/Http/OpenAPI/ContactObj.php../WebFiori/Http/OpenAPI/ExternalDocObj.php../WebFiori/Http/OpenAPI/HeaderObj.php../WebFiori/Http/OpenAPI/InfoObj.php../WebFiori/Http/OpenAPI/LicenseObj.php../WebFiori/Http/OpenAPI/MediaTypeObj.php../WebFiori/Http/OpenAPI/OAuthFlowObj.php../WebFiori/Http/OpenAPI/OAuthFlowsObj.php../WebFiori/Http/OpenAPI/OpenAPIObj.php../WebFiori/Http/OpenAPI/OperationObj.php../WebFiori/Http/OpenAPI/ParameterObj.php../WebFiori/Http/OpenAPI/PathItemObj.php../WebFiori/Http/OpenAPI/PathsObj.php../WebFiori/Http/OpenAPI/ReferenceObj.php../WebFiori/Http/OpenAPI/ResponseObj.php../WebFiori/Http/OpenAPI/ResponsesObj.php../WebFiori/Http/OpenAPI/Schema.php../WebFiori/Http/OpenAPI/SecurityRequirementObj.php../WebFiori/Http/OpenAPI/SecuritySchemeObj.php../WebFiori/Http/OpenAPI/ServerObj.php../WebFiori/Http/OpenAPI/TagObj.php../WebFiori/Http/ManagerInfoService.php../WebFiori/Http/ResponseMessage.php
-
+
diff --git a/tests/phpunit10.xml b/tests/phpunit10.xml
index c63e0e4..26e0c22 100644
--- a/tests/phpunit10.xml
+++ b/tests/phpunit10.xml
@@ -1,22 +1,21 @@
-
+
-
+
-
+ ./WebFiori/Tests/Http
-
- ../WebFiori/Http/AbstractWebService.php
- ../WebFiori/Http/APIFilter.php
+ ../WebFiori/Http/AbstractWebService.php
+ ../WebFiori/Http/WebService.php../WebFiori/Http/APIFilter.php../WebFiori/Http/RequestParameter.php../WebFiori/Http/WebServicesManager.php../WebFiori/Http/Request.php
@@ -31,6 +30,5 @@
../WebFiori/Http/HttpMessage.php../WebFiori/Http/RequestV2.php../WebFiori/Http/RequestUri.php
-
-
-
+ ../WebFiori/Http/OpenAPI/ComponentsObj.php../WebFiori/Http/OpenAPI/ContactObj.php../WebFiori/Http/OpenAPI/ExternalDocObj.php../WebFiori/Http/OpenAPI/HeaderObj.php../WebFiori/Http/OpenAPI/InfoObj.php../WebFiori/Http/OpenAPI/LicenseObj.php../WebFiori/Http/OpenAPI/MediaTypeObj.php../WebFiori/Http/OpenAPI/OAuthFlowObj.php../WebFiori/Http/OpenAPI/OAuthFlowsObj.php../WebFiori/Http/OpenAPI/OpenAPIObj.php../WebFiori/Http/OpenAPI/OperationObj.php../WebFiori/Http/OpenAPI/ParameterObj.php../WebFiori/Http/OpenAPI/PathItemObj.php../WebFiori/Http/OpenAPI/PathsObj.php../WebFiori/Http/OpenAPI/ReferenceObj.php../WebFiori/Http/OpenAPI/ResponseObj.php../WebFiori/Http/OpenAPI/ResponsesObj.php../WebFiori/Http/OpenAPI/Schema.php../WebFiori/Http/OpenAPI/SecurityRequirementObj.php../WebFiori/Http/OpenAPI/SecuritySchemeObj.php../WebFiori/Http/OpenAPI/ServerObj.php../WebFiori/Http/OpenAPI/TagObj.php../WebFiori/Http/ManagerInfoService.php../WebFiori/Http/ResponseMessage.php
+
\ No newline at end of file