Skip to content

Commit

Permalink
Fix Envato package dist/download URL (#15)
Browse files Browse the repository at this point in the history
* Improve PHPDoc types in config

* Check for arrays once in API responses

* Process versions once in package

* Fix Envato package dist/download URL

Resolves #2

When generating the Composer packages for the virtual repository from the configured list of Envato products, this plugin would retrieve the product's download URL via Envato's API and assign it as the package's dist URL. Composer uses this URL as the download's cache key and package URL in a project's `composer.lock` for later installations. Unfortunately, this download URL is signed and expires quickly. This is problematic if ever Composer's cache is cleared or if the project is installed on a different server where Composer's cache is not mirrored.

This is resolved by assigning a more stable versioned dist URL (for the time being, the API request URL to retrieve the download URL) and telling Composer that this plugin needs to update package download URLs before they are downloaded. Then, during the "pre-file-download" event, this plugin will retrieve the download URL from Envato's API while using the stable dist URL as the cache key.

* Improve PHPDoc types and descriptions

* Improve check for download request URL

Ensure the download request URL matches the correct REST URI.
  • Loading branch information
mcaskill authored Feb 24, 2023
1 parent 12039e5 commit b133353
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 27 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"szepeviktor/composer-theme-fusion": "Composer plugin for ThemeFusion"
},
"extra": {
"class": "SzepeViktor\\Composer\\Envato\\EnvatoPlugin"
"class": "SzepeViktor\\Composer\\Envato\\EnvatoPlugin",
"plugin-modifies-downloads": true
},
"autoload": {
"psr-4": {
Expand Down
71 changes: 59 additions & 12 deletions src/EnvatoApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public function __construct(IOInterface $io, Config $config, string $token)
$this->token = $token;
}

/**
* Query the Envato API for the item's current version.
*
* @return non-empty-string
*/
public function getVersion(int $itemId): string
{
$response = $this->httpDownloader->get(
Expand All @@ -41,11 +46,14 @@ public function getVersion(int $itemId): string
if ($response->getStatusCode() === 200) {
$versionData = \json_decode($response->getBody() ?? '', true);
// TODO Check JSON
if (is_array($versionData) && \array_key_exists('wordpress_theme_latest_version', $versionData)) {
return $versionData['wordpress_theme_latest_version'];
}
if (is_array($versionData) && \array_key_exists('wordpress_plugin_latest_version', $versionData)) {
return $versionData['wordpress_plugin_latest_version'];
if (\is_array($versionData)) {
if (\array_key_exists('wordpress_theme_latest_version', $versionData)) {
return $versionData['wordpress_theme_latest_version'];
}

if (\array_key_exists('wordpress_plugin_latest_version', $versionData)) {
return $versionData['wordpress_plugin_latest_version'];
}
}
}

Expand All @@ -54,24 +62,63 @@ public function getVersion(int $itemId): string
}

/**
* Return the Envato API URL to retrieve the item's download URL.
*
* @param int $itemId The item ID to lookup.
* @param string|null $version Optional version to append to the URL.
* This is used to by the plugin for the download's cache key in Composer.
* @return non-empty-string
*/
public function getDownloadRequestUrl(int $itemId, ?string $version = null): string
{
$query = ['item_id' => $itemId];

if ($version !== null) {
$query['version'] = $version;
}

return self::API_BASE_URL . '/market/buyer/download?' . \http_build_query($query);
}

/**
* Query the Envato API to retrieve the item's download URL.
*
* @param int|string $itemIdOrApiUrl The item ID to lookup or the API URL to query.
* @return non-empty-string|null
*/
public function getDownloadUrl(int $itemId): ?string
public function getDownloadUrl($itemIdOrApiUrl): ?string
{
if (
\is_int($itemIdOrApiUrl) &&
$itemIdOrApiUrl > 0
) {
$apiUrl = $this->getDownloadRequestUrl($itemIdOrApiUrl);
} elseif (
\is_string($itemIdOrApiUrl) &&
\strpos($itemIdOrApiUrl, self::API_BASE_URL . '/market/buyer/download') !== false
) {
$apiUrl = $itemIdOrApiUrl;
} else {
return null;
}

$response = $this->httpDownloader->get(
self::API_BASE_URL . '/market/buyer/download?' . \http_build_query(['item_id' => $itemId]),
$apiUrl,
['http' => ['header' => ['Authorization: Bearer ' . $this->token]]]
);

// TODO HTTP 429 response. Included in this response is a HTTP header Retry-After
if ($response->getStatusCode() === 200) {
$urlData = \json_decode($response->getBody() ?? '', true);
// TODO Check JSON
if (is_array($urlData) && \array_key_exists('wordpress_theme', $urlData)) {
return $urlData['wordpress_theme'];
}
if (is_array($urlData) && \array_key_exists('wordpress_plugin', $urlData)) {
return $urlData['wordpress_plugin'];
if (\is_array($urlData)) {
if (\array_key_exists('wordpress_theme', $urlData)) {
return $urlData['wordpress_theme'];
}

if (\array_key_exists('wordpress_plugin', $urlData)) {
return $urlData['wordpress_plugin'];
}
}
}

Expand Down
11 changes: 7 additions & 4 deletions src/EnvatoConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class EnvatoConfig
public const ENV_VAR_TOKEN = 'ENVATO_TOKEN';

/**
* @var array<mixed>
* @var array{token?:string, packages?:array{item-id:int|string|null, type:string|null}}
*/
protected $config;

Expand All @@ -40,6 +40,9 @@ public function __construct(Config $composerConfig)
;
}

/**
* @phpstan-assert-if-true !array{} $this->config
*/
public function isValid(): bool
{
return $this->valid;
Expand All @@ -60,9 +63,9 @@ public function getPackageList(): array
return \array_map(
static function ($name, $data) {
return [
'name' => (string)$name,
'itemId' => $data['item-id'] ?? 0,
'type' => $data['type'] ?? 'wordpress-theme',
'name' => (string)$name,
'itemId' => (int)($data['item-id'] ?? 0),
'type' => (string)($data['type'] ?? 'wordpress-theme'),
];
},
\array_keys($this->config['packages']),
Expand Down
27 changes: 18 additions & 9 deletions src/EnvatoPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ public function getVersion(): string
return $this->version;
}

// Query Envato API
$this->prettyVersion = $this->api->getVersion($this->itemId);
$versionParser = new VersionParser();
$this->version = $versionParser->normalize($this->prettyVersion);
$this->processItemVersion();

return $this->version;
}
Expand All @@ -68,10 +65,7 @@ public function getPrettyVersion(): string
return $this->prettyVersion;
}

// Query Envato API
$this->prettyVersion = $this->api->getVersion($this->itemId);
$versionParser = new VersionParser();
$this->version = $versionParser->normalize($this->prettyVersion);
$this->processItemVersion();

return $this->prettyVersion;
}
Expand All @@ -93,7 +87,11 @@ public function getDistUrl(): ?string
return $this->distUrl;
}

$this->distUrl = $this->api->getDownloadUrl($this->itemId);
/**
* Use the Envato API URL to retrieve the item's download URL
* as the package's dist URL to serve as its cache key.
*/
$this->distUrl = $this->api->getDownloadRequestUrl($this->itemId, $this->getPrettyVersion());

return $this->distUrl;
}
Expand All @@ -105,4 +103,15 @@ public function isAbandoned()
{
return false;
}

