post = new Post(); } public function show(): void { if (!isset($_SESSION['user_id'])) { header('Location: /login'); return; } $overlaysDir = \dirname(__DIR__, 2) . '/public/assets/overlays'; $overlayFiles = glob($overlaysDir . '/*.png'); // Map filesystem paths to URL paths the browser can load $overlays = array_map(static fn($path) => '/assets/overlays/' . basename($path), $overlayFiles); $userPosts = $this->post->findByUserId($_SESSION['user_id']); $content = __DIR__ . '/../Views/editor/index.php'; include __DIR__ . '/../Views/layouts/main.php'; } public function store(): void { header('Content-Type: application/json'); if (!isset($_SESSION['user_id'])) { http_response_code(401); echo json_encode(['error' => 'Not authenticated.']); return; } $input = json_decode(file_get_contents('php://input'), true); if (!$input) { http_response_code(400); echo json_encode(['error' => 'Invalid request.']); return; } if (!Csrf::validate($input['csrf_token'] ?? '')) { http_response_code(403); echo json_encode(['error' => 'Invalid CSRF token.']); return; } if ($this->post->countByUserId($_SESSION['user_id']) >= self::MAX_POSTS_PER_USER) { http_response_code(400); echo json_encode(['error' => 'You have reached the maximum number of posts.']); return; } if ($this->post->countAll() >= self::MAX_POSTS_TOTAL) { http_response_code(400); echo json_encode(['error' => 'The site has reached its storage limit.']); return; } $imageData = $input['image_data'] ?? ''; // Reject oversized payloads before doing any processing — base64 adds ~33% // overhead, so 10 MB base64 ≈ 7.5 MB image, which is more than enough if (\strlen($imageData) > 10 * 1024 * 1024) { http_response_code(400); echo json_encode(['error' => 'Image too large.']); return; } $overlayName = $input['overlay'] ?? ''; $overlayScale = (float) ($input['overlay_scale'] ?? 1.0); // Validate overlay exists — use basename() to prevent path traversal $overlaysDir = \dirname(__DIR__, 2) . '/public/assets/overlays'; $overlayPath = $overlaysDir . '/' . basename($overlayName); if (!file_exists($overlayPath) || !str_ends_with($overlayPath, '.png')) { http_response_code(400); echo json_encode(['error' => 'Invalid overlay.']); return; } $overlayScale = max(0.1, min(2.0, $overlayScale)); // Strip the data URL prefix (e.g. "data:image/jpeg;base64,") to get raw base64 $base64 = preg_replace('#^data:image/\w+;base64,#', '', $imageData); $decoded = base64_decode($base64, true); if ($decoded === false) { http_response_code(400); echo json_encode(['error' => 'Invalid image data.']); return; } $base = @imagecreatefromstring($decoded); if ($base === false) { http_response_code(400); echo json_encode(['error' => 'Could not process image.']); return; } $outputPath = $this->composite($base, $overlayPath, $overlayScale); if ($outputPath === null) { http_response_code(500); echo json_encode(['error' => 'Failed to save image.']); return; } // Store relative path from the web root so it works as a URL $relativePath = 'uploads/posts/' . basename($outputPath); $this->post->create($_SESSION['user_id'], $relativePath); echo json_encode(['success' => true, 'redirect' => '/editor']); } public function destroy(string $id): void { if (!isset($_SESSION['user_id'])) { header('Location: /login'); return; } if (!Csrf::validate($_POST['csrf_token'] ?? '')) { Flash::set('error', 'Invalid CSRF token.'); header('Location: /editor'); return; } $post = $this->post->findById((int) $id); // Only the post owner can delete it if (!$post || $post['user_id'] !== $_SESSION['user_id']) { Flash::set('error', 'Post not found.'); header('Location: /editor'); return; } // Delete the image file from disk $filePath = \dirname(__DIR__, 2) . '/' . $post['image_path']; if (file_exists($filePath)) { unlink($filePath); } $this->post->delete((int) $id); Flash::set('success', 'Post deleted.'); header('Location: /editor'); } /** * Composite the base image with an overlay using GD. * Returns the saved file path, or null on failure. */ private function composite(\GdImage $base, string $overlayPath, float $scale): ?string { $canvasSize = 640; // Create a square canvas and fill with white $canvas = imagecreatetruecolor($canvasSize, $canvasSize); $white = imagecolorallocate($canvas, 255, 255, 255); imagefill($canvas, 0, 0, $white); // Resize base image to cover the 640x640 canvas (center-crop) $srcW = imagesx($base); $srcH = imagesy($base); // Pick the largest square that fits inside the source image $cropSize = min($srcW, $srcH); $srcX = (int) (($srcW - $cropSize) / 2); $srcY = (int) (($srcH - $cropSize) / 2); imagecopyresampled($canvas, $base, 0, 0, $srcX, $srcY, $canvasSize, $canvasSize, $cropSize, $cropSize); // Load the overlay PNG with alpha transparency $overlay = imagecreatefrompng($overlayPath); // Enable alpha blending so transparent overlay pixels don't overwrite the base imagealphablending($canvas, true); $overlayW = imagesx($overlay); $overlayH = imagesy($overlay); $scaledW = (int) ($overlayW * $scale); $scaledH = (int) ($overlayH * $scale); // Center the overlay on the canvas $destX = (int) (($canvasSize - $scaledW) / 2); $destY = (int) (($canvasSize - $scaledH) / 2); imagecopyresampled($canvas, $overlay, $destX, $destY, 0, 0, $scaledW, $scaledH, $overlayW, $overlayH); // Save as JPEG $uploadsDir = \dirname(__DIR__, 2) . '/uploads/posts'; $filename = $_SESSION['user_id'] . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.jpg'; $path = $uploadsDir . '/' . $filename; $ok = imagejpeg($canvas, $path, 85); return $ok ? $path : null; } }