style updates
This commit is contained in:
@@ -1,17 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Marketplace\Services;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
final class CompatibilityChecker
|
||||
{
|
||||
public function __construct(private VersionConstraint $constraints=new VersionConstraint()) {}
|
||||
public function __construct(private VersionConstraint $constraints = new VersionConstraint) {}
|
||||
|
||||
public function check(array $manifest): array
|
||||
{
|
||||
$errors=[];
|
||||
$php=(string)($manifest['php'] ?? '*'); $laravel=(string)($manifest['laravel'] ?? '*');
|
||||
$platform=(string)($manifest['content_manager'] ?? '*'); $platformVersion=(string) config('app.version','1.0.0');
|
||||
if (!$this->constraints->matches(PHP_VERSION,$php)) $errors[]="Requires PHP $php; running ".PHP_VERSION.'.';
|
||||
if (!$this->constraints->matches(App::version(),$laravel)) $errors[]="Requires Laravel $laravel; running ".App::version().'.';
|
||||
if (!$this->constraints->matches($platformVersion,$platform)) $errors[]="Requires Content Manager $platform; running $platformVersion.";
|
||||
return ['compatible'=>$errors===[],'errors'=>$errors];
|
||||
$errors = [];
|
||||
$php = (string) ($manifest['php'] ?? '*');
|
||||
$laravel = (string) ($manifest['laravel'] ?? '*');
|
||||
$platform = (string) ($manifest['content_manager'] ?? '*');
|
||||
$platformVersion = (string) config('app.version', '1.0.0');
|
||||
if (! $this->constraints->matches(PHP_VERSION, $php)) {
|
||||
$errors[] = "Requires PHP $php; running ".PHP_VERSION.'.';
|
||||
}
|
||||
if (! $this->constraints->matches(App::version(), $laravel)) {
|
||||
$errors[] = "Requires Laravel $laravel; running ".App::version().'.';
|
||||
}
|
||||
if (! $this->constraints->matches($platformVersion, $platform)) {
|
||||
$errors[] = "Requires Content Manager $platform; running $platformVersion.";
|
||||
}
|
||||
|
||||
return ['compatible' => $errors === [], 'errors' => $errors];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Marketplace\Services;
|
||||
|
||||
use Modules\Marketplace\Models\InstalledPackage;
|
||||
|
||||
final class DependencyResolver
|
||||
{
|
||||
public function __construct(private VersionConstraint $constraints=new VersionConstraint()) {}
|
||||
public function __construct(private VersionConstraint $constraints = new VersionConstraint) {}
|
||||
|
||||
public function resolve(array $manifest): array
|
||||
{
|
||||
$missing=[]; $incompatible=[];
|
||||
foreach (($manifest['requires'] ?? []) as $package=>$constraint) {
|
||||
$installed=InstalledPackage::query()->where('package_name',$package)->where('status','installed')->first();
|
||||
if (!$installed) { $missing[$package]=$constraint; continue; }
|
||||
if (!$this->constraints->matches($installed->version,(string)$constraint)) $incompatible[$package]=['required'=>$constraint,'installed'=>$installed->version];
|
||||
$missing = [];
|
||||
$incompatible = [];
|
||||
foreach (($manifest['requires'] ?? []) as $package => $constraint) {
|
||||
$installed = InstalledPackage::query()->where('package_name', $package)->where('status', 'installed')->first();
|
||||
if (! $installed) {
|
||||
$missing[$package] = $constraint;
|
||||
|
||||
continue;
|
||||
}
|
||||
if (! $this->constraints->matches($installed->version, (string) $constraint)) {
|
||||
$incompatible[$package] = ['required' => $constraint, 'installed' => $installed->version];
|
||||
}
|
||||
}
|
||||
return ['satisfied'=>$missing===[] && $incompatible===[],'missing'=>$missing,'incompatible'=>$incompatible];
|
||||
|
||||
return ['satisfied' => $missing === [] && $incompatible === [], 'missing' => $missing, 'incompatible' => $incompatible];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Marketplace\Services;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
use Modules\Marketplace\Models\Installation;
|
||||
use Modules\Marketplace\Models\InstalledPackage;
|
||||
use Modules\Marketplace\Models\MarketplacePackage;
|
||||
use RuntimeException;
|
||||
|
||||
final class MarketplaceManager
|
||||
{
|
||||
public function __construct(private PackageManifest $manifests,private CompatibilityChecker $compatibility,private DependencyResolver $dependencies) {}
|
||||
public function install(MarketplacePackage $package,?int $userId=null): InstalledPackage
|
||||
public function __construct(private PackageManifest $manifests, private CompatibilityChecker $compatibility, private DependencyResolver $dependencies) {}
|
||||
|
||||
public function install(MarketplacePackage $package, ?int $userId = null): InstalledPackage
|
||||
{
|
||||
if (!config('marketplace.enabled')) throw new RuntimeException('The marketplace is disabled.');
|
||||
$manifest=$this->manifests->parse($package->manifest ?: ['name'=>$package->package_name,'type'=>$package->type,'version'=>$package->latest_version]);
|
||||
$compatibility=$this->compatibility->check($manifest); if (!$compatibility['compatible']) throw new RuntimeException(implode(' ',$compatibility['errors']));
|
||||
$dependencies=$this->dependencies->resolve($manifest); if (!$dependencies['satisfied']) throw new RuntimeException('Package dependencies are not satisfied.');
|
||||
return DB::transaction(function() use ($package,$manifest,$userId) {
|
||||
$installed=InstalledPackage::query()->firstOrNew(['package_name'=>$package->package_name]); $from=$installed->exists?$installed->version:null;
|
||||
$installed->fill(['marketplace_package_id'=>$package->id,'type'=>$package->type,'version'=>$manifest['version'],'status'=>'installed','manifest'=>$manifest,'checksum'=>$package->checksum,'is_enabled'=>true,'installed_by'=>$userId,'installed_at'=>$installed->installed_at ?: now(),'updated_at_marketplace'=>now()])->save();
|
||||
$this->history($installed,$from?'update':'install',$from,$manifest['version'],$userId,'completed'); return $installed->refresh();
|
||||
if (! config('marketplace.enabled')) {
|
||||
throw new RuntimeException('The marketplace is disabled.');
|
||||
}
|
||||
$manifest = $this->manifests->parse($package->manifest ?: ['name' => $package->package_name, 'type' => $package->type, 'version' => $package->latest_version]);
|
||||
$compatibility = $this->compatibility->check($manifest);
|
||||
if (! $compatibility['compatible']) {
|
||||
throw new RuntimeException(implode(' ', $compatibility['errors']));
|
||||
}
|
||||
$dependencies = $this->dependencies->resolve($manifest);
|
||||
if (! $dependencies['satisfied']) {
|
||||
throw new RuntimeException('Package dependencies are not satisfied.');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($package, $manifest, $userId) {
|
||||
$installed = InstalledPackage::query()->firstOrNew(['package_name' => $package->package_name]);
|
||||
$from = $installed->exists ? $installed->version : null;
|
||||
$installed->fill(['marketplace_package_id' => $package->id, 'type' => $package->type, 'version' => $manifest['version'], 'status' => 'installed', 'manifest' => $manifest, 'checksum' => $package->checksum, 'is_enabled' => true, 'installed_by' => $userId, 'installed_at' => $installed->installed_at ?: now(), 'updated_at_marketplace' => now()])->save();
|
||||
$this->history($installed, $from ? 'update' : 'install', $from, $manifest['version'], $userId, 'completed');
|
||||
|
||||
return $installed->refresh();
|
||||
});
|
||||
}
|
||||
public function setEnabled(InstalledPackage $package,bool $enabled,?int $userId=null): InstalledPackage
|
||||
|
||||
public function setEnabled(InstalledPackage $package, bool $enabled, ?int $userId = null): InstalledPackage
|
||||
{
|
||||
$package->update(['is_enabled'=>$enabled,'status'=>$enabled?'installed':'disabled']); $this->history($package,$enabled?'enable':'disable',$package->version,$package->version,$userId,'completed'); return $package->refresh();
|
||||
$package->update(['is_enabled' => $enabled, 'status' => $enabled ? 'installed' : 'disabled']);
|
||||
$this->history($package, $enabled ? 'enable' : 'disable', $package->version, $package->version, $userId, 'completed');
|
||||
|
||||
return $package->refresh();
|
||||
}
|
||||
public function remove(InstalledPackage $package,?int $userId=null): void
|
||||
|
||||
public function remove(InstalledPackage $package, ?int $userId = null): void
|
||||
{
|
||||
DB::transaction(function() use ($package,$userId) { $this->history($package,'remove',$package->version,null,$userId,'completed'); $package->update(['status'=>'removed','is_enabled'=>false]); });
|
||||
DB::transaction(function () use ($package, $userId) {
|
||||
$this->history($package, 'remove', $package->version, null, $userId, 'completed');
|
||||
$package->update(['status' => 'removed', 'is_enabled' => false]);
|
||||
});
|
||||
}
|
||||
public function rollback(InstalledPackage $package,Installation $installation,?int $userId=null): InstalledPackage
|
||||
|
||||
public function rollback(InstalledPackage $package, Installation $installation, ?int $userId = null): InstalledPackage
|
||||
{
|
||||
if ($installation->package_name!==$package->package_name || blank($installation->from_version)) throw new RuntimeException('This installation cannot be rolled back.');
|
||||
$current=$package->version; $package->update(['version'=>$installation->from_version,'status'=>'installed','is_enabled'=>true]);
|
||||
$this->history($package,'rollback',$current,$installation->from_version,$userId,'completed'); return $package->refresh();
|
||||
if ($installation->package_name !== $package->package_name || blank($installation->from_version)) {
|
||||
throw new RuntimeException('This installation cannot be rolled back.');
|
||||
}
|
||||
$current = $package->version;
|
||||
$package->update(['version' => $installation->from_version, 'status' => 'installed', 'is_enabled' => true]);
|
||||
$this->history($package, 'rollback', $current, $installation->from_version, $userId, 'completed');
|
||||
|
||||
return $package->refresh();
|
||||
}
|
||||
|
||||
private function history(InstalledPackage $package, string $operation, ?string $from, ?string $to, ?int $userId, string $status): Installation
|
||||
{
|
||||
return Installation::query()->create(['installed_package_id' => $package->id, 'package_name' => $package->package_name, 'operation' => $operation, 'from_version' => $from, 'to_version' => $to, 'status' => $status, 'performed_by' => $userId, 'started_at' => now(), 'completed_at' => now()]);
|
||||
}
|
||||
private function history(InstalledPackage $package,string $operation,?string $from,?string $to,?int $userId,string $status): Installation
|
||||
{ return Installation::query()->create(['installed_package_id'=>$package->id,'package_name'=>$package->package_name,'operation'=>$operation,'from_version'=>$from,'to_version'=>$to,'status'=>$status,'performed_by'=>$userId,'started_at'=>now(),'completed_at'=>now()]); }
|
||||
}
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Marketplace\Services;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final class PackageManifest
|
||||
{
|
||||
public function parse(array|string $manifest): array
|
||||
{
|
||||
if (is_string($manifest)) {
|
||||
$decoded=json_decode($manifest,true);
|
||||
if (!is_array($decoded)) throw ValidationException::withMessages(['manifest'=>'The package manifest is not valid JSON.']);
|
||||
$manifest=$decoded;
|
||||
$decoded = json_decode($manifest, true);
|
||||
if (! is_array($decoded)) {
|
||||
throw ValidationException::withMessages(['manifest' => 'The package manifest is not valid JSON.']);
|
||||
}
|
||||
$manifest = $decoded;
|
||||
}
|
||||
foreach (['name','type','version'] as $field) if (!filled(Arr::get($manifest,$field))) throw ValidationException::withMessages(['manifest'=>"Manifest field [$field] is required."]);
|
||||
if (!in_array($manifest['type'],config('marketplace.supported_types',[]),true)) throw ValidationException::withMessages(['manifest'=>'Unsupported marketplace package type.']);
|
||||
$manifest['requires']=$manifest['requires'] ?? [];
|
||||
$manifest['content_manager']=$manifest['content_manager'] ?? '*';
|
||||
$manifest['laravel']=$manifest['laravel'] ?? '*';
|
||||
foreach (['name', 'type', 'version'] as $field) {
|
||||
if (! filled(Arr::get($manifest, $field))) {
|
||||
throw ValidationException::withMessages(['manifest' => "Manifest field [$field] is required."]);
|
||||
}
|
||||
}
|
||||
if (! in_array($manifest['type'], config('marketplace.supported_types', []), true)) {
|
||||
throw ValidationException::withMessages(['manifest' => 'Unsupported marketplace package type.']);
|
||||
}
|
||||
$manifest['requires'] = $manifest['requires'] ?? [];
|
||||
$manifest['content_manager'] = $manifest['content_manager'] ?? '*';
|
||||
$manifest['laravel'] = $manifest['laravel'] ?? '*';
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
public function fromFile(string $path): array { return $this->parse((string) file_get_contents($path)); }
|
||||
|
||||
public function fromFile(string $path): array
|
||||
{
|
||||
return $this->parse((string) file_get_contents($path));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Marketplace\Services;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class PackageVerifier
|
||||
{
|
||||
public function verify(string $path,array $manifest,?string $expectedChecksum=null): array
|
||||
public function verify(string $path, array $manifest, ?string $expectedChecksum = null): array
|
||||
{
|
||||
if (!is_file($path)) throw new RuntimeException("Package archive [$path] does not exist.");
|
||||
$actual=hash_file('sha256',$path);
|
||||
$expected=$expectedChecksum ?: ($manifest['checksum'] ?? null);
|
||||
if (config('marketplace.require_checksum',true) && blank($expected)) throw new RuntimeException('A SHA-256 checksum is required.');
|
||||
if (filled($expected) && !hash_equals(strtolower((string)$expected),strtolower($actual))) throw new RuntimeException('Package checksum verification failed.');
|
||||
if (config('marketplace.require_signature',false) && blank($manifest['signature'] ?? null)) throw new RuntimeException('A package signature is required.');
|
||||
return ['verified'=>true,'checksum'=>$actual,'signature_present'=>filled($manifest['signature'] ?? null)];
|
||||
if (! is_file($path)) {
|
||||
throw new RuntimeException("Package archive [$path] does not exist.");
|
||||
}
|
||||
$actual = hash_file('sha256', $path);
|
||||
$expected = $expectedChecksum ?: ($manifest['checksum'] ?? null);
|
||||
if (config('marketplace.require_checksum', true) && blank($expected)) {
|
||||
throw new RuntimeException('A SHA-256 checksum is required.');
|
||||
}
|
||||
if (filled($expected) && ! hash_equals(strtolower((string) $expected), strtolower($actual))) {
|
||||
throw new RuntimeException('Package checksum verification failed.');
|
||||
}
|
||||
if (config('marketplace.require_signature', false) && blank($manifest['signature'] ?? null)) {
|
||||
throw new RuntimeException('A package signature is required.');
|
||||
}
|
||||
|
||||
return ['verified' => true, 'checksum' => $actual, 'signature_present' => filled($manifest['signature'] ?? null)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Marketplace\Services;
|
||||
|
||||
final class VersionConstraint
|
||||
{
|
||||
public function matches(string $version,string $constraint): bool
|
||||
public function matches(string $version, string $constraint): bool
|
||||
{
|
||||
$constraint=trim($constraint);
|
||||
if ($constraint==='' || $constraint==='*') return true;
|
||||
foreach (preg_split('/\s*\|\|\s*/',$constraint) ?: [] as $alternative) {
|
||||
$ok=true;
|
||||
foreach (preg_split('/\s*,?\s+/',trim($alternative)) ?: [] as $part) {
|
||||
if ($part==='') continue;
|
||||
if (str_starts_with($part,'^')) { $base=substr($part,1); $major=(int) explode('.',$base)[0]; $ok=$ok && version_compare($version,$base,'>=') && version_compare($version,($major+1).'.0.0','<'); continue; }
|
||||
if (str_starts_with($part,'~')) { $base=substr($part,1); $bits=array_pad(explode('.',$base),3,'0'); $upper=$bits[0].'.'.((int)$bits[1]+1).'.0'; $ok=$ok && version_compare($version,$base,'>=') && version_compare($version,$upper,'<'); continue; }
|
||||
if (preg_match('/^(>=|<=|>|<|=)?\s*([0-9][0-9A-Za-z.\-+]*)$/',$part,$m)) $ok=$ok && version_compare($version,$m[2],$m[1] ?: '=');
|
||||
else $ok=$ok && $version===$part;
|
||||
}
|
||||
if ($ok) return true;
|
||||
$constraint = trim($constraint);
|
||||
if ($constraint === '' || $constraint === '*') {
|
||||
return true;
|
||||
}
|
||||
foreach (preg_split('/\s*\|\|\s*/', $constraint) ?: [] as $alternative) {
|
||||
$ok = true;
|
||||
foreach (preg_split('/\s*,?\s+/', trim($alternative)) ?: [] as $part) {
|
||||
if ($part === '') {
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($part, '^')) {
|
||||
$base = substr($part, 1);
|
||||
$major = (int) explode('.', $base)[0];
|
||||
$ok = $ok && version_compare($version, $base, '>=') && version_compare($version, ($major + 1).'.0.0', '<');
|
||||
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($part, '~')) {
|
||||
$base = substr($part, 1);
|
||||
$bits = array_pad(explode('.', $base), 3, '0');
|
||||
$upper = $bits[0].'.'.((int) $bits[1] + 1).'.0';
|
||||
$ok = $ok && version_compare($version, $base, '>=') && version_compare($version, $upper, '<');
|
||||
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(>=|<=|>|<|=)?\s*([0-9][0-9A-Za-z.\-+]*)$/', $part, $m)) {
|
||||
$ok = $ok && version_compare($version, $m[2], $m[1] ?: '=');
|
||||
} else {
|
||||
$ok = $ok && $version === $part;
|
||||
}
|
||||
}
|
||||
if ($ok) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user