Compare commits

..
13 Commits
10 changed files with 116 additions and 58 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
.git
*.sqlite
*.sqlite-journal
.DS_Store
.idea/
.vscode/
-1
View File
@@ -1,4 +1,3 @@
data.json
*.sqlite *.sqlite
*.sqlite-journal *.sqlite-journal
.DS_Store .DS_Store
+18
View File
@@ -0,0 +1,18 @@
FROM php:8.4-apache
RUN a2enmod rewrite
RUN printf '<VirtualHost *:80>\n\
DocumentRoot /var/www/html/public\n\
<Directory /var/www/html/public>\n\
AllowOverride All\n\
Require all granted\n\
</Directory>\n\
</VirtualHost>\n' > /etc/apache2/sites-available/000-default.conf
WORKDIR /var/www/html
COPY . .
RUN mkdir -p database && chown -R www-data:www-data /var/www/html
EXPOSE 80
+63 -9
View File
@@ -4,7 +4,6 @@ API REST en PHP natif (POO) pour gérer des liens de vidéos YouTube.
- PHP natif uniquement : aucun framework, aucune dépendance, aucun Composer - PHP natif uniquement : aucun framework, aucune dépendance, aucun Composer
- Stockage SQLite via PDO - Stockage SQLite via PDO
- À chaque création / modification / suppression, un snapshot complet de la table est écrit dans `data.json`
## Prérequis ## Prérequis
@@ -23,7 +22,25 @@ php -m | grep -i sqlite # doit afficher pdo_sqlite et/ou sqlite3
php -S localhost:8000 -t public php -S localhost:8000 -t public
``` ```
La base `database/api_tube.sqlite` et son schéma sont créés automatiquement au premier appel. La base `database/api_tube.sqlite` et son schéma sont créés automatiquement au premier appel (via les migrations).
## Migrations
Les migrations sont des fichiers SQL numérotés dans `database/migrations/`, appliqués une seule fois et tracés dans la table `migrations`.
Elles sont exécutées automatiquement à la connexion à la base. Pour les appliquer manuellement (hors serveur web) :
```bash
php database/migrate.php
```
Ajouter une migration :
```bash
# fichier database/migrations/002_ajout_colonne.sql
echo "ALTER TABLE links ADD COLUMN visible INTEGER DEFAULT 1;" > database/migrations/002_ajout_colonne.sql
php database/migrate.php
```
## Endpoints ## Endpoints
@@ -48,16 +65,50 @@ La base `database/api_tube.sqlite` et son schéma sont créés automatiquement a
`title` et `link_url` sont obligatoires ; `description` et `thumbnail` sont optionnels. `link_url` doit être une URL valide et unique. `title` et `link_url` sont obligatoires ; `description` et `thumbnail` sont optionnels. `link_url` doit être une URL valide et unique.
## Fichier `data.json`
Régénéré à chaque POST / PUT / DELETE : contient la liste complète des liens, formatée (pretty print).
## Tests manuels ## Tests manuels
```bash ```bash
bash tests/curl.sh bash tests/curl.sh
``` ```
## Sécurité
Éléments de sécurité implémentés dans l'API :
- **Injection SQL** : toutes les requêtes avec paramètres utilisent des requêtes préparées PDO (`prepare()` + `execute()`) dans `src/LinkRepository.php`, jamais de concaténation de variables dans le SQL.
- **Validation des entrées** : champs obligatoires vérifiés, `trim()` systématique, et validation de `link_url` via `FILTER_VALIDATE_URL` (`src/LinkController.php`).
- **Pas de fuite d'informations** : toute erreur (base, rendu) renvoie une réponse 500 générique `Erreur interne du serveur.` sans stack trace ni détail (`public/index.php`). `display_errors` n'est pas activé.
- **Exposition minimale au web** : le DocumentRoot Apache (Docker) pointe vers `public/` uniquement ; `src/`, `database/` et la base SQLite ne sont pas servis publiquement. Le `.htaccess` réécrit toute requête vers `index.php`.
- **CORS restreint** : méthodes limitées à `GET, POST, PUT, DELETE, OPTIONS` et en-têtes à `Content-Type` (`public/index.php`).
- **Base hors de l'image Docker** : `*.sqlite` est exclu via `.dockerignore` et stocké dans le volume `db-data`, pas dans les layers de l'image.
- **Typage strict** : `declare(strict_types=1)` dans tous les fichiers PHP, paramètres et retours typés.
## Docker
### Lancement
```bash
docker compose up -d --build
# API accessible sur http://localhost:8000
```
### Arrêter
```bash
docker compose down # conserve la base (volume)
docker compose down -v # supprime la base (reset complet)
```
### Persistance
La base SQLite est stockée dans le Docker volume `db-data` : elle survit aux `down`/`up` successifs. Un `down -v` la supprime ; le prochain démarrage recrée la base via les migrations.
### Rebuild après modification du code
```bash
docker compose up -d --build
```
## Arborescence ## Arborescence
``` ```
@@ -65,10 +116,13 @@ public/
index.php # point d'entrée unique index.php # point d'entrée unique
.htaccess # réécriture vers index.php .htaccess # réécriture vers index.php
src/ src/
Database.php # connexion PDO SQLite (+ création du schéma) Database.php # connexion PDO SQLite + exécution des migrations
LinkRepository.php # CRUD SQL LinkRepository.php # CRUD SQL
JsonExporter.php # écriture de data.json
LinkController.php # routage, validation, orchestration LinkController.php # routage, validation, orchestration
database/ database/
schema.sql # schéma de la table links migrate.php # runner de migrations (CLI)
migrations/ # fichiers SQL versionnés
Dockerfile # image PHP 8.4 Apache + mod_rewrite
docker-compose.yml # service web + volume db-data
.dockerignore
``` ```
+13
View File
@@ -0,0 +1,13 @@
name: api-yt-links
services:
web:
build: .
restart: unless-stopped
ports:
- "8000:80"
volumes:
- db-data:/var/www/html/database
volumes:
db-data:
-2
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
require_once __DIR__ . '/../src/Database.php'; require_once __DIR__ . '/../src/Database.php';
require_once __DIR__ . '/../src/LinkRepository.php'; require_once __DIR__ . '/../src/LinkRepository.php';
require_once __DIR__ . '/../src/JsonExporter.php';
require_once __DIR__ . '/../src/LinkController.php'; require_once __DIR__ . '/../src/LinkController.php';
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
@@ -22,7 +21,6 @@ try {
$controller = new LinkController( $controller = new LinkController(
new LinkRepository($pdo), new LinkRepository($pdo),
new JsonExporter($pdo, dirname(__DIR__) . '/data.json'),
); );
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
+15 -14
View File
@@ -8,21 +8,22 @@ final class Database
public static function connect(): PDO public static function connect(): PDO
{ {
if (self::$pdo !== null) { if (self::$pdo === null) {
return self::$pdo; self::init();
assert(self::$pdo instanceof PDO);
self::$pdo->exec((string) file_get_contents(dirname(__DIR__) . '/database/schema.sql'));
} }
$root = dirname(__DIR__);
$dsn = 'sqlite:' . $root . '/database/api_tube.sqlite';
self::$pdo = new PDO($dsn);
self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
self::$pdo->exec('PRAGMA foreign_keys = ON');
$schema = file_get_contents($root . '/database/schema.sql');
self::$pdo->exec($schema);
return self::$pdo; return self::$pdo;
} }
}
private static function init(): void
{
$root = dirname(__DIR__);
self::$pdo = new PDO('sqlite:' . $root . '/database/api_tube.sqlite');
self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
self::$pdo->exec('PRAGMA foreign_keys = ON');
}
}
-24
View File
@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
final class JsonExporter
{
private PDO $pdo;
private string $filePath;
public function __construct(PDO $pdo, string $filePath)
{
$this->pdo = $pdo;
$this->filePath = $filePath;
}
public function export(): void
{
$links = $this->pdo->query('SELECT * FROM links ORDER BY id DESC')->fetchAll();
$json = json_encode($links, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
file_put_contents($this->filePath, $json . PHP_EOL, LOCK_EX);
}
}
-4
View File
@@ -6,7 +6,6 @@ final class LinkController
{ {
public function __construct( public function __construct(
private readonly LinkRepository $repository, private readonly LinkRepository $repository,
private readonly JsonExporter $exporter,
) { ) {
} }
@@ -80,7 +79,6 @@ final class LinkController
return; return;
} }
$this->exporter->export();
$this->json($this->repository->find($id), 201); $this->json($this->repository->find($id), 201);
} }
@@ -115,7 +113,6 @@ final class LinkController
return; return;
} }
$this->exporter->export();
$this->json($this->repository->find($id)); $this->json($this->repository->find($id));
} }
@@ -128,7 +125,6 @@ final class LinkController
} }
$this->repository->delete($id); $this->repository->delete($id);
$this->exporter->export();
http_response_code(204); http_response_code(204);
} }
+1 -4
View File
@@ -34,7 +34,4 @@ echo "== GET /links/999 (introuvable -> 404) =="
curl -s -w "\n[%{http_code}]\n" "$URL/999" curl -s -w "\n[%{http_code}]\n" "$URL/999"
echo "== DELETE /links/1 (suppression) ==" echo "== DELETE /links/1 (suppression) =="
curl -s -w "\n[%{http_code}]\n" -X DELETE "$URL/1" curl -s -w "\n[%{http_code}]\n" -X DELETE "$URL/1"
echo "== data.json =="
cat data.json