[ 'name' => 'User ID', 'required' => true, 'exampleValue' => '2813217452', // <3 'title' => 'The numeric user ID from Weibo' ], ]]; private const GUEST_COOKIE_CACHE_KEY_PREFIX = 'WeiboPicsBridge_Guest_'; // Prefix for cache key private const GUEST_COOKIE_TTL = 21600; // 6 hours for guest cookies private $feedTitleName = ''; public function getIcon() { return 'https://weibo.com/favicon.ico'; } public function getName() { if (empty($this->feedTitleName)) { return parent::getName(); } return $this->feedTitleName; } private function getGuestCookieCacheKey() { // Make cache key unique per bridge instance if needed, though guest cookies are general. // For simplicity, using a general key. return self::GUEST_COOKIE_CACHE_KEY_PREFIX . 'cookies'; } private function getGuestCookiesOrLogin() { $cacheKey = $this->getGuestCookieCacheKey(); $cachedCookies = $this->loadCacheValue($cacheKey, self::GUEST_COOKIE_TTL); if ($cachedCookies !== null && is_array($cachedCookies) && !empty($cachedCookies)) { $this->logger->debug('Using cached guest cookies.'); return $cachedCookies; } $this->logger->debug('No valid cached guest cookies found. Attempting guest login.'); // --- Step 1: genvisitor --- $genvisitorUrl = 'https://passport.weibo.com/visitor/genvisitor'; $fpData = [ 'os' => '1', 'browser' => 'Gecko109,0,0,0', 'fonts' => 'undefined', 'screenInfo' => '1920*1080*24', 'plugins' => '' ]; $postFields = http_build_query([ 'cb' => 'gen_callback', 'fp' => json_encode($fpData) ]); $genvisitorHeaders = [ 'Referer: https://passport.weibo.com/visitor/visitor?entry=miniblog&page=request', 'Content-Type: application/x-www-form-urlencoded' ]; $genvisitorCurlOptions = [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postFields ]; /** @var Response|string $genvisitorResponseObj */ $genvisitorResponseObj = getContents($genvisitorUrl, $genvisitorHeaders, $genvisitorCurlOptions, true); if (!$genvisitorResponseObj instanceof Response) { $this->logger->warning('genvisitor request failed to return a Response object.'); return null; } $genvisitorResponseBody = $genvisitorResponseObj->getBody(); if (empty($genvisitorResponseBody)) { $this->logger->warning('genvisitor response body is empty.'); return null; } if (!preg_match('/gen_callback\((.*)\);/s', $genvisitorResponseBody, $matches)) { $this->logger->warning('Could not parse genvisitor JSONP response: ' . substr($genvisitorResponseBody, 0, 200)); return null; } $jsonData = json_decode($matches[1], true); if (!isset($jsonData['data']['tid']) || empty($jsonData['data']['tid'])) { $this->logger->warning('TID not found in genvisitor response: ' . $matches[1]); return null; } $tid = $jsonData['data']['tid']; $newTid = isset($jsonData['data']['new_tid']) && $jsonData['data']['new_tid']; $confidence = $jsonData['data']['confidence'] ?? 100; $this->logger->debug('genvisitor successful. TID: ' . $tid); // --- Step 2: visitor --- $wParam = $newTid ? '3' : '2'; $cParam = sprintf('%03d', $confidence); $randParam = mt_rand() / mt_getrandmax(); $visitorUrl = sprintf( 'https://passport.weibo.com/visitor/visitor?a=incarnate&t=%s&w=%s&c=%s&gc=&cb=cross_domain&from=weibo&_rand=%s', urlencode($tid), urlencode($wParam), urlencode($cParam), $randParam ); $visitorHeaders = [ ]; // No specific cURL options needed for this GET request beyond headers /** @var Response|string $visitorResponseObj */ $visitorResponseObj = getContents($visitorUrl, $visitorHeaders, [], true); if (!$visitorResponseObj instanceof Response) { $this->logger->warning('Visitor request failed to return a Response object.'); return null; } if ($visitorResponseObj->getCode() >= 300) { $this->logger->warning('Visitor request failed with HTTP code: ' . $visitorResponseObj->getCode()); return null; } $this->logger->debug('Visitor request successful.'); $cookiesToStore = []; // Extract Set-Cookie headers from visitor response $vistorHeaders = $visitorResponseObj->getHeaders(); $visitorSetCookieHeaders = $vistorHeaders['set-cookie'] ?? []; if (is_array($visitorSetCookieHeaders)) { foreach ($visitorSetCookieHeaders as $headerValue) { if (preg_match('/^([^=]+=[^;]+)/i', $headerValue, $cookieMatch)) { $cookiesToStore[] = $cookieMatch[1]; } } } // Also check cookies potentially set by genvisitor call if any were relevant $genvisitorHeaders = $genvisitorResponseObj->getHeaders(); $genvisitorSetCookieHeaders = $genvisitorHeaders['set-cookie'] ?? []; if (is_array($genvisitorSetCookieHeaders)) { foreach ($genvisitorSetCookieHeaders as $headerValue) { if (preg_match('/^([^=]+=[^;]+)/i', $headerValue, $cookieMatch)) { // Avoid duplicates if same cookie name was set by both $isDuplicate = false; $newCookieName = explode('=', $cookieMatch[1])[0]; foreach($cookiesToStore as $existingCookie) { if (explode('=', $existingCookie)[0] === $newCookieName) { $isDuplicate = true; break; } } if (!$isDuplicate) { $cookiesToStore[] = $cookieMatch[1]; } } } } if (empty($cookiesToStore)) { $this->logger->warning('No cookies were set by the visitor login flow.'); return null; } $this->saveCacheValue($cacheKey, $cookiesToStore, self::GUEST_COOKIE_TTL); $this->logger->info('Guest cookies obtained and cached: ' . implode('; ', $cookiesToStore)); return $cookiesToStore; } public function collectData() { $uid = $this->getInput('uid'); if (!is_numeric($uid)) { returnClientError('User ID must be numeric.'); } $guestCookiesArray = $this->getGuestCookiesOrLogin(); if ($guestCookiesArray === null) { returnServerError('Failed to obtain Weibo guest cookies. The API might be inaccessible or login failed.'); } $commonRequestHeaders = [ 'X-Requested-With: XMLHttpRequest', 'Referer: https://weibo.com/u/' . $uid, 'Cookie: ' . implode('; ', $guestCookiesArray) ]; $imageWallUrl = 'https://weibo.com/ajax/profile/getImageWall?uid=' . $uid . '&sinceid=0&has_album=true'; $maxAttempts = 3; $attempt = 0; $imageWallResponseObj = null; // Initialize do { $attempt++; if ($attempt > 1) { $this->logger->debug("Retrying getImageWall for UID {$uid}, attempt {$attempt}/{$maxAttempts}."); // Optional: consider a small delay, e.g., sleep(1); } /** @var Response|string|false $currentAttemptResponseObj */ $currentAttemptResponseObj = getContents($imageWallUrl, $commonRequestHeaders, [], true); if ($currentAttemptResponseObj instanceof Response) { $httpCode = $currentAttemptResponseObj->getCode(); if ($httpCode === 500) { $imageWallResponseObj = $currentAttemptResponseObj; // Store the 500 response for now if ($attempt < $maxAttempts) { $this->logger->info("Weibo getImageWall API for UID {$uid} returned HTTP 500 on attempt {$attempt}/{$maxAttempts}. Retrying..."); continue; // Go to next attempt } // Max attempts reached for 500, loop will terminate, and this 500 response will be handled below $this->logger->warning("Weibo getImageWall API for UID {$uid} still returned HTTP 500 after {$maxAttempts} attempts."); break; } // Not a 500 error, so this is our final response from the loop $imageWallResponseObj = $currentAttemptResponseObj; break; } elseif ($currentAttemptResponseObj === false || $currentAttemptResponseObj === null) { // Critical getContents failure $imageWallResponseObj = $currentAttemptResponseObj; // Store false/null break; } else { // Unexpected response type from getContents $imageWallResponseObj = $currentAttemptResponseObj; // Store unexpected type break; } } while ($attempt < $maxAttempts); $imageWallJson = null; // Default to empty, successful-like structure for imageWallData $imageWallData = ['ok' => 1, 'data' => ['list' => [], 'user' => []]]; if ($imageWallResponseObj instanceof Response) { $httpCode = $imageWallResponseObj->getCode(); if ($httpCode === 500) { // This means all retries (if any) resulted in 500 $this->logger->info('Weibo getImageWall API for UID ' . $uid . ' returned HTTP 500 (after all retries). Proceeding with empty data for image wall.'); // $imageWallData is already set to an empty valid structure, $imageWallJson remains null. } elseif ($httpCode >= 300) { // Other HTTP errors (excluding 500 which is handled above) returnServerError('Weibo getImageWall API request failed for UID ' . $uid . '. HTTP Code: ' . $httpCode . '. Body: ' . substr($imageWallResponseObj->getBody(), 0, 200)); } else { // Successful response (2xx) $imageWallJson = $imageWallResponseObj->getBody(); $decodedData = json_decode($imageWallJson, true); if (json_last_error() !== JSON_ERROR_NONE) { returnServerError('Failed to decode JSON from image wall for UID ' . $uid . ': ' . json_last_error_msg() . ' Response: ' . substr($imageWallJson, 0, 500)); } if (!isset($decodedData['ok']) || $decodedData['ok'] != 1) { $msg = $decodedData['msg'] ?? 'Unknown error from Weibo getImageWall API.'; if (stripos($msg, 'login') !== false || ($imageWallJson && stripos($imageWallJson, 'login.sina.com.cn') !== false)) { $this->logger->warning('Weibo API redirected to login for getImageWall, UID ' . $uid . '. Guest cookies might be invalid. Clearing cache.'); $this->saveCacheValue($this->getGuestCookieCacheKey(), null, 0); // Invalidate cache returnServerError('Weibo getImageWall API requires login for UID ' . $uid . '. Guest session failed or expired. Message: ' . $msg); } returnServerError('Invalid response from Weibo getImageWall API (after 2xx) for UID ' . $uid . ': ' . $msg . ' Raw: ' . substr($imageWallJson, 0, 200)); } $imageWallData = $decodedData; // Use the successfully decoded data } } elseif ($imageWallResponseObj === false || $imageWallResponseObj === null) { // getContents itself failed critically (e.g., cURL error, DNS issue) returnServerError('Could not request image wall (getContents failed critically) for UID ' . $uid . ': ' . $imageWallUrl); } else { // Should not happen if getContents with true as 4th param behaves as expected (Response or false/null) $this->logger->warning('Unexpected response type from getContents for imageWall, UID ' . $uid . ': ' . gettype($imageWallResponseObj)); returnServerError('Unexpected response from getContents for imageWall API for UID ' . $uid); } $postsToFetchDetails = []; if (isset($imageWallData['data']['list']) && is_array($imageWallData['data']['list'])) { $processedMids = []; foreach ($imageWallData['data']['list'] as $postSummary) { if (isset($postSummary['mid']) && !empty($postSummary['mid'])) { if (!in_array($postSummary['mid'], $processedMids)) { $processedMids[] = $postSummary['mid']; $postsToFetchDetails[] = $postSummary['mid']; } } } } else { $this->logger->debug('Image wall list is not an array or not set, or empty. Response: ' . $imageWallJson); } if (empty($postsToFetchDetails)) { $this->logger->debug('No posts found in image wall for UID: ' . $uid); } $feedTitleName = 'User ' . $uid; if (isset($imageWallData['data']['user']['screen_name'])) { $feedTitleName = $imageWallData['data']['user']['screen_name']; } elseif (isset($imageWallData['data']['list'][0]['user']['screen_name'])) { // If list not empty $feedTitleName = $imageWallData['data']['list'][0]['user']['screen_name']; } else { $userInfoUrl = 'https://weibo.com/ajax/profile/info?uid=' . $uid; $userInfoJson = getContents($userInfoUrl, $commonRequestHeaders); if ($userInfoJson) { $userInfoData = json_decode($userInfoJson, true); if (isset($userInfoData['data']['user']['screen_name'])) { $feedTitleName = $userInfoData['data']['user']['screen_name']; } } } $this->feedTitleName = $feedTitleName . ' - Weibo Pictures'; foreach (array_slice($postsToFetchDetails, 0, 15) as $mid) { $postDetailUrl = 'https://weibo.com/ajax/statuses/show?id=' . $mid; $postJson = getContents($postDetailUrl, $commonRequestHeaders); if ($postJson === false || $postJson === null) { $this->logger->warning('Could not request post details for MID: ' . $mid); continue; } $postData = json_decode($postJson, true); if (json_last_error() !== JSON_ERROR_NONE) { $this->logger->warning('Failed to decode JSON for post MID ' . $mid . ': ' . json_last_error_msg() . ' Raw: ' . substr($postJson, 0, 200)); continue; } if (!isset($postData['ok']) || $postData['ok'] != 1) { $this->logger->warning('Post detail API error for MID ' . $mid . ': ' . ($postData['msg'] ?? 'Unknown error')); continue; } $mblogid = $postData['mblogid'] ?? $mid; $postUser = $postData['user'] ?? null; $postUid = $postUser['idstr'] ?? $uid; $postLink = 'https://weibo.com/' . $postUid . '/' . $mblogid; $titleText = $postData['text_raw'] ?? 'Weibo Post ' . $mid; $title = mb_strlen($titleText) > 70 ? mb_substr($titleText, 0, 70, 'UTF-8') . '...' : $titleText; if (empty(trim($title))) $title = 'Weibo Image Post ' . $mid; $htmlContent = '

' . nl2br(htmlspecialchars($postData['text_raw'] ?? '')) . '

'; $timestamp = isset($postData['created_at']) ? strtotime($postData['created_at']) : time(); $imageHtml = ''; if (isset($postData['pic_ids']) && is_array($postData['pic_ids']) && isset($postData['pic_infos']) && is_array($postData['pic_infos'])) { foreach ($postData['pic_ids'] as $pic_id) { if (isset($postData['pic_infos'][$pic_id]['largest']['url'])) { $imageUrl = $postData['pic_infos'][$pic_id]['largest']['url']; if (strpos($imageUrl, 'http:') === 0) $imageUrl = 'https:' . substr($imageUrl, 5); $imageHtml .= '
' . htmlspecialchars($imageUrl) . '
'; } } } elseif (isset($postData['page_info']['type'])) { $pageInfo = $postData['page_info']; $imgUrlKey = null; if (isset($pageInfo['page_pic']['url'])) $imgUrlKey = $pageInfo['page_pic']['url']; elseif (isset($pageInfo['media_info']['video_poster'])) $imgUrlKey = $pageInfo['media_info']['video_poster']; if ($imgUrlKey) { $imageUrl = $imgUrlKey; if (strpos($imageUrl, 'http:') === 0) $imageUrl = 'https:' . substr($imageUrl, 5); $imageHtml .= '
Cover Image: ' . htmlspecialchars($imageUrl) . '
'; } } if (empty($imageHtml)) { $this->logger->debug('Post MID ' . $mid . ' had no extractable images in detail view.'); continue; } $htmlContent .= $imageHtml; $item = []; $item['uri'] = $postLink; $item['title'] = $title; $item['content'] = $htmlContent; $item['timestamp'] = $timestamp; $item['uid'] = $mid; $item['author'] = $postUser['screen_name'] ?? $feedTitleName; $this->items[] = $item; if (count($this->items) >= 10) break; } } }