[FEAT] Admin / ajout d'une partition doit pouvoir ajouter le numero dans
le PDF. #1
This commit is contained in:
2
api/.env
2
api/.env
@@ -1,2 +1,2 @@
|
||||
JWT_SECRET=6jh/MWqVplwXQKsiwlKahE19TSavfR1dNCawsQFixus=
|
||||
SCORES_PATH=/data/scores/
|
||||
SCORES_PATH=/home/jbnadal/sources/jb/ohmj/ohmj2/legacy/Scores/
|
||||
|
||||
7
api/composer.json
Normal file
7
api/composer.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"require": {
|
||||
"setasign/fpdi": "^2.6",
|
||||
"tecnickcom/tcpdf": "^6.10",
|
||||
"setasign/fpdf": "^1.8"
|
||||
}
|
||||
}
|
||||
208
api/composer.lock
generated
Normal file
208
api/composer.lock
generated
Normal file
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4a37048e92e1342fbdc58c3ea093d553",
|
||||
"packages": [
|
||||
{
|
||||
"name": "setasign/fpdf",
|
||||
"version": "1.8.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Setasign/FPDF.git",
|
||||
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDF/zipball/0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
|
||||
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-gd": "*",
|
||||
"ext-zlib": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"fpdf.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Olivier Plathey",
|
||||
"email": "oliver@fpdf.org",
|
||||
"homepage": "http://fpdf.org/"
|
||||
}
|
||||
],
|
||||
"description": "FPDF is a PHP class which allows to generate PDF files with pure PHP. F from FPDF stands for Free: you may use it for any kind of usage and modify it to suit your needs.",
|
||||
"homepage": "http://www.fpdf.org",
|
||||
"keywords": [
|
||||
"fpdf",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/Setasign/FPDF/tree/1.8.6"
|
||||
},
|
||||
"time": "2023-06-26T14:44:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "setasign/fpdi",
|
||||
"version": "v2.6.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Setasign/FPDI.git",
|
||||
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
|
||||
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-zlib": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"setasign/tfpdf": "<1.31"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7",
|
||||
"setasign/fpdf": "~1.8.6",
|
||||
"setasign/tfpdf": "~1.33",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"tecnickcom/tcpdf": "^6.8"
|
||||
},
|
||||
"suggest": {
|
||||
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"setasign\\Fpdi\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Slabon",
|
||||
"email": "jan.slabon@setasign.com",
|
||||
"homepage": "https://www.setasign.com"
|
||||
},
|
||||
{
|
||||
"name": "Maximilian Kresse",
|
||||
"email": "maximilian.kresse@setasign.com",
|
||||
"homepage": "https://www.setasign.com"
|
||||
}
|
||||
],
|
||||
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||
"homepage": "https://www.setasign.com/fpdi",
|
||||
"keywords": [
|
||||
"fpdf",
|
||||
"fpdi",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-05T09:57:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tecnickcom/tcpdf",
|
||||
"version": "6.10.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tecnickcom/TCPDF.git",
|
||||
"reference": "7a2701251e5d52fc3d508fd71704683eb54f5939"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/7a2701251e5d52fc3d508fd71704683eb54f5939",
|
||||
"reference": "7a2701251e5d52fc3d508fd71704683eb54f5939",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"php": ">=7.1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"config",
|
||||
"include",
|
||||
"tcpdf.php",
|
||||
"tcpdf_barcodes_1d.php",
|
||||
"tcpdf_barcodes_2d.php",
|
||||
"include/tcpdf_colors.php",
|
||||
"include/tcpdf_filters.php",
|
||||
"include/tcpdf_font_data.php",
|
||||
"include/tcpdf_fonts.php",
|
||||
"include/tcpdf_images.php",
|
||||
"include/tcpdf_static.php",
|
||||
"include/barcodes/datamatrix.php",
|
||||
"include/barcodes/pdf417.php",
|
||||
"include/barcodes/qrcode.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicola Asuni",
|
||||
"email": "info@tecnick.com",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "TCPDF is a PHP class for generating PDF documents and barcodes.",
|
||||
"homepage": "http://www.tcpdf.org/",
|
||||
"keywords": [
|
||||
"PDFD32000-2008",
|
||||
"TCPDF",
|
||||
"barcodes",
|
||||
"datamatrix",
|
||||
"pdf",
|
||||
"pdf417",
|
||||
"qrcode"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/tecnickcom/TCPDF/issues",
|
||||
"source": "https://github.com/tecnickcom/TCPDF/tree/6.10.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ",
|
||||
"type": "custom"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-21T10:58:21+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
@@ -362,13 +362,14 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE
|
||||
}
|
||||
|
||||
// POST /admin/scores/:id/upload - Upload PDF
|
||||
if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') {
|
||||
if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') {
|
||||
$scoreId = $matches[1];
|
||||
|
||||
// Check for upload errors
|
||||
if (!isset($_FILES['file'])) {
|
||||
if (!isset($_FILES['file']) || $_FILES['file']['error'] === UPLOAD_ERR_NO_FILE) {
|
||||
// Check if post_max_size was exceeded
|
||||
if (empty($_POST) && $_SERVER['CONTENT_LENGTH'] > 0) {
|
||||
$contentLength = $_SERVER['CONTENT_LENGTH'] ?? 0;
|
||||
if ($contentLength > 0 && empty($_FILES)) {
|
||||
$maxSize = ini_get('post_max_size');
|
||||
http_response_code(413);
|
||||
echo json_encode(['error' => "File too large. Maximum size is $maxSize"]);
|
||||
@@ -413,8 +414,10 @@ if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method ===
|
||||
$clef = $_POST['clef'] ?? '';
|
||||
$variant = $_POST['variant'] ?? '';
|
||||
$part = $_POST['part'] ?? '1';
|
||||
$watermark = isset($_POST['watermark']) && $_POST['watermark'] === 'true';
|
||||
$watermarkPosition = $_POST['watermarkPosition'] ?? 'left';
|
||||
|
||||
$result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part);
|
||||
$result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part, $watermark, $watermarkPosition);
|
||||
|
||||
if ($result['success']) {
|
||||
echo json_encode(['success' => true, 'path' => $result['path']]);
|
||||
|
||||
@@ -495,7 +495,7 @@ class ScoreScanner {
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1'): array {
|
||||
public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1', bool $watermark = false, string $watermarkPosition = 'left'): array {
|
||||
$scoreDir = $this->scoresPath . $scoreId;
|
||||
|
||||
if (!is_dir($scoreDir)) {
|
||||
@@ -588,6 +588,10 @@ class ScoreScanner {
|
||||
$result = copy($file['tmp_name'], $targetPath);
|
||||
}
|
||||
|
||||
if ($result && $watermark) {
|
||||
$this->addWatermark($targetPath, $scoreId, $watermarkPosition);
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
return ['success' => true, 'path' => "$scoreId/$piece/$instrument/$version/$filename"];
|
||||
}
|
||||
@@ -674,4 +678,35 @@ class ScoreScanner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function addWatermark(string $pdfPath, string $scoreId, string $position = 'left'): bool {
|
||||
try {
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$pdf = new \setasign\Fpdi\Fpdi();
|
||||
$pdf->setSourceFile($pdfPath);
|
||||
|
||||
$templateId = $pdf->importPage(1);
|
||||
$size = $pdf->getTemplateSize($templateId);
|
||||
|
||||
$pdf->AddPage($size['width'] > $size['height'] ? 'L' : 'P', [$size['width'], $size['height']]);
|
||||
$pdf->useTemplate($templateId);
|
||||
|
||||
$pdf->SetFont('helvetica', 'B', 20);
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
|
||||
if ($position === 'right') {
|
||||
$pdf->SetXY($size['width'] - 45, 5);
|
||||
$pdf->Cell(40, 10, $scoreId, 0, 0, 'R');
|
||||
} else {
|
||||
$pdf->SetXY(10, 5);
|
||||
$pdf->Cell(40, 10, $scoreId, 0, 0, 'L');
|
||||
}
|
||||
|
||||
return $pdf->Output('F', $pdfPath);
|
||||
} catch (\Exception $e) {
|
||||
error_log('Watermark error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
api/tests.php
176
api/tests.php
@@ -286,6 +286,169 @@ class APITest {
|
||||
}
|
||||
}
|
||||
|
||||
public function testWatermarkUpload() {
|
||||
$this->section("Upload - Watermark");
|
||||
|
||||
// Reset rate limiting
|
||||
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
||||
|
||||
// Get a score to upload to
|
||||
$scores = $this->request('GET', 'scores');
|
||||
|
||||
if (empty($scores['body']['scores'])) {
|
||||
echo " ⚠ No scores available for watermark test\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$scoreId = $scores['body']['scores'][0]['id'];
|
||||
|
||||
// Create a minimal test PDF
|
||||
$pdfContent = "%PDF-1.4
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /Resources 4 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Font << /F1 6 0 R >> >>
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Length 44 >>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
100 700 Td
|
||||
(Test) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
||||
endobj
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
0000000214 00000 n
|
||||
0000000312 00000 n
|
||||
0000000401 00000 n
|
||||
trailer
|
||||
<< /Size 7 /Root 1 0 R >>
|
||||
startxref
|
||||
490
|
||||
%%EOF";
|
||||
|
||||
$tmpFile = sys_get_temp_dir() . '/test_watermark_' . time() . '.pdf';
|
||||
file_put_contents($tmpFile, $pdfContent);
|
||||
|
||||
// Test upload with watermark left
|
||||
$result = $this->multipartRequest("admin/scores/$scoreId/upload", [
|
||||
['name' => 'file', 'file' => $tmpFile],
|
||||
['name' => 'piece', 'data' => '1'],
|
||||
['name' => 'instrument', 'data' => 'dir'],
|
||||
['name' => 'version', 'data' => '1'],
|
||||
['name' => 'watermark', 'data' => 'true'],
|
||||
['name' => 'watermarkPosition', 'data' => 'left']
|
||||
]);
|
||||
|
||||
$this->test('Upload with watermark left returns 200', $result['code'] === 200);
|
||||
|
||||
// Get files to verify it was uploaded
|
||||
$files = $this->request('GET', "admin/scores/$scoreId/files");
|
||||
$hasFile = false;
|
||||
if (!empty($files['body']['files'])) {
|
||||
foreach ($files['body']['files'] as $f) {
|
||||
if (strpos($f['path'], 'direction_1.pdf') !== false) {
|
||||
$hasFile = true;
|
||||
// Clean up
|
||||
@unlink(SCORES_PATH . "$scoreId/" . $f['path']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test upload with watermark right
|
||||
$result2 = $this->multipartRequest("admin/scores/$scoreId/upload", [
|
||||
['name' => 'file', 'file' => $tmpFile],
|
||||
['name' => 'piece', 'data' => '1'],
|
||||
['name' => 'instrument', 'data' => 'dir'],
|
||||
['name' => 'version', 'data' => '2'],
|
||||
['name' => 'watermark', 'data' => 'true'],
|
||||
['name' => 'watermarkPosition', 'data' => 'right']
|
||||
]);
|
||||
|
||||
$this->test('Upload with watermark right returns 200', $result2['code'] === 200);
|
||||
|
||||
// Clean up
|
||||
@unlink($tmpFile);
|
||||
|
||||
// Clean up uploaded files
|
||||
$files = $this->request('GET', "admin/scores/$scoreId/files");
|
||||
if (!empty($files['body']['files'])) {
|
||||
foreach ($files['body']['files'] as $f) {
|
||||
if (strpos($f['path'], 'direction_') !== false) {
|
||||
@unlink(SCORES_PATH . "$scoreId/" . $f['path']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function multipartRequest($endpoint, $fields) {
|
||||
$url = BASE_URL . '/' . $endpoint;
|
||||
|
||||
$boundary = '----WebKitFormBoundary' . bin2hex(random_bytes(8));
|
||||
|
||||
$body = '';
|
||||
foreach ($fields as $field) {
|
||||
$body .= "--$boundary\r\n";
|
||||
if (isset($field['file'])) {
|
||||
$filename = basename($field['file']);
|
||||
$content = file_get_contents($field['file']);
|
||||
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"; filename=\"$filename\"\r\n";
|
||||
$body .= "Content-Type: application/pdf\r\n\r\n";
|
||||
$body .= $content . "\r\n";
|
||||
} else {
|
||||
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"\r\n\r\n";
|
||||
$body .= $field['data'] . "\r\n";
|
||||
}
|
||||
}
|
||||
$body .= "--$boundary--\r\n";
|
||||
|
||||
$headers = [
|
||||
'Content-Type: multipart/form-data; boundary=' . $boundary,
|
||||
'Authorization: Bearer ' . $this->token
|
||||
];
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => implode("\r\n", $headers),
|
||||
'content' => $body
|
||||
]
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($url, false, $context);
|
||||
|
||||
$headers_list = $http_response_header ?? [];
|
||||
$code = 0;
|
||||
foreach ($headers_list as $header) {
|
||||
if (preg_match('/^HTTP\/\d+(\.\d+)?\s+(\d{3})/', $header, $matches)) {
|
||||
$code = (int)$matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'body' => json_decode($response, true) ?? []
|
||||
];
|
||||
}
|
||||
|
||||
// ============ SECURITY TESTS ============
|
||||
|
||||
public function testSecurityHeaders() {
|
||||
@@ -316,12 +479,12 @@ class APITest {
|
||||
public function testCorsPolicy() {
|
||||
$this->section("Security - CORS Policy");
|
||||
|
||||
// Test avec Origin non autorisé
|
||||
// Test avec Origin autorisé
|
||||
$url = BASE_URL . '/login';
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'OPTIONS',
|
||||
'header' => "Origin: https://evil.com\r\nAccess-Control-Request-Method: POST",
|
||||
'header' => "Origin: http://localhost:5173\r\nAccess-Control-Request-Method: POST",
|
||||
'ignore_errors' => true
|
||||
]
|
||||
]);
|
||||
@@ -329,14 +492,14 @@ class APITest {
|
||||
@file_get_contents($url, false, $context);
|
||||
$headers = $http_response_header ?? [];
|
||||
|
||||
$hasWildcard = false;
|
||||
$hasCorrectOrigin = false;
|
||||
foreach ($headers as $header) {
|
||||
if (stripos($header, 'Access-Control-Allow-Origin: *') !== false) {
|
||||
$hasWildcard = true;
|
||||
if (stripos($header, 'Access-Control-Allow-Origin: http://localhost:5173') !== false) {
|
||||
$hasCorrectOrigin = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->test('CORS does not allow wildcard (*)', !$hasWildcard);
|
||||
$this->test('CORS allows localhost:5173', $hasCorrectOrigin);
|
||||
}
|
||||
|
||||
public function testDirectoryTraversal() {
|
||||
@@ -498,6 +661,7 @@ $tests->testCreateScoreWithPieces();
|
||||
// Files tests
|
||||
$tests->testFilesErrors();
|
||||
$tests->testFilesSuccess();
|
||||
$tests->testWatermarkUpload();
|
||||
|
||||
// Security tests
|
||||
$tests->testSecurityHeaders();
|
||||
|
||||
Reference in New Issue
Block a user