diff --git a/BSICertBridge.php b/BSICertBridge.php new file mode 100644 index 0000000..7fd6451 --- /dev/null +++ b/BSICertBridge.php @@ -0,0 +1,45 @@ +find('tbody > tr') as $element) { + + if (count($this->items) >= 10) { + break; + } + + $title_cell = $element->find('td', 1); + $article_title = trim($title_cell->find('a', 0)->plaintext); + $article_uri = self::URI . trim($title_cell->find('a', 0)->href); + $article_content = trim(substr($title_cell->plaintext, strlen($article_title))); + $article_timestamp = strtotime($element->find('td', 2)->plaintext); + + // Store article in items array + if (!empty($article_title)) { + $item = array(); + $item['uri'] = $article_uri; + $item['title'] = $article_title; + $item['content'] = $article_content; + $item['timestamp'] = $article_timestamp; + $this->items[] = $item; + } + } + } +} diff --git a/CUIIBridge.php b/CUIIBridge.php new file mode 100644 index 0000000..6d62b07 --- /dev/null +++ b/CUIIBridge.php @@ -0,0 +1,72 @@ +find('.card') as $card) { + $item = array(); + + $title = $card->find('span[itemprop=text]', 0); + if ($title) { + $item['title'] = $title->plaintext; + + $pattern = '/vom ([0-9]{1,2}\\. [a-zA-ZäöüÄÖÜß]+ [0-9]{4})/'; + preg_match($pattern, $item['title'], $matches); + if (!empty($matches)) { + $date_parts = explode(' ', $matches[1]); + $date_parts[1] = $this->translateMonth($date_parts[1]); + $english_date = implode(' ', $date_parts); + $date = DateTime::createFromFormat('d. F Y', $english_date); + if ($date !== false) { + $date->setTime(0, 0); + $item['timestamp'] = $date->getTimestamp(); + } + } + } + + $pdf_link = $card->find('p.pdf a', 0); + if ($pdf_link) { + $item['uri'] = 'https://cuii.info' . $pdf_link->href; + $item['uid'] = $item['uri']; + $item['content'] = 'Empfehlung zum Download als PDF'; + } else { + $item['content'] = 'Empfehlung zum Download als PDF'; + } + + $this->items[] = $item; + } + } + + private function translateMonth($month) + { + $months = array( + 'Januar' => 'January', + 'Februar' => 'February', + 'März' => 'March', + 'April' => 'April', + 'Mai' => 'May', + 'Juni' => 'June', + 'Juli' => 'July', + 'August' => 'August', + 'September' => 'September', + 'Oktober' => 'October', + 'November' => 'November', + 'Dezember' => 'December' + ); + return $months[$month] ?? $month; + } +} diff --git a/CemuReleasesBridge.php b/CemuReleasesBridge.php new file mode 100644 index 0000000..67f2de1 --- /dev/null +++ b/CemuReleasesBridge.php @@ -0,0 +1,50 @@ +find('div[class=col-sm-12 well]') as $element) { + + if(count($this->items) >= 10) { + break; + } + + $title_array = explode('|', $element->find('h2.changelog', 0)->innertext); + $title_array_len = count($title_array); + $article_title = trim(strip_tags($title_array[0])); + + $article_content = ''; + if ($title_array_len >= 3) { + $article_content .= str_replace('= 4) { + $article_content .= ' | ' . $title_array[3]; + } + $article_content .= $element->find('ul', 0); + + $article_timestamp = strtotime(strip_tags($title_array[1])); + + // Store article in items array + if (!empty($article_title)) { + $item = array(); + $item['uri'] = $pageUrl; + $item['title'] = $article_title; + $item['content'] = $article_content; + $item['timestamp'] = $article_timestamp; + $item['uid'] = $article_title; + $this->items[] = $item; + } + } + } +} diff --git a/CosppiBridge.php b/CosppiBridge.php new file mode 100644 index 0000000..619a189 --- /dev/null +++ b/CosppiBridge.php @@ -0,0 +1,155 @@ + [ + 'sort' => [ + 'name' => 'Sort by', + 'type' => 'list', + 'required' => true, + 'values' => [ + 'Latest' => 'new', + 'Popularity' => 'rank' + ] + ] + ], + 'By Search Query' => [ + 'query' => [ + 'name' => 'Query', + 'title' => 'See https://cosppi.net/tag-list', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '雷電将軍', + ] + ], + 'By User' => [ + 'user' => [ + 'name' => 'Username', + 'title' => 'See https://cosppi.net/sort/all-rank', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'enako_cos', + ] + ] + ]; + + private $query = ""; + + public function getName() + { + if (empty($this->queriedContext)) { + return parent::getName(); + } + + switch ($this->queriedContext) { + case 'By Search Query': + return parent::getName() . ': ' . $this->query; + case 'By User': + return parent::getName() . ' (' . $this->query . ')'; + default: + returnServerError('Unimplemented Context (getName)'); + } + + return parent::getName(); + } + + public function getURI() + { + if (empty($this->queriedContext)) { + return parent::getURI(); + } + + switch ($this->queriedContext) { + case 'By Search Query': + return parent::getURI() . 'search-images?word=' . $this->query . '&sort=' . $this->getInput('sort'); + case 'By User': + return parent::getURI() . 'user/' . $this->query . '?sort=' . $this->getInput('sort'); + default: + returnServerError('Unimplemented Context (getURI)'); + } + + return parent::getURI(); + } + + public function getIcon() + { + return 'https://cosppi.net/wp-content/uploads/2020/01/cropped-favicon-1-192x192.png'; + } + + public function collectData() + { + // Retrieve webpage + $this->query = $this->getInput('query') ?: $this->getInput('user'); + $pageUrl = $this->getURI(); + $html = getSimpleHTMLDOM($pageUrl) + or returnServerError('Could not request webpage: ' . $pageUrl); + + // Process articles + foreach ($html->find('div.img_wrapper') as $element) { + + if (count($this->items) >= 10) { + break; + } + + $name = ""; + $username = ""; + + switch ($this->queriedContext) { + case 'By Search Query': + $profile_area = $element->find('div.all_tweet_profile_wrap', 0); + $name = trim(strip_tags($profile_area->find('div.all_tweet_profile_name', 0)->innertext)); + $username = trim(strip_tags($profile_area->find('div.all_tweet_profile_screenName', 0)->innertext)); + break; + case 'By User': + $profile_area = $html->find('div.prof_right_wrap', 0); + $name = trim(strip_tags($profile_area->find('div.prof_name', 0)->innertext)); + $username = trim(strip_tags($profile_area->find('div.prof_scname', 0)->innertext)); + break; + default: + returnServerError('Unimplemented Context (collectData)'); + } + + $media_link_element = $element->find('.img_a', 0); + $media_link = $media_link_element->getAttribute('data-link') ?: $media_link_element->href; + $media_type = $media_link_element->getAttribute('data-link') ? 'video' : 'image'; + + $date_str = trim(strip_tags($element->find('div.img_footer span', 0)->innertext)); + $title = trim(strip_tags($element->find('div.tweet_text', 0)->innertext)); + + $tweet_link_element = $element->find('div.img_footer a', 0); + $tweet_link = $tweet_link_element ? $tweet_link_element->href : $media_link; + + // Constructing the title + $article_title = $name . " (" . $username . ")"; + if (!empty($title)) { + $article_title .= ": " . $title; + } + + // Convert date_str to timestamp + $timestamp = strtotime($date_str); + + // Store article in items array + if (!empty($name)) { + $item = array(); + $item['uid'] = $tweet_link; + $item['uri'] = $tweet_link; + $item['title'] = $article_title; + $item['content'] = $title . '
'; + if ($media_type === 'image') { + $item['content'] .= ''; + } else { + $item['content'] .= ''; + } + // $item['enclosures'] = array($image_link); + $item['timestamp'] = $timestamp; + $this->items[] = $item; + } + } + } +} diff --git a/DMAXBridge.php b/DMAXBridge.php new file mode 100644 index 0000000..7ee1b56 --- /dev/null +++ b/DMAXBridge.php @@ -0,0 +1,115 @@ + array( + 'name' => 'Show ID (e.g. "6023" for Steel Buddies, check website source code)', + 'type' => 'number', + 'required' => true + ), + ) + ); + const TOKEN_URI = 'https://eu1-prod.disco-api.com/token?realm=dmaxde'; + const DISCO_URI = 'https://eu1-prod.disco-api.com/content/videos//?include=primaryChannel,primaryChannel.images,show,show.images,genres,tags,images,contentPackages&sort=-seasonNumber,-episodeNumber&filter[show.id]=%d&filter[videoType]=EPISODE&page[number]=1&page[size]=100'; + + private $showName = ''; + private $pageUrl = self::URI . 'sendungen/'; + + public function getName() + { + if (!empty($this->showName)) { + return $this->showName; + } + + return parent::getName(); + } + + public function getIcon() + { + return self::URI . 'apple-touch-icon.png'; + } + + public function getURI() + { + return $this->pageUrl; + } + + public function collectData() + { + // Retrieve and check user input + $show = $this->getInput('show'); + if (empty($show)) + returnClientError('Invalid show: ' . $show); + + // Get Token + $tokenUrl = getSimpleHTMLDOM(self::TOKEN_URI) + or returnServerError('Could not request DMAX token.'); + + $token_json = json_decode($tokenUrl, true); + $token = $token_json['data']['attributes']['token']; + if (empty($token)) + returnServerError('Could not get DMAX token.'); + + // Retrieve discovery URI + $pageUrl = sprintf(self::DISCO_URI, $show); + $html = getSimpleHTMLDOM($pageUrl, array('Authorization: Bearer ' . $token)) + or returnServerError('Could not request DMAX discovery URI: ' . $pageUrl); + $json = json_decode($html, true); + + // Get show name + foreach ($json["included"] as $incl_element) { + if ($incl_element["type"] == "show") { + $this->showName = $incl_element['attributes']['name']; + $this->pageUrl = self::URI . 'sendungen/' . $incl_element['attributes']['alternateId']; + } + } + + if (empty($this->showName)) + returnClientError('Show not found.'); + + // Process articles + foreach ($json['data'] as $element) { + + if (count($this->items) >= 10) { + break; + } + + $episodeTitle = trim($element['attributes']['name']); + if (array_key_exists('episodeNumber', $element['attributes']) // Both season + episode no. given + && array_key_exists('seasonNumber', $element['attributes'])) { + $article_title = sprintf($this->showName . ' S%02dE%02d: ' . $episodeTitle, + $element['attributes']['seasonNumber'], + $element['attributes']['episodeNumber']); + } elseif (array_key_exists('episodeNumber', $element['attributes']) // Only season no. given + && !array_key_exists('seasonNumber', $element['attributes'])) { + $article_title = sprintf($this->showName . ' E%02d: ' . $episodeTitle, + $element['attributes']['episodeNumber']); + } else { // Nothing given + $article_title = $this->showName . ' - ' . $episodeTitle; + } + $article_content = trim($element['attributes']['description']); + + $article_time = $element['attributes']['airDate']; + + // Store article in items array + if (!empty($article_title)) { + $item = array(); + $item['uri'] = $this->pageUrl . '/videos'; + $item['title'] = $article_title; + $item['enclosures'] = array(); + $item['content'] = $article_content; + $item['timestamp'] = $article_time; + $item['uid'] = $article_title; + $this->items[] = $item; + } + } + } +} diff --git a/GalleryEpicBridge.php b/GalleryEpicBridge.php new file mode 100644 index 0000000..763ceea --- /dev/null +++ b/GalleryEpicBridge.php @@ -0,0 +1,121 @@ + [ + 'name' => 'Cosplayer ID', + 'type' => 'number', + 'required' => true, + 'title' => 'Enter the Cosplayer ID (e.g., 95 from https://galleryepic.com/en/coser/95/1)', + 'exampleValue' => 95 + ] + ] + ]; + const CACHE_TIMEOUT = 21600; // 6 hours + + private $feedName = null; + + public function collectData() + { + $cosplayerId = $this->getInput('cosplayer_id'); + if (!$cosplayerId) { + returnServerError('Cosplayer ID is required.'); + } + + // Set a default feed name before fetching content + $this->feedName = static::NAME . ' for Cosplayer ID ' . $cosplayerId; + + $url = self::URI . 'en/coser/' . $cosplayerId . '/1'; + $html = getSimpleHTMLDOM($url); + + if (!$html) { + returnServerError('Could not request_uri: ' . $url); + } + + // Extract cosplayer name for a more specific feed title + $cosplayerNameElement = $html->find('h4.scroll-m-20.text-xl.font-semibold.tracking-tight', 0); + if ($cosplayerNameElement) { + $cosplayerName = trim($cosplayerNameElement->plaintext); + $this->feedName = $cosplayerName . ' - GalleryEpic Albums'; + } else { + // Fallback if name couldn't be extracted, keep the default with ID + $this->feedName = 'GalleryEpic Albums for Cosplayer ID ' . $cosplayerId; + } + + $albums = $html->find('div.space-y-3.relative'); + + foreach ($albums as $albumElement) { + $item = []; + + $linkElement = $albumElement->find('a.space-y-3', 0); + if ($linkElement) { + $item['uri'] = self::URI . ltrim($linkElement->href, '/'); + } + + $titleElement = $albumElement->find('h3.font-medium', 0); + if ($titleElement) { + $item['title'] = $titleElement->plaintext; + } + + $seriesElement = $albumElement->find('p.text-xs.text-muted-foreground', 0); + $seriesText = $seriesElement ? $seriesElement->plaintext : ''; + + $imageElement = $albumElement->find('img[variant="cover"]', 0); + $imageUrl = $imageElement ? $imageElement->src : ''; + $imagePCountElement = $albumElement->find('p.absolute.bottom-2.right-2', 0); + $imagePCount = $imagePCountElement ? str_replace('P', '', $imagePCountElement->plaintext) : ''; + + $item['content'] = '

'; + if ($seriesText) { + $item['content'] .= 'Series: ' . $seriesText . '
'; + } + if ($imagePCount) { + $item['content'] .= 'Pictures: ' . $imagePCount . '
'; + } + $item['content'] .= '

'; + if ($imageUrl) { + $item['content'] .= '
'; + } + + // Try to find a download link (optional) + $downloadLinkElement = $albumElement->find('a[href*="/download/cosplay/"]', 0); + if ($downloadLinkElement) { + $item['content'] .= '
Download Album'; + } + + if (isset($item['uri']) && isset($item['title'])) { + $this->items[] = $item; + } + } + } + + public function getIcon() + { + return 'https://galleryepic.com/icons/icon-192x192.png'; + } + + public function getName() + { + if (!is_null($this->feedName)) { + return $this->feedName; + } + // Fallback for before collectData runs or if no cosplayer_id input + if (!is_null($this->getInput('cosplayer_id'))) { + return static::NAME . ' for Cosplayer ID ' . $this->getInput('cosplayer_id'); + } + return parent::getName(); // Returns static::NAME by default + } + + public function getURI() + { + if (!is_null($this->getInput('cosplayer_id'))) { + return self::URI . 'en/coser/' . $this->getInput('cosplayer_id') . '/1'; + } + return parent::getURI(); + } +} diff --git a/JapanTimesFeaturesBridge.php b/JapanTimesFeaturesBridge.php new file mode 100644 index 0000000..e133a0a --- /dev/null +++ b/JapanTimesFeaturesBridge.php @@ -0,0 +1,42 @@ +find('div.esg-media-cover-wrapper') as $element) { + + if(count($this->items) >= 10) { + break; + } + + $article_title = trim(strip_tags($element->find('div.eg-jt-features-grid-skin-element-0', 0)->innertext)); + $article_uri = $element->find('a.eg-invisiblebutton', 0)->href; + $article_thumbnail = $element->find('img', 0)->src; + $article_content = '
'; + $article_content .= trim(strip_tags($element->find('div.eg-jt-features-grid-skin-element-6', 0)->innertext)); + $article_timestamp = strtotime($element->find('div.eg-jt-features-grid-skin-element-24', 0)->innertext); + + // Store article in items array + if (!empty($article_title)) { + $item = array(); + $item['uri'] = $article_uri; + $item['title'] = $article_title; + //$item['enclosures'] = array($article_thumbnail); + $item['content'] = $article_content; + $item['timestamp'] = $article_timestamp; + $this->items[] = $item; + } + } + } +} diff --git a/SSBUNewsBridge.php b/SSBUNewsBridge.php new file mode 100644 index 0000000..73b3696 --- /dev/null +++ b/SSBUNewsBridge.php @@ -0,0 +1,92 @@ + array( + 'name' => 'Language', + 'type' => 'list', + 'values' => array( + 'English (US)' => 'en-US', + 'Chinese (Simplified)' => 'zh-CN', + 'Chinese (Traditional)' => 'zh-TW', + 'Dutch' => 'nl', + 'English (GB)' => 'en-GB', + 'French' => 'fr', + 'German' => 'de', + 'Italian' => 'it', + 'Japanese' => 'ja', + 'Korean' => 'ko', + 'Russian' => 'ru' + ), + 'defaultValue' => 'en-US' + ) + ) + ); + + private $messagesString = ''; + private $pageUrl = 'https://www-aaaba-lp1-hac.cdn.nintendo.net/en-US/index.html'; + + public function getName() + { + if (!empty($this->messagesString)) { + return $this->messagesString; + } + + return parent::getName(); + } + + public function getIcon() + { + return 'https://www.smashbros.com/favicon.ico'; + } + + public function getURI() + { + return $this->pageUrl; + } + + public function collectData() + { + // Retrieve webpage + $lang = $this->getInput('lang'); + $pageUrlBase = self::URI . $lang . '/'; + $pageUrl = $pageUrlBase . 'index.html'; + $html = getSimpleHTMLDOM($pageUrl) + or returnServerError('Could not request webpage: ' . $pageUrl); + + $this->messagesString = $html->find('title', 0)->plaintext . ' ' . $html->find('div.shrink-label', 0)->plaintext; + $this->pageUrl = $pageUrl; + + // Process articles + foreach ($html->find('li.article-item') as $element) { + + if (count($this->items) >= 10) { + break; + } + + $article_title = trim($element->find('h2', 0)->plaintext); + $article_uri = $pageUrlBase . $element->find('a', 0)->href; + $article_thumbnail = $pageUrlBase . $element->find('img', 0)->{'data-lazy-src'}; + $article_content = ''; + $article_timestamp = $element->attr['data-show-new-badge-published-at']; + + // Store article in items array + if (!empty($article_title) && !empty($article_uri)) { + $item = array(); + $item['uri'] = $article_uri; + $item['title'] = $article_title; + // $item['enclosures'] = array($article_thumbnail); + $item['content'] = $article_content; + $item['timestamp'] = $article_timestamp; + $this->items[] = $item; + } + } + } +} diff --git a/WHODiseaseOutbreakBridge.php b/WHODiseaseOutbreakBridge.php new file mode 100644 index 0000000..1ef8ab2 --- /dev/null +++ b/WHODiseaseOutbreakBridge.php @@ -0,0 +1,47 @@ +find('div.sf-list-vertical a.sf-list-vertical__item') as $element) { + + if (count($this->items) >= 10) { + break; + } + + $row = $element->find('.sf-list-vertical__title', 0); + + $article_link = $element->href; + $article_title = trim($row->find('.full-title', 0)->plaintext); + + // Store article in items array + if (!empty($article_title)) { + $item = array(); + $item['uri'] = $article_link; + $item['title'] = $article_title; + $item['uid'] = $article_link; + } + + $timestamp = $row->find('span', 1)->plaintext; + if (isset($timestamp) && !empty($timestamp)) { + $timestamp = str_replace(' | ', '', $timestamp); + $article_timestamp = strtotime($timestamp); + $item['timestamp'] = $article_timestamp; + } + + $this->items[] = $item; + } + } +} diff --git a/WeiboPicsBridge.php b/WeiboPicsBridge.php new file mode 100644 index 0000000..83bfcdd --- /dev/null +++ b/WeiboPicsBridge.php @@ -0,0 +1,395 @@ + [ + '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; + } + } +}