Skip to content

Apple App Store bridge fix #4516

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 151 additions & 72 deletions bridges/AppleAppStoreBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,37 @@ class AppleAppStoreBridge extends BridgeAbstract
],
'defaultValue' => 'US',
],
'debug' => [
'name' => 'Debug Mode',
'type' => 'checkbox',
'defaultValue' => false
]
]];

const PLATFORM_MAPPING = [
'iphone' => 'ios',
'ipad' => 'ios',
'iphone' => 'ios',
'ipad' => 'ios',
'mac' => 'osx'
];

private function makeHtmlUrl($id, $country)
private $name;

private function makeHtmlUrl()
{
return 'https://apps.apple.com/' . $country . '/app/id' . $id;
$id = $this->getInput('id');
$country = $this->getInput('country');
return "https://apps.apple.com/{$country}/app/id{$id}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls choose an alternative way to interpolate string, e.g. sprintf

}

private function makeJsonUrl($id, $platform, $country)
private function makeJsonUrl()
{
return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory";
$id = $this->getInput('id');
$country = $this->getInput('country');
$platform = $this->getInput('p');

$platform_param = ($platform === 'mac') ? 'mac' : $platform;

return "https://amp-api.apps.apple.com/v1/catalog/{$country}/apps/{$id}?platform={$platform_param}&extend=versionHistory";
}

public function getName()
Expand All @@ -78,94 +94,157 @@ public function getName()
return parent::getName();
}

/**
* In case of some platforms, the data is present in the initial response
*/
private function getDataFromShoebox($id, $platform, $country)
private function debugLog($message)
{
$uri = $this->makeHtmlUrl($id, $country);
$html = getSimpleHTMLDOMCached($uri, 3600);
$script = $html->find('script[id="shoebox-ember-data-store"]', 0);

$json = json_decode($script->innertext, true);
return $json['data'];
if ($this->getInput('debug')) {
error_log("[AppleAppStoreBridge] $message");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls dont use error_log.

use:

$this->logger->info('hello world')

$this->logger->warning('hello world')

$this->logger->error('hello world')

}
}

private function getJWTToken($id, $platform, $country)
private function getHtml()
{
$uri = $this->makeHtmlUrl($id, $country);

$html = getSimpleHTMLDOMCached($uri, 3600);
$url = $this->makeHtmlUrl();
$this->debugLog("Fetching HTML from: $url");

$html = getSimpleHTMLDOM($url);

if (!$html) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will never happen because return value is always \simple_html_dom, or it throws exception

throw new \Exception("Failed to retrieve HTML from App Store");
}

$this->debugLog("HTML fetch successful");
return $html;
}

private function getJWTToken()
{
$html = $this->getHtml();
$meta = $html->find('meta[name="web-experience-app/config/environment"]', 0);

$json = urldecode($meta->content);

$json = json_decode($json);

return $json->MEDIA_API->token;

if (!$meta || !isset($meta->content)) {
throw new \Exception("JWT token not found in page content");
}

try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put as little code as possible in the try clause

$decoded_content = urldecode($meta->content);
$this->debugLog("Found meta tag content");
$decoded_json = json_decode($decoded_content, true);
Copy link
Contributor

@dvikan dvikan Apr 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See Json::decode


if (!isset($decoded_json['MEDIA_API']['token'])) {
throw new \Exception("Token field not found in JSON structure");
}

$token = $decoded_json['MEDIA_API']['token'];
$this->debugLog("Successfully extracted JWT token");
return $token;
} catch (\Exception $e) {
throw new \Exception("Failed to extract JWT token: " . $e->getMessage());
}
}

private function getAppData($id, $platform, $country, $token)
private function getAppData()
{
$uri = $this->makeJsonUrl($id, $platform, $country);

$token = $this->getJWTToken();

$url = $this->makeJsonUrl();
$this->debugLog("Fetching data from API: $url");

$headers = [
"Authorization: Bearer $token",
'Authorization: Bearer ' . $token,
'Origin: https://apps.apple.com',
'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
];

$json = json_decode(getContents($uri, $headers), true);


$content = getContents($url, $headers);
if (!$content) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getContents returns string|Response, most commonly a string

throw new \Exception("Failed to get content from API");
}