/**
* Retrieve the item's version from the Envato API.
*/
protected function processItemVersion(): void
{
// Query Envato API
$this->prettyVersion = $this->api->getVersion($this->itemId);
$versionParser = new VersionParser();
$this->version = $versionParser->normalize($this->prettyVersion);
}
}
44 changes: 43 additions & 1 deletion src/EnvatoPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
namespace SzepeViktor\Composer\Envato;

use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PluginInterface;
use Composer\Plugin\PreFileDownloadEvent;
use Composer\Repository\ArrayRepository;

/**
* Composer Plugin for Envato Marketplace.
*/
class EnvatoPlugin implements PluginInterface
class EnvatoPlugin implements PluginInterface, EventSubscriberInterface
{
/**
* @var Composer
Expand Down Expand Up @@ -63,6 +66,16 @@ public function uninstall(Composer $composer, IOInterface $io)
{
}

/**
* @return array<key-of<PluginEvents::*>, (string|array{string, int})>
*/
public static function getSubscribedEvents()
{
return [
PluginEvents::PRE_FILE_DOWNLOAD => [ 'handlePreDownloadEvent', -1 ],
];
}

protected function generateRepository(): ArrayRepository
{
$api = $this->api;
Expand All @@ -81,4 +94,33 @@ static function ($packageConfig) use ($api) {
$this->config->getPackageList()
));
}

/**
* Retrieve the item's download URL from the Envato API
* and use the item's dist URL as the cache key.
*/
public function handlePreDownloadEvent(PreFileDownloadEvent $event): void
{
/**
* Bail early if this event is not for a package.
*
* @see https://github.com/composer/composer/pull/8975
*/
if ($event->getType() !== 'package') {
return;
}

$processedUrl = $event->getProcessedUrl();
$downloadUrl = $this->api->getDownloadUrl($processedUrl);

// Submit changes to Composer, if any
if (
\is_string($downloadUrl) &&
$downloadUrl !== '' &&
$downloadUrl !== $processedUrl
) {
$event->setProcessedUrl($downloadUrl);
$event->setCustomCacheKey($processedUrl);
}
}
}

0 comments on commit b133353

Please sign in to comment.