diff --git a/TODO.md b/TODO.md index e7626de..f8f759c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,24 @@ # TODO +## Move Restrictions d'accès aux fichiers to acces.php + +- [x] Remove fieldset from templates/admin/contenus.php +- [x] Add fieldset to templates/admin/acces.php +- [x] Load $siteSettings in admin/acces.php controller +- [x] Update redirect in settings.php for formulaire_restrictions → /admin/acces.php + +## Fix PeerTube upload — Google-resumable protocol adherence + +- [x] Use Location header from init response (not reconstruct URL from JSON body) +- [x] Switch chunk method from PUT → PATCH (Google-resumable variant) +- [x] Use actual file MIME type in chunk Content-Type (not application/octet-stream) +- [x] Ensure chunk size is multiple of 256 KB +- [x] Add PATCH/HEAD methods to httpRequest() +- [x] Add CURLOPT_HEADERFUNCTION to capture response headers +- [x] Disable CURLOPT_FOLLOWLOCATION to preserve Location header +- [x] Add cancelUpload() helper for Delete-on-error cleanup +- [ ] Test with actual PeerTube instance + ## HTMX Toast Feedback for Settings Checkboxes (contenus.php) - [x] Add `hx-target` response divs to the three fieldsets in contenus.php diff --git a/app/public/admin/acces.php b/app/public/admin/acces.php index 83999a1..2f1c645 100644 --- a/app/public/admin/acces.php +++ b/app/public/admin/acces.php @@ -15,8 +15,12 @@ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' $baseUrl = $protocol . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost'); // ── Demandes d'accès aux fichiers ───────────────────────────────────────────── +require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/Controllers/FileAccessController.php'; +$db = new Database(); +$siteSettings = $db->getAllSettings(); + $controller = FileAccessController::create(); $vars = $controller->handle(); extract($vars); diff --git a/app/public/admin/actions/settings.php b/app/public/admin/actions/settings.php index 2d00c85..f373f2a 100644 --- a/app/public/admin/actions/settings.php +++ b/app/public/admin/actions/settings.php @@ -162,8 +162,10 @@ $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // Redirect back to wherever the form came from, defaulting to parametres $redirect = '/admin/parametres.php'; -if (in_array($section, ['formulaire_restrictions', 'formulaire_acces', 'objet_types'], true)) { +if (in_array($section, ['formulaire_acces', 'objet_types'], true)) { $redirect = '/admin/contenus.php'; +} elseif ($section === 'formulaire_restrictions') { + $redirect = '/admin/acces.php'; } header('Location: ' . $redirect); exit; diff --git a/app/src/PeerTubeService.php b/app/src/PeerTubeService.php index 87193f6..d451ddc 100644 --- a/app/src/PeerTubeService.php +++ b/app/src/PeerTubeService.php @@ -17,10 +17,11 @@ * instance's /api/v1/oauth-clients/local endpoint and cached in-memory * per process lifetime. * - * Upload uses the resumable protocol: - * POST /api/v1/videos/upload-resumable — init - * PUT /api/v1/videos/upload-resumable — send chunk - * DELETE /api/v1/videos/upload-resumable — cancel + * Upload uses the Google-resumable protocol: + * POST /api/v1/videos/upload-resumable — init (→ Location header with upload URL token) + * PATCH — send chunk + * HEAD — resume check + * DELETE — cancel */ class PeerTubeService { @@ -205,33 +206,31 @@ class PeerTubeService 'Content-Length: ' . strlen($initBody), ]); - $initJson = json_decode($initResponse['body'], true); - if ($initResponse['status'] < 200 || $initResponse['status'] >= 300) { + // PeerTube Google-resumable returns the upload session URL in the Location header. + // The JSON body contains video.id (upload session ID, not final video ID). + $chunkUrl = $initResponse['headers']['location'] ?? $initResponse['headers']['Location'] ?? null; + if (!$chunkUrl) { + $initJson = json_decode($initResponse['body'], true); $msg = $initJson['error'] ?? $initJson['detail'] ?? $initResponse['body']; - throw new \RuntimeException('PeerTube upload init failed (' . $initResponse['status'] . '): ' . $msg); + throw new \RuntimeException('PeerTube upload init: no Location header (' . $initResponse['status'] . '): ' . $msg); } - // Small files may complete in one shot - $shortUuid = $initJson['video']['shortUUID'] ?? $initJson['video']['uuid'] ?? null; - if ($shortUuid && !isset($initJson['video']['id'])) { - $watchUrl = rtrim($baseUrl, '/') . '/videos/watch/' . $shortUuid; - return ['uuid' => $shortUuid, 'watchUrl' => $watchUrl]; + // Relative Location? Make it absolute. + if (!str_starts_with($chunkUrl, 'http')) { + $chunkUrl = rtrim($baseUrl, '/') . $chunkUrl; } - $uploadId = $initJson['video']['id'] ?? null; - if (!$uploadId) { - throw new \RuntimeException('PeerTube upload init: no upload session returned.'); - } - - // ── Step 2: Send chunks ── - $chunkUrl = $baseUrl . '/api/v1/videos/upload-resumable?upload_id=' . urlencode((string)$uploadId); - + // ── Step 2: Send chunks via PATCH (Google-resumable variant) ── $fh = fopen($filePath, 'rb'); if (!$fh) { throw new \RuntimeException('Cannot open file for resumable upload.'); } - $chunkSize = 4 * 1024 * 1024; + // Chunk size: 1 MB, must be a multiple of 256 KB (262144 bytes). + $chunkSizeBase = 256 * 1024; + $chunkSize = max($chunkSizeBase, min(4 * 1024 * 1024, (int)ceil($fileSize / 100))); + $chunkSize = (int)ceil($chunkSize / $chunkSizeBase) * $chunkSizeBase; + $offset = 0; $lastResponse = null; @@ -240,9 +239,9 @@ class PeerTubeService $chunkLen = strlen($chunk); $end = $offset + $chunkLen - 1; - $resp = self::httpRequest($chunkUrl, 'PUT', $chunk, [ + $resp = self::httpRequest($chunkUrl, 'PATCH', $chunk, [ 'Authorization: Bearer ' . $token, - 'Content-Type: application/octet-stream', + 'Content-Type: ' . $mimeType, 'Content-Range: bytes ' . $offset . '-' . $end . '/' . $fileSize, 'Content-Length: ' . $chunkLen, ], 600); @@ -256,13 +255,12 @@ class PeerTubeService break; } } elseif ($resp['status'] === 308) { + // Resume Incomplete — chunk accepted, continue continue; } else { fclose($fh); try { - self::httpRequest($chunkUrl, 'DELETE', '', [ - 'Authorization: Bearer ' . $token, 'Content-Length: 0', - ], 10); + self::cancelUpload($chunkUrl, $token); } catch (\Throwable $e) { /* ignore */ } $errJson = json_decode($resp['body'], true); $msg = $errJson['error'] ?? $errJson['detail'] ?? $resp['body']; @@ -436,10 +434,25 @@ class PeerTubeService // HTTP helper // ------------------------------------------------------------------------- + /** + * Cancel a resumable upload session. + */ + private static function cancelUpload(string $chunkUrl, string $token): void + { + self::httpRequest($chunkUrl, 'DELETE', '', [ + 'Authorization: Bearer ' . $token, + 'Content-Length: 0', + ], 10); + } + + // ------------------------------------------------------------------------- + // HTTP helper + // ------------------------------------------------------------------------- + /** * Minimal cURL HTTP helper. * - * @return array{status:int, body:string} + * @return array{status:int, body:string, headers:array} */ public static function httpRequest( string $url, @@ -457,31 +470,47 @@ class PeerTubeService CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => 15, - CURLOPT_FOLLOWLOCATION => true, + CURLOPT_FOLLOWLOCATION => false, // Must be false to capture Location header CURLOPT_MAXREDIRS => 3, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HTTPHEADER => $headers, + CURLOPT_HEADERFUNCTION => function ($ch, $headerLine) use (&$responseHeaders) { + $len = strlen($headerLine); + $parts = explode(':', $headerLine, 2); + if (count($parts) === 2) { + $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); + } + return $len; + }, ]); + $responseHeaders = []; + if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } elseif ($method === 'PUT') { curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } elseif ($method === 'PATCH') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } elseif ($method === 'DELETE') { curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + } elseif ($method === 'HEAD') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'HEAD'); + curl_setopt($ch, CURLOPT_NOBODY, true); } $responseBody = curl_exec($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); - if ($responseBody === false) { + if ($responseBody === false && $method !== 'HEAD') { throw new \RuntimeException('Erreur réseau PeerTube : ' . $error); } - return ['status' => $status, 'body' => (string)$responseBody]; + return ['status' => $status, 'body' => (string)$responseBody, 'headers' => $responseHeaders]; } } diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index dfe07ce..52a63cb 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -844,6 +844,32 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: usmyqlwr 6acee66b "cleanup: merge SMTP fields into single fieldset, rename to Emails" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: usmyqlwr 6acee66b "cleanup: merge SMTP fields into single fieldset, rename to Emails" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: omwsuqoy 6cde5a47 "move Restrictions d'accès aux fichiers from contenus.php to acces.php" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: omwsuqoy 5886b400 "move Restrictions d'accès aux fichiers from contenus.php to acces.php" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: omwsuqoy 5886b400 "move Restrictions d'accès aux fichiers from contenus.php to acces.php" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: usxlqwxk 4dda0271 "Cleanup acces fichier section" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: usxlqwxk 3cd56fd1 "Cleanup acces fichier section" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?> @@ -970,7 +996,39 @@ DEMANDES D'ACCÈS AUX FICHIERS ══════════════════════════════════════════════════════════════ -->
-

