Files
InvoiceShelf/app/Platform/Modules/Marketplace/MarketplaceClient.php
T
Darko Gjorgjijoski 5ef7804e60 refactor: adopt modular domain architecture (#747)
* refactor: stabilize model identities for domain migration

* refactor: extract module platform context

* refactor: assign models to domain contexts

* refactor: extract ai platform context

* refactor: extract storage platform context

* refactor: extract mail platform context

* refactor: extract pdf platform context

* refactor: extract operations platform context

* refactor: move installation into operations platform

* refactor: extract money domain context

* refactor: extract taxation domain context

* refactor: extract catalog domain context

* refactor: extract metadata domain context

* refactor: extract reporting domain context

* refactor: extract purchases domain context

* refactor: extract receivables domain context

* refactor: extract accounts domain context

* refactor: complete reporting statement boundary

* refactor: extract contacts domain context

* refactor: extract sales domain context

* refactor: remove legacy application layers

* fix: migrate legacy bouncer role identities
2026-08-05 17:40:03 +02:00

122 lines
4.3 KiB
PHP

<?php
namespace App\Platform\Modules\Marketplace;
use App\Platform\Modules\Models\MarketplaceCredential;
use App\Platform\Operations\Models\Setting;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class MarketplaceClient
{
private const API_PREFIX = 'api/marketplace/v1';
public function catalog(): Response
{
return $this->request()->get(self::API_PREFIX.'/modules', [
'channel' => config('invoiceshelf.marketplace.channel', 'stable'),
]);
}
public function module(string $slug): Response
{
return $this->request()->get(self::API_PREFIX.'/modules/'.$slug, [
'channel' => config('invoiceshelf.marketplace.channel', 'stable'),
]);
}
public function beginPairing(): Response
{
return $this->request()->post(self::API_PREFIX.'/device/code', [
'installation_name' => (string) config('app.name', 'InvoiceShelf').' — '.parse_url((string) config('app.url'), PHP_URL_HOST),
'invoiceshelf_version' => (string) Setting::getSetting('version'),
'module_api_version' => (string) config('invoiceshelf.marketplace.module_api_version'),
'php_version' => PHP_VERSION,
'extensions' => collect(get_loaded_extensions())
->map(fn (string $extension): string => 'ext-'.str_replace(' ', '-', strtolower($extension)))
->filter(fn (string $extension): bool => preg_match('/^ext-[a-z0-9][a-z0-9_-]*$/', $extension) === 1)
->unique()
->sort()->values()->all(),
]);
}
public function pollPairing(string $deviceCode): Response
{
return $this->request()->post(self::API_PREFIX.'/device/token', [
'device_code' => $deviceCode,
]);
}
public function revokeInstallation(): Response
{
return $this->request()->delete(self::API_PREFIX.'/device');
}
public function release(string $slug, string $version, string $channel): Response
{
return $this->request()->post(self::API_PREFIX."/modules/{$slug}/releases/{$version}/download", [
'channel' => $channel,
]);
}
/**
* Signed artifact URLs are intentionally fetched with a separate request:
* marketplace credentials must never be sent to an object-storage host.
*/
public function artifact(string $url, string $destination): Response
{
if (! $this->allowsArtifactUrl($url)) {
throw new RuntimeException('Marketplace returned an unsafe artifact URL.');
}
return Http::withOptions(['verify' => true, 'allow_redirects' => false])
->timeout(120)
->sink($destination)
->get($url);
}
private function allowsArtifactUrl(string $url): bool
{
$parts = parse_url($url);
if (! is_array($parts) || isset($parts['user'], $parts['pass']) || ! isset($parts['scheme'], $parts['host'])) {
return false;
}
if (strtolower($parts['scheme']) === 'https') {
return true;
}
$base = parse_url((string) config('invoiceshelf.base_url'));
return strtolower($parts['scheme']) === 'http'
&& is_array($base)
&& strtolower((string) ($base['scheme'] ?? '')) === 'http'
&& strtolower($parts['host']) === strtolower((string) ($base['host'] ?? ''))
&& ($parts['port'] ?? 80) === ($base['port'] ?? 80);
}
private function request(): PendingRequest
{
$request = Http::baseUrl(rtrim((string) config('invoiceshelf.base_url'), '/'))
->acceptJson()
->timeout(30)
->connectTimeout(10)
->withOptions(['verify' => true, 'allow_redirects' => false])
->withHeaders([
'Referer' => url('/'),
'invoiceshelf' => (string) Setting::getSetting('version'),
]);
$credential = MarketplaceCredential::query()->latest('id')->first();
if ($credential !== null && ($credential->expires_at === null || $credential->expires_at->isFuture())) {
$request = $request->withToken(Crypt::decryptString($credential->credential));
}
return $request;
}
}