[ 'method' => 'GET', 'header' => implode("\r\n", self::DDG_BROWSER_HEADERS), 'timeout' => 10, ]]); $html = @file_get_contents($url, false, $ctx); if (!$html) return null; // The vqd token appears in the page JS in several formats depending on // DDG's current build. Try the quoted forms first (vqd="4-xxx" / // vqd='4-xxx' / vqd:"4-xxx"), then fall back to the bare form. $patterns = [ '/vqd=["\']([0-9a-zA-Z._\-]+)["\']/', // vqd="4-123..." or vqd='4-123...' '/vqd["\']?\s*[:=]\s*["\']([0-9a-zA-Z._\-]+)["\']/', // vqd:"4-123..." '/vqd=([0-9a-zA-Z._\-]+)/', // bare vqd=4-123... ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $html, $m) && !empty($m[1])) { return $m[1]; } } return null; } // GET /api/products/photo-search?q=...&page=1 public function search(Request $request) { if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::SearchStockPhotos)) { return response()->json(['error' => 'Unauthorized'], 403); } $query = trim($request->input('q', '')); $page = max(1, (int) $request->input('page', 1)); if (!$query) { return response()->json(['error' => 'Query required'], 422); } $vqd = self::getDdgVqd($query); if (!$vqd) { return response()->json(['error' => 'Could not reach image search service'], 502); } // s = offset; DDG returns ~15 results per call; page 1 = s=0, page 2 = s=15, etc. $offset = ($page - 1) * 15; $url = 'https://duckduckgo.com/i.js?' . http_build_query([ 'q' => $query, 'vqd' => $vqd, 'o' => 'json', 'p' => '1', 'f' => ',,,', 'l' => 'us-en', 's' => $offset, ]); $ctx = stream_context_create(['http' => [ 'method' => 'GET', 'header' => implode("\r\n", self::DDG_BROWSER_HEADERS), 'timeout' => 10, ]]); $raw = @file_get_contents($url, false, $ctx); if ($raw === false) { return response()->json(['error' => 'Failed to fetch image results'], 502); } $data = json_decode($raw, true); $results = $data['results'] ?? []; $photos = array_map(fn($r) => [ 'id' => md5($r['image']), // stable ID from image URL 'thumb' => $r['thumbnail'], // DDG-proxied small thumb (safe to display) 'src' => $r['image'], // actual source image URL (used for download) 'title' => $r['title'] ?? '', ], array_slice($results, 0, 15)); return response()->json([ 'success' => true, 'photos' => $photos, 'page' => $page, 'has_more' => count($results) >= 15, ]); } // POST /api/products/photo-download // body: { src: "https://..." } — the actual source image URL from DDG results public function download(Request $request) { if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::DownloadStockPhoto)) { return response()->json(['error' => 'Unauthorized'], 403); } $src = $request->input('src', ''); // SSRF guard: must be http/https and must not target private/loopback IPs $parsed = parse_url($src); $scheme = $parsed['scheme'] ?? ''; $host = strtolower($parsed['host'] ?? ''); if (!in_array($scheme, ['http', 'https']) || !$host) { return response()->json(['error' => 'Invalid URL'], 422); } // Block private/loopback ranges if (preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.0\.0\.0|::1)/i', $host)) { return response()->json(['error' => 'Forbidden URL'], 403); } $ctx = stream_context_create(['http' => [ 'method' => 'GET', 'header' => 'User-Agent: Mozilla/5.0' . "\r\n", 'timeout' => 15, ]]); $raw = @file_get_contents($src, false, $ctx); if ($raw === false || strlen($raw) < 500) { return response()->json(['error' => 'Failed to fetch image'], 502); } // Resize to max 1280x720 using PHP GD (bundled — no Intervention Image needed) $srcImg = @imagecreatefromstring($raw); if (!$srcImg) { return response()->json(['error' => 'Invalid image data'], 422); } $origW = imagesx($srcImg); $origH = imagesy($srcImg); $maxW = 1280; $maxH = 720; $ratio = min($maxW / $origW, $maxH / $origH, 1.0); // never upscale $newW = (int) round($origW * $ratio); $newH = (int) round($origH * $ratio); $dstImg = imagescale($srcImg, $newW, $newH, IMG_BILINEAR_FIXED); imagedestroy($srcImg); ob_start(); imagejpeg($dstImg, null, 85); $binary = ob_get_clean(); imagedestroy($dstImg); // Save via existing pipeline — binary string branch in uploadFileContent handles this $result = FilesMainController::uploadFileList( $binary, 'stock-photo', 'stock_photo_' . time() . '.jpg', '', [], 'ProductMarket', [], 0, 'image/jpeg' ); if (!$result || empty($result->hashkey)) { return response()->json(['error' => 'Save failed'], 500); } return response()->json([ 'success' => true, 'hashkey' => $result->hashkey, 'url' => $result->resolvedUrl(), ]); } }