Demandes d'accès aux fichiers

+ +

Fichiers

+ +

Restrictions d'accès aux fichiers

+ +
+ Paramètre global + +
+ + + + +
+ +
+
+ +

Demandes d'accès aux fichiers

diff --git a/app/templates/admin/contenus.php b/app/templates/admin/contenus.php index 0f1b5b5..af9a969 100644 --- a/app/templates/admin/contenus.php +++ b/app/templates/admin/contenus.php @@ -85,33 +85,6 @@

Paramètres du Formulaire

-
- Restrictions d'accès aux fichiers - -
- - - - -
- -
-
-
Degré d'ouverture

Options de visibilité disponibles dans le formulaire d'ajout de TFE.

diff --git a/app/templates/admin/parametres.php b/app/templates/admin/parametres.php index 9fef6b5..39240a5 100644 --- a/app/templates/admin/parametres.php +++ b/app/templates/admin/parametres.php @@ -78,7 +78,9 @@ -
+
+ + Paramètres email
Reçoit les notifications (demandes d'accès, etc.). Si vide, utilise l'adresse d'expédition.
-
+
@@ -182,10 +184,6 @@ Intégration avec une instance PeerTube pour l'hébergement des vidéos et fichiers audio. Les fichiers sont uploadés via l'API PeerTube et intégrés comme lecteurs embarqués sur la page du TFE.

-

- ⚠ L'activation nécessite un quota d'upload suffisant sur l'instance PeerTube. - Laissez désactivé jusqu'à obtention du quota. -

@@ -213,12 +211,14 @@ + +
+ Paramètres Peertube

ℹ L'authentification PeerTube utilise les mêmes identifiants que le relay SMTP configuré ci-dessus.

-
>Privée
-
+