Compare commits
10
Commits
ec26db0bfa
...
9c5e089573
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c5e089573 | ||
|
|
dc5a8897e0 | ||
|
|
56f7798d32 | ||
|
|
5dcb63e101 | ||
|
|
2243aa24fd | ||
|
|
956aff9fe3 | ||
|
|
d90c5887fc | ||
|
|
2e0a1b991c | ||
|
|
e8288f73bd | ||
|
|
85843e99c7 |
@@ -1,46 +0,0 @@
|
|||||||
# Debug API PHP con VS Code e Docker
|
|
||||||
|
|
||||||
Questa sezione descrive la procedura per fare debug delle API PHP servite da Docker in questa repository.
|
|
||||||
|
|
||||||
## Passaggi
|
|
||||||
|
|
||||||
1. Avvia il container Docker dalla root del progetto:
|
|
||||||
```bash
|
|
||||||
docker compose up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Apri Visual Studio Code e vai su "Run and Debug".
|
|
||||||
|
|
||||||
3. Seleziona la configurazione:
|
|
||||||
```text
|
|
||||||
Debug PHP in Docker
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Premi F5 per avviare il debugger.
|
|
||||||
|
|
||||||
5. Imposta un breakpoint in un controller, ad esempio in:
|
|
||||||
```text
|
|
||||||
src/Api/Controllers/V1/UsersController.php
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Richiama l’endpoint tramite browser o curl:
|
|
||||||
```bash
|
|
||||||
curl -H "XDEBUG_TRIGGER: 1" http://localhost:8000/api/users
|
|
||||||
```
|
|
||||||
|
|
||||||
## Nota
|
|
||||||
|
|
||||||
Il container deve esporre:
|
|
||||||
- porta `8000` per HTTP
|
|
||||||
- porta `9003` per Xdebug
|
|
||||||
|
|
||||||
Il progetto contiene già i file necessari per il debug:
|
|
||||||
- `.docker/php/Dockerfile`
|
|
||||||
- `docker-compose.yml`
|
|
||||||
- `.vscode/launch.json`
|
|
||||||
- `.vscode/settings.json`
|
|
||||||
|
|
||||||
## Problemi comuni
|
|
||||||
|
|
||||||
- Se VS Code non si ferma sul breakpoint, verifica che Xdebug sia abilitato nel container e che la configurazione `client_host` punti a `host.docker.internal`.
|
|
||||||
- Se il container non si avvia, controlla che Docker Desktop sia in esecuzione.
|
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
---
|
||||||
|
name: markdown-linting
|
||||||
|
description: Markdown formatting standards following markdownlint practices
|
||||||
|
applyTo: [markdown]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Markdown Linting Skill
|
||||||
|
|
||||||
|
This skill documents markdown formatting standards for this project, following markdownlint practices.
|
||||||
|
|
||||||
|
## Core Rules
|
||||||
|
|
||||||
|
### Headings
|
||||||
|
- Use `#` for headings (not underlines with `===` or `---`)
|
||||||
|
- Headings must have space after `#`: `# Heading` ✓, `#Heading` ✗
|
||||||
|
- Heading levels must increment by 1: `# > ## > ###` (don't skip levels)
|
||||||
|
- File must start with a level 1 heading (H1)
|
||||||
|
- Use sentence case for headings (capitalize only first word unless proper noun)
|
||||||
|
|
||||||
|
### Line Endings & Spacing
|
||||||
|
- Trim trailing whitespace at end of lines
|
||||||
|
- No more than one blank line between elements
|
||||||
|
- Use consistent list marker spacing: 1 space after marker
|
||||||
|
- Between major sections: exactly 1 blank line
|
||||||
|
|
||||||
|
### Lists
|
||||||
|
- Use `-` for unordered lists (not `*` or `+`)
|
||||||
|
- Lists must be indented consistently (2 spaces or 4 spaces)
|
||||||
|
- Ordered lists use `1. 2. 3.` (always `1.` for first item)
|
||||||
|
- List items with multiple paragraphs: indent continuation 4 spaces
|
||||||
|
- Blank line before and after lists (if between other content)
|
||||||
|
|
||||||
|
### Code
|
||||||
|
- Inline code with backticks: `` `code` ``
|
||||||
|
- Code blocks use triple backticks with language: ` ```php `, ` ```bash `, ` ```json `
|
||||||
|
- Blank line before code block
|
||||||
|
- Blank line after code block
|
||||||
|
- Use fenced code blocks (` ``` `), not indentation
|
||||||
|
|
||||||
|
### Links & Images
|
||||||
|
- Use reference-style or inline links: `[text](url)`
|
||||||
|
- Image syntax: ``
|
||||||
|
- URLs must be valid and properly formatted
|
||||||
|
- Don't use bare URLs (wrap in `<>` or use markdown link syntax)
|
||||||
|
|
||||||
|
### Emphasis
|
||||||
|
- Use `**bold**` for bold (not `__bold__`)
|
||||||
|
- Use `*italic*` for italic (not `_italic_`)
|
||||||
|
- Underscores on word boundaries only
|
||||||
|
|
||||||
|
### Line Length
|
||||||
|
- Keep lines under 120 characters where practical
|
||||||
|
- Long URLs and code blocks are exceptions
|
||||||
|
- Wrap long text at sentence boundaries
|
||||||
|
|
||||||
|
### Blockquotes
|
||||||
|
- Use `>` for blockquotes with space after: `> quote` ✓
|
||||||
|
- Blank line after blockquote if followed by text
|
||||||
|
|
||||||
|
## Template Structure
|
||||||
|
|
||||||
|
### Documentation Files
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Main Title
|
||||||
|
|
||||||
|
Brief introduction (1-2 sentences).
|
||||||
|
|
||||||
|
## Section One
|
||||||
|
|
||||||
|
Content here.
|
||||||
|
|
||||||
|
### Subsection
|
||||||
|
|
||||||
|
Details.
|
||||||
|
|
||||||
|
## Section Two
|
||||||
|
|
||||||
|
More content.
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Link text](url)
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# API Endpoint Name
|
||||||
|
|
||||||
|
Brief description.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
What this does.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Prerequisite 1
|
||||||
|
- Prerequisite 2
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl command
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
json example
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Option 1: description
|
||||||
|
- Option 2: description
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Solution.
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Related](link)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Guides & Tutorials
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Tutorial Title
|
||||||
|
|
||||||
|
Brief intro.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Item 1
|
||||||
|
|
||||||
|
## Step 1: Title
|
||||||
|
|
||||||
|
Description and code.
|
||||||
|
|
||||||
|
## Step 2: Title
|
||||||
|
|
||||||
|
Description and code.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
How to test.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Issues and fixes.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Checklist
|
||||||
|
|
||||||
|
- [ ] File starts with `# Title` (H1)
|
||||||
|
- [ ] Headings have space after `#`
|
||||||
|
- [ ] No heading level jumps
|
||||||
|
- [ ] No trailing whitespace
|
||||||
|
- [ ] Max 1 blank line between sections
|
||||||
|
- [ ] Lists use `-` consistently
|
||||||
|
- [ ] Code blocks have language specified
|
||||||
|
- [ ] Inline code uses backticks
|
||||||
|
- [ ] Links are properly formatted
|
||||||
|
- [ ] No bare URLs
|
||||||
|
- [ ] Lines under 120 chars where practical
|
||||||
|
- [ ] Bold uses `**text**`, italic uses `*text*`
|
||||||
|
- [ ] All section transitions are clear
|
||||||
|
|
||||||
|
## Common Violations to Avoid
|
||||||
|
|
||||||
|
❌ `#No space after hash`
|
||||||
|
✓ `# Space after hash`
|
||||||
|
|
||||||
|
❌ `# Heading\n\n\n## Next` (two blank lines)
|
||||||
|
✓ `# Heading\n\n## Next` (one blank line)
|
||||||
|
|
||||||
|
❌ `* or + for lists`
|
||||||
|
✓ `- for all lists`
|
||||||
|
|
||||||
|
❌ Bare URL `http://example.com`
|
||||||
|
✓ Wrapped URL `<http://example.com>`
|
||||||
|
✓ Link `[text](http://example.com)`
|
||||||
|
|
||||||
|
❌ Indented code blocks
|
||||||
|
✓ Fenced code blocks ` ``` `
|
||||||
|
|
||||||
|
❌ `# Skip to ### level`
|
||||||
|
✓ `# Then ## Then ###`
|
||||||
|
|
||||||
|
❌ __emphasis with underscores__
|
||||||
|
✓ **emphasis with asterisks**
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Markdownlint Rules](https://github.com/markdownlint/markdownlint/blob/main/README.md)
|
||||||
|
- [CommonMark Spec](https://spec.commonmark.org/)
|
||||||
@@ -3,6 +3,6 @@ FROM php:8.5.8-cli
|
|||||||
RUN pecl install xdebug && docker-php-ext-enable xdebug
|
RUN pecl install xdebug && docker-php-ext-enable xdebug
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8000 9003
|
EXPOSE 8000 9013
|
||||||
|
|
||||||
CMD ["php", "-S", "0.0.0.0:8000", "-t", "public", "public/index.php"]
|
CMD ["php", "-S", "0.0.0.0:8000", "-t", "public", "public/index.php"]
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
xdebug.mode=develop,debug
|
xdebug.mode=develop,debug
|
||||||
xdebug.start_with_request=yes
|
xdebug.start_with_request=yes
|
||||||
xdebug.client_host=host.docker.internal
|
xdebug.client_host=host.docker.internal
|
||||||
xdebug.client_port=9003
|
xdebug.client_port=9013
|
||||||
xdebug.log_level=0
|
xdebug.log_level=0
|
||||||
xdebug.idekey=VSCODE
|
xdebug.idekey=VSCODE
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Istruzioni per lo sviluppo di questa repository
|
|
||||||
|
|
||||||
## Debug delle API PHP con Docker
|
|
||||||
|
|
||||||
Quando si lavora su questa repository e si deve fare debug delle API PHP servite via Docker, usare il setup seguente:
|
|
||||||
|
|
||||||
- avviare il container con `docker compose up --build`
|
|
||||||
- usare la configurazione VS Code `Debug PHP in Docker`
|
|
||||||
- mettere breakpoint nei controller sotto `src/Api/Controllers`
|
|
||||||
- testare le richieste su `http://localhost:8000`
|
|
||||||
- se il debugger non parte, usare `curl -H "XDEBUG_TRIGGER: 1" http://localhost:8000/api/users`
|
|
||||||
|
|
||||||
Queste istruzioni valgono anche per eventuali problemi di routing o di esecuzione delle API.
|
|
||||||
Vendored
+8
-27
@@ -5,40 +5,17 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Attach to Chrome",
|
"name": "API - Listen for Xdebug",
|
||||||
"port": 9222,
|
|
||||||
"request": "attach",
|
|
||||||
"type": "chrome",
|
|
||||||
"webRoot": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Listen for Xdebug (Docker)",
|
|
||||||
"type": "php",
|
"type": "php",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"port": 9003,
|
"port": 9013,
|
||||||
"pathMappings": {
|
"pathMappings": {
|
||||||
"/app": "${workspaceFolder}"
|
"/app": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
"log": true
|
"log": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Launch currently open script",
|
"name": "API - Launch Built-in web server",
|
||||||
"type": "php",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${file}",
|
|
||||||
"cwd": "${fileDirname}",
|
|
||||||
"port": 0,
|
|
||||||
"runtimeArgs": [
|
|
||||||
"-dxdebug.start_with_request=yes"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"XDEBUG_MODE": "debug,develop",
|
|
||||||
"XDEBUG_CONFIG": "client_port=${port}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Launch Built-in web server",
|
|
||||||
"type": "php",
|
"type": "php",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
@@ -49,11 +26,15 @@
|
|||||||
],
|
],
|
||||||
"program": "",
|
"program": "",
|
||||||
"cwd": "${workspaceRoot}",
|
"cwd": "${workspaceRoot}",
|
||||||
"port": 9003,
|
"port": 9013,
|
||||||
"serverReadyAction": {
|
"serverReadyAction": {
|
||||||
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
|
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
|
||||||
"uriFormat": "http://localhost:%s",
|
"uriFormat": "http://localhost:%s",
|
||||||
"action": "openExternally"
|
"action": "openExternally"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"XDEBUG_MODE": "debug,develop",
|
||||||
|
"XDEBUG_CONFIG": "client_port=${port}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ require_once __DIR__ . '/vendor/autoload.php';
|
|||||||
use Api\Core\Container;
|
use Api\Core\Container;
|
||||||
use Api\Core\Config;
|
use Api\Core\Config;
|
||||||
use Api\Core\HttpClient;
|
use Api\Core\HttpClient;
|
||||||
|
use Api\Core\CorsManager;
|
||||||
use Api\Core\RateLimiter\RateLimiterInterface;
|
use Api\Core\RateLimiter\RateLimiterInterface;
|
||||||
use Api\Core\RateLimiter\FileRateLimiter;
|
use Api\Core\RateLimiter\FileRateLimiter;
|
||||||
use Api\Core\RateLimiter\InMemoryRateLimiter;
|
use Api\Core\RateLimiter\InMemoryRateLimiter;
|
||||||
use Api\Core\Log\LoggerFactory;
|
use Api\Core\Log\LoggerFactory;
|
||||||
use ElixForms\ElixFormsClient;
|
use ElixForms\ElixFormsClient;
|
||||||
|
use ElixForms\Auth\ElixFormsApiClient;
|
||||||
|
use ElixForms\Auth\ElixFormsAuthenticationClient;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
$container = new Container();
|
$container = new Container();
|
||||||
@@ -25,6 +28,13 @@ $container->singleton(HttpClient::class, function() {
|
|||||||
return new HttpClient();
|
return new HttpClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// CORS manager binding
|
||||||
|
$container->singleton(CorsManager::class, function($c) {
|
||||||
|
$config = $c->make(Config::class);
|
||||||
|
$corsConfig = $config->get('cors', []);
|
||||||
|
return new CorsManager($corsConfig);
|
||||||
|
});
|
||||||
|
|
||||||
// Rate limiter driver configurabile via config or env: 'file' or 'memory'
|
// Rate limiter driver configurabile via config or env: 'file' or 'memory'
|
||||||
$rlDriver = $config->get('rate_limiter_driver', 'file');
|
$rlDriver = $config->get('rate_limiter_driver', 'file');
|
||||||
|
|
||||||
@@ -59,6 +69,21 @@ $container->singleton(ElixFormsClient::class, function($c) {
|
|||||||
return new ElixFormsClient($baseUrl);
|
return new ElixFormsClient($baseUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$container->singleton(ElixFormsAuthenticationClient::class, function($c) {
|
||||||
|
$config = $c->make(Config::class);
|
||||||
|
|
||||||
|
return new ElixFormsAuthenticationClient($config->get('elixforms_api_base_url'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$container->singleton(ElixFormsApiClient::class, function($c) {
|
||||||
|
$config = $c->make(Config::class);
|
||||||
|
|
||||||
|
return new ElixFormsApiClient(
|
||||||
|
$config->get('elixforms_api_base_url'),
|
||||||
|
$c->make(HttpClient::class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// If you have other services, bind them here, for example:
|
// If you have other services, bind them here, for example:
|
||||||
// $container->singleton(SomeService::class, function($c) {
|
// $container->singleton(SomeService::class, function($c) {
|
||||||
// return new SomeService($c->make(LoggerInterface::class), ...);
|
// return new SomeService($c->make(LoggerInterface::class), ...);
|
||||||
|
|||||||
@@ -1,10 +1,49 @@
|
|||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
'elixforms_api_base_url' => 'https://api.example.com',
|
'elixforms_api_base_url' => 'https://api.example.com',
|
||||||
|
|
||||||
'rate_limiter_driver' => 'file', // o 'memory'
|
'rate_limiter_driver' => 'file', // o 'memory'
|
||||||
// configuration for file-based rate limiter
|
// configuration for file-based rate limiter
|
||||||
'rate_limit_storage_dir' => sys_get_temp_dir() . '/api_rate_limit',
|
'rate_limit_storage_dir' => sys_get_temp_dir() . '/api_rate_limit',
|
||||||
// configuration for memory-based rate limiter
|
// configuration for memory-based rate limiter
|
||||||
'rate_limit_requests' => 100,
|
'rate_limit_requests' => 100,
|
||||||
'rate_limit_window_seconds' => 60,
|
'rate_limit_window_seconds' => 60,
|
||||||
|
|
||||||
|
'cors' => [
|
||||||
|
'enabled' => true,
|
||||||
|
// Allowed origins - requests from other origins will be rejected
|
||||||
|
'allowed_origins' => [
|
||||||
|
'http://localhost:3000', // Local development - frontend
|
||||||
|
'http://localhost:8080', // Local development - alternative port
|
||||||
|
'https://app.example.com', // Production frontend
|
||||||
|
'https://admin.example.com', // Production admin panel
|
||||||
|
// 'http://localhost:*', // Allow any port on localhost (not recommended)
|
||||||
|
// '*' // Allow all origins (HIGHLY NOT RECOMMENDED for production)
|
||||||
|
],
|
||||||
|
// HTTP methods allowed for CORS requests
|
||||||
|
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
|
// HTTP headers allowed in the request
|
||||||
|
'allowed_headers' => [
|
||||||
|
'Content-Type',
|
||||||
|
'Authorization',
|
||||||
|
'X-Requested-With',
|
||||||
|
'Accept',
|
||||||
|
'Accept-Language',
|
||||||
|
'Content-Language',
|
||||||
|
'X-API-Key',
|
||||||
|
],
|
||||||
|
// HTTP headers exposed to the client
|
||||||
|
'exposed_headers' => [
|
||||||
|
'Content-Length',
|
||||||
|
'X-JSON-Response-Code',
|
||||||
|
'X-Rate-Limit-Limit',
|
||||||
|
'X-Rate-Limit-Remaining',
|
||||||
|
'X-Rate-Limit-Reset',
|
||||||
|
],
|
||||||
|
// Allow credentials (cookies, authorization headers) in cross-origin requests
|
||||||
|
// Only set to true if you understand the security implications
|
||||||
|
'allow_credentials' => false,
|
||||||
|
// How long (in seconds) the browser can cache the preflight response
|
||||||
|
'max_age' => 86400, // 24 hours
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
|
// elixforms web services credentials
|
||||||
|
'api_access_username' => 'myUsername',
|
||||||
|
'api_access_password' => 'myPassword',
|
||||||
|
// Omit or leave the following empty to disable API token
|
||||||
|
'api_access_token' => 'myApiAccessToken',
|
||||||
|
|
||||||
// elixforms API credentials
|
// elixforms API credentials
|
||||||
'elixforms_api_username' => 'myUser',
|
'elixforms_api_username' => 'myUser',
|
||||||
'elixforms_api_password' => 'myPass',
|
'elixforms_api_password' => 'myPass',
|
||||||
'elixforms_api_token' => 'mySecretToken',
|
'elixforms_api_token' => 'mySecretToken',
|
||||||
'api_access_token' => 'myApiAccessToken',
|
|
||||||
];
|
];
|
||||||
|
|||||||
+4
-2
@@ -1,9 +1,11 @@
|
|||||||
|
name: elixforms-ws
|
||||||
|
|
||||||
services:
|
services:
|
||||||
elixforms-webservices:
|
elixforms-ws:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: .docker/php/Dockerfile
|
dockerfile: .docker/php/Dockerfile
|
||||||
container_name: elixforms-webservices
|
container_name: elixforms-ws
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
|||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
# CORS Management Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This API now includes comprehensive CORS (Cross-Origin Resource Sharing) management. CORS allows your API to be accessed from web applications hosted on different domains.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Automatic CORS Header Application
|
||||||
|
|
||||||
|
All API responses automatically include CORS headers when CORS is enabled. This allows web applications from configured origins to access your API.
|
||||||
|
|
||||||
|
### Preflight Request Handling
|
||||||
|
|
||||||
|
The API automatically handles preflight OPTIONS requests (sent by browsers before actual requests to CORS-protected resources). These are handled with a 204 No Content response.
|
||||||
|
|
||||||
|
### Origin Validation
|
||||||
|
|
||||||
|
Only requests from configured allowed origins are accepted. Requests from unauthorized origins are rejected.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit `config/config.php` to configure CORS:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'cors' => [
|
||||||
|
'enabled' => true, // Enable/disable CORS
|
||||||
|
'allowed_origins' => [
|
||||||
|
'http://localhost:3000',
|
||||||
|
'https://example.com',
|
||||||
|
],
|
||||||
|
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
|
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept'],
|
||||||
|
'exposed_headers' => ['Content-Length', 'X-JSON-Response-Code'],
|
||||||
|
'allow_credentials' => false,
|
||||||
|
'max_age' => 86400,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
| ------------------- | ----- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
|
| `enabled` | bool | `true` | Enable or disable CORS |
|
||||||
|
| `allowed_origins` | array | `[]` | List of domains allowed to access the API. Use `'*'` to allow all (NOT recommended) |
|
||||||
|
| `allowed_methods` | array | `GET, POST, PUT, DELETE, PATCH, OPTIONS` | HTTP methods allowed |
|
||||||
|
| `allowed_headers` | array | `Content-Type, Authorization, X-Requested-With, Accept` | Headers allowed in requests |
|
||||||
|
| `exposed_headers` | array | `Content-Length, X-JSON-Response-Code` | Headers exposed to the client |
|
||||||
|
| `allow_credentials` | bool | `false` | Allow credentials (cookies, auth) in requests |
|
||||||
|
| `max_age` | int | `86400` | Browser cache time for preflight (seconds) |
|
||||||
|
|
||||||
|
## Setup for Different Environments
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```php
|
||||||
|
'cors' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'allowed_origins' => [
|
||||||
|
'http://localhost:3000',
|
||||||
|
'http://localhost:8080',
|
||||||
|
'http://127.0.0.1:3000',
|
||||||
|
],
|
||||||
|
'allow_credentials' => false,
|
||||||
|
'max_age' => 3600,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```php
|
||||||
|
'cors' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'allowed_origins' => [
|
||||||
|
'https://app.example.com',
|
||||||
|
'https://admin.example.com',
|
||||||
|
],
|
||||||
|
'allow_credentials' => false,
|
||||||
|
'max_age' => 86400,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### Allow All Origins (NOT RECOMMENDED for Production)
|
||||||
|
|
||||||
|
```php
|
||||||
|
'cors' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'allowed_origins' => ['*'],
|
||||||
|
'allow_credentials' => false,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Preflight Requests
|
||||||
|
|
||||||
|
When making cross-origin requests with certain headers or methods (like PUT or DELETE), browsers automatically send a preflight OPTIONS request. The API handles these automatically:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Browser sends: OPTIONS /api/resource
|
||||||
|
API responds: 204 No Content + CORS headers
|
||||||
|
Browser sees: Request is allowed, proceeds with actual request
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing CORS
|
||||||
|
|
||||||
|
### Using curl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test CORS with curl
|
||||||
|
curl -H "Origin: http://localhost:3000" \
|
||||||
|
-H "Access-Control-Request-Method: POST" \
|
||||||
|
-H "Access-Control-Request-Headers: Content-Type" \
|
||||||
|
-X OPTIONS \
|
||||||
|
http://localhost:8000/api/users/index -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response Headers
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Access-Control-Allow-Origin: http://localhost:3000
|
||||||
|
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||||
|
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, Accept
|
||||||
|
Access-Control-Max-Age: 86400
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Be Specific with Origins**: Always specify exact allowed origins. Don't use `'*'` in production unless absolutely necessary.
|
||||||
|
|
||||||
|
2. **Credentials**: Only set `allow_credentials: true` if you understand the security implications. This allows cross-origin requests to send cookies.
|
||||||
|
|
||||||
|
3. **Sensitive Headers**: Don't expose sensitive headers in `exposed_headers`. Only expose what clients actually need.
|
||||||
|
|
||||||
|
4. **HTTPS in Production**: Always use HTTPS in production to prevent man-in-the-middle attacks.
|
||||||
|
|
||||||
|
## CorsManager Class
|
||||||
|
|
||||||
|
The `CorsManager` class handles all CORS logic. You can also use it programmatically:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$corsManager = $container->make(\Api\Core\CorsManager::class);
|
||||||
|
|
||||||
|
// Check if current request is allowed
|
||||||
|
if ($corsManager->isOriginAllowed()) {
|
||||||
|
// Process request
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply CORS headers manually
|
||||||
|
$corsManager->applyHeaders();
|
||||||
|
|
||||||
|
// Check for preflight request
|
||||||
|
if ($corsManager->isPreflightRequest()) {
|
||||||
|
$corsManager->handlePreflight();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Preflight request fails
|
||||||
|
|
||||||
|
- Check that the origin in the request matches one in `allowed_origins`
|
||||||
|
- Verify CORS is enabled in config
|
||||||
|
- Check browser console for CORS error messages
|
||||||
|
|
||||||
|
### Missing CORS headers in response
|
||||||
|
|
||||||
|
- Ensure CORS is enabled: `'enabled' => true`
|
||||||
|
- Verify the requesting origin is in `allowed_origins`
|
||||||
|
- Check that `CorsManager` is properly initialized in the container
|
||||||
|
|
||||||
|
### "No Access-Control-Allow-Origin header"
|
||||||
|
|
||||||
|
- Browser origin is not in `allowed_origins`
|
||||||
|
- Add the origin or use `'*'` (only for development)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [MDN: CORS Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
||||||
|
- [OWASP: CORS](https://owasp.org/www-community/CORS)
|
||||||
@@ -8,7 +8,21 @@ Queste istruzioni permettono di fare debug delle API PHP servite con Docker dire
|
|||||||
- Visual Studio Code
|
- Visual Studio Code
|
||||||
- Estensione PHP per VS Code
|
- Estensione PHP per VS Code
|
||||||
|
|
||||||
## 1. Avvia il container
|
## 1. Configurazione del progetto
|
||||||
|
|
||||||
|
Il progetto contiene già i file necessari per il debug:
|
||||||
|
|
||||||
|
- `.docker/php/Dockerfile` - container PHP con Xdebug
|
||||||
|
- `docker-compose.yml` - configurazione Docker
|
||||||
|
- `.vscode/launch.json` - configurazione di avvio VS Code
|
||||||
|
- `.vscode/settings.json` - impostazioni VS Code
|
||||||
|
|
||||||
|
Il container espone:
|
||||||
|
|
||||||
|
- **porta `8000`** per HTTP
|
||||||
|
- **porta `9003`** per Xdebug
|
||||||
|
|
||||||
|
## 2. Avvia il container
|
||||||
|
|
||||||
Dalla root del progetto esegui:
|
Dalla root del progetto esegui:
|
||||||
|
|
||||||
@@ -22,52 +36,56 @@ Il server PHP sarà disponibile su:
|
|||||||
http://localhost:8000
|
http://localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Avvia il debug in VS Code
|
## 3. Avvia il debug in VS Code
|
||||||
|
|
||||||
Apri la sezione Run and Debug e seleziona la configurazione:
|
1. Apri la sezione **Run and Debug** in VS Code (Ctrl+Shift+D)
|
||||||
|
2. Seleziona la configurazione: **Debug PHP in Docker**
|
||||||
|
3. Premi **F5** per avviare il debugger
|
||||||
|
|
||||||
```text
|
## 4. Imposta un breakpoint e testa
|
||||||
Debug PHP in Docker
|
|
||||||
```
|
|
||||||
|
|
||||||
Poi premi F5.
|
1. Aggiungi un breakpoint in un controller, ad esempio:
|
||||||
|
|
||||||
## 3. Imposta un breakpoint
|
```text
|
||||||
|
src/Api/Controllers/V1/UsersController.php
|
||||||
|
```
|
||||||
|
|
||||||
Aggiungi un breakpoint in un controller, ad esempio in:
|
2. Richiama un endpoint tramite browser:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/Api/Controllers/V1/UsersController.php
|
http://localhost:8000/api/users
|
||||||
```
|
```
|
||||||
|
|
||||||
Quindi richiama un endpoint come:
|
3. Oppure usa curl con il trigger Xdebug:
|
||||||
|
|
||||||
```text
|
```bash
|
||||||
http://localhost:8000/api/users
|
curl -H "XDEBUG_TRIGGER: 1" http://localhost:8000/api/users
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Se il debugger non parte
|
Il debugger dovrebbe fermarsi sul breakpoint e permetterti di ispezionare le variabili.
|
||||||
|
|
||||||
Prova a inviare la richiesta con il trigger Xdebug:
|
## 5. Risoluzione dei problemi
|
||||||
|
|
||||||
```bash
|
### VS Code non si ferma sul breakpoint
|
||||||
curl -H "XDEBUG_TRIGGER: 1" http://localhost:8000/api/users
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Configurazione attesa
|
- Verifica che Xdebug sia abilitato nel container
|
||||||
|
- Controlla che la configurazione `client_host` nel Dockerfile punti a `host.docker.internal`
|
||||||
|
- Verifica che la porta 9003 sia disponibile e non bloccata dal firewall
|
||||||
|
|
||||||
Il progetto contiene già:
|
### Il container non si avvia
|
||||||
|
|
||||||
- un Dockerfile PHP con Xdebug
|
- Assicurati che Docker Desktop sia in esecuzione
|
||||||
- un file docker-compose.yml
|
- Verifica che le porte 8000 e 9003 non siano già in uso
|
||||||
- una configurazione di avvio VS Code in .vscode/launch.json
|
- Controlla i log di Docker per errori specifici
|
||||||
|
|
||||||
## 6. Nota importante
|
### "Failed to fetch" dalle richieste del frontend
|
||||||
|
|
||||||
Il server PHP viene eseguito con:
|
- Verifica che il frontend abbia il CORS correttamente configurato
|
||||||
|
- Controlla che `allowed_origins` in `config/config.php` includa l'origine del frontend
|
||||||
|
- Testa con l'endpoint di diagnostica `/cors-check`
|
||||||
|
|
||||||
```bash
|
## Note
|
||||||
php -S 0.0.0.0:8000 -t public public/index.php
|
|
||||||
```
|
|
||||||
|
|
||||||
Questa modalità è adatta per il debug delle API HTTP del progetto.
|
- La procedura di debug influisce solo sulla sessione VS Code, il server continua a funzionare normalmente
|
||||||
|
- È possibile debug con i breakpoint condizionali per limitare i fermi
|
||||||
|
- Usa la console di debug di VS Code per eseguire comandi PHP durante il debug
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
@term=MA
|
||||||
|
|
||||||
|
GET http://localhost:8000/api/dipendenti/cerca?term={{term}}
|
||||||
|
Authorization: Basic elixforms_ws:password123
|
||||||
|
X-API-Key: myApiAccessToken
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
@term=ID
|
||||||
|
|
||||||
|
GET http://localhost:8000/api/contratti/cerca?term={{term}}&cod_fis=MMMPPL74T17E463A
|
||||||
|
Authorization: Basic elixforms_ws:password123
|
||||||
|
X-API-Key: myApiAccessToken
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
@term=ID
|
||||||
|
|
||||||
|
GET http://localhost:8000/contratti/cerca?term={{term}}
|
||||||
+47
-5
@@ -5,6 +5,7 @@ use Api\Auth\ApiTokenAuthenticator;
|
|||||||
use Api\Core\Router;
|
use Api\Core\Router;
|
||||||
use Api\Core\Request;
|
use Api\Core\Request;
|
||||||
use Api\Core\Response;
|
use Api\Core\Response;
|
||||||
|
use Api\Core\CorsManager;
|
||||||
use Api\Core\RateLimiter\RateLimiterInterface;
|
use Api\Core\RateLimiter\RateLimiterInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
@@ -32,6 +33,48 @@ $router = new Router($container); // vedi nota: router può ricevere container
|
|||||||
$request = new Request();
|
$request = new Request();
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
|
|
||||||
|
// CORS handling
|
||||||
|
$corsManager = $container->make(CorsManager::class);
|
||||||
|
$corsConfig = $container->make(\Api\Core\Config::class)->get('cors', []);
|
||||||
|
|
||||||
|
if (!empty($corsConfig['enabled'])) {
|
||||||
|
$origin = $corsManager->getOrigin();
|
||||||
|
$requestOrigin = $_SERVER['HTTP_ORIGIN'] ?? 'none';
|
||||||
|
|
||||||
|
// Log CORS request details for debugging
|
||||||
|
$logger->debug('CORS request received', [
|
||||||
|
'request_origin' => $requestOrigin,
|
||||||
|
'allowed_origin' => $origin,
|
||||||
|
'is_preflight' => $corsManager->isPreflightRequest(),
|
||||||
|
'method' => $_SERVER['REQUEST_METHOD'],
|
||||||
|
'path' => $request->path()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Apply CORS headers to all responses
|
||||||
|
$corsManager->applyHeaders();
|
||||||
|
|
||||||
|
// Handle preflight OPTIONS requests
|
||||||
|
if ($corsManager->isPreflightRequest()) {
|
||||||
|
$corsManager->handlePreflight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnostic CORS endpoint (no auth required)
|
||||||
|
if ($request->path() === '/cors-check' && $request->method() === 'GET') {
|
||||||
|
$corsManager = $container->make(CorsManager::class);
|
||||||
|
$corsConfig = $container->make(\Api\Core\Config::class)->get('cors', []);
|
||||||
|
|
||||||
|
$response->json([
|
||||||
|
'cors_enabled' => !empty($corsConfig['enabled']),
|
||||||
|
'request_origin' => $_SERVER['HTTP_ORIGIN'] ?? null,
|
||||||
|
'allowed_origins' => $corsConfig['allowed_origins'] ?? [],
|
||||||
|
'is_origin_allowed' => $corsManager->isOriginAllowed(),
|
||||||
|
'is_preflight' => $corsManager->isPreflightRequest(),
|
||||||
|
'request_method' => $_SERVER['REQUEST_METHOD'],
|
||||||
|
'headers_sent' => function_exists('getallheaders') ? getallheaders() : $_SERVER,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Autenticazione separata per le API esterne
|
// Autenticazione separata per le API esterne
|
||||||
if (strpos($request->path(), '/api/') === 0) {
|
if (strpos($request->path(), '/api/') === 0) {
|
||||||
try {
|
try {
|
||||||
@@ -39,15 +82,14 @@ if (strpos($request->path(), '/api/') === 0) {
|
|||||||
$authenticator->authenticate($request);
|
$authenticator->authenticate($request);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$logger->warning('External API auth failed', ['path' => $request->path(), 'error' => $e->getMessage()]);
|
$logger->warning('External API auth failed', ['path' => $request->path(), 'error' => $e->getMessage()]);
|
||||||
$response->json(['error' => 'Unauthorized'], 401);
|
$response->unauthorized();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// register routes (path without version prefix)
|
// register routes (path without version prefix)
|
||||||
$router->get('/users/index', 'UsersController@index');
|
$router->get('/contratti/cerca', 'ContrattiController@search');
|
||||||
$router->post('/users/create', 'UsersController@create');
|
$router->get('/dipendenti/cerca', 'DipendentiController@search');
|
||||||
$router->get('/example/test', 'ExampleController@test');
|
$router->get('/elixforms/instances/search', 'ElixFormsController@searchInstances');
|
||||||
$router->get('/contratti/cerca', 'ContrattiController@cercaContratti');
|
|
||||||
|
|
||||||
// Rate limiting by IP address
|
// Rate limiting by IP address
|
||||||
$key = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
$key = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ Deploy Apache PHP
|
|||||||
throw "Compress-Archive non disponibile. Usa Windows PowerShell 5+ o PowerShell 7."
|
throw "Compress-Archive non disponibile. Usa Windows PowerShell 5+ o PowerShell 7."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$outputFile = Resolve-Path -LiteralPath $outputFile
|
||||||
|
|
||||||
Write-Host "[4/4] Archivio pronto"
|
Write-Host "[4/4] Archivio pronto"
|
||||||
Write-Host "Percorso: '$outputFile'"
|
Write-Host "Percorso: '$outputFile'"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ if (-not (Get-Command composer -ErrorAction SilentlyContinue)) {
|
|||||||
Push-Location $root
|
Push-Location $root
|
||||||
try {
|
try {
|
||||||
if ($useDockerComposer) {
|
if ($useDockerComposer) {
|
||||||
docker run --rm -p 8000:8000 -v "$($dockerRoot):/app" -w /app --name php-serve php:8.5.8-cli php -S 0.0.0.0:8000 -t public public/index.php
|
docker run --rm -p 8000:8000 -v "$($dockerRoot):/app" -w /app --name elixforms-serve-ws php:8.5.8-cli php -S 0.0.0.0:8000 -t public public/index.php
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
php -S 0.0.0.0:8000 -t public public/index.php
|
php -S 0.0.0.0:8000 -t public public/index.php
|
||||||
|
|||||||
@@ -15,20 +15,32 @@ class ApiTokenAuthenticator
|
|||||||
|
|
||||||
public function authenticate(Request $request): void
|
public function authenticate(Request $request): void
|
||||||
{
|
{
|
||||||
$authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
|
$authorizationHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
|
||||||
if (!$authorization) {
|
if (!$authorizationHeader) {
|
||||||
throw new \Exception('Missing Authorization header');
|
throw new \Exception('Missing Authorization header');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preg_match('/^Bearer\s+(.*)$/i', trim($authorization), $matches)) {
|
// Basic authorization formal test
|
||||||
|
if (!preg_match('/^Basic\s+(.*)$/i', trim($authorizationHeader), $matches)) {
|
||||||
throw new \Exception('Invalid Authorization header format');
|
throw new \Exception('Invalid Authorization header format');
|
||||||
}
|
}
|
||||||
|
// Username and password test
|
||||||
|
$decoded = explode(':', base64_decode($matches[1]), 2);
|
||||||
|
$authorized = empty(array_diff([ $this->config->secret('api_access_username'), $this->config->secret('api_access_password')], $decoded));
|
||||||
|
if (!$authorized) {
|
||||||
|
throw new \Exception('Authorization failed');
|
||||||
|
}
|
||||||
|
|
||||||
$token = $matches[1];
|
// Now, for the X-API-Key only if it was defined in the config (simple way to disable it for testing)
|
||||||
$expected = $this->config->secret('api_access_token');
|
$expectedApiKey = $this->config->secret('api_access_token');
|
||||||
|
if ($expectedApiKey !== null && trim($expectedApiKey) !== '') {
|
||||||
if (empty($expected) || !hash_equals((string) $expected, (string) $token)) {
|
$apiKeyHeader = $_SERVER['HTTP_X_API_KEY'] ?? $_SERVER['REDIRECT_HTTP_X_API_KEY'] ?? '';
|
||||||
|
if (!$apiKeyHeader) {
|
||||||
|
throw new \Exception('Missing API access token');
|
||||||
|
}
|
||||||
|
if (!hash_equals((string) $expectedApiKey, (string) $apiKeyHeader)) {
|
||||||
throw new \Exception('Invalid API access token');
|
throw new \Exception('Invalid API access token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ContrattiController {
|
|||||||
[ 'idContratto' => 'ID_CONTRATTO_005', 'titoloContratto' => 'Contratto Custom', 'idDomanda' => 'ID_0105', 'idRicevuta' => 'RIC_1005' ],
|
[ 'idContratto' => 'ID_CONTRATTO_005', 'titoloContratto' => 'Contratto Custom', 'idDomanda' => 'ID_0105', 'idRicevuta' => 'RIC_1005' ],
|
||||||
];
|
];
|
||||||
|
|
||||||
public function cercaContratti(Request $req, Response $res) {
|
public function search(Request $req, Response $res) {
|
||||||
$term = $req->query()['term'] ?? "";
|
$term = $req->query()['term'] ?? "";
|
||||||
|
|
||||||
$filtered = array_filter($this->contratti, function($contratto) use ($term) {
|
$filtered = array_filter($this->contratti, function($contratto) use ($term) {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
namespace Api\Controllers\V1;
|
||||||
|
|
||||||
|
use Api\Core\Request;
|
||||||
|
use Api\Core\Response;
|
||||||
|
|
||||||
|
class DipendentiController {
|
||||||
|
private $dipendenti = [
|
||||||
|
[ 'codiceFiscale' => 'ABCDEF12G34H567J', 'nominativo' => 'ROSSI Mario', 'email' => 'mario.rossi@unipr.it' ],
|
||||||
|
[ 'codiceFiscale' => 'KLMNOP89Q01R234S', 'nominativo' => 'VERDI Giuseppe', 'email' => 'giuseppe.verdi@unipr.it' ],
|
||||||
|
[ 'codiceFiscale' => 'TUVWXY56Z78A901B', 'nominativo' => 'BIANCHI Luca', 'email' => 'luca.bianchi@unipr.it' ],
|
||||||
|
[ 'codiceFiscale' => 'CDEFGH23I45J678K', 'nominativo' => 'MAMMI Pier-Paolo', 'email' => 'pierpaolo.mammi@unipr.it' ],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function search(Request $req, Response $res) {
|
||||||
|
$term = $req->query()['term'] ?? "";
|
||||||
|
|
||||||
|
$filtered = array_filter($this->dipendenti, function($contratto) use ($term) {
|
||||||
|
if (empty($term)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$term = strtolower($term);
|
||||||
|
return stripos($contratto['codiceFiscale'], $term) !== false ||
|
||||||
|
stripos($contratto['nominativo'], $term) !== false ||
|
||||||
|
stripos($contratto['email'], $term) !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $res->json(array_values($filtered));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
namespace Api\Controllers\V1;
|
||||||
|
|
||||||
|
use Api\Core\Config;
|
||||||
|
use Api\Core\Request;
|
||||||
|
use Api\Core\Response;
|
||||||
|
use ElixForms\Auth\ElixFormsApiClient;
|
||||||
|
use ElixForms\Auth\ElixFormsAuthenticationClient;
|
||||||
|
use ElixForms\Exceptions\ElixFormsException;
|
||||||
|
|
||||||
|
class ElixFormsController
|
||||||
|
{
|
||||||
|
private ElixFormsAuthenticationClient $authenticationClient;
|
||||||
|
private ElixFormsApiClient $apiClient;
|
||||||
|
private Config $config;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
ElixFormsAuthenticationClient $authenticationClient,
|
||||||
|
ElixFormsApiClient $apiClient,
|
||||||
|
Config $config
|
||||||
|
) {
|
||||||
|
$this->authenticationClient = $authenticationClient;
|
||||||
|
$this->apiClient = $apiClient;
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchInstances(Request $req, Response $res)
|
||||||
|
{
|
||||||
|
$query = $req->query();
|
||||||
|
$moduleTag = $this->requiredString($query, 'moduleTag');
|
||||||
|
$fieldName = $this->requiredString($query, 'fieldName');
|
||||||
|
$fieldValue = $this->requiredString($query, 'fieldValue');
|
||||||
|
$exportGroup = $this->optionalNonEmptyString($query, 'exportGroup', 'API');
|
||||||
|
|
||||||
|
if ($moduleTag === null || $fieldName === null || $fieldValue === null || $exportGroup === null) {
|
||||||
|
return $res->json([
|
||||||
|
'error' => 'moduleTag, fieldName e fieldValue sono obbligatori; exportGroup, se specificato, deve essere una stringa non vuota.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $this->config->secret('elixforms_api_username');
|
||||||
|
$password = $this->config->secret('elixforms_api_password');
|
||||||
|
|
||||||
|
if (!is_string($username) || trim($username) === '' || !is_string($password) || $password === '') {
|
||||||
|
throw new ElixFormsException('Credenziali elixForms non configurate.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->authenticationClient->login($username, $password);
|
||||||
|
$instances = $this->apiClient->lookupByStatus($moduleTag, $token, $username);
|
||||||
|
$matchingInstances = [];
|
||||||
|
|
||||||
|
foreach ($instances as $instance) {
|
||||||
|
if (!is_array($instance)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestId = $this->requestId($instance);
|
||||||
|
if ($requestId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exportTags = $this->apiClient->getExportTags(
|
||||||
|
$requestId,
|
||||||
|
$moduleTag,
|
||||||
|
$token,
|
||||||
|
$username,
|
||||||
|
$exportGroup
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($exportTags as $exportTag) {
|
||||||
|
if (!is_array($exportTag) || !isset($exportTag['name'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = (string) $exportTag['name'];
|
||||||
|
$value = isset($exportTag['value']) ? (string) $exportTag['value'] : '';
|
||||||
|
|
||||||
|
if (strcasecmp($name, $fieldName) === 0 && stripos($value, $fieldValue) !== false) {
|
||||||
|
$matchingInstances[] = $instance;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res->json($matchingInstances);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requiredString(array $values, string $key): ?string
|
||||||
|
{
|
||||||
|
if (!isset($values[$key]) || !is_string($values[$key])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = trim($values[$key]);
|
||||||
|
|
||||||
|
return $value === '' ? null : $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function optionalNonEmptyString(array $values, string $key, string $default): ?string
|
||||||
|
{
|
||||||
|
if (!array_key_exists($key, $values)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($values[$key])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = trim($values[$key]);
|
||||||
|
|
||||||
|
return $value === '' ? null : $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requestId(array $instance)
|
||||||
|
{
|
||||||
|
foreach (['idDomanda', 'requestId', 'idRequest'] as $key) {
|
||||||
|
if (isset($instance[$key]) && (is_int($instance[$key]) || is_string($instance[$key]))) {
|
||||||
|
return $instance[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
namespace Api\Core;
|
||||||
|
|
||||||
|
class CorsManager {
|
||||||
|
private $allowedOrigins;
|
||||||
|
private $allowedMethods;
|
||||||
|
private $allowedHeaders;
|
||||||
|
private $exposedHeaders;
|
||||||
|
private $allowCredentials;
|
||||||
|
private $maxAge;
|
||||||
|
|
||||||
|
public function __construct(array $config = []) {
|
||||||
|
$this->allowedOrigins = $config['allowed_origins'] ?? [];
|
||||||
|
$this->allowedMethods = $config['allowed_methods'] ?? ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
|
||||||
|
$this->allowedHeaders = $config['allowed_headers'] ?? ['Content-Type', 'Authorization', 'X-Requested-With'];
|
||||||
|
$this->exposedHeaders = $config['exposed_headers'] ?? ['Content-Length', 'X-JSON-Response-Code'];
|
||||||
|
$this->allowCredentials = $config['allow_credentials'] ?? false;
|
||||||
|
$this->maxAge = $config['max_age'] ?? 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the allowed origin for the current request
|
||||||
|
*/
|
||||||
|
public function getOrigin(): ?string {
|
||||||
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? null;
|
||||||
|
|
||||||
|
if (!$origin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if origin is in allowed list
|
||||||
|
if (in_array('*', $this->allowedOrigins)) {
|
||||||
|
return '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($origin, $this->allowedOrigins)) {
|
||||||
|
return $origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current request is a preflight OPTIONS request
|
||||||
|
*/
|
||||||
|
public function isPreflightRequest(): bool {
|
||||||
|
return $_SERVER['REQUEST_METHOD'] === 'OPTIONS';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply CORS headers to the response
|
||||||
|
*/
|
||||||
|
public function applyHeaders(?string $origin = null): void {
|
||||||
|
if ($origin === null) {
|
||||||
|
$origin = $this->getOrigin();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$origin) {
|
||||||
|
// If no valid origin, don't set CORS headers
|
||||||
|
// This ensures blocked origins don't accidentally get access
|
||||||
|
header('Vary: Origin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Access-Control-Allow-Origin: ' . $origin);
|
||||||
|
header('Access-Control-Allow-Methods: ' . implode(', ', $this->allowedMethods));
|
||||||
|
header('Access-Control-Allow-Headers: ' . implode(', ', $this->allowedHeaders));
|
||||||
|
header('Access-Control-Expose-Headers: ' . implode(', ', $this->exposedHeaders));
|
||||||
|
header('Vary: Origin');
|
||||||
|
|
||||||
|
if ($this->allowCredentials) {
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Access-Control-Max-Age: ' . $this->maxAge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle preflight OPTIONS request
|
||||||
|
*/
|
||||||
|
public function handlePreflight(): void {
|
||||||
|
$origin = $this->getOrigin();
|
||||||
|
|
||||||
|
if ($origin) {
|
||||||
|
$this->applyHeaders($origin);
|
||||||
|
http_response_code(204);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(403);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if origin is allowed
|
||||||
|
*/
|
||||||
|
public function isOriginAllowed(?string $origin = null): bool {
|
||||||
|
if ($origin === null) {
|
||||||
|
$origin = $this->getOrigin();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$origin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('*', $this->allowedOrigins)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($origin, $this->allowedOrigins);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,15 @@ class Response {
|
|||||||
echo json_encode($data);
|
echo json_encode($data);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function unauthorized(?string $data = null) {
|
||||||
|
http_response_code(401);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('HTTP/1.1 401 Unauthorized');
|
||||||
|
header('Content-Length: 0');
|
||||||
|
if ($data !== null && $data !== '') {
|
||||||
|
echo json_encode($data);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
namespace ElixForms\Auth;
|
||||||
|
|
||||||
|
use Api\Core\HttpClient;
|
||||||
|
use ElixForms\Exceptions\ElixFormsException;
|
||||||
|
|
||||||
|
class ElixFormsApiClient
|
||||||
|
{
|
||||||
|
private string $baseUrl;
|
||||||
|
private HttpClient $httpClient;
|
||||||
|
|
||||||
|
public function __construct(string $baseUrl, HttpClient $httpClient)
|
||||||
|
{
|
||||||
|
$this->baseUrl = rtrim($baseUrl, '/');
|
||||||
|
$this->httpClient = $httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restituisce le istanze del modulo, opzionalmente filtrate per stato.
|
||||||
|
* I flag di ElixFormsRequestStatus possono essere combinati con l'operatore |.
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public function lookupByStatus(
|
||||||
|
string $moduleTag,
|
||||||
|
string $authToken,
|
||||||
|
string $username,
|
||||||
|
?int $status = null
|
||||||
|
): array
|
||||||
|
{
|
||||||
|
$queryParts = [];
|
||||||
|
if ($status !== null) {
|
||||||
|
foreach (ElixFormsRequestStatus::toApiValues($status) as $statusValue) {
|
||||||
|
$queryParts[] = 'requestStatus=' . rawurlencode($statusValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$queryParts[] = 'moduleTag=' . rawurlencode($moduleTag);
|
||||||
|
|
||||||
|
$url = $this->baseUrl . '/eF/api/request/lookup/by-status?' . implode('&', $queryParts);
|
||||||
|
$payload = $this->getJson($url, $authToken, $username, 'LookupByStatus');
|
||||||
|
$requests = $payload['value']['requests'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($requests)) {
|
||||||
|
throw new ElixFormsException('Risposta LookupByStatus non valida: requests deve essere un array.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int|string $requestId
|
||||||
|
* @return array<int,array{name?:mixed,value?:mixed}>
|
||||||
|
*/
|
||||||
|
public function getExportTags(
|
||||||
|
$requestId,
|
||||||
|
string $moduleTag,
|
||||||
|
string $authToken,
|
||||||
|
string $username,
|
||||||
|
string $exportGroup = 'API'
|
||||||
|
): array
|
||||||
|
{
|
||||||
|
$exportGroup = trim($exportGroup);
|
||||||
|
if ($exportGroup === '') {
|
||||||
|
throw new \InvalidArgumentException('exportGroup deve essere una stringa non vuota.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = sprintf(
|
||||||
|
'%s/eF/services/api/request/%s/view/_DEFAULT/exportTags/get/v1?moduleTag=%s&exportGroup=%s',
|
||||||
|
$this->baseUrl,
|
||||||
|
rawurlencode((string) $requestId),
|
||||||
|
rawurlencode($moduleTag),
|
||||||
|
rawurlencode($exportGroup)
|
||||||
|
);
|
||||||
|
$payload = $this->getJson($url, $authToken, $username, 'GetExportTags');
|
||||||
|
$exportTags = $payload['value']['exportTags'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($exportTags)) {
|
||||||
|
throw new ElixFormsException('Risposta GetExportTags non valida: exportTags deve essere un array.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($exportTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getJson(
|
||||||
|
string $url,
|
||||||
|
string $authToken,
|
||||||
|
string $username,
|
||||||
|
string $operation
|
||||||
|
): array {
|
||||||
|
$response = $this->httpClient->get($url, [
|
||||||
|
'Authorization' => 'Bearer ' . $authToken,
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'x-requested-with' => 'XMLHttpRequest',
|
||||||
|
'x-api-username' => $username,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$status = $response['status'] ?? 500;
|
||||||
|
if ($status !== 200) {
|
||||||
|
throw new ElixFormsException(sprintf(
|
||||||
|
'Errore durante %s. HTTP Status: %d',
|
||||||
|
$operation,
|
||||||
|
$status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $response['json'] ?? null;
|
||||||
|
$jsonError = JSON_ERROR_NONE;
|
||||||
|
if (!is_array($payload) && isset($response['body']) && is_string($response['body'])) {
|
||||||
|
$payload = json_decode($response['body'], true);
|
||||||
|
$jsonError = json_last_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($payload) || $jsonError !== JSON_ERROR_NONE) {
|
||||||
|
throw new ElixFormsException(sprintf(
|
||||||
|
'Risposta non valida da %s: atteso JSON.',
|
||||||
|
$operation
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$globalStatus = $payload['value']['globalStatus'] ?? null;
|
||||||
|
if ($globalStatus === 'ERROR') {
|
||||||
|
$description = $payload['value']['description'] ?? 'errore non specificato';
|
||||||
|
throw new ElixFormsException(sprintf(
|
||||||
|
'%s ha restituito un errore: %s',
|
||||||
|
$operation,
|
||||||
|
$description
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ class ElixFormsAuthenticationClient {
|
|||||||
private $baseUrl;
|
private $baseUrl;
|
||||||
private $httpClient;
|
private $httpClient;
|
||||||
|
|
||||||
public function __construct(string $baseUrl, Client $httpClient = null) {
|
public function __construct(string $baseUrl, ?Client $httpClient = null) {
|
||||||
$this->baseUrl = rtrim($baseUrl, '/');
|
$this->baseUrl = rtrim($baseUrl, '/');
|
||||||
$this->httpClient = $httpClient ?? new Client(['timeout' => 10.0, 'http_errors' => false]);
|
$this->httpClient = $httpClient ?? new Client(['timeout' => 10.0, 'http_errors' => false]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
namespace ElixForms\Auth;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum-like di flag combinabili per gli stati delle istanze elixForms.
|
||||||
|
*
|
||||||
|
* Esempio: ElixFormsRequestStatus::IN_PROGRESS | ElixFormsRequestStatus::PROCESSED
|
||||||
|
*/
|
||||||
|
final class ElixFormsRequestStatus
|
||||||
|
{
|
||||||
|
public const IN_PROGRESS = 1;
|
||||||
|
public const SUBMITTED = 2;
|
||||||
|
public const PROCESSED = 4;
|
||||||
|
public const ALL = self::IN_PROGRESS | self::SUBMITTED | self::PROCESSED;
|
||||||
|
|
||||||
|
private const API_VALUES = [
|
||||||
|
self::IN_PROGRESS => 'IN_PROGRESS',
|
||||||
|
self::SUBMITTED => 'SUBMITTED',
|
||||||
|
self::PROCESSED => 'PROCESSED',
|
||||||
|
];
|
||||||
|
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
public static function toApiValues(int $status): array
|
||||||
|
{
|
||||||
|
if ($status <= 0 || ($status & ~self::ALL) !== 0) {
|
||||||
|
throw new InvalidArgumentException('La combinazione di stati elixForms non è valida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = [];
|
||||||
|
foreach (self::API_VALUES as $flag => $apiValue) {
|
||||||
|
if (($status & $flag) === $flag) {
|
||||||
|
$values[] = $apiValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use Api\Core\HttpClient;
|
||||||
|
use ElixForms\Auth\ElixFormsApiClient;
|
||||||
|
use ElixForms\Auth\ElixFormsRequestStatus;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ElixFormsApiClientTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testLookupByStatusOmitsStatusWhenItIsNull(): void
|
||||||
|
{
|
||||||
|
$httpClient = new class extends HttpClient {
|
||||||
|
public string $url = '';
|
||||||
|
public array $headers = [];
|
||||||
|
|
||||||
|
public function get(string $url, array $headers = [])
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
$this->headers = $headers;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 200,
|
||||||
|
'json' => [
|
||||||
|
'value' => [
|
||||||
|
'globalStatus' => 'OK',
|
||||||
|
'requests' => [['requestId' => 123]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$client = new ElixFormsApiClient('https://example.test', $httpClient);
|
||||||
|
|
||||||
|
$requests = $client->lookupByStatus('MODULO TEST', 'token', 'user');
|
||||||
|
|
||||||
|
self::assertSame([['requestId' => 123]], $requests);
|
||||||
|
self::assertStringNotContainsString('requestStatus=', $httpClient->url);
|
||||||
|
self::assertStringContainsString('moduleTag=MODULO%20TEST', $httpClient->url);
|
||||||
|
self::assertSame('Bearer token', $httpClient->headers['Authorization']);
|
||||||
|
self::assertSame('user', $httpClient->headers['x-api-username']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLookupByStatusExpandsCombinedFlags(): void
|
||||||
|
{
|
||||||
|
$httpClient = new class extends HttpClient {
|
||||||
|
public string $url = '';
|
||||||
|
|
||||||
|
public function get(string $url, array $headers = [])
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 200,
|
||||||
|
'json' => [
|
||||||
|
'value' => [
|
||||||
|
'globalStatus' => 'OK',
|
||||||
|
'requests' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$client = new ElixFormsApiClient('https://example.test', $httpClient);
|
||||||
|
|
||||||
|
$client->lookupByStatus(
|
||||||
|
'MODULO',
|
||||||
|
'token',
|
||||||
|
'user',
|
||||||
|
ElixFormsRequestStatus::IN_PROGRESS | ElixFormsRequestStatus::PROCESSED
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertStringContainsString('requestStatus=IN_PROGRESS', $httpClient->url);
|
||||||
|
self::assertStringNotContainsString('requestStatus=SUBMITTED', $httpClient->url);
|
||||||
|
self::assertStringContainsString('requestStatus=PROCESSED', $httpClient->url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRequestStatusRejectsUnknownFlags(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
|
||||||
|
ElixFormsRequestStatus::toApiValues(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetExportTagsSupportsVendorJsonContentTypeResponses(): void
|
||||||
|
{
|
||||||
|
$httpClient = new class extends HttpClient {
|
||||||
|
public string $url = '';
|
||||||
|
|
||||||
|
public function get(string $url, array $headers = [])
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 200,
|
||||||
|
'body' => json_encode([
|
||||||
|
'value' => [
|
||||||
|
'globalStatus' => 'OK',
|
||||||
|
'exportTags' => [
|
||||||
|
['name' => 'contratto.id', 'value' => 'ABC-123'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'json' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$client = new ElixFormsApiClient('https://example.test/', $httpClient);
|
||||||
|
|
||||||
|
$tags = $client->getExportTags(42, 'MODULO', 'token', 'user');
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[['name' => 'contratto.id', 'value' => 'ABC-123']],
|
||||||
|
$tags
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
'https://example.test/eF/services/api/request/42/view/_DEFAULT/exportTags/get/v1?moduleTag=MODULO&exportGroup=API',
|
||||||
|
$httpClient->url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetExportTagsUsesTheProvidedExportGroup(): void
|
||||||
|
{
|
||||||
|
$httpClient = new class extends HttpClient {
|
||||||
|
public string $url = '';
|
||||||
|
|
||||||
|
public function get(string $url, array $headers = [])
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 200,
|
||||||
|
'json' => [
|
||||||
|
'value' => [
|
||||||
|
'globalStatus' => 'OK',
|
||||||
|
'exportTags' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$client = new ElixFormsApiClient('https://example.test', $httpClient);
|
||||||
|
|
||||||
|
$client->getExportTags(42, 'MODULO', 'token', 'user', 'REPORT ORE');
|
||||||
|
|
||||||
|
self::assertStringContainsString('exportGroup=REPORT%20ORE', $httpClient->url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetExportTagsRejectsAnEmptyExportGroup(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
|
||||||
|
$httpClient = new HttpClient();
|
||||||
|
$client = new ElixFormsApiClient('https://example.test', $httpClient);
|
||||||
|
$client->getExportTags(42, 'MODULO', 'token', 'user', ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use Api\Controllers\V1\ElixFormsController;
|
||||||
|
use Api\Core\Config;
|
||||||
|
use Api\Core\Request;
|
||||||
|
use Api\Core\Response;
|
||||||
|
use ElixForms\Auth\ElixFormsApiClient;
|
||||||
|
use ElixForms\Auth\ElixFormsAuthenticationClient;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ElixFormsControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testSearchInstancesReturnsOnlyMatchingInstances(): void
|
||||||
|
{
|
||||||
|
$authenticationClient = new class extends ElixFormsAuthenticationClient {
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(string $username, string $password): string
|
||||||
|
{
|
||||||
|
return 'token';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$apiClient = new class extends ElixFormsApiClient {
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lookupByStatus(
|
||||||
|
string $moduleTag,
|
||||||
|
string $authToken,
|
||||||
|
string $username,
|
||||||
|
?int $status = null
|
||||||
|
): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['idDomanda' => 10],
|
||||||
|
['requestId' => 20],
|
||||||
|
['idRequest' => 30],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExportTags(
|
||||||
|
$requestId,
|
||||||
|
string $moduleTag,
|
||||||
|
string $authToken,
|
||||||
|
string $username,
|
||||||
|
string $exportGroup = 'API'
|
||||||
|
): array
|
||||||
|
{
|
||||||
|
if ($exportGroup !== 'CUSTOM') {
|
||||||
|
throw new \RuntimeException('exportGroup non inoltrato al client.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
10 => 'Nessuna corrispondenza',
|
||||||
|
20 => 'Il contratto ABC-123 è presente',
|
||||||
|
30 => 'Altro valore',
|
||||||
|
];
|
||||||
|
|
||||||
|
return [[
|
||||||
|
'name' => 'contratto.id',
|
||||||
|
'value' => $values[$requestId],
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$config = new class extends Config {
|
||||||
|
public function secret($key, $default = null)
|
||||||
|
{
|
||||||
|
return $key === 'elixforms_api_username' ? 'user' : 'password';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$request = new class extends Request {
|
||||||
|
public function query()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'moduleTag' => 'MODULO',
|
||||||
|
'fieldName' => 'contratto.id',
|
||||||
|
'fieldValue' => 'abc-123',
|
||||||
|
'exportGroup' => ' CUSTOM ',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$response = new CapturingResponse();
|
||||||
|
$controller = new ElixFormsController($authenticationClient, $apiClient, $config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$controller->searchInstances($request, $response);
|
||||||
|
self::fail('La risposta avrebbe dovuto interrompere il flusso del test.');
|
||||||
|
} catch (CapturedResponseException $exception) {
|
||||||
|
self::assertSame(200, $exception->status);
|
||||||
|
self::assertSame([['requestId' => 20]], $exception->payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearchInstancesRejectsMissingParameters(): void
|
||||||
|
{
|
||||||
|
$authenticationClient = new class extends ElixFormsAuthenticationClient {
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$apiClient = new class extends ElixFormsApiClient {
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$config = new Config();
|
||||||
|
$request = new class extends Request {
|
||||||
|
public function query()
|
||||||
|
{
|
||||||
|
return ['moduleTag' => 'MODULO'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$controller = new ElixFormsController($authenticationClient, $apiClient, $config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$controller->searchInstances($request, new CapturingResponse());
|
||||||
|
self::fail('La risposta avrebbe dovuto interrompere il flusso del test.');
|
||||||
|
} catch (CapturedResponseException $exception) {
|
||||||
|
self::assertSame(400, $exception->status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class CapturingResponse extends Response
|
||||||
|
{
|
||||||
|
public function json($data, $status = 200)
|
||||||
|
{
|
||||||
|
throw new CapturedResponseException($data, $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class CapturedResponseException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public $payload;
|
||||||
|
public int $status;
|
||||||
|
|
||||||
|
public function __construct($payload, int $status)
|
||||||
|
{
|
||||||
|
parent::__construct('Response captured');
|
||||||
|
$this->payload = $payload;
|
||||||
|
$this->status = $status;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user