From f828cec22984ffc00eb41b47d95c3ac2844f34b7 Mon Sep 17 00:00:00 2001 From: Terence Ong Date: Mon, 6 Apr 2026 19:42:22 +0700 Subject: [PATCH] Use Google batchexecute API to resolve Google News article URLs Replace broken base64 decoding with Google's internal batchexecute API endpoint to resolve encrypted article IDs to actual publisher URLs. This is the approach used by known Google News decoder libraries. Co-Authored-By: Claude Opus 4.6 --- includes/class-peptide-news-fetcher.php | 69 ++++++++++++++++--------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/includes/class-peptide-news-fetcher.php b/includes/class-peptide-news-fetcher.php index 8eaae1e..cd924b6 100644 --- a/includes/class-peptide-news-fetcher.php +++ b/includes/class-peptide-news-fetcher.php @@ -772,45 +772,64 @@ class Peptide_News_Fetcher { } /** - * Decode a Google News RSS article URL to extract the actual publisher URL. - * - * Google News RSS URLs encode the real article URL in a base64 protobuf - * payload in the path: /rss/articles/CBMi... + * Resolve a Google News article URL to the actual publisher URL + * using Google's internal batchexecute API. * * @param string $url Google News article URL. - * @return string|false The decoded article URL, or false on failure. + * @return string|false The resolved article URL, or false on failure. */ private function decode_google_news_url( $url ) { - // Extract the base64 segment from the URL path. - if ( ! preg_match( '#/articles/([A-Za-z0-9_-]+)#', $url, $matches ) ) { + // Extract the article ID from the URL path. + if ( ! preg_match( '#/(?:rss/)?articles/([A-Za-z0-9_-]+)#', $url, $matches ) ) { return false; } - $encoded = $matches[1]; + $article_id = $matches[1]; - // Google uses URL-safe base64 — convert to standard base64. - $encoded = str_replace( array( '-', '_' ), array( '+', '/' ), $encoded ); + // Build the batchexecute request payload. + $inner_payload = json_encode( array( + 'Fbv4je', + '[\"garturlreq\",[[\"en\",\"US\",[\"FINANCE_TOP_INDICES\",\"WEB_TEST_1_0_0\"],null,null,1,1,\"US:en\",null,180,null,null,null,null,null,0,null,null,[1608992183,723341000]],\"en\",\"US\",1,[2,3,4,8],1,1,\"648780909\",0,0,null,0],\"' . $article_id . '\"]', + 'generic', + ) ); - // Add padding if needed. - $remainder = strlen( $encoded ) % 4; - if ( $remainder ) { - $encoded .= str_repeat( '=', 4 - $remainder ); - } + $body = 'f.req=' . urlencode( '[[' . $inner_payload . ']]' ); - $decoded = base64_decode( $encoded, true ); - if ( false === $decoded ) { + $response = wp_remote_post( 'https://news.google.com/_/DotsSplashUi/data/batchexecute', array( + 'timeout' => 10, + 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'headers' => array( + 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8', + 'Referer' => 'https://news.google.com/', + ), + 'body' => $body, + ) ); + + if ( is_wp_error( $response ) ) { return false; } - // The decoded protobuf contains the URL as a string field. - // Extract any http(s) URL from the binary data. - if ( preg_match( '#(https?://[^\x00-\x1f\x7f-\x9f"<>\s]+)#', $decoded, $url_matches ) ) { - $article_url = $url_matches[1]; + $response_body = wp_remote_retrieve_body( $response ); - // Validate it's not another Google URL. - $article_domain = $this->extract_domain( $article_url ); - if ( 'news.google.com' !== $article_domain && 'google.com' !== $article_domain ) { - return $article_url; + if ( empty( $response_body ) ) { + return false; + } + + // The response is a multi-line format. The actual data is in the JSON payload. + // Extract URLs from the response — the real article URL appears in the JSON. + if ( preg_match_all( '#(https?://[^"\\\\]+)#', $response_body, $url_matches ) ) { + foreach ( $url_matches[1] as $candidate ) { + $candidate_domain = $this->extract_domain( $candidate ); + // Skip Google's own domains. + if ( in_array( $candidate_domain, array( 'news.google.com', 'google.com', 'consent.google.com', 'accounts.google.com' ), true ) ) { + continue; + } + // Skip common non-article URLs. + if ( strpos( $candidate, 'googleapis.com' ) !== false || strpos( $candidate, 'gstatic.com' ) !== false ) { + continue; + } + // Found a non-Google URL — this is likely the article. + return esc_url_raw( $candidate ); } }