$json = json_decode($content, true);

if (!isset($json['data']) || empty($json['data'])) {
throw new \Exception("No app data found in API response");
}

$this->debugLog("Successfully retrieved app data from API");
return $json['data'][0];
}

/**
* Parses the version history from the data received
* @return array list of versions with details on each element
*/
private function getVersionHistory($data, $platform)
private function extractAppDetails($data)
{
switch ($platform) {
case 'mac':
return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory'];
default:
$os = self::PLATFORM_MAPPING[$platform];
return $data['attributes']['platformAttributes'][$os]['versionHistory'];
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code does not need to be wrapped in a try catch

if (isset($data['attributes'])) {
$this->name = $data['attributes']['name'] ?? null;
$author = $data['attributes']['artistName'] ?? null;
$this->debugLog("Found app details in attributes: {$this->name} by {$author}");
return [$this->name, $author];
}

$this->name = "App " . $this->getInput('id');
$this->debugLog("App details not found, using default: {$this->name}");
return [$this->name, "Unknown Developer"];
} catch (\Exception $e) {
$this->debugLog("Error extracting app details: " . $e->getMessage());
$this->name = "App " . $this->getInput('id');
return [$this->name, "Unknown Developer"];
}
}

public function collectData()
private function getVersionHistory($data)
{
$id = $this->getInput('id');
$country = $this->getInput('country');
$platform = $this->getInput('p');

switch ($platform) {
case 'mac':
$data = $this->getDataFromShoebox($id, $platform, $country);
break;

default:
$token = $this->getJWTToken($id, $platform, $country);
$data = $this->getAppData($id, $platform, $country, $token);
$this->debugLog("Extracting version history for platform: {$platform}");

try {
$platform_key = self::PLATFORM_MAPPING[$platform] ?? $platform;

if (isset($data['attributes']['platformAttributes'][$platform_key]['versionHistory'])) {
return $data['attributes']['platformAttributes'][$platform_key]['versionHistory'];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should work:

$version = $data['attributes']['platformAttributes'][$platform_key]['versionHistory'] ?? null


$this->debugLog("No version history found for {$platform}");
return [];

} catch (\Exception $e) {
$this->debugLog("Error extracting version history: " . $e->getMessage());
return [];
}
}

$versionHistory = $this->getVersionHistory($data, $platform);
$name = $this->name = $data['attributes']['name'];
$author = $data['attributes']['artistName'];

foreach ($versionHistory as $row) {
$item = [];

$item['content'] = nl2br($row['releaseNotes']);
$item['title'] = $name . ' - ' . $row['versionDisplay'];
$item['timestamp'] = $row['releaseDate'];
$item['author'] = $author;

$item['uri'] = $this->makeHtmlUrl($id, $country);

$this->items[] = $item;
public function collectData()
{
try {
$this->debugLog("Getting data for " . $this->getInput('p') . " app");
$data = $this->getAppData();

list($name, $author) = $this->extractAppDetails($data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls dont use list(). I think php has proper lang syntax for this unpacking.

Something like:

[$name, $auth] = $this->extractAppDetails($data);


$version_history = $this->getVersionHistory($data);
$this->debugLog("Found " . count($version_history) . " versions for {$name}");

foreach ($version_history as $entry) {
try {
$version = $entry['versionDisplay'] ?? 'Unknown Version';
$release_notes = $entry['releaseNotes'] ?? 'No release notes available';
$release_date = $entry['releaseDate'] ?? 'Unknown Date';

$item = [];
$item['title'] = "{$name} - {$version}";
$item['content'] = nl2br($release_notes) ?: 'No release notes available';
$item['timestamp'] = $release_date;
$item['author'] = $author;
$item['uri'] = $this->makeHtmlUrl();

$this->items[] = $item;
} catch (\Exception $e) {
$this->debugLog("Error processing version entry: " . $e->getMessage());
}
}

$this->debugLog("Successfully collected " . count($this->items) . " items");
} catch (\Exception $e) {
$this->debugLog("Error collecting data: " . $e->getMessage());
throw $e;
}
}
}
}
Loading